Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add if_not_exists to create_view and if_exists to drop_view #382

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
32 changes: 25 additions & 7 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,15 @@ 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 && views.any? { |view| view.name == name }
return
end

execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
end

Expand Down Expand Up @@ -112,10 +118,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 +134,22 @@ 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 +187,17 @@ 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 Expand Up @@ -225,6 +242,7 @@ def refresh_materialized_view(name, concurrently: false, cascade: false)
private

attr_reader :connectable

delegate :execute, :quote_table_name, to: :connection

def connection
Expand Down
2 changes: 1 addition & 1 deletion lib/scenic/adapters/postgres/index_reapplication.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def with_savepoint(name)
yield
connection.execute("RELEASE SAVEPOINT #{name}")
true
rescue
rescue StandardError
connection.execute("ROLLBACK TO SAVEPOINT #{name}")
false
end
Expand Down
1 change: 1 addition & 0 deletions lib/scenic/adapters/postgres/indexes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def on(name)
private

attr_reader :connection

delegate :quote_table_name, to: :connection

def indexes_on(name)
Expand Down
20 changes: 15 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,8 @@ 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 +44,11 @@ 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,18 +60,23 @@ 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)
# rubocop:disable Lint/UnusedMethodArgument
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
# rubocop:enable Lint/UnusedMethodArgument

# Update a database view to a new version.
#
Expand Down
59 changes: 59 additions & 0 deletions spec/scenic/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,27 @@ 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 +98,23 @@ 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 +130,27 @@ 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
40 changes: 34 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,36 @@ 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 +113,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
4 changes: 2 additions & 2 deletions spec/support/generator_spec_setup.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
require "rspec/rails"
require "ammeter/rspec/generator/example.rb"
require "ammeter/rspec/generator/matchers.rb"
require "ammeter/rspec/generator/example"
require "ammeter/rspec/generator/matchers"
require "ammeter/init"

RSpec.configure do |config|
Expand Down