Skip to content

Commit

Permalink
Only refresh concurrently if view is populated
Browse files Browse the repository at this point in the history
This implementation will still error if you ask for a concurrent refresh
of a non-populated view if your database does not support concurrent
refreshes. I'm happy with this because the user has asked for a thing
that we know cannot succeed, even under the right circumstances, and we
should tell them that., and we should tell them that.

Co-authored-by: Derek Prior <[email protected]>
Co-authored-by: Caleb Hearth <[email protected]>
  • Loading branch information
3 people committed Dec 29, 2024
1 parent 1b70bf7 commit a828baa
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 10 deletions.
26 changes: 16 additions & 10 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module Scenic
module Adapters
# An adapter for managing Postgres views.
#
# These methods are used interally by Scenic and are not intended for direct
# These methods are used internally by Scenic and are not intended for direct
# use. Methods that alter database schema are intended to be called via
# {Statements}, while {#refresh_materialized_view} is called via
# {Scenic.database}.
Expand Down Expand Up @@ -124,7 +124,7 @@ def drop_view(name)
# @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.
# to perform a refresh to populate with data.
#
# This is typically called in a migration via {Statements#create_view}.
#
Expand Down Expand Up @@ -154,7 +154,7 @@ def create_materialized_view(name, sql_definition, no_data: false)
# @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.
# to perform a refresh to populate with data.
#
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
# in use does not support materialized views.
Expand Down Expand Up @@ -193,7 +193,10 @@ def drop_materialized_view(name)
# refreshed without locking the view for select but requires that the
# table have at least one unique index that covers all rows. Attempts to
# refresh concurrently without a unique index will raise a descriptive
# error.
# error. This option is ignored if the view is not populated, as it
# would cause an error to be raised by Postgres. Default: false.
# @param cascade [Boolean] Whether to refresh dependent materialized
# views. Default: false.
#
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
# in use does not support materialized views.
Expand All @@ -205,26 +208,29 @@ def drop_materialized_view(name)
# Scenic.database.refresh_materialized_view(:search_results)
# @example Concurrent refresh
# Scenic.database.refresh_materialized_view(:posts, concurrently: true)
# @example Cascade refresh
# Scenic.database.refresh_materialized_view(:posts, cascade: true)
#
# @return [void]
def refresh_materialized_view(name, concurrently: false, cascade: false)
raise_unless_materialized_views_supported

if concurrently
raise_unless_concurrent_refresh_supported
end

if cascade
refresh_dependencies_for(name, concurrently: concurrently)
end

if concurrently
raise_unless_concurrent_refresh_supported
if concurrently && populated?(name)
execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
else
execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
end
end

# True if supplied relation name is populated. Useful for checking the
# state of materialized views which may error if created `WITH NO DATA`
# and used before they are refreshed. True for all other relation types.
# True if supplied relation name is populated.
#
# @param name The name of the relation
#
Expand All @@ -235,7 +241,7 @@ def refresh_materialized_view(name, concurrently: false, cascade: false)
def populated?(name)
raise_unless_materialized_views_supported

schemaless_name = name.split(".").last
schemaless_name = name.to_s.split(".").last

sql = "SELECT relispopulated FROM pg_class WHERE relname = '#{schemaless_name}'"
relations = execute(sql)
Expand Down
8 changes: 8 additions & 0 deletions spec/scenic/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ module Adapters
adapter.refresh_materialized_view(:tests, concurrently: true)
}.to raise_error e
end

it "falls back to non-concurrent refresh if not populated" do
adapter = Postgres.new
adapter.create_materialized_view(:testing, "SELECT unnest('{1, 2}'::int[])", no_data: true)

expect { adapter.refresh_materialized_view(:testing, concurrently: true) }
.not_to raise_error
end
end
end

Expand Down

0 comments on commit a828baa

Please sign in to comment.