scenic 1.0.0 → 1.1.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/.gitignore +1 -0
- data/.travis.yml +14 -4
- data/.yardopts +0 -1
- data/Appraisals +25 -0
- data/CONTRIBUTING.md +5 -4
- data/LICENSE.txt +1 -1
- data/NEWS.md +20 -0
- data/README.md +68 -28
- data/bin/appraisal +16 -0
- data/bin/setup +5 -0
- data/bin/yard +16 -0
- data/gemfiles/rails40.gemfile +8 -0
- data/gemfiles/rails41.gemfile +8 -0
- data/gemfiles/rails42.gemfile +8 -0
- data/gemfiles/rails50.gemfile +14 -0
- data/lib/generators/scenic/generators.rb +1 -0
- data/lib/generators/scenic/model/templates/model.erb +1 -1
- data/lib/scenic.rb +1 -0
- data/lib/scenic/adapters/postgres.rb +140 -33
- data/lib/scenic/adapters/postgres/connection.rb +57 -0
- data/lib/scenic/adapters/postgres/errors.rb +26 -0
- data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
- data/lib/scenic/adapters/postgres/indexes.rb +53 -0
- data/lib/scenic/adapters/postgres/views.rb +51 -0
- data/lib/scenic/configuration.rb +2 -2
- data/lib/scenic/index.rb +36 -0
- data/lib/scenic/schema_dumper.rb +1 -1
- data/lib/scenic/statements.rb +7 -13
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +0 -3
- data/scenic.gemspec +4 -1
- data/spec/dummy/config/application.rb +3 -0
- data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
- data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
- data/spec/scenic/adapters/postgres_spec.rb +84 -26
- data/spec/scenic/statements_spec.rb +14 -15
- data/spec/smoke +16 -3
- data/spec/spec_helper.rb +4 -0
- metadata +64 -8
- data/spec/dummy/config/environments/development.rb +0 -6
- data/spec/dummy/config/environments/test.rb +0 -5
@@ -0,0 +1,57 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Decorates an ActiveRecord connection with methods that help determine
|
5
|
+
# the connections capabilities.
|
6
|
+
#
|
7
|
+
# Every attempt is made to use the versions of these methods defined by
|
8
|
+
# Rails where they are available and public before falling back to our own
|
9
|
+
# implementations for older Rails versions.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class Connection < SimpleDelegator
|
13
|
+
# True if the connection supports materialized views.
|
14
|
+
#
|
15
|
+
# Delegates to the method of the same name if it is already defined on
|
16
|
+
# the connection. This is the case for Rails 4.2 or higher.
|
17
|
+
#
|
18
|
+
# @return [Boolean]
|
19
|
+
def supports_materialized_views?
|
20
|
+
if undecorated_connection.respond_to?(:supports_materialized_views?)
|
21
|
+
super
|
22
|
+
else
|
23
|
+
postgresql_version >= 90300
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# True if the connection supports concurrent refreshes of materialized
|
28
|
+
# views.
|
29
|
+
#
|
30
|
+
# @return [Boolean]
|
31
|
+
def supports_concurrent_refreshes?
|
32
|
+
postgresql_version >= 90400
|
33
|
+
end
|
34
|
+
|
35
|
+
# An integer representing the version of Postgres we're connected to.
|
36
|
+
#
|
37
|
+
# postgresql_version is public in Rails 5, but protected in earlier
|
38
|
+
# versions.
|
39
|
+
#
|
40
|
+
# @return [Integer]
|
41
|
+
def postgresql_version
|
42
|
+
if undecorated_connection.respond_to?(:postgresql_version)
|
43
|
+
super
|
44
|
+
else
|
45
|
+
undecorated_connection.send(:postgresql_version)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def undecorated_connection
|
52
|
+
__getobj__
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Raised when a materialized view operation is attempted on a database
|
5
|
+
# version that does not support materialized views.
|
6
|
+
#
|
7
|
+
# Materialized views are supported on Postgres 9.3 or newer.
|
8
|
+
class MaterializedViewsNotSupportedError < StandardError
|
9
|
+
def initialize
|
10
|
+
super("Materialized views require Postgres 9.3 or newer")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when attempting a concurrent materialized view refresh on a
|
15
|
+
# database version that does not support that.
|
16
|
+
#
|
17
|
+
# Concurrent materialized view refreshes are supported on Postgres 9.4 or
|
18
|
+
# newer.
|
19
|
+
class ConcurrentRefreshesNotSupportedError < StandardError
|
20
|
+
def initialize
|
21
|
+
super("Concurrent materialized view refreshes require Postgres 9.4 or newer")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Updatine a materialized view causes the view to be dropped and
|
5
|
+
# recreated. This causes any associated indexes to be dropped as well.
|
6
|
+
# This object can be used to capture the existing indexes before the drop
|
7
|
+
# and then reapply appropriate indexes following the create.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class IndexReapplication
|
11
|
+
# Creates the index reapplication object.
|
12
|
+
#
|
13
|
+
# @param connection [Connection] The connection to execute SQL against.
|
14
|
+
# @param speaker [#say] (ActiveRecord::Migration) The object used for
|
15
|
+
# logging the results of reapplying indexes.
|
16
|
+
def initialize(connection:, speaker: ActiveRecord::Migration)
|
17
|
+
@connection = connection
|
18
|
+
@speaker = speaker
|
19
|
+
end
|
20
|
+
|
21
|
+
# Caches indexes on the provided object before executing the block and
|
22
|
+
# then reapplying the indexes. Each recreated or skipped index is
|
23
|
+
# announced to STDOUT by default. This can be overridden in the
|
24
|
+
# constructor.
|
25
|
+
#
|
26
|
+
# @param name The name of the object we are reapplying indexes on.
|
27
|
+
# @yield Operations to perform before reapplying indexes.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def on(name)
|
31
|
+
indexes = Indexes.new(connection: connection).on(name)
|
32
|
+
|
33
|
+
yield
|
34
|
+
|
35
|
+
indexes.each(&method(:try_index_create))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :connection, :speaker
|
41
|
+
|
42
|
+
def try_index_create(index)
|
43
|
+
success = with_savepoint(index.index_name) do
|
44
|
+
connection.execute(index.definition)
|
45
|
+
end
|
46
|
+
|
47
|
+
if success
|
48
|
+
say "index '#{index.index_name}' on '#{index.object_name}' has been recreated"
|
49
|
+
else
|
50
|
+
say "index '#{index.index_name}' on '#{index.object_name}' is no longer valid and has been dropped."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def with_savepoint(name)
|
55
|
+
connection.execute("SAVEPOINT #{name}")
|
56
|
+
yield
|
57
|
+
connection.execute("RELEASE SAVEPOINT #{name}")
|
58
|
+
true
|
59
|
+
rescue
|
60
|
+
connection.execute("ROLLBACK TO SAVEPOINT #{name}")
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def say(message)
|
65
|
+
subitem = true
|
66
|
+
speaker.say(message, subitem)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Fetches indexes on objects from the Postgres connection.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
class Indexes
|
8
|
+
def initialize(connection:)
|
9
|
+
@connection = connection
|
10
|
+
end
|
11
|
+
|
12
|
+
# Indexes on the provided object.
|
13
|
+
#
|
14
|
+
# @param name [String] The name of the object we want indexes from.
|
15
|
+
# @return [Array<Scenic::Index>]
|
16
|
+
def on(name)
|
17
|
+
indexes_on(name).map(&method(:index_from_database))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :connection
|
23
|
+
delegate :quote_table_name, to: :connection
|
24
|
+
|
25
|
+
def indexes_on(name)
|
26
|
+
connection.execute(<<-SQL)
|
27
|
+
SELECT
|
28
|
+
t.relname as object_name,
|
29
|
+
i.relname as index_name,
|
30
|
+
pg_get_indexdef(d.indexrelid) AS definition
|
31
|
+
FROM pg_class t
|
32
|
+
INNER JOIN pg_index d ON t.oid = d.indrelid
|
33
|
+
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
34
|
+
LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
|
35
|
+
WHERE i.relkind = 'i'
|
36
|
+
AND d.indisprimary = 'f'
|
37
|
+
AND t.relname = '#{quote_table_name(name)}'
|
38
|
+
AND n.nspname = ANY (current_schemas(false))
|
39
|
+
ORDER BY i.relname
|
40
|
+
SQL
|
41
|
+
end
|
42
|
+
|
43
|
+
def index_from_database(result)
|
44
|
+
Scenic::Index.new(
|
45
|
+
object_name: result["object_name"],
|
46
|
+
index_name: result["index_name"],
|
47
|
+
definition: result["definition"],
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Fetches defined views from the postgres connection.
|
5
|
+
# @api private
|
6
|
+
class Views
|
7
|
+
def initialize(connection)
|
8
|
+
@connection = connection
|
9
|
+
end
|
10
|
+
|
11
|
+
# All of the views that this connection has defined.
|
12
|
+
#
|
13
|
+
# This will include materialized views if those are supported by the
|
14
|
+
# connection.
|
15
|
+
#
|
16
|
+
# @return [Array<Scenic::View>]
|
17
|
+
def all
|
18
|
+
views_from_postgres.map(&method(:to_scenic_view))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :connection
|
24
|
+
|
25
|
+
def views_from_postgres
|
26
|
+
connection.execute(<<-SQL)
|
27
|
+
SELECT
|
28
|
+
c.relname as viewname,
|
29
|
+
pg_get_viewdef(c.oid) AS definition,
|
30
|
+
c.relkind AS kind
|
31
|
+
FROM pg_class c
|
32
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
33
|
+
WHERE
|
34
|
+
c.relkind IN ('m', 'v')
|
35
|
+
AND c.relname NOT IN (SELECT extname FROM pg_extension)
|
36
|
+
AND n.nspname = ANY (current_schemas(false))
|
37
|
+
ORDER BY c.oid
|
38
|
+
SQL
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_scenic_view(result)
|
42
|
+
Scenic::View.new(
|
43
|
+
name: result["viewname"],
|
44
|
+
definition: result["definition"].strip,
|
45
|
+
materialized: result["kind"] == "m",
|
46
|
+
)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/scenic/configuration.rb
CHANGED
@@ -2,7 +2,7 @@ module Scenic
|
|
2
2
|
class Configuration
|
3
3
|
# The Scenic database adapter instance to use when executing SQL.
|
4
4
|
#
|
5
|
-
# Defualts to an instance of
|
5
|
+
# Defualts to an instance of {Adapters::Postgres}
|
6
6
|
# @return Scenic adapter
|
7
7
|
attr_accessor :database
|
8
8
|
|
@@ -28,7 +28,7 @@ module Scenic
|
|
28
28
|
# @yieldparam [Scenic::Configuration] config current Scenic config
|
29
29
|
# ```
|
30
30
|
# Scenic.configure do |config|
|
31
|
-
# config.database = Scenic::Adapters::Postgres
|
31
|
+
# config.database = Scenic::Adapters::Postgres.new
|
32
32
|
# end
|
33
33
|
# ```
|
34
34
|
def self.configure
|
data/lib/scenic/index.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
module Scenic
|
2
|
+
# The in-memory representation of a database index.
|
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
|
9
|
+
class Index
|
10
|
+
# The name of the object that has the index
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :object_name
|
13
|
+
|
14
|
+
# The name of the index
|
15
|
+
# @return [String]
|
16
|
+
attr_reader :index_name
|
17
|
+
|
18
|
+
# The SQL statement that defines the index
|
19
|
+
# @return [String]
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# "CREATE INDEX index_users_on_email ON users USING btree (email)"
|
23
|
+
attr_reader :definition
|
24
|
+
|
25
|
+
# Returns a new instance of Index
|
26
|
+
#
|
27
|
+
# @param object_name [String] The name of the object that has the index
|
28
|
+
# @param index_name [String] The name of the index
|
29
|
+
# @param definition [String] The SQL statements that defined the index
|
30
|
+
def initialize(object_name:, index_name:, definition:)
|
31
|
+
@object_name = object_name
|
32
|
+
@index_name = index_name
|
33
|
+
@definition = definition
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/scenic/schema_dumper.rb
CHANGED
data/lib/scenic/statements.rb
CHANGED
@@ -68,10 +68,8 @@ module Scenic
|
|
68
68
|
# @param version [Fixnum] The version number of the view.
|
69
69
|
# @param revert_to_version [Fixnum] The version number to rollback to on
|
70
70
|
# `rake db rollback`
|
71
|
-
# @param materialized [Boolean]
|
72
|
-
#
|
73
|
-
# explicitly use {#drop_view} followed by {#create_view} and recreate
|
74
|
-
# applicable indexes. Setting this to `true` will raise an error.
|
71
|
+
# @param materialized [Boolean] True if updating a materialized view.
|
72
|
+
# Defaults to false.
|
75
73
|
# @return The database response from executing the create statement.
|
76
74
|
#
|
77
75
|
# @example
|
@@ -82,17 +80,13 @@ module Scenic
|
|
82
80
|
raise ArgumentError, "version is required"
|
83
81
|
end
|
84
82
|
|
83
|
+
sql_definition = definition(name, version)
|
84
|
+
|
85
85
|
if materialized
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
"any previously-existing indexes."
|
86
|
+
Scenic.database.update_materialized_view(name, sql_definition)
|
87
|
+
else
|
88
|
+
Scenic.database.update_view(name, sql_definition)
|
90
89
|
end
|
91
|
-
|
92
|
-
drop_view name,
|
93
|
-
revert_to_version: revert_to_version,
|
94
|
-
materialized: materialized
|
95
|
-
create_view(name, version: version, materialized: materialized)
|
96
90
|
end
|
97
91
|
|
98
92
|
private
|
data/lib/scenic/version.rb
CHANGED
data/lib/scenic/view.rb
CHANGED
data/scenic.gemspec
CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.test_files = spec.files.grep(%r{^spec/})
|
21
21
|
spec.require_paths = ['lib']
|
22
22
|
|
23
|
+
spec.add_development_dependency 'appraisal'
|
23
24
|
spec.add_development_dependency 'bundler', '>= 1.5'
|
24
25
|
spec.add_development_dependency 'database_cleaner'
|
25
26
|
spec.add_development_dependency 'rake'
|
@@ -27,9 +28,11 @@ Gem::Specification.new do |spec|
|
|
27
28
|
spec.add_development_dependency 'pg'
|
28
29
|
spec.add_development_dependency 'pry'
|
29
30
|
spec.add_development_dependency 'ammeter', '>= 1.1.3'
|
31
|
+
spec.add_development_dependency 'yard'
|
32
|
+
spec.add_development_dependency 'redcarpet'
|
30
33
|
|
31
34
|
spec.add_dependency 'activerecord', '>= 4.0.0'
|
32
35
|
spec.add_dependency 'railties', '>= 4.0.0'
|
33
36
|
|
34
|
-
spec.required_ruby_version = '~> 2.
|
37
|
+
spec.required_ruby_version = '~> 2.1'
|
35
38
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Scenic
|
4
|
+
module Adapters
|
5
|
+
describe Postgres::Connection do
|
6
|
+
describe "supports_materialized_views?" do
|
7
|
+
context "supports_materialized_views? was defined on connection" do
|
8
|
+
it "uses the previously defined version" do
|
9
|
+
base_response = double("response from base connection")
|
10
|
+
base_connection = double(
|
11
|
+
"Connection",
|
12
|
+
supports_materialized_views?: base_response,
|
13
|
+
)
|
14
|
+
|
15
|
+
connection = Postgres::Connection.new(base_connection)
|
16
|
+
|
17
|
+
expect(connection.supports_materialized_views?).to be base_response
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "supports_materialized_views? is not already defined" do
|
22
|
+
it "is true if postgres version is at least than 9.3.0" do
|
23
|
+
base_connection = double("Connection", postgresql_version: 90300)
|
24
|
+
|
25
|
+
connection = Postgres::Connection.new(base_connection)
|
26
|
+
|
27
|
+
expect(connection.supports_materialized_views?).to be true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is false if postgres version is less than 9.3.0" do
|
31
|
+
base_connection = double("Connection", postgresql_version: 90299)
|
32
|
+
|
33
|
+
connection = Postgres::Connection.new(base_connection)
|
34
|
+
|
35
|
+
expect(connection.supports_materialized_views?).to be false
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#postgresql_version" do
|
41
|
+
it "uses the public method on the provided connection if defined" do
|
42
|
+
base_connection = Class.new do
|
43
|
+
def postgresql_version
|
44
|
+
123
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
connection = Postgres::Connection.new(base_connection.new)
|
49
|
+
|
50
|
+
expect(connection.postgresql_version).to eq 123
|
51
|
+
end
|
52
|
+
|
53
|
+
it "uses the protected method if the underlying method is not public" do
|
54
|
+
base_connection = Class.new do
|
55
|
+
protected
|
56
|
+
|
57
|
+
def postgresql_version
|
58
|
+
123
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
connection = Postgres::Connection.new(base_connection.new)
|
63
|
+
|
64
|
+
expect(connection.postgresql_version).to eq 123
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#supports_concurrent_refresh" do
|
69
|
+
it "is true if postgres version is at least 9.4.0" do
|
70
|
+
base_connection = double("Connection", postgresql_version: 90400)
|
71
|
+
|
72
|
+
connection = Postgres::Connection.new(base_connection)
|
73
|
+
|
74
|
+
expect(connection.supports_concurrent_refreshes?).to be true
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|