scenic 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|