diff --git a/.gitignore b/.gitignore index 92bd1ab4..d15da594 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tmp gemfiles/*.lock .DS_Store .ruby-version +.ruby-gemset diff --git a/lib/scenic/adapters/postgres.rb b/lib/scenic/adapters/postgres.rb index 2c94e015..6c4775c9 100644 --- a/lib/scenic/adapters/postgres.rb +++ b/lib/scenic/adapters/postgres.rb @@ -55,9 +55,14 @@ def views # # @param name The name of the view to create # @param sql_definition The SQL schema for the view. + # @param if_not_exists [Boolean] Default: false. Set to true to create + # view only if it does not exist. # # @return [void] - def create_view(name, sql_definition) + def create_view(name, sql_definition, if_not_exists: false) + if if_not_exists + return if views.any? { |view| view.name == name } + end execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};" end @@ -112,10 +117,13 @@ def replace_view(name, sql_definition) # This is typically called in a migration via {Statements#drop_view}. # # @param name The name of the view to drop + # @param if_exists [Boolean] Default: false. Set to true to drop + # view only if it exists. # # @return [void] - def drop_view(name) - execute "DROP VIEW #{quote_table_name(name)};" + def drop_view(name, if_exists: false) + definition_if_exists = if_exists ? "IF EXISTS " : "" + execute "DROP VIEW #{definition_if_exists}#{quote_table_name(name)};" end # Creates a materialized view in the database @@ -125,6 +133,8 @@ def drop_view(name) # @param no_data [Boolean] Default: false. Set to true to create # materialized view without running the associated query. You will need # to perform a non-concurrent refresh to populate with data. + # @param if_not_exists [Boolean] Default: false. Set to true to create + # materialized view only if it does not exist. # # This is typically called in a migration via {Statements#create_view}. # @@ -132,11 +142,12 @@ def drop_view(name) # in use does not support materialized views. # # @return [void] - def create_materialized_view(name, sql_definition, no_data: false) + def create_materialized_view(name, sql_definition, no_data: false, if_not_exists: false) raise_unless_materialized_views_supported + definition_if_not_exists = if_not_exists ? "IF NOT EXISTS " : "" execute <<-SQL - CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS + CREATE MATERIALIZED VIEW #{definition_if_not_exists}#{quote_table_name(name)} AS #{sql_definition.rstrip.chomp(';')} #{'WITH NO DATA' if no_data}; SQL @@ -174,13 +185,16 @@ def update_materialized_view(name, sql_definition, no_data: false) # This is typically called in a migration via {Statements#update_view}. # # @param name The name of the materialized view to drop. + # @param if_exists [Boolean] Default: false. Set to true to drop + # materialized view only if it exists. # @raise [MaterializedViewsNotSupportedError] if the version of Postgres # in use does not support materialized views. # # @return [void] - def drop_materialized_view(name) + def drop_materialized_view(name, if_exists: false) raise_unless_materialized_views_supported - execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};" + definition_if_exists = if_exists ? "IF EXISTS " : "" + execute "DROP MATERIALIZED VIEW #{definition_if_exists}#{quote_table_name(name)};" end # Refreshes a materialized view from its SQL schema. diff --git a/lib/scenic/statements.rb b/lib/scenic/statements.rb index 84bf24b0..6acf6a36 100644 --- a/lib/scenic/statements.rb +++ b/lib/scenic/statements.rb @@ -12,6 +12,8 @@ module Statements # @param materialized [Boolean, Hash] Set to true to create a materialized # view. Set to { no_data: true } to create materialized view without # loading data. Defaults to false. + # @param if_not_exists [Boolean] Set to true to only create the view if it + # does not already exist. Defaults to false. # @return The database response from executing the create statement. # # @example Create from `db/views/searches_v02.sql` @@ -22,7 +24,7 @@ module Statements # SELECT * FROM users WHERE users.active = 't' # SQL # - def create_view(name, version: nil, sql_definition: nil, materialized: false) + def create_view(name, version: nil, sql_definition: nil, materialized: false, if_not_exists: false) if version.present? && sql_definition.present? raise( ArgumentError, @@ -41,9 +43,10 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false) name, sql_definition, no_data: no_data(materialized), + if_not_exists: if_not_exists, ) else - Scenic.database.create_view(name, sql_definition) + Scenic.database.create_view(name, sql_definition, if_not_exists: if_not_exists) end end @@ -55,16 +58,18 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false) # `version` argument to {#create_view}. # @param materialized [Boolean] Set to true if dropping a meterialized view. # defaults to false. + # @param if_exists [Boolean] Set to true to only drop the view if itexists. + # Defaults to false. # @return The database response from executing the drop statement. # # @example Drop a view, rolling back to version 3 on rollback # drop_view(:users_who_recently_logged_in, revert_to_version: 3) # - def drop_view(name, revert_to_version: nil, materialized: false) + def drop_view(name, revert_to_version: nil, materialized: false, if_exists: false) if materialized - Scenic.database.drop_materialized_view(name) + Scenic.database.drop_materialized_view(name, if_exists: if_exists) else - Scenic.database.drop_view(name) + Scenic.database.drop_view(name, if_exists: if_exists) end end diff --git a/spec/scenic/adapters/postgres_spec.rb b/spec/scenic/adapters/postgres_spec.rb index 0fa438ec..00ff1e15 100644 --- a/spec/scenic/adapters/postgres_spec.rb +++ b/spec/scenic/adapters/postgres_spec.rb @@ -11,6 +11,22 @@ module Adapters expect(adapter.views.map(&:name)).to include("greetings") end + + it "successfully creates a view with :if_not_exists if view does not exist" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting", if_not_exists: true) + + expect(adapter.views.map(&:name)).to include("greetings") + end + + it "does not raise an error with :if_not_exists if view exists" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting") + expect { adapter.create_view("greetings", "SELECT text 'hi' AS greeting", if_not_exists: true) } + .not_to raise_error + end end describe "#create_materialized_view" do @@ -77,6 +93,21 @@ module Adapters expect(adapter.views.map(&:name)).not_to include("greetings") end + + it "successfully drops with :if_exists if view exists" do + adapter = Postgres.new + + adapter.create_view("greetings", "SELECT text 'hi' AS greeting") + adapter.drop_view("greetings", if_exists: true) + + expect(adapter.views.map(&:name)).not_to include("greetings") + end + + it "does not raise error with :if_exists if view does not exist" do + adapter = Postgres.new + + expect { adapter.drop_view("greetings", if_exists: true) }.not_to raise_error + end end describe "#drop_materialized_view" do @@ -92,6 +123,24 @@ module Adapters expect(adapter.views.map(&:name)).not_to include("greetings") end + it "successfully drops with :if_exists if view exists" do + adapter = Postgres.new + + adapter.create_materialized_view( + "greetings", + "SELECT text 'hi' AS greeting", + ) + adapter.drop_materialized_view("greetings", if_exists: true) + + expect(adapter.views.map(&:name)).not_to include("greetings") + end + + it "does not raise error with :if_exists if view does not exist" do + adapter = Postgres.new + + expect { adapter.drop_materialized_view("greetings", if_exists: true) }.not_to raise_error + end + it "raises an exception if the version of PostgreSQL is too old" do connection = double("Connection", supports_materialized_views?: false) connectable = double("Connectable", connection: connection) diff --git a/spec/scenic/statements_spec.rb b/spec/scenic/statements_spec.rb index b0aa5120..97bab4bd 100644 --- a/spec/scenic/statements_spec.rb +++ b/spec/scenic/statements_spec.rb @@ -18,7 +18,7 @@ module Scenic connection.create_view :views, version: version expect(Scenic.database).to have_received(:create_view) - .with(:views, definition_stub.to_sql) + .with(:views, definition_stub.to_sql, if_not_exists: false) end it "creates a view from a text definition" do @@ -27,7 +27,7 @@ module Scenic connection.create_view(:views, sql_definition: sql_definition) expect(Scenic.database).to have_received(:create_view) - .with(:views, sql_definition) + .with(:views, sql_definition, if_not_exists: false) end it "creates version 1 of the view if neither version nor sql_defintion are provided" do @@ -40,7 +40,7 @@ module Scenic connection.create_view :views expect(Scenic.database).to have_received(:create_view) - .with(:views, definition_stub.to_sql) + .with(:views, definition_stub.to_sql, if_not_exists: false) end it "raises an error if both version and sql_defintion are provided" do @@ -58,7 +58,7 @@ module Scenic connection.create_view(:views, version: 1, materialized: true) expect(Scenic.database).to have_received(:create_materialized_view) - .with(:views, definition.to_sql, no_data: false) + .with(:views, definition.to_sql, no_data: false, if_not_exists: false) end end @@ -74,7 +74,19 @@ module Scenic ) expect(Scenic.database).to have_received(:create_materialized_view) - .with(:views, definition.to_sql, no_data: true) + .with(:views, definition.to_sql, no_data: true, if_not_exists: false) + end + end + + describe "create_view :materialized with :if_not_exists" do + it "sends the create_materialized_view message with if not exists" do + definition = instance_double("Scenic::Definition", to_sql: "definition") + allow(Definition).to receive(:new).and_return(definition) + + connection.create_view(:views, version: 1, materialized: true, if_not_exists: true) + + expect(Scenic.database).to have_received(:create_materialized_view) + .with(:views, definition.to_sql, no_data: false, if_not_exists: true) end end @@ -82,7 +94,14 @@ module Scenic it "removes a view from the database" do connection.drop_view :name - expect(Scenic.database).to have_received(:drop_view).with(:name) + expect(Scenic.database).to have_received(:drop_view).with(:name, if_exists: false) + end + + it "removes a view from the database if it exists" do + connection.drop_view :name, if_exists: true + + expect(Scenic.database).to have_received(:drop_view) + .with(:name, if_exists: true) end end @@ -92,6 +111,13 @@ module Scenic expect(Scenic.database).to have_received(:drop_materialized_view) end + + it "removes a materialized view from the database if it exists" do + connection.drop_view :name, materialized: true, if_exists: true + + expect(Scenic.database).to have_received(:drop_materialized_view) + .with(:name, if_exists: true) + end end describe "update_view" do