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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -4
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +55 -17
  5. data/CONTRIBUTING.md +1 -0
  6. data/FUNDING.yml +1 -0
  7. data/Gemfile +4 -4
  8. data/README.md +84 -25
  9. data/Rakefile +1 -1
  10. data/bin/standardrb +27 -0
  11. data/lib/generators/scenic/materializable.rb +27 -1
  12. data/lib/generators/scenic/model/model_generator.rb +7 -16
  13. data/lib/generators/scenic/model/templates/model.erb +4 -0
  14. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
  15. data/lib/generators/scenic/view/view_generator.rb +5 -5
  16. data/lib/scenic/adapters/postgres/index_creation.rb +68 -0
  17. data/lib/scenic/adapters/postgres/index_migration.rb +70 -0
  18. data/lib/scenic/adapters/postgres/index_reapplication.rb +3 -28
  19. data/lib/scenic/adapters/postgres/indexes.rb +1 -1
  20. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +3 -3
  21. data/lib/scenic/adapters/postgres/side_by_side.rb +50 -0
  22. data/lib/scenic/adapters/postgres/temporary_name.rb +34 -0
  23. data/lib/scenic/adapters/postgres/views.rb +85 -12
  24. data/lib/scenic/adapters/postgres.rb +64 -16
  25. data/lib/scenic/definition.rb +1 -1
  26. data/lib/scenic/schema_dumper.rb +0 -14
  27. data/lib/scenic/statements.rb +49 -16
  28. data/lib/scenic/version.rb +1 -1
  29. data/scenic.gemspec +15 -11
  30. data/spec/acceptance/user_manages_views_spec.rb +11 -0
  31. data/spec/dummy/Rakefile +5 -5
  32. data/spec/dummy/bin/bundle +2 -2
  33. data/spec/dummy/bin/rails +3 -3
  34. data/spec/dummy/bin/rake +2 -2
  35. data/spec/dummy/config/application.rb +4 -0
  36. data/spec/dummy/config.ru +1 -1
  37. data/spec/dummy/db/migrate/20220112154220_add_pg_stat_statements_extension.rb +1 -1
  38. data/spec/dummy/db/schema.rb +0 -2
  39. data/spec/generators/scenic/model/model_generator_spec.rb +9 -1
  40. data/spec/generators/scenic/view/view_generator_spec.rb +28 -2
  41. data/spec/integration/revert_spec.rb +1 -1
  42. data/spec/scenic/adapters/postgres/connection_spec.rb +1 -1
  43. data/spec/scenic/adapters/postgres/index_creation_spec.rb +54 -0
  44. data/spec/scenic/adapters/postgres/index_migration_spec.rb +24 -0
  45. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +9 -9
  46. data/spec/scenic/adapters/postgres/side_by_side_spec.rb +24 -0
  47. data/spec/scenic/adapters/postgres/temporary_name_spec.rb +23 -0
  48. data/spec/scenic/adapters/postgres_spec.rb +95 -8
  49. data/spec/scenic/command_recorder/statement_arguments_spec.rb +4 -4
  50. data/spec/scenic/command_recorder_spec.rb +30 -12
  51. data/spec/scenic/schema_dumper_spec.rb +35 -14
  52. data/spec/scenic/statements_spec.rb +66 -8
  53. data/spec/spec_helper.rb +19 -4
  54. data/spec/support/database_schema_helpers.rb +28 -0
  55. data/spec/support/generator_spec_setup.rb +2 -2
  56. data/spec/support/view_definition_helpers.rb +1 -1
  57. metadata +35 -48
  58. data/.hound.yml +0 -2
  59. 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.parents AS SELECT text 'Maarten' AS name;
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.parents",
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, { foo: :bar }]
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, { revert_to_version: 42 }]
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, { foo: :bar }]
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, { version: 42, revert_to_version: 15 }]
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, { revert_to_version: 3 }]
41
- revert_args = [:users, { version: 3 }]
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, { another_argument: 1 }]
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, { version: 2 }]
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, { version: 2, revert_to_version: 1 }]
67
- revert_args = [:users, { version: 1 }]
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, { version: 42, another_argument: 1 }]
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, { version: 2 }]
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, { version: 2, revert_to_version: 1 }]
93
- revert_args = [:users, { version: 1 }]
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, { version: 42, another_argument: 1 }]
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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(STDOUT) { eval(output) }
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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(STDOUT) { eval(output) }
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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 :'scenic.searches'
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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(STDOUT) { eval(output) }
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
- ActiveRecord::SchemaDumper.dump(Search.connection, stream)
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
- silence_stream(STDOUT) { eval(output) }
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: { no_data: true },
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: { no_data: true },
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
- Class.new { extend Statements }
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
- require "support/rails_configuration_helpers"
6
- require "support/generator_spec_setup"
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::SchemaMigration.create_table
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
@@ -1,6 +1,6 @@
1
1
  require "rspec/rails"
2
- require "ammeter/rspec/generator/example.rb"
3
- require "ammeter/rspec/generator/matchers.rb"
2
+ require "ammeter/rspec/generator/example"
3
+ require "ammeter/rspec/generator/matchers"
4
4
  require "ammeter/init"
5
5
 
6
6
  RSpec.configure do |config|
@@ -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.open(definition.full_path, "w") { |f| f.write(schema) }
5
+ File.write(definition.full_path, schema)
6
6
  yield
7
7
  ensure
8
8
  FileUtils.rm_f(definition.full_path)