Skip to content

Commit

Permalink
manage version when rename view
Browse files Browse the repository at this point in the history
  • Loading branch information
gagalago committed Jan 27, 2021
1 parent 0e0be30 commit 4ca42b9
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 50 deletions.
30 changes: 29 additions & 1 deletion lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,24 @@ def refresh_materialized_view(name, concurrently: false, cascade: false)
end
end

# Compare the SQL definition of the view stored in the database
# with the definition used to create the migrations.
#
# @param definition [Scenic::Definition] view definition to compare
# with the database.
#
# @return [Boolean]
def view_with_similar_definition?(definition)
decompiled_view_sql(definition.name) == decompiled_sql(definition.to_sql)
end

private

attr_reader :connectable
delegate :execute, :quote_table_name, :rename_index, to: :connection
delegate(
:execute, :quote_table_name, :rename_index, :select_value, :transaction,
to: :connection
)

def connection
Connection.new(connectable.connection)
Expand All @@ -294,6 +308,20 @@ def refresh_dependencies_for(name, concurrently: false)
concurrently: concurrently,
)
end

def decompiled_sql(sql_definition)
temporary_view_name = "temp_view_for_decompilation"
view_name = quote_view_name(temporary_view_name)
transaction do
execute "CREATE TEMPORARY VIEW #{view_name} AS #{sql_definition};"
decompiled_view_sql(temporary_view_name)
end
end

def decompiled_view_sql(name)
view_name = quote_table_name(name)
select_value "SELECT pg_get_viewdef(to_regclass(#{view_name}))"
end
end
end
end
11 changes: 4 additions & 7 deletions lib/scenic/command_recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@ def invert_replace_view(args)
end

def invert_rename_view(args)
options = args.extract_options!
old_name, new_name = args

args = [new_name, old_name]
args << options unless options.empty?

[:rename_view, args]
perform_scenic_inversion(
:rename_view,
StatementArguments.new(args).invert_names.to_a,
)
end

private
Expand Down
29 changes: 20 additions & 9 deletions lib/scenic/command_recorder/statement_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module CommandRecorder
# @api private
class StatementArguments
def initialize(args)
@args = args.freeze
@args = args.clone
@options = @args.extract_options!
end

def view
@args[0]
args[0]
end

def version
Expand All @@ -19,27 +20,37 @@ def revert_to_version
end

def invert_version
StatementArguments.new([view, options_for_revert])
StatementArguments.new([*args, options_for_revert])
end

def remove_version
StatementArguments.new([view, options_without_version])
StatementArguments.new([*args, options_without_version])
end

def invert_names
StatementArguments.new([*args.reverse, options])
end

def to_a
@args.to_a.dup.delete_if(&:empty?)
args.to_a.dup.delete_if(&:empty?).tap do |array|
array << options if options.present?
end
end

private

def options
@options ||= @args[1] || {}
end
attr_reader :args, :options

def options_for_revert
options.clone.tap do |revert_options|
revert_options[:version] = revert_to_version
revert_options.delete(:version)
revert_options.delete(:revert_to_version)
if revert_to_version.present?
revert_options[:version] = revert_to_version
end
if version.present?
revert_options[:revert_to_version] = version
end
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/scenic/definition.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Scenic
# @api private
class Definition
attr_reader :name

def initialize(name, version)
@name = name
@version = version.to_i
Expand Down
10 changes: 10 additions & 0 deletions lib/scenic/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Scenic
# Raised when a view definition in the database is different
# from the definition used to create migrations.
class StoredDefinitionError < StandardError
def initialize
path = Scenic.configuration.definitions_path
super("View definition in the database different from in the #{path}")
end
end
end
51 changes: 36 additions & 15 deletions lib/scenic/statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false)
version = 1
end

sql_definition ||= definition(name, version)
sql_definition ||= definition(name, version).to_sql

if materialized
Scenic.database.create_materialized_view(
database.create_materialized_view(
name,
sql_definition,
no_data: no_data(materialized),
)
else
Scenic.database.create_view(name, sql_definition)
database.create_view(name, sql_definition)
end
end

Expand All @@ -62,9 +62,9 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false)
#
def drop_view(name, revert_to_version: nil, materialized: false)
if materialized
Scenic.database.drop_materialized_view(name)
database.drop_materialized_view(name)
else
Scenic.database.drop_view(name)
database.drop_view(name)
end
end

