scenic 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +6 -4
- data/.yardopts +5 -0
- data/CONTRIBUTING.md +25 -0
- data/LICENSE.txt +1 -1
- data/NEWS.md +61 -9
- data/README.md +98 -62
- data/Rakefile +5 -0
- data/bin/setup +7 -0
- data/lib/generators/scenic/generators.rb +11 -0
- data/lib/generators/scenic/materializable.rb +22 -0
- data/lib/generators/scenic/model/USAGE +2 -0
- data/lib/generators/scenic/model/model_generator.rb +31 -4
- data/lib/generators/scenic/model/templates/model.erb +3 -2
- data/lib/generators/scenic/view/USAGE +2 -0
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +7 -0
- data/lib/generators/scenic/view/view_generator.rb +5 -2
- data/lib/scenic.rb +13 -2
- data/lib/scenic/adapters/postgres.rb +74 -7
- data/lib/scenic/command_recorder.rb +1 -0
- data/lib/scenic/command_recorder/statement_arguments.rb +1 -0
- data/lib/scenic/configuration.rb +37 -0
- data/lib/scenic/definition.rb +2 -1
- data/lib/scenic/railtie.rb +4 -0
- data/lib/scenic/schema_dumper.rb +3 -6
- data/lib/scenic/statements.rb +61 -41
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +41 -9
- data/scenic.gemspec +2 -2
- data/spec/dummy/db/views/.keep +0 -0
- data/spec/generators/scenic/model/model_generator_spec.rb +11 -0
- data/spec/generators/scenic/view/view_generator_spec.rb +13 -0
- data/spec/scenic/adapters/postgres_spec.rb +57 -14
- data/spec/scenic/configuration_spec.rb +27 -0
- data/spec/scenic/statements_spec.rb +33 -2
- data/spec/smoke +92 -67
- data/spec/support/generator_spec_setup.rb +1 -0
- metadata +19 -10
@@ -1,2 +1,3 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
def self.refresh
|
2
|
+
Scenic.database.refresh_materialized_view(table_name)
|
3
|
+
end
|
@@ -5,6 +5,8 @@ Description:
|
|
5
5
|
If a view of the given name already exists, create a new version of the view
|
6
6
|
and a migration to replace the old version with the new.
|
7
7
|
|
8
|
+
To create a materialized view, pass the '--materialized' option.
|
9
|
+
|
8
10
|
Examples:
|
9
11
|
rails generate scenic:view searches
|
10
12
|
|
@@ -1,5 +1,12 @@
|
|
1
1
|
class <%= migration_class_name %> < ActiveRecord::Migration
|
2
2
|
def change
|
3
|
+
<%- if materialized? -%>
|
4
|
+
update_view :<%= plural_name %>,
|
5
|
+
version: <%= version %>,
|
6
|
+
revert_to_version: <%= previous_version %>,
|
7
|
+
materialized: true
|
8
|
+
<%- else -%>
|
3
9
|
update_view :<%= plural_name %>, version: <%= version %>, revert_to_version: <%= previous_version %>
|
10
|
+
<%- end -%>
|
4
11
|
end
|
5
12
|
end
|
@@ -1,10 +1,13 @@
|
|
1
1
|
require "rails/generators"
|
2
2
|
require "rails/generators/active_record"
|
3
|
+
require "generators/scenic/materializable"
|
3
4
|
|
4
5
|
module Scenic
|
5
6
|
module Generators
|
7
|
+
# @api private
|
6
8
|
class ViewGenerator < Rails::Generators::NamedBase
|
7
9
|
include Rails::Generators::Migration
|
10
|
+
include Scenic::Generators::Materializable
|
8
11
|
source_root File.expand_path("../templates", __FILE__)
|
9
12
|
|
10
13
|
def create_views_directory
|
@@ -25,12 +28,12 @@ module Scenic
|
|
25
28
|
if creating_new_view? || destroying_initial_view?
|
26
29
|
migration_template(
|
27
30
|
"db/migrate/create_view.erb",
|
28
|
-
"db/migrate/create_#{plural_file_name}.rb"
|
31
|
+
"db/migrate/create_#{plural_file_name}.rb",
|
29
32
|
)
|
30
33
|
else
|
31
34
|
migration_template(
|
32
35
|
"db/migrate/update_view.erb",
|
33
|
-
"db/migrate/update_#{plural_file_name}_to_version_#{version}.rb"
|
36
|
+
"db/migrate/update_#{plural_file_name}_to_version_#{version}.rb",
|
34
37
|
)
|
35
38
|
end
|
36
39
|
end
|
data/lib/scenic.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require "scenic/configuration"
|
1
2
|
require "scenic/adapters/postgres"
|
2
3
|
require "scenic/command_recorder"
|
3
4
|
require "scenic/definition"
|
@@ -7,14 +8,24 @@ require "scenic/statements"
|
|
7
8
|
require "scenic/version"
|
8
9
|
require "scenic/view"
|
9
10
|
|
11
|
+
# Scenic adds methods `ActiveRecord::Migration` to create and manage database
|
12
|
+
# views in Rails applications.
|
10
13
|
module Scenic
|
14
|
+
# Hooks Scenic into Rails.
|
15
|
+
#
|
16
|
+
# Enables scenic migration methods, migration reversability, and `schema.rb`
|
17
|
+
# dumping.
|
11
18
|
def self.load
|
12
19
|
ActiveRecord::ConnectionAdapters::AbstractAdapter.include Scenic::Statements
|
13
20
|
ActiveRecord::Migration::CommandRecorder.include Scenic::CommandRecorder
|
14
|
-
ActiveRecord::SchemaDumper.
|
21
|
+
ActiveRecord::SchemaDumper.prepend Scenic::SchemaDumper
|
15
22
|
end
|
16
23
|
|
24
|
+
# The current database adapter used by Scenic.
|
25
|
+
#
|
26
|
+
# This defaults to {Adapters::Postgres} but can be overridden
|
27
|
+
# via {Configuration}.
|
17
28
|
def self.database
|
18
|
-
|
29
|
+
configuration.database
|
19
30
|
end
|
20
31
|
end
|
@@ -1,28 +1,95 @@
|
|
1
1
|
module Scenic
|
2
|
+
# Scenic database adapters.
|
3
|
+
#
|
4
|
+
# Scenic ships with a Postgres adapter only but can be extended with
|
5
|
+
# additional adapters. The {Adapters::Postgres} adapter provides the
|
6
|
+
# interface.
|
2
7
|
module Adapters
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
8
|
+
# An adapter for managing Postgres views.
|
9
|
+
#
|
10
|
+
# **This object is used internally by adapters and the schema dumper and is
|
11
|
+
# not intended to be used by application code. It is documented here for
|
12
|
+
# use by adapter gems.**
|
13
|
+
#
|
14
|
+
# For methods usable in migrations see {Statements}.
|
15
|
+
#
|
16
|
+
# @api extension
|
17
|
+
class Postgres
|
18
|
+
# Returns an array of views in the database.
|
19
|
+
#
|
20
|
+
# This collection of views is used by the [Scenic::SchemaDumper] to
|
21
|
+
# populate the `schema.rb` file.
|
22
|
+
#
|
23
|
+
# @return [Array<Scenic::View>]
|
24
|
+
def views
|
25
|
+
execute(<<-SQL).map { |result| view_from_database(result) }
|
26
|
+
SELECT viewname, definition, FALSE AS materialized
|
7
27
|
FROM pg_views
|
8
28
|
WHERE schemaname = ANY (current_schemas(false))
|
9
29
|
AND viewname NOT IN (SELECT extname FROM pg_extension)
|
30
|
+
UNION
|
31
|
+
SELECT matviewname AS viewname, definition, TRUE AS materialized
|
32
|
+
FROM pg_matviews
|
33
|
+
WHERE schemaname = ANY (current_schemas(false))
|
34
|
+
ORDER BY viewname
|
10
35
|
SQL
|
11
36
|
end
|
12
37
|
|
13
|
-
|
38
|
+
# Creates a view in the database.
|
39
|
+
#
|
40
|
+
# @param name The name of the view to create
|
41
|
+
# @param sql_definition the SQL schema for the view.
|
42
|
+
# @return [void]
|
43
|
+
def create_view(name, sql_definition)
|
14
44
|
execute "CREATE VIEW #{name} AS #{sql_definition};"
|
15
45
|
end
|
16
46
|
|
17
|
-
|
47
|
+
# Drops the named view from the database
|
48
|
+
#
|
49
|
+
# @param name The name of the view to drop
|
50
|
+
# @return [void]
|
51
|
+
def drop_view(name)
|
18
52
|
execute "DROP VIEW #{name};"
|
19
53
|
end
|
20
54
|
|
55
|
+
# Creates a materialized view in the database
|
56
|
+
#
|
57
|
+
# @param name The name of the materialized view to create
|
58
|
+
# @param sql_definition The SQL schema that defines the materialized view.
|
59
|
+
# @return [void]
|
60
|
+
def create_materialized_view(name, sql_definition)
|
61
|
+
execute "CREATE MATERIALIZED VIEW #{name} AS #{sql_definition};"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Drops a materialized view in the database
|
65
|
+
#
|
66
|
+
# @param name The name of the materialized view to drop.
|
67
|
+
# @return [void]
|
68
|
+
def drop_materialized_view(name)
|
69
|
+
execute "DROP MATERIALIZED VIEW #{name};"
|
70
|
+
end
|
71
|
+
|
72
|
+
# Refreshes a materialized view from its SQL schema.
|
73
|
+
#
|
74
|
+
# @param name The name of the materialized view to refresh..
|
75
|
+
# @return [void]
|
76
|
+
def refresh_materialized_view(name)
|
77
|
+
execute "REFRESH MATERIALIZED VIEW #{name};"
|
78
|
+
end
|
79
|
+
|
21
80
|
private
|
22
81
|
|
23
|
-
def
|
82
|
+
def execute(sql, base = ActiveRecord::Base)
|
24
83
|
base.connection.execute sql
|
25
84
|
end
|
85
|
+
|
86
|
+
def view_from_database(result)
|
87
|
+
Scenic::View.new(
|
88
|
+
name: result["viewname"],
|
89
|
+
definition: result["definition"].strip,
|
90
|
+
materialized: result["materialized"] == "t",
|
91
|
+
)
|
92
|
+
end
|
26
93
|
end
|
27
94
|
end
|
28
95
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Scenic
|
2
|
+
class Configuration
|
3
|
+
# The Scenic database adapter instance to use when executing SQL.
|
4
|
+
#
|
5
|
+
# Defualts to an instance of [Scenic::Adapters::Postgres]
|
6
|
+
# @return Scenic adapter
|
7
|
+
attr_accessor :database
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@database = Scenic::Adapters::Postgres.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Scenic::Configuration] Scenic's current configuration
|
15
|
+
def self.configuration
|
16
|
+
@configuration ||= Configuration.new
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set Scenic's configuration
|
20
|
+
#
|
21
|
+
# @param config [Scenic::Configuration]
|
22
|
+
def self.configuration=(config)
|
23
|
+
@configuration = config
|
24
|
+
end
|
25
|
+
|
26
|
+
# Modify Scenic's current configuration
|
27
|
+
#
|
28
|
+
# @yieldparam [Scenic::Configuration] config current Scenic config
|
29
|
+
# ```
|
30
|
+
# Scenic.configure do |config|
|
31
|
+
# config.database = Scenic::Adapters::Postgres
|
32
|
+
# end
|
33
|
+
# ```
|
34
|
+
def self.configure
|
35
|
+
yield configuration
|
36
|
+
end
|
37
|
+
end
|
data/lib/scenic/definition.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module Scenic
|
2
|
+
# @api private
|
2
3
|
class Definition
|
3
4
|
def initialize(name, version)
|
4
5
|
@name = name
|
@@ -8,7 +9,7 @@ module Scenic
|
|
8
9
|
def to_sql
|
9
10
|
File.read(full_path).tap do |content|
|
10
11
|
if content.empty?
|
11
|
-
raise "Define view query in #{
|
12
|
+
raise "Define view query in #{path} before migrating."
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
data/lib/scenic/railtie.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
require "rails/railtie"
|
2
2
|
|
3
3
|
module Scenic
|
4
|
+
# Automatically initializes Scenic in the context of a Rails application when
|
5
|
+
# ActiveRecord is loaded.
|
6
|
+
#
|
7
|
+
# @see Scenic.load
|
4
8
|
class Railtie < Rails::Railtie
|
5
9
|
initializer "scenic.load" do
|
6
10
|
ActiveSupport.on_load :active_record do
|
data/lib/scenic/schema_dumper.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
1
|
require "rails"
|
2
2
|
|
3
3
|
module Scenic
|
4
|
+
# @api private
|
4
5
|
module SchemaDumper
|
5
|
-
|
6
|
-
|
7
|
-
included { alias_method_chain :tables, :views }
|
8
|
-
|
9
|
-
def tables_with_views(stream)
|
10
|
-
tables_without_views(stream)
|
6
|
+
def tables(stream)
|
7
|
+
super
|
11
8
|
views(stream)
|
12
9
|
end
|
13
10
|
|
data/lib/scenic/statements.rb
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
module Scenic
|
2
|
+
# Methods that are made available in migrations for managing Scenic views.
|
2
3
|
module Statements
|
3
|
-
#
|
4
|
+
# Create a new database view.
|
4
5
|
#
|
5
|
-
# name
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# Examples
|
6
|
+
# @param name [String, Symbol] The name of the database view.
|
7
|
+
# @param version [Fixnum] The version number of the view, used to find the
|
8
|
+
# definition file in `db/views`. This defaults to `1` if not provided.
|
9
|
+
# @param sql_definition [String] The SQL query for the view schema. If both
|
10
|
+
# `sql_defintiion` and `version` are provided, `sql_definition` takes
|
11
|
+
# prescedence.
|
12
|
+
# @param materialized [Boolean] Set to true to create a materialized view.
|
13
|
+
# Defaults to false.
|
14
|
+
# @return The database response from executing the create statement.
|
16
15
|
#
|
16
|
+
# @example Create from `db/views/searches_v02.sql`
|
17
17
|
# create_view(:searches, version: 2)
|
18
18
|
#
|
19
|
+
# @example Create from provided SQL string
|
19
20
|
# create_view(:active_users, sql_definition: <<-SQL)
|
20
21
|
# SELECT * FROM users WHERE users.active = 't'
|
21
22
|
# SQL
|
22
23
|
#
|
23
|
-
|
24
|
-
def create_view(name, version: 1, sql_definition: nil)
|
24
|
+
def create_view(name, version: 1, sql_definition: nil, materialized: false)
|
25
25
|
if version.blank? && sql_definition.nil?
|
26
26
|
raise(
|
27
27
|
ArgumentError,
|
@@ -31,48 +31,68 @@ module Scenic
|
|
31
31
|
|
32
32
|
sql_definition ||= definition(name, version)
|
33
33
|
|
34
|
-
|
34
|
+
if materialized
|
35
|
+
Scenic.database.create_materialized_view(name, sql_definition)
|
36
|
+
else
|
37
|
+
Scenic.database.create_view(name, sql_definition)
|
38
|
+
end
|
35
39
|
end
|
36
40
|
|
37
|
-
#
|
38
|
-
#
|
39
|
-
# name - A string or symbol containing the singular name of the database
|
40
|
-
# view. Must be an existing view.
|
41
|
-
# revert_to_version - Used to revert the drop_view command in the
|
42
|
-
# db:rollback rake task, which would pass the version
|
43
|
-
# number to create_view. Usually the most recent
|
44
|
-
# version.
|
41
|
+
# Drop a database view by name.
|
45
42
|
#
|
46
|
-
#
|
43
|
+
# @param name [String, Symbol] The name of the database view.
|
44
|
+
# @param revert_to_version [Fixnum] Used to reverse the `drop_view` command
|
45
|
+
# on `rake db:rollback`. The provided version will be passed as the
|
46
|
+
# `version` argument to {#create_view}.
|
47
|
+
# @param materialized [Boolean] Set to true if dropping a meterialized view.
|
48
|
+
# defaults to false.
|
49
|
+
# @return The database response from executing the drop statement.
|
47
50
|
#
|
48
|
-
#
|
51
|
+
# @example Drop a view, rolling back to version 3 on rollback
|
52
|
+
# drop_view(:users_who_recently_logged_in, revert_to_version: 3)
|
49
53
|
#
|
50
|
-
|
51
|
-
|
52
|
-
|
54
|
+
def drop_view(name, revert_to_version: nil, materialized: false)
|
55
|
+
if materialized
|
56
|
+
Scenic.database.drop_materialized_view(name)
|
57
|
+
else
|
58
|
+
Scenic.database.drop_view(name)
|
59
|
+
end
|
53
60
|
end
|
54
61
|
|
55
|
-
#
|
56
|
-
# previous version then creating the new version.
|
62
|
+
# Update a database view to a new version.
|
57
63
|
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
# version - The version number of the view. See create_view for details.
|
61
|
-
# revert_to_version - The version to revert to for db:rollback. Usually the
|
62
|
-
# previous version. See drop_view for details.
|
64
|
+
# The existing view is dropped and recreated using the supplied `version`
|
65
|
+
# parameter.
|
63
66
|
#
|
64
|
-
#
|
67
|
+
# @param name [String, Symbol] The name of the database view.
|
68
|
+
# @param version [Fixnum] The version number of the view.
|
69
|
+
# @param revert_to_version [Fixnum] The version number to rollback to on
|
70
|
+
# `rake db rollback`
|
71
|
+
# @param materialized [Boolean] Must be false. Updating a meterialized view
|
72
|
+
# causes indexes on it to be dropped. For this reason you should
|
73
|
+
# explicitly use {#drop_view} followed by {#create_view} and recreate
|
74
|
+
# applicable indexes. Setting this to `true` will raise an error.
|
75
|
+
# @return The database response from executing the create statement.
|
65
76
|
#
|
66
|
-
#
|
77
|
+
# @example
|
78
|
+
# update_view :engagement_reports, version: 3, revert_to_version: 2
|
67
79
|
#
|
68
|
-
|
69
|
-
def update_view(name, version: nil, revert_to_version: nil)
|
80
|
+
def update_view(name, version: nil, revert_to_version: nil, materialized: false)
|
70
81
|
if version.blank?
|
71
82
|
raise ArgumentError, "version is required"
|
72
83
|
end
|
73
84
|
|
74
|
-
|
75
|
-
|
85
|
+
if materialized
|
86
|
+
raise ArgumentError, "Updating materialized views is not supported "\
|
87
|
+
"because it would cause any indexes to be dropped. Please use "\
|
88
|
+
"'drop_view' followed by 'create_view', being sure to also recreate "\
|
89
|
+
"any previously-existing indexes."
|
90
|
+
end
|
91
|
+
|
92
|
+
drop_view name,
|
93
|
+
revert_to_version: revert_to_version,
|
94
|
+
materialized: materialized
|
95
|
+
create_view(name, version: version, materialized: materialized)
|
76
96
|
end
|
77
97
|
|
78
98
|
private
|
data/lib/scenic/version.rb
CHANGED
data/lib/scenic/view.rb
CHANGED
@@ -1,24 +1,56 @@
|
|
1
1
|
module Scenic
|
2
|
+
# The in-memory representation of a view definition.
|
3
|
+
#
|
4
|
+
# **This object is used internally by adapters and the schema dumper and is
|
5
|
+
# not intended to be used by application code. It is documented here for
|
6
|
+
# use by adapter gems.**
|
7
|
+
#
|
8
|
+
# @api extension
|
2
9
|
class View
|
3
|
-
|
10
|
+
# The name of the view
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# The SQL schema for the query that defines the view
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# "SELECT name, email FROM users UNION SELECT name, email FROM contacts"
|
19
|
+
attr_reader :definition
|
20
|
+
|
21
|
+
# True if the view is materialized
|
22
|
+
# @return [Boolean]
|
23
|
+
attr_reader :materialized
|
24
|
+
|
25
|
+
# @api private
|
4
26
|
delegate :<=>, to: :name
|
5
27
|
|
6
|
-
|
7
|
-
|
8
|
-
|
28
|
+
# Returns a new instance of View.
|
29
|
+
#
|
30
|
+
# @param name [String] The name of the view.
|
31
|
+
# @param definition [String] The SQL for the query that defines the view.
|
32
|
+
# @param materialized [String] `true` if the view is materialized.
|
33
|
+
def initialize(name:, definition:, materialized:)
|
34
|
+
@name = name
|
35
|
+
@definition = definition
|
36
|
+
@materialized = materialized
|
9
37
|
end
|
10
38
|
|
39
|
+
# @api private
|
11
40
|
def ==(other)
|
12
41
|
name == other.name &&
|
13
|
-
definition == other.definition
|
42
|
+
definition == other.definition &&
|
43
|
+
materialized == other.materialized
|
14
44
|
end
|
15
45
|
|
46
|
+
# @api private
|
16
47
|
def to_schema
|
17
|
-
|
18
|
-
|
19
|
-
#{definition}
|
20
|
-
SQL
|
48
|
+
materialized_option = materialized ? "materialized: true, " : ""
|
49
|
+
<<-DEFINITION
|
21
50
|
|
51
|
+
create_view :#{name}, #{materialized_option} sql_definition: <<-\SQL
|
52
|
+
#{definition.indent(2)}
|
53
|
+
SQL
|
22
54
|
DEFINITION
|
23
55
|
end
|
24
56
|
end
|