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