scenic 1.7.0 → 1.9.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/.github/workflows/ci.yml +29 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +55 -17
- data/CONTRIBUTING.md +1 -0
- data/FUNDING.yml +1 -0
- data/Gemfile +4 -4
- data/README.md +84 -25
- data/Rakefile +1 -1
- data/bin/standardrb +27 -0
- data/lib/generators/scenic/materializable.rb +27 -1
- data/lib/generators/scenic/model/model_generator.rb +7 -16
- data/lib/generators/scenic/model/templates/model.erb +4 -0
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
- data/lib/generators/scenic/view/view_generator.rb +5 -5
- data/lib/scenic/adapters/postgres/index_creation.rb +68 -0
- data/lib/scenic/adapters/postgres/index_migration.rb +70 -0
- data/lib/scenic/adapters/postgres/index_reapplication.rb +3 -28
- data/lib/scenic/adapters/postgres/indexes.rb +1 -1
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +3 -3
- data/lib/scenic/adapters/postgres/side_by_side.rb +50 -0
- data/lib/scenic/adapters/postgres/temporary_name.rb +34 -0
- data/lib/scenic/adapters/postgres/views.rb +85 -12
- data/lib/scenic/adapters/postgres.rb +64 -16
- data/lib/scenic/definition.rb +1 -1
- data/lib/scenic/schema_dumper.rb +0 -14
- data/lib/scenic/statements.rb +49 -16
- data/lib/scenic/version.rb +1 -1
- data/scenic.gemspec +15 -11
- data/spec/acceptance/user_manages_views_spec.rb +11 -0
- data/spec/dummy/Rakefile +5 -5
- data/spec/dummy/bin/bundle +2 -2
- data/spec/dummy/bin/rails +3 -3
- data/spec/dummy/bin/rake +2 -2
- data/spec/dummy/config/application.rb +4 -0
- data/spec/dummy/config.ru +1 -1
- data/spec/dummy/db/migrate/20220112154220_add_pg_stat_statements_extension.rb +1 -1
- data/spec/dummy/db/schema.rb +0 -2
- data/spec/generators/scenic/model/model_generator_spec.rb +9 -1
- data/spec/generators/scenic/view/view_generator_spec.rb +28 -2
- data/spec/integration/revert_spec.rb +1 -1
- data/spec/scenic/adapters/postgres/connection_spec.rb +1 -1
- data/spec/scenic/adapters/postgres/index_creation_spec.rb +54 -0
- data/spec/scenic/adapters/postgres/index_migration_spec.rb +24 -0
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +9 -9
- data/spec/scenic/adapters/postgres/side_by_side_spec.rb +24 -0
- data/spec/scenic/adapters/postgres/temporary_name_spec.rb +23 -0
- data/spec/scenic/adapters/postgres_spec.rb +95 -8
- data/spec/scenic/command_recorder/statement_arguments_spec.rb +4 -4
- data/spec/scenic/command_recorder_spec.rb +30 -12
- data/spec/scenic/schema_dumper_spec.rb +35 -14
- data/spec/scenic/statements_spec.rb +66 -8
- data/spec/spec_helper.rb +19 -4
- data/spec/support/database_schema_helpers.rb +28 -0
- data/spec/support/generator_spec_setup.rb +2 -2
- data/spec/support/view_definition_helpers.rb +1 -1
- metadata +35 -48
- data/.hound.yml +0 -2
- data/.rubocop.yml +0 -129
@@ -19,7 +19,7 @@ module Scenic
|
|
19
19
|
|
20
20
|
adapter.create_materialized_view(
|
21
21
|
"greetings",
|
22
|
-
"SELECT text 'hi' AS greeting"
|
22
|
+
"SELECT text 'hi' AS greeting"
|
23
23
|
)
|
24
24
|
|
25
25
|
view = adapter.views.first
|
@@ -33,7 +33,7 @@ module Scenic
|
|
33
33
|
adapter.create_materialized_view(
|
34
34
|
"greetings",
|
35
35
|
"SELECT text 'hi' AS greeting; \n",
|
36
|
-
no_data: true
|
36
|
+
no_data: true
|
37
37
|
)
|
38
38
|
|
39
39
|
view = adapter.views.first
|
@@ -85,7 +85,7 @@ module Scenic
|
|
85
85
|
|
86
86
|
adapter.create_materialized_view(
|
87
87
|
"greetings",
|
88
|
-
"SELECT text 'hi' AS greeting"
|
88
|
+
"SELECT text 'hi' AS greeting"
|
89
89
|
)
|
90
90
|
adapter.drop_materialized_view("greetings")
|
91
91
|
|
@@ -125,7 +125,7 @@ module Scenic
|
|
125
125
|
adapter.refresh_materialized_view(
|
126
126
|
:tests,
|
127
127
|
cascade: true,
|
128
|
-
concurrently: true
|
128
|
+
concurrently: true
|
129
129
|
)
|
130
130
|
end
|
131
131
|
|
@@ -149,6 +149,14 @@ module Scenic
|
|
149
149
|
adapter.refresh_materialized_view(:tests, concurrently: true)
|
150
150
|
}.to raise_error e
|
151
151
|
end
|
152
|
+
|
153
|
+
it "falls back to non-concurrent refresh if not populated" do
|
154
|
+
adapter = Postgres.new
|
155
|
+
adapter.create_materialized_view(:testing, "SELECT unnest('{1, 2}'::int[])", no_data: true)
|
156
|
+
|
157
|
+
expect { adapter.refresh_materialized_view(:testing, concurrently: true) }
|
158
|
+
.not_to raise_error
|
159
|
+
end
|
152
160
|
end
|
153
161
|
end
|
154
162
|
|
@@ -176,10 +184,10 @@ module Scenic
|
|
176
184
|
SQL
|
177
185
|
|
178
186
|
expect(adapter.views.map(&:name)).to eq [
|
179
|
-
"parents",
|
180
187
|
"children",
|
188
|
+
"parents",
|
181
189
|
"people",
|
182
|
-
"people_with_names"
|
190
|
+
"people_with_names"
|
183
191
|
]
|
184
192
|
end
|
185
193
|
|
@@ -193,17 +201,96 @@ module Scenic
|
|
193
201
|
|
194
202
|
ActiveRecord::Base.connection.execute <<-SQL
|
195
203
|
CREATE SCHEMA scenic;
|
196
|
-
CREATE VIEW scenic.
|
204
|
+
CREATE VIEW scenic.more_parents AS SELECT text 'Maarten' AS name;
|
197
205
|
SET search_path TO scenic, public;
|
198
206
|
SQL
|
199
207
|
|
200
208
|
expect(adapter.views.map(&:name)).to eq [
|
201
209
|
"parents",
|
202
|
-
"scenic.
|
210
|
+
"scenic.more_parents"
|
203
211
|
]
|
204
212
|
end
|
205
213
|
end
|
206
214
|
end
|
215
|
+
|
216
|
+
describe "#populated?" do
|
217
|
+
it "returns false if a materialized view is not populated" do
|
218
|
+
adapter = Postgres.new
|
219
|
+
|
220
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
221
|
+
CREATE MATERIALIZED VIEW greetings AS
|
222
|
+
SELECT text 'hi' AS greeting
|
223
|
+
WITH NO DATA
|
224
|
+
SQL
|
225
|
+
|
226
|
+
expect(adapter.populated?("greetings")).to be false
|
227
|
+
end
|
228
|
+
|
229
|
+
it "returns true if a materialized view is populated" do
|
230
|
+
adapter = Postgres.new
|
231
|
+
|
232
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
233
|
+
CREATE MATERIALIZED VIEW greetings AS
|
234
|
+
SELECT text 'hi' AS greeting
|
235
|
+
SQL
|
236
|
+
|
237
|
+
expect(adapter.populated?("greetings")).to be true
|
238
|
+
end
|
239
|
+
|
240
|
+
it "strips out the schema from table_name" do
|
241
|
+
adapter = Postgres.new
|
242
|
+
|
243
|
+
ActiveRecord::Base.connection.execute <<-SQL
|
244
|
+
CREATE MATERIALIZED VIEW greetings AS
|
245
|
+
SELECT text 'hi' AS greeting
|
246
|
+
WITH NO DATA
|
247
|
+
SQL
|
248
|
+
|
249
|
+
expect(adapter.populated?("public.greetings")).to be false
|
250
|
+
end
|
251
|
+
|
252
|
+
it "raises an exception if the version of PostgreSQL is too old" do
|
253
|
+
connection = double("Connection", supports_materialized_views?: false)
|
254
|
+
connectable = double("Connectable", connection: connection)
|
255
|
+
adapter = Postgres.new(connectable)
|
256
|
+
err = Scenic::Adapters::Postgres::MaterializedViewsNotSupportedError
|
257
|
+
|
258
|
+
expect { adapter.populated?("greetings") }.to raise_error err
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "#update_materialized_view" do
|
263
|
+
it "updates the definition of a materialized view in place" do
|
264
|
+
adapter = Postgres.new
|
265
|
+
create_materialized_view("hi", "SELECT 'hi' AS greeting")
|
266
|
+
new_definition = "SELECT 'hello' AS greeting"
|
267
|
+
|
268
|
+
adapter.update_materialized_view("hi", new_definition)
|
269
|
+
result = adapter.connection.execute("SELECT * FROM hi").first["greeting"]
|
270
|
+
|
271
|
+
expect(result).to eq "hello"
|
272
|
+
end
|
273
|
+
|
274
|
+
it "updates the definition of a materialized view side by side", :silence do
|
275
|
+
adapter = Postgres.new
|
276
|
+
create_materialized_view("hi", "SELECT 'hi' AS greeting")
|
277
|
+
new_definition = "SELECT 'hello' AS greeting"
|
278
|
+
|
279
|
+
adapter.update_materialized_view("hi", new_definition, side_by_side: true)
|
280
|
+
result = adapter.connection.execute("SELECT * FROM hi").first["greeting"]
|
281
|
+
|
282
|
+
expect(result).to eq "hello"
|
283
|
+
end
|
284
|
+
|
285
|
+
it "raises an exception if the version of PostgreSQL is too old" do
|
286
|
+
connection = double("Connection", supports_materialized_views?: false)
|
287
|
+
connectable = double("Connectable", connection: connection)
|
288
|
+
adapter = Postgres.new(connectable)
|
289
|
+
|
290
|
+
expect { adapter.create_materialized_view("greetings", "select 1") }
|
291
|
+
.to raise_error Postgres::MaterializedViewsNotSupportedError
|
292
|
+
end
|
293
|
+
end
|
207
294
|
end
|
208
295
|
end
|
209
296
|
end
|
@@ -4,7 +4,7 @@ module Scenic::CommandRecorder
|
|
4
4
|
describe StatementArguments do
|
5
5
|
describe "#view" do
|
6
6
|
it "is the view name" do
|
7
|
-
raw_args = [:spaceships, {
|
7
|
+
raw_args = [:spaceships, {foo: :bar}]
|
8
8
|
args = StatementArguments.new(raw_args)
|
9
9
|
|
10
10
|
expect(args.view).to eq :spaceships
|
@@ -13,14 +13,14 @@ module Scenic::CommandRecorder
|
|
13
13
|
|
14
14
|
describe "#revert_to_version" do
|
15
15
|
it "is the revert_to_version from the keyword arguments" do
|
16
|
-
raw_args = [:spaceships, {
|
16
|
+
raw_args = [:spaceships, {revert_to_version: 42}]
|
17
17
|
args = StatementArguments.new(raw_args)
|
18
18
|
|
19
19
|
expect(args.revert_to_version).to eq 42
|
20
20
|
end
|
21
21
|
|
22
22
|
it "is nil if the revert_to_version was not supplied" do
|
23
|
-
raw_args = [:spaceships, {
|
23
|
+
raw_args = [:spaceships, {foo: :bar}]
|
24
24
|
args = StatementArguments.new(raw_args)
|
25
25
|
|
26
26
|
expect(args.revert_to_version).to be nil
|
@@ -29,7 +29,7 @@ module Scenic::CommandRecorder
|
|
29
29
|
|
30
30
|
describe "#invert_version" do
|
31
31
|
it "returns object with version set to revert_to_version" do
|
32
|
-
raw_args = [:meatballs, {
|
32
|
+
raw_args = [:meatballs, {version: 42, revert_to_version: 15}]
|
33
33
|
|
34
34
|
inverted_args = StatementArguments.new(raw_args).invert_version
|
35
35
|
|
@@ -24,7 +24,7 @@ describe Scenic::CommandRecorder do
|
|
24
24
|
recorder.revert { recorder.create_view :greetings, materialized: true }
|
25
25
|
|
26
26
|
expect(recorder.commands).to eq [
|
27
|
-
[:drop_view, [:greetings, materialized: true]]
|
27
|
+
[:drop_view, [:greetings, materialized: true]]
|
28
28
|
]
|
29
29
|
end
|
30
30
|
end
|
@@ -37,8 +37,8 @@ describe Scenic::CommandRecorder do
|
|
37
37
|
end
|
38
38
|
|
39
39
|
it "reverts to create_view with specified revert_to_version" do
|
40
|
-
args = [:users, {
|
41
|
-
revert_args = [:users, {
|
40
|
+
args = [:users, {revert_to_version: 3}]
|
41
|
+
revert_args = [:users, {version: 3}]
|
42
42
|
|
43
43
|
recorder.revert { recorder.drop_view(*args) }
|
44
44
|
|
@@ -46,7 +46,7 @@ describe Scenic::CommandRecorder do
|
|
46
46
|
end
|
47
47
|
|
48
48
|
it "raises when reverting without revert_to_version set" do
|
49
|
-
args = [:users, {
|
49
|
+
args = [:users, {another_argument: 1}]
|
50
50
|
|
51
51
|
expect { recorder.revert { recorder.drop_view(*args) } }
|
52
52
|
.to raise_error(ActiveRecord::IrreversibleMigration)
|
@@ -55,7 +55,7 @@ describe Scenic::CommandRecorder do
|
|
55
55
|
|
56
56
|
describe "#update_view" do
|
57
57
|
it "records the updated view" do
|
58
|
-
args = [:users, {
|
58
|
+
args = [:users, {version: 2}]
|
59
59
|
|
60
60
|
recorder.update_view(*args)
|
61
61
|
|
@@ -63,8 +63,8 @@ describe Scenic::CommandRecorder do
|
|
63
63
|
end
|
64
64
|
|
65
65
|
it "reverts to update_view with the specified revert_to_version" do
|
66
|
-
args = [:users, {
|
67
|
-
revert_args = [:users, {
|
66
|
+
args = [:users, {version: 2, revert_to_version: 1}]
|
67
|
+
revert_args = [:users, {version: 1}]
|
68
68
|
|
69
69
|
recorder.revert { recorder.update_view(*args) }
|
70
70
|
|
@@ -72,16 +72,34 @@ describe Scenic::CommandRecorder do
|
|
72
72
|
end
|
73
73
|
|
74
74
|
it "raises when reverting without revert_to_version set" do
|
75
|
-
args = [:users, {
|
75
|
+
args = [:users, {version: 42, another_argument: 1}]
|
76
76
|
|
77
77
|
expect { recorder.revert { recorder.update_view(*args) } }
|
78
78
|
.to raise_error(ActiveRecord::IrreversibleMigration)
|
79
79
|
end
|
80
|
+
|
81
|
+
it "reverts materialized views with no_data option appropriately" do
|
82
|
+
args = [:users, {version: 2, revert_to_version: 1, materialized: {no_data: true}}]
|
83
|
+
revert_args = [:users, {version: 1, materialized: {no_data: true}}]
|
84
|
+
|
85
|
+
recorder.revert { recorder.update_view(*args) }
|
86
|
+
|
87
|
+
expect(recorder.commands).to eq [[:update_view, revert_args]]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "reverts materialized views with side_by_side option appropriately" do
|
91
|
+
args = [:users, {version: 2, revert_to_version: 1, materialized: {side_by_side: true}}]
|
92
|
+
revert_args = [:users, {version: 1, materialized: {side_by_side: true}}]
|
93
|
+
|
94
|
+
recorder.revert { recorder.update_view(*args) }
|
95
|
+
|
96
|
+
expect(recorder.commands).to eq [[:update_view, revert_args]]
|
97
|
+
end
|
80
98
|
end
|
81
99
|
|
82
100
|
describe "#replace_view" do
|
83
101
|
it "records the replaced view" do
|
84
|
-
args = [:users, {
|
102
|
+
args = [:users, {version: 2}]
|
85
103
|
|
86
104
|
recorder.replace_view(*args)
|
87
105
|
|
@@ -89,8 +107,8 @@ describe Scenic::CommandRecorder do
|
|
89
107
|
end
|
90
108
|
|
91
109
|
it "reverts to replace_view with the specified revert_to_version" do
|
92
|
-
args = [:users, {
|
93
|
-
revert_args = [:users, {
|
110
|
+
args = [:users, {version: 2, revert_to_version: 1}]
|
111
|
+
revert_args = [:users, {version: 1}]
|
94
112
|
|
95
113
|
recorder.revert { recorder.replace_view(*args) }
|
96
114
|
|
@@ -98,7 +116,7 @@ describe Scenic::CommandRecorder do
|
|
98
116
|
end
|
99
117
|
|
100
118
|
it "raises when reverting without revert_to_version set" do
|
101
|
-
args = [:users, {
|
119
|
+
args = [:users, {version: 42, another_argument: 1}]
|
102
120
|
|
103
121
|
expect { recorder.revert { recorder.replace_view(*args) } }
|
104
122
|
.to raise_error(ActiveRecord::IrreversibleMigration)
|
@@ -12,7 +12,7 @@ describe Scenic::SchemaDumper, :db do
|
|
12
12
|
Search.connection.create_view :searches, sql_definition: view_definition
|
13
13
|
stream = StringIO.new
|
14
14
|
|
15
|
-
|
15
|
+
dump_schema(stream)
|
16
16
|
|
17
17
|
output = stream.string
|
18
18
|
|
@@ -21,7 +21,7 @@ describe Scenic::SchemaDumper, :db do
|
|
21
21
|
|
22
22
|
Search.connection.drop_view :searches
|
23
23
|
|
24
|
-
silence_stream(
|
24
|
+
silence_stream($stdout) { eval(output) } # standard:disable Security/Eval
|
25
25
|
|
26
26
|
expect(Search.first.haystack).to eq "needle"
|
27
27
|
end
|
@@ -31,13 +31,13 @@ describe Scenic::SchemaDumper, :db do
|
|
31
31
|
Search.connection.create_view :searches, sql_definition: view_definition
|
32
32
|
stream = StringIO.new
|
33
33
|
|
34
|
-
|
34
|
+
dump_schema(stream)
|
35
35
|
|
36
36
|
output = stream.string
|
37
37
|
expect(output).to include "~ '\\\\d+'::text"
|
38
38
|
|
39
39
|
Search.connection.drop_view :searches
|
40
|
-
silence_stream(
|
40
|
+
silence_stream($stdout) { eval(output) } # standard:disable Security/Eval
|
41
41
|
|
42
42
|
expect(Search.first.haystack).to eq "needle"
|
43
43
|
end
|
@@ -47,7 +47,7 @@ describe Scenic::SchemaDumper, :db do
|
|
47
47
|
Search.connection.create_view :searches, materialized: true, sql_definition: view_definition
|
48
48
|
stream = StringIO.new
|
49
49
|
|
50
|
-
|
50
|
+
dump_schema(stream)
|
51
51
|
|
52
52
|
output = stream.string
|
53
53
|
|
@@ -62,12 +62,28 @@ describe Scenic::SchemaDumper, :db do
|
|
62
62
|
Search.connection.create_view :"scenic.searches", sql_definition: view_definition
|
63
63
|
stream = StringIO.new
|
64
64
|
|
65
|
-
|
65
|
+
dump_schema(stream)
|
66
66
|
|
67
67
|
output = stream.string
|
68
68
|
expect(output).to include 'create_view "scenic.searches",'
|
69
69
|
|
70
|
-
Search.connection.drop_view :
|
70
|
+
Search.connection.drop_view :"scenic.searches"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "sorts dependency order when views exist in a non-public schema" do
|
74
|
+
Search.connection.execute("CREATE SCHEMA IF NOT EXISTS scenic; SET search_path TO public, scenic")
|
75
|
+
Search.connection.execute("CREATE VIEW scenic.apples AS SELECT 1;")
|
76
|
+
Search.connection.execute("CREATE VIEW scenic.bananas AS SELECT 2;")
|
77
|
+
Search.connection.execute("CREATE OR REPLACE VIEW scenic.apples AS SELECT * FROM scenic.bananas;")
|
78
|
+
stream = StringIO.new
|
79
|
+
|
80
|
+
dump_schema(stream)
|
81
|
+
views = stream.string.lines.grep(/create_view/).map do |view_line|
|
82
|
+
view_line.match('create_view "(?<name>.*)"')[:name]
|
83
|
+
end
|
84
|
+
expect(views).to eq(%w[scenic.bananas scenic.apples])
|
85
|
+
|
86
|
+
Search.connection.execute("DROP SCHEMA IF EXISTS scenic CASCADE; SET search_path TO public")
|
71
87
|
end
|
72
88
|
end
|
73
89
|
|
@@ -77,7 +93,7 @@ describe Scenic::SchemaDumper, :db do
|
|
77
93
|
Search.connection.create_view :a_searches_z, sql_definition: view_definition
|
78
94
|
stream = StringIO.new
|
79
95
|
|
80
|
-
|
96
|
+
dump_schema(stream)
|
81
97
|
|
82
98
|
output = stream.string
|
83
99
|
|
@@ -90,7 +106,7 @@ describe Scenic::SchemaDumper, :db do
|
|
90
106
|
Search.connection.create_view :searches, sql_definition: view_definition
|
91
107
|
stream = StringIO.new
|
92
108
|
|
93
|
-
|
109
|
+
dump_schema(stream)
|
94
110
|
|
95
111
|
output = stream.string
|
96
112
|
|
@@ -105,7 +121,7 @@ describe Scenic::SchemaDumper, :db do
|
|
105
121
|
Search.connection.create_view '"search in a haystack"', sql_definition: view_definition
|
106
122
|
stream = StringIO.new
|
107
123
|
|
108
|
-
|
124
|
+
dump_schema(stream)
|
109
125
|
|
110
126
|
output = stream.string
|
111
127
|
expect(output).to include 'create_view "\"search in a haystack\"",'
|
@@ -113,7 +129,7 @@ describe Scenic::SchemaDumper, :db do
|
|
113
129
|
|
114
130
|
Search.connection.drop_view :'"search in a haystack"'
|
115
131
|
|
116
|
-
silence_stream(
|
132
|
+
silence_stream($stdout) { eval(output) } # standard:disable Security/Eval
|
117
133
|
|
118
134
|
expect(SearchInAHaystack.take.haystack).to eq "needle"
|
119
135
|
end
|
@@ -123,13 +139,13 @@ describe Scenic::SchemaDumper, :db do
|
|
123
139
|
it "dumps a create_view for a view in the database" do
|
124
140
|
view_definition = "SELECT 'needle'::text AS haystack"
|
125
141
|
Search.connection.execute(
|
126
|
-
"CREATE SCHEMA scenic; SET search_path TO scenic, public"
|
142
|
+
"CREATE SCHEMA scenic; SET search_path TO scenic, public"
|
127
143
|
)
|
128
144
|
Search.connection.create_view 'scenic."search in a haystack"',
|
129
145
|
sql_definition: view_definition
|
130
146
|
stream = StringIO.new
|
131
147
|
|
132
|
-
|
148
|
+
dump_schema(stream)
|
133
149
|
|
134
150
|
output = stream.string
|
135
151
|
expect(output).to include 'create_view "scenic.\"search in a haystack\"",'
|
@@ -137,7 +153,12 @@ describe Scenic::SchemaDumper, :db do
|
|
137
153
|
|
138
154
|
Search.connection.drop_view :'scenic."search in a haystack"'
|
139
155
|
|
140
|
-
|
156
|
+
case ActiveRecord.gem_version
|
157
|
+
when Gem::Requirement.new(">= 7.1")
|
158
|
+
Search.connection.drop_schema "scenic"
|
159
|
+
end
|
160
|
+
|
161
|
+
silence_stream($stdout) { eval(output) } # standard:disable Security/Eval
|
141
162
|
|
142
163
|
expect(SearchInAHaystack.take.haystack).to eq "needle"
|
143
164
|
end
|
@@ -70,7 +70,7 @@ module Scenic
|
|
70
70
|
connection.create_view(
|
71
71
|
:views,
|
72
72
|
version: 1,
|
73
|
-
materialized: {
|
73
|
+
materialized: {no_data: true}
|
74
74
|
)
|
75
75
|
|
76
76
|
expect(Scenic.database).to have_received(:create_materialized_view)
|
@@ -125,7 +125,7 @@ module Scenic
|
|
125
125
|
connection.update_view(:name, version: 3, materialized: true)
|
126
126
|
|
127
127
|
expect(Scenic.database).to have_received(:update_materialized_view)
|
128
|
-
.with(:name, definition.to_sql, no_data: false)
|
128
|
+
.with(:name, definition.to_sql, no_data: false, side_by_side: false)
|
129
129
|
end
|
130
130
|
|
131
131
|
it "updates the materialized view in the database with NO DATA" do
|
@@ -137,17 +137,33 @@ module Scenic
|
|
137
137
|
connection.update_view(
|
138
138
|
:name,
|
139
139
|
version: 3,
|
140
|
-
materialized: {
|
140
|
+
materialized: {no_data: true}
|
141
141
|
)
|
142
142
|
|
143
143
|
expect(Scenic.database).to have_received(:update_materialized_view)
|
144
|
-
.with(:name, definition.to_sql, no_data: true)
|
144
|
+
.with(:name, definition.to_sql, no_data: true, side_by_side: false)
|
145
|
+
end
|
146
|
+
|
147
|
+
it "updates the materialized view with side-by-side mode" do
|
148
|
+
definition = instance_double("Definition", to_sql: "definition")
|
149
|
+
allow(Definition).to receive(:new)
|
150
|
+
.with(:name, 3)
|
151
|
+
.and_return(definition)
|
152
|
+
|
153
|
+
connection.update_view(
|
154
|
+
:name,
|
155
|
+
version: 3,
|
156
|
+
materialized: {side_by_side: true}
|
157
|
+
)
|
158
|
+
|
159
|
+
expect(Scenic.database).to have_received(:update_materialized_view)
|
160
|
+
.with(:name, definition.to_sql, no_data: false, side_by_side: true)
|
145
161
|
end
|
146
162
|
|
147
163
|
it "raises an error if not supplied a version or sql_defintion" do
|
148
164
|
expect { connection.update_view :views }.to raise_error(
|
149
165
|
ArgumentError,
|
150
|
-
/sql_definition or version must be specified
|
166
|
+
/sql_definition or version must be specified/
|
151
167
|
)
|
152
168
|
end
|
153
169
|
|
@@ -156,10 +172,40 @@ module Scenic
|
|
156
172
|
connection.update_view(
|
157
173
|
:views,
|
158
174
|
version: 1,
|
159
|
-
sql_definition: "a defintion"
|
175
|
+
sql_definition: "a defintion"
|
160
176
|
)
|
161
177
|
end.to raise_error ArgumentError, /cannot both be set/
|
162
178
|
end
|
179
|
+
|
180
|
+
it "raises an error is no_data and side_by_side are both set" do
|
181
|
+
definition = instance_double("Definition", to_sql: "definition")
|
182
|
+
allow(Definition).to receive(:new)
|
183
|
+
.with(:name, 3)
|
184
|
+
.and_return(definition)
|
185
|
+
|
186
|
+
expect do
|
187
|
+
connection.update_view(
|
188
|
+
:name,
|
189
|
+
version: 3,
|
190
|
+
materialized: {no_data: true, side_by_side: true}
|
191
|
+
)
|
192
|
+
end.to raise_error ArgumentError, /cannot be combined/
|
193
|
+
end
|
194
|
+
|
195
|
+
it "raises an error if not in a transaction" do
|
196
|
+
definition = instance_double("Definition", to_sql: "definition")
|
197
|
+
allow(Definition).to receive(:new)
|
198
|
+
.with(:name, 3)
|
199
|
+
.and_return(definition)
|
200
|
+
|
201
|
+
expect do
|
202
|
+
connection(transactions_enabled: false).update_view(
|
203
|
+
:name,
|
204
|
+
version: 3,
|
205
|
+
materialized: {side_by_side: true}
|
206
|
+
)
|
207
|
+
end.to raise_error RuntimeError, /transaction is required/
|
208
|
+
end
|
163
209
|
end
|
164
210
|
|
165
211
|
describe "replace_view" do
|
@@ -192,8 +238,20 @@ module Scenic
|
|
192
238
|
end
|
193
239
|
end
|
194
240
|
|
195
|
-
def connection
|
196
|
-
|
241
|
+
def connection(transactions_enabled: true)
|
242
|
+
DummyConnection.new(transactions_enabled: transactions_enabled)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class DummyConnection
|
247
|
+
include Statements
|
248
|
+
|
249
|
+
def initialize(transactions_enabled:)
|
250
|
+
@transactions_enabled = transactions_enabled
|
251
|
+
end
|
252
|
+
|
253
|
+
def transaction_open?
|
254
|
+
@transactions_enabled
|
197
255
|
end
|
198
256
|
end
|
199
257
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -2,24 +2,39 @@ ENV["RAILS_ENV"] = "test"
|
|
2
2
|
require "database_cleaner"
|
3
3
|
|
4
4
|
require File.expand_path("dummy/config/environment", __dir__)
|
5
|
-
|
6
|
-
|
7
|
-
require "support/view_definition_helpers"
|
5
|
+
|
6
|
+
Dir.glob("#{__dir__}/support/**/*.rb").each { |f| require f }
|
8
7
|
|
9
8
|
RSpec.configure do |config|
|
10
9
|
config.order = "random"
|
10
|
+
config.include DatabaseSchemaHelpers
|
11
11
|
config.include ViewDefinitionHelpers
|
12
12
|
config.include RailsConfigurationHelpers
|
13
13
|
DatabaseCleaner.strategy = :transaction
|
14
14
|
|
15
15
|
config.around(:each, db: true) do |example|
|
16
|
-
ActiveRecord
|
16
|
+
case ActiveRecord.gem_version
|
17
|
+
when Gem::Requirement.new(">= 7.2")
|
18
|
+
ActiveRecord::SchemaMigration
|
19
|
+
.new(ActiveRecord::Tasks::DatabaseTasks.migration_connection_pool)
|
20
|
+
.create_table
|
21
|
+
when Gem::Requirement.new("~> 7.1.0")
|
22
|
+
ActiveRecord::SchemaMigration
|
23
|
+
.new(ActiveRecord::Tasks::DatabaseTasks.migration_connection)
|
24
|
+
.create_table
|
25
|
+
when Gem::Requirement.new("< 7.1")
|
26
|
+
ActiveRecord::SchemaMigration.create_table
|
27
|
+
end
|
17
28
|
|
18
29
|
DatabaseCleaner.start
|
19
30
|
example.run
|
20
31
|
DatabaseCleaner.clean
|
21
32
|
end
|
22
33
|
|
34
|
+
config.before(:each, silence: true) do |example|
|
35
|
+
allow_any_instance_of(ActiveRecord::Migration).to receive(:say)
|
36
|
+
end
|
37
|
+
|
23
38
|
if defined? ActiveSupport::Testing::Stream
|
24
39
|
config.include ActiveSupport::Testing::Stream
|
25
40
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module DatabaseSchemaHelpers
|
2
|
+
def dump_schema(stream)
|
3
|
+
case ActiveRecord.gem_version
|
4
|
+
when Gem::Requirement.new(">= 7.2")
|
5
|
+
ActiveRecord::SchemaDumper.dump(Search.connection_pool, stream)
|
6
|
+
else
|
7
|
+
ActiveRecord::SchemaDumper.dump(Search.connection, stream)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def ar_connection
|
12
|
+
ActiveRecord::Base.connection
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_materialized_view(name, sql)
|
16
|
+
ar_connection.execute("CREATE MATERIALIZED VIEW #{name} AS #{sql}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_index(view, columns, name: nil)
|
20
|
+
ar_connection.add_index(view, columns, name: name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def indexes_for(view_name)
|
24
|
+
Scenic::Adapters::Postgres::Indexes
|
25
|
+
.new(connection: ar_connection)
|
26
|
+
.on(view_name)
|
27
|
+
end
|
28
|
+
end
|
@@ -2,7 +2,7 @@ module ViewDefinitionHelpers
|
|
2
2
|
def with_view_definition(name, version, schema)
|
3
3
|
definition = Scenic::Definition.new(name, version)
|
4
4
|
FileUtils.mkdir_p(File.dirname(definition.full_path))
|
5
|
-
File.
|
5
|
+
File.write(definition.full_path, schema)
|
6
6
|
yield
|
7
7
|
ensure
|
8
8
|
FileUtils.rm_f(definition.full_path)
|