Expand Down Expand Up @@ -103,16 +103,16 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil,
)
end

sql_definition ||= definition(name, version)
sql_definition ||= definition(name, version).to_sql

if materialized
Scenic.database.update_materialized_view(
database.update_materialized_view(
name,
sql_definition,
no_data: no_data(materialized),
)
else
Scenic.database.update_view(name, sql_definition)
database.update_view(name, sql_definition)
end
end

Expand Down Expand Up @@ -141,15 +141,18 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false
raise ArgumentError, "Cannot replace materialized views"
end

sql_definition = definition(name, version)
sql_definition = definition(name, version).to_sql

Scenic.database.replace_view(name, sql_definition)
database.replace_view(name, sql_definition)
end

# Rename a database view by name.
#
# @param from_name [String, Symbol] The previous name of the database view.
# @param from_name [String, Symbol] The next name of the database view.
# @param to_name [String, Symbol] The next name of the database view.
# @param version [Fixnum] The version number of the view `to_name`.
# @param revert_to_version [Fixnum] The version number
# of the view `from_name` to rollback to on `rake db rollback`
# @param materialized [Boolean, Hash] True if updating a materialized view.
# Set to { rename_indexes: true } to rename materialized view indexes
# by substituing in their name the previous view name
Expand All @@ -159,22 +162,40 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false
# @example Rename a view
# drop_view(:engggggement_reports, :engagement_reports)
#
def rename_view(from_name, to_name, materialized: false)
def rename_view(
from_name, to_name,
version: nil, revert_to_version: nil, materialized: false
)
if version.blank?
raise ArgumentError, "version is required"
end

if revert_to_version.blank?
raise ArgumentError, "revert_to_version is required"
end

if materialized
Scenic.database.rename_materialized_view(
database.rename_materialized_view(
from_name,
to_name,
rename_indexes: rename_indexes(materialized),
)
else
Scenic.database.rename_view(from_name, to_name)
database.rename_view(from_name, to_name)
end

similar_definition = database.view_with_similar_definition?(
definition(to_name, version)
)
raise StoredDefinitionError unless similar_definition
end

private

delegate :database, to: :Scenic

def definition(name, version)
Scenic::Definition.new(name, version).to_sql
Scenic::Definition.new(name, version)
end

def no_data(materialized)
Expand Down
53 changes: 53 additions & 0 deletions spec/scenic/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,59 @@ module Adapters
end
end
end

describe "#view_with_similar_definition?" do
context "when the view has a similar definition" do
it "returns true" do
adapter = Postgres.new
ActiveRecord::Base.connection.execute <<~SQL
CREATE VIEW greetings AS SELECT text 'hi' AS greeting
SQL

sql_defintion = <<~SQL
SELECT text 'hi'
AS greeting
SQL
definition = instance_double(
"Scenic::Definition",
name: "greeting", to_sql: sql_defintion
)

expect(
adapter.view_with_similar_definition?(definition)
).to be_true
end
end
context "when the view doesn't exists on the database" do
it "returns false" do
adapter = Postgres.new

definition = instance_double(
"Scenic::Definition",
name: "greeting", to_sql: "SELECT text 'hi' AS greeting"
)

expect(
adapter.view_with_similar_definition?(definition)
).to be_false
end
end
context "when the view has a different definition" do
it "returns false" do
adapter = Postgres.new
ActiveRecord::Base.connection.execute <<~SQL
CREATE VIEW greetings AS SELECT text 'hi' AS hello
SQL

definition = instance_double(
"Scenic::Definition",
name: "greeting", to_sql: "SELECT text 'hi' AS greeting"
)

expect(adapter.view_with_similar_definition?(definition)).to be_false
end
end
end
end
end
end
4 changes: 2 additions & 2 deletions spec/scenic/command_recorder/statement_arguments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ module Scenic::CommandRecorder
end

describe "#invert_version" do
it "returns object with version set to revert_to_version" do
it "returns object with version interverted with revert_to_version" do
raw_args = [:meatballs, { version: 42, revert_to_version: 15 }]

inverted_args = StatementArguments.new(raw_args).invert_version

expect(inverted_args.version).to eq 15
expect(inverted_args.revert_to_version).to be nil
expect(inverted_args.revert_to_version).to be 42
end
end
end
Expand Down
Loading

0 comments on commit 4ca42b9

Please sign in to comment.