Skip to content

Commit

Permalink
Add no_data option to materialized views
Browse files Browse the repository at this point in the history
By default, creating a materialized view causes the associated query to
be run immediately. The `create view` statement does not complete until
the view is populated. While this is a good default, it may be
particularly expensive and potentially not desired during deploys.
Specifying `no_data: true`, allows the view to be created without a
refresh. It will not be usable until it is manually refreshed.
  • Loading branch information
Siarhei Kavaliou authored and derekprior committed Apr 10, 2018
1 parent 05be54f commit a21c476
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 17 deletions.
9 changes: 9 additions & 0 deletions lib/generators/scenic/materializable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ module Materializable
required: false,
desc: "Makes the view materialized",
default: false
class_option :no_data,
type: :boolean,
required: false,
desc: "Adds WITH NO DATA when materialized view creates/updates",
default: false
end

private

def materialized?
options[:materialized]
end

def no_data?
options[:no_data]
end
end
end
end
1 change: 1 addition & 0 deletions lib/generators/scenic/view/USAGE
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Description:
and a migration to replace the old version with the new.

To create a materialized view, pass the '--materialized' option.
To create a materialized view with NO DATA, pass '--no-data' option.

Examples:
rails generate scenic:view searches
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class <%= migration_class_name %> < <%= activerecord_migration_class %>
def change
create_view <%= formatted_plural_name %><%= ", materialized: true" if materialized? %>
create_view <%= formatted_plural_name %><%= create_view_options %>
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class <%= migration_class_name %> < <%= activerecord_migration_class %>
update_view <%= formatted_plural_name %>,
version: <%= version %>,
revert_to_version: <%= previous_version %>,
materialized: true
materialized: <%= no_data? ? "{ no_data: true }" : true %>
<%- else -%>
update_view <%= formatted_plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
<%- end -%>
Expand Down
5 changes: 5 additions & 0 deletions lib/generators/scenic/view/view_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ def formatted_plural_name
end
end

def create_view_options
return "" unless materialized?
", materialized: #{no_data? ? '{ no_data: true }' : true}"
end

def destroying_initial_view?
destroying? && version == 1
end
Expand Down
18 changes: 14 additions & 4 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,23 @@ def drop_view(name)
#
# @param name The name of the materialized view to create
# @param sql_definition The SQL schema that defines the materialized view.
# @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.
#
# 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)
def create_materialized_view(name, sql_definition, no_data: false)
raise_unless_materialized_views_supported
execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
execute <<-SQL
CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS
#{sql_definition}
#{'WITH NO DATA' if no_data};
SQL
end

# Updates a materialized view in the database.
Expand All @@ -144,17 +151,20 @@ def create_materialized_view(name, sql_definition)
#
# @param name The name of the view to update
# @param sql_definition The SQL schema for the updated view.
# @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.
#
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
# in use does not support materialized views.
#
# @return [void]
def update_materialized_view(name, sql_definition)
def update_materialized_view(name, sql_definition, no_data: false)
raise_unless_materialized_views_supported

IndexReapplication.new(connection: connection).on(name) do
drop_materialized_view(name)
create_materialized_view(name, sql_definition)
create_materialized_view(name, sql_definition, no_data: no_data)
end
end

Expand Down
27 changes: 21 additions & 6 deletions lib/scenic/statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ module Statements
# @param sql_definition [String] The SQL query for the view schema. An error
# will be raised if `sql_definition` and `version` are both set,
# as they are mutually exclusive.
# @param materialized [Boolean] Set to true to create a materialized view.
# Defaults to false.
# @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.
# @return The database response from executing the create statement.
#
# @example Create from `db/views/searches_v02.sql`
Expand All @@ -36,7 +37,11 @@ def create_view(name, version: nil, sql_definition: nil, materialized: false)
sql_definition ||= definition(name, version)

if materialized
Scenic.database.create_materialized_view(name, sql_definition)
Scenic.database.create_materialized_view(
name,
sql_definition,
no_data: no_data(materialized),
)
else
Scenic.database.create_view(name, sql_definition)
end
Expand Down Expand Up @@ -75,8 +80,9 @@ def drop_view(name, revert_to_version: nil, materialized: false)
# as they are mutually exclusive.
# @param revert_to_version [Fixnum] The version number to rollback to on
# `rake db rollback`
# @param materialized [Boolean] True if updating a materialized view.
# Defaults to false.
# @param materialized [Boolean, Hash] True if updating a materialized view.
# Set to { no_data: true } to update materialized view without loading
# data. Defaults to false.
# @return The database response from executing the create statement.
#
# @example
Expand All @@ -100,7 +106,11 @@ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil,
sql_definition ||= definition(name, version)

if materialized
Scenic.database.update_materialized_view(name, sql_definition)
Scenic.database.update_materialized_view(
name,
sql_definition,
no_data: no_data(materialized),
)
else
Scenic.database.update_view(name, sql_definition)
end
Expand Down Expand Up @@ -141,5 +151,10 @@ def replace_view(name, version: nil, revert_to_version: nil, materialized: false
def definition(name, version)
Scenic::Definition.new(name, version).to_sql
end

def no_data(materialized)
return false unless materialized.is_a? Hash
materialized.fetch(:no_data, false)
end
end
end
1 change: 0 additions & 1 deletion lib/scenic/view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def to_schema
create_view #{name.inspect}, #{materialized_option} sql_definition: <<-\SQL
#{definition.indent(2)}
SQL
DEFINITION
end
end
Expand Down
41 changes: 37 additions & 4 deletions spec/scenic/statements_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,29 @@ module Scenic

describe "create_view :materialized" do
it "sends the create_materialized_view message" do
allow(Definition).to receive(:new)
.and_return(instance_double("Scenic::Definition").as_null_object)
definition = instance_double("Scenic::Definition", to_sql: "definition")
allow(Definition).to receive(:new).and_return(definition)

connection.create_view(:views, version: 1, materialized: true)

expect(Scenic.database).to have_received(:create_materialized_view)
expect(Scenic.database).to have_received(:create_materialized_view).
with(:views, definition.to_sql, no_data: false)
end
end

describe "create_view :materialized with :no_data" do
it "sends the create_materialized_view message" do
definition = instance_double("Scenic::Definition", to_sql: "definition")
allow(Definition).to receive(:new).and_return(definition)

connection.create_view(
:views,
version: 1,
materialized: { no_data: true },
)

expect(Scenic.database).to have_received(:create_materialized_view).
with(:views, definition.to_sql, no_data: true)
end
end

Expand Down Expand Up @@ -108,7 +125,23 @@ module Scenic
connection.update_view(:name, version: 3, materialized: true)

expect(Scenic.database).to have_received(:update_materialized_view).
with(:name, definition.to_sql)
with(:name, definition.to_sql, no_data: false)
end

it "updates the materialized view in the database with NO DATA" do
definition = instance_double("Definition", to_sql: "definition")
allow(Definition).to receive(:new).
with(:name, 3).
and_return(definition)

connection.update_view(
:name,
version: 3,
materialized: { no_data: true },
)

expect(Scenic.database).to have_received(:update_materialized_view).
with(:name, definition.to_sql, no_data: true)
end

it "raises an error if not supplied a version or sql_defintion" do
Expand Down

0 comments on commit a21c476

Please sign in to comment.