Skip to content

Commit

Permalink
Add if_not_exists to create_view and if_exists to drop_view
Browse files Browse the repository at this point in the history
  • Loading branch information
serg-kovalev committed Feb 8, 2023
1 parent be0980d commit 32b0327
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ tmp
gemfiles/*.lock
.DS_Store
.ruby-version
.ruby-gemset
28 changes: 21 additions & 7 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -125,18 +133,21 @@ 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}.
#
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
# 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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions lib/scenic/statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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,
Expand All @@ -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

Expand All @@ -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

Expand Down
49 changes: 49 additions & 0 deletions spec/scenic/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
38 changes: 32 additions & 6 deletions spec/scenic/statements_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -74,15 +74,34 @@ 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

describe "drop_view" do
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

Expand All @@ -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
Expand Down

0 comments on commit 32b0327

Please sign in to comment.