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 a7d91c8
Show file tree
Hide file tree
Showing 9 changed files with 134 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
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
33 changes: 22 additions & 11 deletions spec/scenic/command_recorder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@

it "reverts to update_view with the specified revert_to_version" do
args = [:users, { version: 2, revert_to_version: 1 }]
revert_args = [:users, { version: 1 }]
revert_args = [:users, { version: 1, revert_to_version: 2 }]

recorder.revert { recorder.update_view(*args) }

Expand All @@ -90,7 +90,7 @@

it "reverts to replace_view with the specified revert_to_version" do
args = [:users, { version: 2, revert_to_version: 1 }]
revert_args = [:users, { version: 1 }]
revert_args = [:users, { version: 1, revert_to_version: 2 }]

recorder.revert { recorder.replace_view(*args) }

Expand All @@ -107,23 +107,34 @@

describe "#rename_view" do
it "records the created view" do
recorder.rename_view :from, :to
recorder.rename_view :from, :to, version: 1, revert_to_version: 2

expect(recorder.commands).to eq [[:rename_view, [:from, :to], nil]]
expect(recorder.commands).to eq [[
:rename_view, [:from, :to, version: 1, revert_to_version: 2], nil
]]
end

it "reverts to drop_view when not passed a version" do
recorder.revert { recorder.rename_view :from, :to }
recorder.revert do
recorder.rename_view :from, :to, version: 1, revert_to_version: 2
end

expect(recorder.commands).to eq [[:rename_view, [:to, :from]]]
expect(recorder.commands).to eq [[
:rename_view, [:to, :from, version: 2, revert_to_version: 1]
]]
end

it "reverts materialized views appropriately" do
recorder.revert { recorder.rename_view :from, :to, materialized: true }

expect(recorder.commands).to eq [
[:rename_view, [:to, :from, materialized: true]],
]
recorder.revert do
recorder.rename_view(
:from, :to, version: 1, revert_to_version: 2, materialized: true
)
end

expect(recorder.commands).to eq [[
:rename_view,
[:to, :from, version: 2, revert_to_version: 1, materialized: true],
]]
end
end

Expand Down
14 changes: 9 additions & 5 deletions spec/scenic/statements_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -194,24 +194,28 @@ module Scenic

describe "rename_view" do
it "rename the view in the database" do
connection.rename_view(:from, :to)
connection.rename_view(:from, :to, version: 1, revert_to_version: 1)

expect(Scenic.database).to have_received(:rename_view)
.with(:from, :to)
end

it "rename the materialized view in the database" do
connection.rename_view(:from, :to, materialized: true)
connection.rename_view(
:from, :to,
version: 1, revert_to_version: 1, materialized: true
)

expect(Scenic.database).to have_received(:rename_materialized_view)
.with(:from, :to, rename_indexes: false)
end

it "rename the materialized view in the database and rename indexes" do
connection.rename_view(
:from,
:to,
materialized: { rename_indexes: true },
:from, :to,
version: 1,
revert_to_version: 1,
materialized: { rename_indexes: true }
)

expect(Scenic.database).to have_received(:rename_materialized_view)
Expand Down

0 comments on commit a7d91c8

Please sign in to comment.