scenic 1.0.0 → 1.2.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 +26 -0
- data/CONTRIBUTING.md +5 -4
- data/LICENSE.txt +1 -1
- data/NEWS.md +49 -0
- data/README.md +71 -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 +13 -0
- data/lib/generators/scenic/generators.rb +1 -0
- data/lib/generators/scenic/model/templates/model.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
- data/lib/generators/scenic/view/view_generator.rb +13 -1
- 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/adapters/postgres.rb +144 -33
- data/lib/scenic/configuration.rb +2 -2
- data/lib/scenic/index.rb +36 -0
- data/lib/scenic/schema_dumper.rb +12 -5
- data/lib/scenic/statements.rb +7 -13
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +1 -4
- data/lib/scenic.rb +1 -0
- data/scenic.gemspec +4 -1
- data/spec/dummy/config/application.rb +3 -0
- data/spec/generators/scenic/view/view_generator_spec.rb +12 -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 +88 -26
- data/spec/scenic/statements_spec.rb +14 -15
- data/spec/smoke +24 -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,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 = '#{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
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
require_relative "postgres/connection"
|
|
2
|
+
require_relative "postgres/errors"
|
|
3
|
+
require_relative "postgres/index_reapplication"
|
|
4
|
+
require_relative "postgres/indexes"
|
|
5
|
+
require_relative "postgres/views"
|
|
6
|
+
|
|
1
7
|
module Scenic
|
|
2
8
|
# Scenic database adapters.
|
|
3
9
|
#
|
|
@@ -7,14 +13,31 @@ module Scenic
|
|
|
7
13
|
module Adapters
|
|
8
14
|
# An adapter for managing Postgres views.
|
|
9
15
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
# For methods usable in migrations see {Statements}.
|
|
16
|
+
# These methods are used interally by Scenic and are not intended for direct
|
|
17
|
+
# use. Methods that alter database schema are intended to be called via
|
|
18
|
+
# {Statements}, while {#refresh_materialized_view} is called via
|
|
19
|
+
# {Scenic.database}.
|
|
15
20
|
#
|
|
16
|
-
#
|
|
21
|
+
# The methods are documented here for insight into specifics of how Scenic
|
|
22
|
+
# integrates with Postgres and the responsibilities of {Adapters}.
|
|
17
23
|
class Postgres
|
|
24
|
+
# Creates an instance of the Scenic Postgres adapter.
|
|
25
|
+
#
|
|
26
|
+
# This is the default adapter for Scenic. Configuring it via
|
|
27
|
+
# {Scenic.configure} is not required, but the example below shows how one
|
|
28
|
+
# would explicitly set it.
|
|
29
|
+
#
|
|
30
|
+
# @param [#connection] connectable An object that returns the connection
|
|
31
|
+
# for Scenic to use. Defaults to `ActiveRecord::Base`.
|
|
32
|
+
#
|
|
33
|
+
# @example
|
|
34
|
+
# Scenic.configure do |config|
|
|
35
|
+
# config.adapter = Scenic::Adapters::Postgres.new
|
|
36
|
+
# end
|
|
37
|
+
def initialize(connectable = ActiveRecord::Base)
|
|
38
|
+
@connectable = connectable
|
|
39
|
+
end
|
|
40
|
+
|
|
18
41
|
# Returns an array of views in the database.
|
|
19
42
|
#
|
|
20
43
|
# This collection of views is used by the [Scenic::SchemaDumper] to
|
|
@@ -22,73 +45,161 @@ module Scenic
|
|
|
22
45
|
#
|
|
23
46
|
# @return [Array<Scenic::View>]
|
|
24
47
|
def views
|
|
25
|
-
|
|
26
|
-
SELECT viewname, definition, FALSE AS materialized
|
|
27
|
-
FROM pg_views
|
|
28
|
-
WHERE schemaname = ANY (current_schemas(false))
|
|
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
|
|
35
|
-
SQL
|
|
48
|
+
Views.new(connection).all
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
# Creates a view in the database.
|
|
39
52
|
#
|
|
53
|
+
# This is typically called in a migration via {Statements#create_view}.
|
|
54
|
+
#
|
|
40
55
|
# @param name The name of the view to create
|
|
41
|
-
# @param sql_definition
|
|
56
|
+
# @param sql_definition The SQL schema for the view.
|
|
57
|
+
#
|
|
42
58
|
# @return [void]
|
|
43
59
|
def create_view(name, sql_definition)
|
|
44
|
-
execute "CREATE VIEW #{name} AS #{sql_definition};"
|
|
60
|
+
execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Updates a view in the database.
|
|
64
|
+
#
|
|
65
|
+
# This results in a {#drop_view} followed by a {#create_view}. The
|
|
66
|
+
# explicitness of that two step process is preferred to `CREATE OR
|
|
67
|
+
# REPLACE VIEW` because the former ensures that the view you are trying to
|
|
68
|
+
# update did, in fact, already exist. Additionally, `CREATE OR REPLACE
|
|
69
|
+
# VIEW` is allowed only to add new columns to the end of an existing
|
|
70
|
+
# view schema. Existing columns cannot be re-ordered, removed, or have
|
|
71
|
+
# their types changed. Drop and create overcomes this limitation as well.
|
|
72
|
+
#
|
|
73
|
+
# This is typically called in a migration via {Statements#update_view}.
|
|
74
|
+
#
|
|
75
|
+
# @param name The name of the view to update
|
|
76
|
+
# @param sql_definition The SQL schema for the updated view.
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
def update_view(name, sql_definition)
|
|
80
|
+
drop_view(name)
|
|
81
|
+
create_view(name, sql_definition)
|
|
45
82
|
end
|
|
46
83
|
|
|
47
84
|
# Drops the named view from the database
|
|
48
85
|
#
|
|
86
|
+
# This is typically called in a migration via {Statements#drop_view}.
|
|
87
|
+
#
|
|
49
88
|
# @param name The name of the view to drop
|
|
89
|
+
#
|
|
50
90
|
# @return [void]
|
|
51
91
|
def drop_view(name)
|
|
52
|
-
execute "DROP VIEW #{name};"
|
|
92
|
+
execute "DROP VIEW #{quote_table_name(name)};"
|
|
53
93
|
end
|
|
54
94
|
|
|
55
95
|
# Creates a materialized view in the database
|
|
56
96
|
#
|
|
57
97
|
# @param name The name of the materialized view to create
|
|
58
98
|
# @param sql_definition The SQL schema that defines the materialized view.
|
|
99
|
+
#
|
|
100
|
+
# This is typically called in a migration via {Statements#create_view}.
|
|
101
|
+
#
|
|
102
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
|
103
|
+
# in use does not support materialized views.
|
|
104
|
+
#
|
|
59
105
|
# @return [void]
|
|
60
106
|
def create_materialized_view(name, sql_definition)
|
|
61
|
-
|
|
107
|
+
raise_unless_materialized_views_supported
|
|
108
|
+
execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Updates a materialized view in the database.
|
|
112
|
+
#
|
|
113
|
+
# Drops and recreates the materialized view. Attempts to maintain all
|
|
114
|
+
# previously existing and still applicable indexes on the materialized
|
|
115
|
+
# view after the view is recreated.
|
|
116
|
+
#
|
|
117
|
+
# This is typically called in a migration via {Statements#update_view}.
|
|
118
|
+
#
|
|
119
|
+
# @param name The name of the view to update
|
|
120
|
+
# @param sql_definition The SQL schema for the updated view.
|
|
121
|
+
#
|
|
122
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
|
123
|
+
# in use does not support materialized views.
|
|
124
|
+
#
|
|
125
|
+
# @return [void]
|
|
126
|
+
def update_materialized_view(name, sql_definition)
|
|
127
|
+
raise_unless_materialized_views_supported
|
|
128
|
+
|
|
129
|
+
IndexReapplication.new(connection: connection).on(name) do
|
|
130
|
+
drop_materialized_view(name)
|
|
131
|
+
create_materialized_view(name, sql_definition)
|
|
132
|
+
end
|
|
62
133
|
end
|
|
63
134
|
|
|
64
135
|
# Drops a materialized view in the database
|
|
65
136
|
#
|
|
137
|
+
# This is typically called in a migration via {Statements#update_view}.
|
|
138
|
+
#
|
|
66
139
|
# @param name The name of the materialized view to drop.
|
|
140
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
|
141
|
+
# in use does not support materialized views.
|
|
142
|
+
#
|
|
67
143
|
# @return [void]
|
|
68
144
|
def drop_materialized_view(name)
|
|
69
|
-
|
|
145
|
+
raise_unless_materialized_views_supported
|
|
146
|
+
execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};"
|
|
70
147
|
end
|
|
71
148
|
|
|
72
149
|
# Refreshes a materialized view from its SQL schema.
|
|
73
150
|
#
|
|
74
|
-
#
|
|
151
|
+
# This is typically called from application code via {Scenic.database}.
|
|
152
|
+
#
|
|
153
|
+
# @param name The name of the materialized view to refresh.
|
|
154
|
+
# @param concurrently [Boolean] Whether the refreshs hould happen
|
|
155
|
+
# concurrently or not. A concurrent refresh allows the view to be
|
|
156
|
+
# refreshed without locking the view for select but requires that the
|
|
157
|
+
# table have at least one unique index that covers all rows. Attempts to
|
|
158
|
+
# refresh concurrently without a unique index will raise a descriptive
|
|
159
|
+
# error.
|
|
160
|
+
#
|
|
161
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
|
162
|
+
# in use does not support materialized views.
|
|
163
|
+
# @raise [ConcurrentRefreshesNotSupportedError] when attempting a
|
|
164
|
+
# concurrent refresh on version of Postgres that does not support
|
|
165
|
+
# concurrent materialized view refreshes.
|
|
166
|
+
#
|
|
167
|
+
# @example Non-concurrent refresh
|
|
168
|
+
# Scenic.database.refresh_materialized_view(:search_results)
|
|
169
|
+
# @example Concurrent refresh
|
|
170
|
+
# Scenic.database.refresh_materialized_view(:posts, concurrent: true)
|
|
171
|
+
#
|
|
75
172
|
# @return [void]
|
|
76
|
-
def refresh_materialized_view(name)
|
|
77
|
-
|
|
173
|
+
def refresh_materialized_view(name, concurrently: false)
|
|
174
|
+
raise_unless_materialized_views_supported
|
|
175
|
+
|
|
176
|
+
if concurrently
|
|
177
|
+
raise_unless_concurrent_refresh_supported
|
|
178
|
+
execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
|
|
179
|
+
else
|
|
180
|
+
execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
|
|
181
|
+
end
|
|
78
182
|
end
|
|
79
183
|
|
|
80
184
|
private
|
|
81
185
|
|
|
82
|
-
|
|
83
|
-
|
|
186
|
+
attr_reader :connectable
|
|
187
|
+
delegate :execute, :quote_table_name, to: :connection
|
|
188
|
+
|
|
189
|
+
def connection
|
|
190
|
+
Connection.new(connectable.connection)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def raise_unless_materialized_views_supported
|
|
194
|
+
unless connection.supports_materialized_views?
|
|
195
|
+
raise MaterializedViewsNotSupportedError
|
|
196
|
+
end
|
|
84
197
|
end
|
|
85
198
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
materialized: result["materialized"] == "t",
|
|
91
|
-
)
|
|
199
|
+
def raise_unless_concurrent_refresh_supported
|
|
200
|
+
unless connection.supports_concurrent_refreshes?
|
|
201
|
+
raise ConcurrentRefreshesNotSupportedError
|
|
202
|
+
end
|
|
92
203
|
end
|
|
93
204
|
end
|
|
94
205
|
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
|
@@ -9,17 +9,24 @@ module Scenic
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def views(stream)
|
|
12
|
-
|
|
13
|
-
stream.puts
|
|
12
|
+
if dumpable_views_in_database.any?
|
|
13
|
+
stream.puts
|
|
14
14
|
end
|
|
15
|
-
end
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
dumpable_views_in_database.each do |view|
|
|
17
|
+
stream.puts(view.to_schema)
|
|
18
|
+
indexes(view.name, stream)
|
|
19
|
+
end
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
private
|
|
22
23
|
|
|
24
|
+
def dumpable_views_in_database
|
|
25
|
+
@dumpable_views_in_database ||= Scenic.database.views.reject do |view|
|
|
26
|
+
ignored?(view.name)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
23
30
|
unless ActiveRecord::SchemaDumper.instance_methods(false).include?(:ignored?)
|
|
24
31
|
# This method will be present in Rails 4.2.0 and can be removed then.
|
|
25
32
|
def ignored?(table_name)
|
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
|
@@ -22,9 +22,6 @@ module Scenic
|
|
|
22
22
|
# @return [Boolean]
|
|
23
23
|
attr_reader :materialized
|
|
24
24
|
|
|
25
|
-
# @api private
|
|
26
|
-
delegate :<=>, to: :name
|
|
27
|
-
|
|
28
25
|
# Returns a new instance of View.
|
|
29
26
|
#
|
|
30
27
|
# @param name [String] The name of the view.
|
|
@@ -47,10 +44,10 @@ module Scenic
|
|
|
47
44
|
def to_schema
|
|
48
45
|
materialized_option = materialized ? "materialized: true, " : ""
|
|
49
46
|
<<-DEFINITION
|
|
50
|
-
|
|
51
47
|
create_view :#{name}, #{materialized_option} sql_definition: <<-\SQL
|
|
52
48
|
#{definition.indent(2)}
|
|
53
49
|
SQL
|
|
50
|
+
|
|
54
51
|
DEFINITION
|
|
55
52
|
end
|
|
56
53
|
end
|
data/lib/scenic.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
|
|
@@ -36,4 +36,16 @@ describe Scenic::Generators::ViewGenerator, :generator do
|
|
|
36
36
|
expect(migration).to contain "materialized: true"
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
context "for views created in a schema other than 'public'" do
|
|
41
|
+
it "creates view definition and migration files" do
|
|
42
|
+
migration = file("db/migrate/create_non_public_searches.rb")
|
|
43
|
+
view_definition = file("db/views/non_public_searches_v01.sql")
|
|
44
|
+
|
|
45
|
+
run_generator ["non_public.search"]
|
|
46
|
+
|
|
47
|
+
expect(migration).to be_a_migration
|
|
48
|
+
expect(view_definition).to exist
|
|
49
|
+
end
|
|
50
|
+
end
|
|
39
51
|
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
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
module Scenic
|
|
4
|
+
module Adapters
|
|
5
|
+
describe Postgres::Views, :db do
|
|
6
|
+
it "returns scenic view objects for plain old views" do
|
|
7
|
+
connection = ActiveRecord::Base.connection
|
|
8
|
+
connection.execute <<-SQL
|
|
9
|
+
CREATE VIEW children AS SELECT text 'Elliot' AS name
|
|
10
|
+
SQL
|
|
11
|
+
|
|
12
|
+
views = Postgres::Views.new(connection).all
|
|
13
|
+
first = views.first
|
|
14
|
+
|
|
15
|
+
expect(views.size).to eq 1
|
|
16
|
+
expect(first.name).to eq "children"
|
|
17
|
+
expect(first.materialized).to be false
|
|
18
|
+
expect(first.definition).to eq "SELECT 'Elliot'::text AS name;"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "returns scenic view objects for materialized views" do
|
|
22
|
+
connection = ActiveRecord::Base.connection
|
|
23
|
+
connection.execute <<-SQL
|
|
24
|
+
CREATE MATERIALIZED VIEW children AS SELECT text 'Owen' AS name
|
|
25
|
+
SQL
|
|
26
|
+
|
|
27
|
+
views = Postgres::Views.new(connection).all
|
|
28
|
+
first = views.first
|
|
29
|
+
|
|
30
|
+
expect(views.size).to eq 1
|
|
31
|
+
expect(first.name).to eq "children"
|
|
32
|
+
expect(first.materialized).to be true
|
|
33
|
+
expect(first.definition).to eq "SELECT 'Owen'::text AS name;"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|