Skip to content

Commit

Permalink
Add method to check materialized view population
Browse files Browse the repository at this point in the history
REFRESH MATERIALIZED VIEW CONCURRENTLY doesn't work for a view which is
not populated, even it has unique index. This method is useful to
perform concurrent refresh if it's possible.

Real use case in Rails will be like this:

* Database is loaded from structure.sql to run specs, but the view isn't
  populated yet so concurrent refresh is not possible. We want to
  fallback to normal(non-concurrent) refresh in this case.
* After initial population, views are expected to be refreshed
  concurrently. It's possible because the view is already populated.

Co-authored By: Derek Prior <[email protected]>
  • Loading branch information
fujimura authored and derekprior committed Sep 29, 2023
1 parent bb053d4 commit 104d888
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/generators/scenic/model/templates/model.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
end

def self.populated?
Scenic.database.populated?(table_name)
end
23 changes: 23 additions & 0 deletions lib/scenic/adapters/postgres.rb
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,29 @@ def refresh_materialized_view(name, concurrently: false, cascade: false)
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.
#
# @param name The name of the relation
#
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
# in use does not support materialized views.
#
# @return [boolean]
def populated?(name)
raise_unless_materialized_views_supported

sql = "SELECT relispopulated FROM pg_class WHERE relname = '#{name}'"
relations = execute(sql)

if relations.count.positive?
relations.first["relispopulated"].in?(["t", true])
else
false
end
end

private

attr_reader :connectable
Expand Down
8 changes: 8 additions & 0 deletions spec/generators/scenic/model/model_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,13 @@ module Scenic::Generators
expect(model_definition).to contain("self.refresh")
expect(model_definition).to have_correct_syntax
end

it "adds a populated? method to materialized models" do
run_generator ["active_user", "--materialized"]
model_definition = file("app/models/active_user.rb")

expect(model_definition).to contain("self.populated?")
expect(model_definition).to have_correct_syntax
end
end
end
34 changes: 34 additions & 0 deletions spec/scenic/adapters/postgres_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,40 @@ module Adapters
end
end
end

describe "#populated?" do
it "returns false if a materialized view is not populated" do
adapter = Postgres.new

ActiveRecord::Base.connection.execute <<-SQL
CREATE MATERIALIZED VIEW greetings AS
SELECT text 'hi' AS greeting
WITH NO DATA
SQL

expect(adapter.populated?("greetings")).to be false
end

it "returns true if a materialized view is populated" do
adapter = Postgres.new

ActiveRecord::Base.connection.execute <<-SQL
CREATE MATERIALIZED VIEW greetings AS
SELECT text 'hi' AS greeting
SQL

expect(adapter.populated?("greetings")).to be true
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)
adapter = Postgres.new(connectable)
err = Scenic::Adapters::Postgres::MaterializedViewsNotSupportedError

expect { adapter.populated?("greetings") }.to raise_error err
end
end
end
end
end

0 comments on commit 104d888

Please sign in to comment.