class Scenic::Adapters::Postgres

An adapter for managing Postgres views.

These methods are used interally 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}.

The methods are documented here for insight into specifics of how Scenic integrates with Postgres and the responsibilities of {Adapters}.

Attributes

connectable[R]

Public Class Methods

new(connectable = ActiveRecord::Base) click to toggle source

Creates an instance of the Scenic Postgres adapter.

This is the default adapter for Scenic. Configuring it via {Scenic.configure} is not required, but the example below shows how one would explicitly set it.

@param [#connection] connectable An object that returns the connection

for Scenic to use. Defaults to `ActiveRecord::Base`.

@example

Scenic.configure do |config|
  config.database = Scenic::Adapters::Postgres.new
end
# File lib/scenic/adapters/postgres.rb, line 38
def initialize(connectable = ActiveRecord::Base)
  @connectable = connectable
end

Public Instance Methods

create_materialized_view(name, sql_definition, no_data: false) click to toggle source

Creates a materialized view in the database

@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]

# File lib/scenic/adapters/postgres.rb, line 135
      def create_materialized_view(name, sql_definition, no_data: false)
        raise_unless_materialized_views_supported

        execute <<-SQL
  CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS
  #{sql_definition.rstrip.chomp(";")}
  #{"WITH NO DATA" if no_data};
        SQL
      end
create_view(name, sql_definition) click to toggle source

Creates a view in the database.

This is typically called in a migration via {Statements#create_view}.

@param name The name of the view to create @param sql_definition The SQL schema for the view.

@return [void]

# File lib/scenic/adapters/postgres.rb, line 60
def create_view(name, sql_definition)
  execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
end
drop_materialized_view(name) click to toggle source

Drops a materialized view in the database

This is typically called in a migration via {Statements#update_view}.

@param name The name of the materialized view to drop. @raise [MaterializedViewsNotSupportedError] if the version of Postgres

in use does not support materialized views.

@return [void]

# File lib/scenic/adapters/postgres.rb, line 181
def drop_materialized_view(name)
  raise_unless_materialized_views_supported
  execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};"
end
drop_view(name) click to toggle source

Drops the named view from the database

This is typically called in a migration via {Statements#drop_view}.

@param name The name of the view to drop

@return [void]

# File lib/scenic/adapters/postgres.rb, line 117
def drop_view(name)
  execute "DROP VIEW #{quote_table_name(name)};"
end
populated?(name) click to toggle source

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]

# File lib/scenic/adapters/postgres.rb, line 235
def populated?(name)
  raise_unless_materialized_views_supported

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

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

  if relations.count.positive?
    relations.first["relispopulated"].in?(["t", true])
  else
    false
  end
end
refresh_materialized_view(name, concurrently: false, cascade: false) click to toggle source

Refreshes a materialized view from its SQL schema.

This is typically called from application code via {Scenic.database}.

@param name The name of the materialized view to refresh. @param concurrently [Boolean] Whether the refreshs hould happen

concurrently or not. A concurrent refresh allows the view to be
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.

@raise [MaterializedViewsNotSupportedError] if the version of Postgres

in use does not support materialized views.

@raise [ConcurrentRefreshesNotSupportedError] when attempting a

concurrent refresh on version of Postgres that does not support
concurrent materialized view refreshes.

@example Non-concurrent refresh

Scenic.database.refresh_materialized_view(:search_results)

@example Concurrent refresh

Scenic.database.refresh_materialized_view(:posts, concurrently: true)

@return [void]

# File lib/scenic/adapters/postgres.rb, line 210
def refresh_materialized_view(name, concurrently: false, cascade: false)
  raise_unless_materialized_views_supported

  if cascade
    refresh_dependencies_for(name, concurrently: concurrently)
  end

  if concurrently
    raise_unless_concurrent_refresh_supported
    execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
  else
    execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
  end
end
replace_view(name, sql_definition) click to toggle source

Replaces a view in the database using ‘CREATE OR REPLACE VIEW`.

This results in a ‘CREATE OR REPLACE VIEW`. Most of the time the explicitness of the two step process used in {#update_view} is preferred to `CREATE OR REPLACE VIEW` because the former ensures that the view you are trying to update did, in fact, already exist. Additionally, `CREATE OR REPLACE VIEW` is allowed only to add new columns to the end of an existing view schema. Existing columns cannot be re-ordered, removed, or have their types changed. Drop and create overcomes this limitation as well.

However, when there is a tangled dependency tree ‘CREATE OR REPLACE VIEW` can be preferable.

This is typically called in a migration via {Statements#replace_view}.

@param name The name of the view to update @param sql_definition The SQL schema for the updated view.

@return [void]

# File lib/scenic/adapters/postgres.rb, line 106
def replace_view(name, sql_definition)
  execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} AS #{sql_definition};"
end
update_materialized_view(name, sql_definition, no_data: false) click to toggle source

Updates a materialized view in the database.

Drops and recreates the materialized view. Attempts to maintain all previously existing and still applicable indexes on the materialized view after the view is recreated.

This is typically called in a migration via {Statements#update_view}.

@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]

# File lib/scenic/adapters/postgres.rb, line 163
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, no_data: no_data)
  end
end
update_view(name, sql_definition) click to toggle source

Updates a view in the database.

This results in a {#drop_view} followed by a {#create_view}. The explicitness of that two step process is preferred to ‘CREATE OR REPLACE VIEW` because the former ensures that the view you are trying to update did, in fact, already exist. Additionally, `CREATE OR REPLACE VIEW` is allowed only to add new columns to the end of an existing view schema. Existing columns cannot be re-ordered, removed, or have their types changed. Drop and create overcomes this limitation as well.

This is typically called in a migration via {Statements#update_view}.

@param name The name of the view to update @param sql_definition The SQL schema for the updated view.

@return [void]

# File lib/scenic/adapters/postgres.rb, line 80
def update_view(name, sql_definition)
  drop_view(name)
  create_view(name, sql_definition)
end
views() click to toggle source

Returns an array of views in the database.

This collection of views is used by the [Scenic::SchemaDumper] to populate the ‘schema.rb` file.

@return [Array<Scenic::View>]

# File lib/scenic/adapters/postgres.rb, line 48
def views
  Views.new(connection).all
end

Private Instance Methods

connection() click to toggle source
# File lib/scenic/adapters/postgres.rb, line 255
def connection
  Connection.new(connectable.connection)
end
raise_unless_concurrent_refresh_supported() click to toggle source
# File lib/scenic/adapters/postgres.rb, line 265
def raise_unless_concurrent_refresh_supported
  unless connection.supports_concurrent_refreshes?
    raise ConcurrentRefreshesNotSupportedError
  end
end
raise_unless_materialized_views_supported() click to toggle source
# File lib/scenic/adapters/postgres.rb, line 259
def raise_unless_materialized_views_supported
  unless connection.supports_materialized_views?
    raise MaterializedViewsNotSupportedError
  end
end
refresh_dependencies_for(name, concurrently: false) click to toggle source
# File lib/scenic/adapters/postgres.rb, line 271
def refresh_dependencies_for(name, concurrently: false)
  Scenic::Adapters::Postgres::RefreshDependencies.call(
    name,
    self,
    connection,
    concurrently: concurrently
  )
end