scenic-jets 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +78 -0
  3. data/.gitignore +19 -0
  4. data/.hound.yml +2 -0
  5. data/.rubocop.yml +129 -0
  6. data/.yardopts +4 -0
  7. data/CHANGELOG.md +223 -0
  8. data/CODE_OF_CONDUCT.md +76 -0
  9. data/CONTRIBUTING.md +24 -0
  10. data/Gemfile +16 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +5 -0
  13. data/Rakefile +29 -0
  14. data/SECURITY.md +14 -0
  15. data/bin/rake +17 -0
  16. data/bin/rspec +17 -0
  17. data/bin/setup +18 -0
  18. data/bin/yard +16 -0
  19. data/lib/generators/scenic/generators.rb +12 -0
  20. data/lib/generators/scenic/materializable.rb +31 -0
  21. data/lib/generators/scenic/model/USAGE +12 -0
  22. data/lib/generators/scenic/model/model_generator.rb +52 -0
  23. data/lib/generators/scenic/model/templates/model.erb +3 -0
  24. data/lib/generators/scenic/view/USAGE +20 -0
  25. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +5 -0
  26. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +12 -0
  27. data/lib/generators/scenic/view/view_generator.rb +127 -0
  28. data/lib/scenic.rb +31 -0
  29. data/lib/scenic/adapters/postgres.rb +256 -0
  30. data/lib/scenic/adapters/postgres/connection.rb +57 -0
  31. data/lib/scenic/adapters/postgres/errors.rb +26 -0
  32. data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
  33. data/lib/scenic/adapters/postgres/indexes.rb +53 -0
  34. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +116 -0
  35. data/lib/scenic/adapters/postgres/views.rb +74 -0
  36. data/lib/scenic/command_recorder.rb +52 -0
  37. data/lib/scenic/command_recorder/statement_arguments.rb +51 -0
  38. data/lib/scenic/configuration.rb +37 -0
  39. data/lib/scenic/definition.rb +35 -0
  40. data/lib/scenic/index.rb +36 -0
  41. data/lib/scenic/schema_dumper.rb +44 -0
  42. data/lib/scenic/statements.rb +163 -0
  43. data/lib/scenic/version.rb +3 -0
  44. data/lib/scenic/view.rb +54 -0
  45. data/scenic.gemspec +36 -0
  46. data/spec/acceptance/user_manages_views_spec.rb +88 -0
  47. data/spec/acceptance_helper.rb +33 -0
  48. data/spec/dummy/.gitignore +16 -0
  49. data/spec/dummy/Rakefile +13 -0
  50. data/spec/dummy/app/models/application_record.rb +5 -0
  51. data/spec/dummy/bin/bundle +3 -0
  52. data/spec/dummy/bin/rails +4 -0
  53. data/spec/dummy/bin/rake +4 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +15 -0
  56. data/spec/dummy/config/boot.rb +5 -0
  57. data/spec/dummy/config/database.yml +14 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/db/migrate/.keep +0 -0
  60. data/spec/dummy/db/views/.keep +0 -0
  61. data/spec/generators/scenic/model/model_generator_spec.rb +36 -0
  62. data/spec/generators/scenic/view/view_generator_spec.rb +57 -0
  63. data/spec/integration/revert_spec.rb +74 -0
  64. data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
  65. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +82 -0
  66. data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
  67. data/spec/scenic/adapters/postgres_spec.rb +209 -0
  68. data/spec/scenic/command_recorder/statement_arguments_spec.rb +41 -0
  69. data/spec/scenic/command_recorder_spec.rb +111 -0
  70. data/spec/scenic/configuration_spec.rb +27 -0
  71. data/spec/scenic/definition_spec.rb +62 -0
  72. data/spec/scenic/schema_dumper_spec.rb +115 -0
  73. data/spec/scenic/statements_spec.rb +199 -0
  74. data/spec/spec_helper.rb +22 -0
  75. data/spec/support/generator_spec_setup.rb +14 -0
  76. data/spec/support/view_definition_helpers.rb +10 -0
  77. metadata +307 -0
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic::CommandRecorder
4
+ describe StatementArguments do
5
+ describe "#view" do
6
+ it "is the view name" do
7
+ raw_args = [:spaceships, { foo: :bar }]
8
+ args = StatementArguments.new(raw_args)
9
+
10
+ expect(args.view).to eq :spaceships
11
+ end
12
+ end
13
+
14
+ describe "#revert_to_version" do
15
+ it "is the revert_to_version from the keyword arguments" do
16
+ raw_args = [:spaceships, { revert_to_version: 42 }]
17
+ args = StatementArguments.new(raw_args)
18
+
19
+ expect(args.revert_to_version).to eq 42
20
+ end
21
+
22
+ it "is nil if the revert_to_version was not supplied" do
23
+ raw_args = [:spaceships, { foo: :bar }]
24
+ args = StatementArguments.new(raw_args)
25
+
26
+ expect(args.revert_to_version).to be nil
27
+ end
28
+ end
29
+
30
+ describe "#invert_version" do
31
+ it "returns object with version set to revert_to_version" do
32
+ raw_args = [:meatballs, { version: 42, revert_to_version: 15 }]
33
+
34
+ inverted_args = StatementArguments.new(raw_args).invert_version
35
+
36
+ expect(inverted_args.version).to eq 15
37
+ expect(inverted_args.revert_to_version).to be nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,111 @@
1
+ require "spec_helper"
2
+
3
+ describe Scenic::CommandRecorder do
4
+ describe "#create_view" do
5
+ it "records the created view" do
6
+ recorder.create_view :greetings
7
+
8
+ expect(recorder.commands).to eq [[:create_view, [:greetings], nil]]
9
+ end
10
+
11
+ it "reverts to drop_view when not passed a version" do
12
+ recorder.revert { recorder.create_view :greetings }
13
+
14
+ expect(recorder.commands).to eq [[:drop_view, [:greetings]]]
15
+ end
16
+
17
+ it "reverts to drop_view when passed a version" do
18
+ recorder.revert { recorder.create_view :greetings, version: 2 }
19
+
20
+ expect(recorder.commands).to eq [[:drop_view, [:greetings]]]
21
+ end
22
+
23
+ it "reverts materialized views appropriately" do
24
+ recorder.revert { recorder.create_view :greetings, materialized: true }
25
+
26
+ expect(recorder.commands).to eq [
27
+ [:drop_view, [:greetings, materialized: true]],
28
+ ]
29
+ end
30
+ end
31
+
32
+ describe "#drop_view" do
33
+ it "records the dropped view" do
34
+ recorder.drop_view :users
35
+
36
+ expect(recorder.commands).to eq [[:drop_view, [:users], nil]]
37
+ end
38
+
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 }]
42
+
43
+ recorder.revert { recorder.drop_view(*args) }
44
+
45
+ expect(recorder.commands).to eq [[:create_view, revert_args]]
46
+ end
47
+
48
+ it "raises when reverting without revert_to_version set" do
49
+ args = [:users, { another_argument: 1 }]
50
+
51
+ expect { recorder.revert { recorder.drop_view(*args) } }
52
+ .to raise_error(ActiveRecord::IrreversibleMigration)
53
+ end
54
+ end
55
+
56
+ describe "#update_view" do
57
+ it "records the updated view" do
58
+ args = [:users, { version: 2 }]
59
+
60
+ recorder.update_view(*args)
61
+
62
+ expect(recorder.commands).to eq [[:update_view, args, nil]]
63
+ end
64
+
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 }]
68
+
69
+ recorder.revert { recorder.update_view(*args) }
70
+
71
+ expect(recorder.commands).to eq [[:update_view, revert_args]]
72
+ end
73
+
74
+ it "raises when reverting without revert_to_version set" do
75
+ args = [:users, { version: 42, another_argument: 1 }]
76
+
77
+ expect { recorder.revert { recorder.update_view(*args) } }
78
+ .to raise_error(ActiveRecord::IrreversibleMigration)
79
+ end
80
+ end
81
+
82
+ describe "#replace_view" do
83
+ it "records the replaced view" do
84
+ args = [:users, { version: 2 }]
85
+
86
+ recorder.replace_view(*args)
87
+
88
+ expect(recorder.commands).to eq [[:replace_view, args, nil]]
89
+ end
90
+
91
+ 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 }]
94
+
95
+ recorder.revert { recorder.replace_view(*args) }
96
+
97
+ expect(recorder.commands).to eq [[:replace_view, revert_args]]
98
+ end
99
+
100
+ it "raises when reverting without revert_to_version set" do
101
+ args = [:users, { version: 42, another_argument: 1 }]
102
+
103
+ expect { recorder.revert { recorder.replace_view(*args) } }
104
+ .to raise_error(ActiveRecord::IrreversibleMigration)
105
+ end
106
+ end
107
+
108
+ def recorder
109
+ @recorder ||= ActiveRecord::Migration::CommandRecorder.new
110
+ end
111
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ describe Configuration do
5
+ after { restore_default_config }
6
+
7
+ it "defaults the database adapter to postgres" do
8
+ expect(Scenic.configuration.database).to be_a Adapters::Postgres
9
+ expect(Scenic.database).to be_a Adapters::Postgres
10
+ end
11
+
12
+ it "allows the database adapter to be set" do
13
+ adapter = double("Scenic Adapter")
14
+
15
+ Scenic.configure do |config|
16
+ config.database = adapter
17
+ end
18
+
19
+ expect(Scenic.configuration.database).to eq adapter
20
+ expect(Scenic.database).to eq adapter
21
+ end
22
+
23
+ def restore_default_config
24
+ Scenic.configuration = Configuration.new
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,62 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ describe Definition do
5
+ describe "to_sql" do
6
+ it "returns the content of a view definition" do
7
+ sql_definition = "SELECT text 'Hi' as greeting"
8
+ allow(File).to receive(:read).and_return(sql_definition)
9
+
10
+ definition = Definition.new("searches", 1)
11
+
12
+ expect(definition.to_sql).to eq sql_definition
13
+ end
14
+
15
+ it "raises an error if the file is empty" do
16
+ allow(File).to receive(:read).and_return("")
17
+
18
+ expect do
19
+ Definition.new("searches", 1).to_sql
20
+ end.to raise_error RuntimeError
21
+ end
22
+ end
23
+
24
+ describe "path" do
25
+ it "returns a sql file in db/views with padded version and view name" do
26
+ expected = "db/views/searches_v01.sql"
27
+
28
+ definition = Definition.new("searches", 1)
29
+
30
+ expect(definition.path).to eq expected
31
+ end
32
+
33
+ it "handles schema qualified view names" do
34
+ definition = Definition.new("non_public.searches", 1)
35
+
36
+ expect(definition.path).to eq "db/views/non_public_searches_v01.sql"
37
+ end
38
+ end
39
+
40
+ describe "full_path" do
41
+ it "joins the path with Rails.root" do
42
+ definition = Definition.new("searches", 15)
43
+
44
+ expect(definition.full_path).to eq Rails.root.join(definition.path)
45
+ end
46
+ end
47
+
48
+ describe "version" do
49
+ it "pads the version number with 0" do
50
+ definition = Definition.new(:_, 1)
51
+
52
+ expect(definition.version).to eq "01"
53
+ end
54
+
55
+ it "doesn't pad more than 2 characters" do
56
+ definition = Definition.new(:_, 15)
57
+
58
+ expect(definition.version).to eq "15"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,115 @@
1
+ require "spec_helper"
2
+
3
+ class Search < ActiveRecord::Base; end
4
+
5
+ class SearchInAHaystack < ActiveRecord::Base
6
+ self.table_name = '"search in a haystack"'
7
+ end
8
+
9
+ describe Scenic::SchemaDumper, :db do
10
+ it "dumps a create_view for a view in the database" do
11
+ view_definition = "SELECT 'needle'::text AS haystack"
12
+ Search.connection.create_view :searches, sql_definition: view_definition
13
+ stream = StringIO.new
14
+
15
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
16
+
17
+ output = stream.string
18
+
19
+ expect(output).to include 'create_view "searches", sql_definition: <<-SQL'
20
+ expect(output).to include view_definition
21
+
22
+ Search.connection.drop_view :searches
23
+
24
+ silence_stream(STDOUT) { eval(output) }
25
+
26
+ expect(Search.first.haystack).to eq "needle"
27
+ end
28
+
29
+ it "dumps a create_view for a materialized view in the database" do
30
+ view_definition = "SELECT 'needle'::text AS haystack"
31
+ Search.connection.create_view :searches, materialized: true, sql_definition: view_definition
32
+ stream = StringIO.new
33
+
34
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
35
+
36
+ output = stream.string
37
+
38
+ expect(output).to include 'create_view "searches", materialized: true, sql_definition: <<-SQL'
39
+ expect(output).to include view_definition
40
+ end
41
+
42
+ context "with views in non public schemas" do
43
+ it "dumps a create_view including namespace for a view in the database" do
44
+ view_definition = "SELECT 'needle'::text AS haystack"
45
+ Search.connection.execute "CREATE SCHEMA scenic; SET search_path TO scenic, public"
46
+ Search.connection.create_view :"scenic.searches", sql_definition: view_definition
47
+ stream = StringIO.new
48
+
49
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
50
+
51
+ output = stream.string
52
+ expect(output).to include 'create_view "scenic.searches",'
53
+
54
+ Search.connection.drop_view :'scenic.searches'
55
+ end
56
+ end
57
+
58
+ it "ignores tables internal to Rails" do
59
+ view_definition = "SELECT 'needle'::text AS haystack"
60
+ Search.connection.create_view :searches, sql_definition: view_definition
61
+ stream = StringIO.new
62
+
63
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
64
+
65
+ output = stream.string
66
+
67
+ expect(output).to include 'create_view "searches"'
68
+ expect(output).not_to include "ar_internal_metadata"
69
+ expect(output).not_to include "schema_migrations"
70
+ end
71
+
72
+ context "with views using unexpected characters in name" do
73
+ it "dumps a create_view for a view in the database" do
74
+ view_definition = "SELECT 'needle'::text AS haystack"
75
+ Search.connection.create_view '"search in a haystack"', sql_definition: view_definition
76
+ stream = StringIO.new
77
+
78
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
79
+
80
+ output = stream.string
81
+ expect(output).to include 'create_view "\"search in a haystack\"",'
82
+ expect(output).to include view_definition
83
+
84
+ Search.connection.drop_view :'"search in a haystack"'
85
+
86
+ silence_stream(STDOUT) { eval(output) }
87
+
88
+ expect(SearchInAHaystack.take.haystack).to eq "needle"
89
+ end
90
+ end
91
+
92
+ context "with views using unexpected characters, name including namespace" do
93
+ it "dumps a create_view for a view in the database" do
94
+ view_definition = "SELECT 'needle'::text AS haystack"
95
+ Search.connection.execute(
96
+ "CREATE SCHEMA scenic; SET search_path TO scenic, public",
97
+ )
98
+ Search.connection.create_view 'scenic."search in a haystack"',
99
+ sql_definition: view_definition
100
+ stream = StringIO.new
101
+
102
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
103
+
104
+ output = stream.string
105
+ expect(output).to include 'create_view "scenic.\"search in a haystack\"",'
106
+ expect(output).to include view_definition
107
+
108
+ Search.connection.drop_view :'scenic."search in a haystack"'
109
+
110
+ silence_stream(STDOUT) { eval(output) }
111
+
112
+ expect(SearchInAHaystack.take.haystack).to eq "needle"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,199 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ describe Scenic::Statements do
5
+ before do
6
+ adapter = instance_double("Scenic::Adapters::Postgres").as_null_object
7
+ allow(Scenic).to receive(:database).and_return(adapter)
8
+ end
9
+
10
+ describe "create_view" do
11
+ it "creates a view from a file" do
12
+ version = 15
13
+ definition_stub = instance_double("Definition", to_sql: "foo")
14
+ allow(Definition).to receive(:new)
15
+ .with(:views, version)
16
+ .and_return(definition_stub)
17
+
18
+ connection.create_view :views, version: version
19
+
20
+ expect(Scenic.database).to have_received(:create_view)
21
+ .with(:views, definition_stub.to_sql)
22
+ end
23
+
24
+ it "creates a view from a text definition" do
25
+ sql_definition = "a defintion"
26
+
27
+ connection.create_view(:views, sql_definition: sql_definition)
28
+
29
+ expect(Scenic.database).to have_received(:create_view)
30
+ .with(:views, sql_definition)
31
+ end
32
+
33
+ it "creates version 1 of the view if neither version nor sql_defintion are provided" do
34
+ version = 1
35
+ definition_stub = instance_double("Definition", to_sql: "foo")
36
+ allow(Definition).to receive(:new)
37
+ .with(:views, version)
38
+ .and_return(definition_stub)
39
+
40
+ connection.create_view :views
41
+
42
+ expect(Scenic.database).to have_received(:create_view)
43
+ .with(:views, definition_stub.to_sql)
44
+ end
45
+
46
+ it "raises an error if both version and sql_defintion are provided" do
47
+ expect do
48
+ connection.create_view :foo, version: 1, sql_definition: "a defintion"
49
+ end.to raise_error ArgumentError
50
+ end
51
+ end
52
+
53
+ describe "create_view :materialized" do
54
+ it "sends the create_materialized_view message" do
55
+ definition = instance_double("Scenic::Definition", to_sql: "definition")
56
+ allow(Definition).to receive(:new).and_return(definition)
57
+
58
+ connection.create_view(:views, version: 1, materialized: true)
59
+
60
+ expect(Scenic.database).to have_received(:create_materialized_view)
61
+ .with(:views, definition.to_sql, no_data: false)
62
+ end
63
+ end
64
+
65
+ describe "create_view :materialized with :no_data" do
66
+ it "sends the create_materialized_view message" do
67
+ definition = instance_double("Scenic::Definition", to_sql: "definition")
68
+ allow(Definition).to receive(:new).and_return(definition)
69
+
70
+ connection.create_view(
71
+ :views,
72
+ version: 1,
73
+ materialized: { no_data: true },
74
+ )
75
+
76
+ expect(Scenic.database).to have_received(:create_materialized_view)
77
+ .with(:views, definition.to_sql, no_data: true)
78
+ end
79
+ end
80
+
81
+ describe "drop_view" do
82
+ it "removes a view from the database" do
83
+ connection.drop_view :name
84
+
85
+ expect(Scenic.database).to have_received(:drop_view).with(:name)
86
+ end
87
+ end
88
+
89
+ describe "drop_view :materialized" do
90
+ it "removes a materialized view from the database" do
91
+ connection.drop_view :name, materialized: true
92
+
93
+ expect(Scenic.database).to have_received(:drop_materialized_view)
94
+ end
95
+ end
96
+
97
+ describe "update_view" do
98
+ it "updates the view in the database" do
99
+ definition = instance_double("Definition", to_sql: "definition")
100
+ allow(Definition).to receive(:new)
101
+ .with(:name, 3)
102
+ .and_return(definition)
103
+
104
+ connection.update_view(:name, version: 3)
105
+
106
+ expect(Scenic.database).to have_received(:update_view)
107
+ .with(:name, definition.to_sql)
108
+ end
109
+
110
+ it "updates a view from a text definition" do
111
+ sql_definition = "a defintion"
112
+
113
+ connection.update_view(:name, sql_definition: sql_definition)
114
+
115
+ expect(Scenic.database).to have_received(:update_view)
116
+ .with(:name, sql_definition)
117
+ end
118
+
119
+ it "updates the materialized view in the database" do
120
+ definition = instance_double("Definition", to_sql: "definition")
121
+ allow(Definition).to receive(:new)
122
+ .with(:name, 3)
123
+ .and_return(definition)
124
+
125
+ connection.update_view(:name, version: 3, materialized: true)
126
+
127
+ expect(Scenic.database).to have_received(:update_materialized_view)
128
+ .with(:name, definition.to_sql, no_data: false)
129
+ end
130
+
131
+ it "updates the materialized view in the database with NO DATA" do
132
+ definition = instance_double("Definition", to_sql: "definition")
133
+ allow(Definition).to receive(:new)
134
+ .with(:name, 3)
135
+ .and_return(definition)
136
+
137
+ connection.update_view(
138
+ :name,
139
+ version: 3,
140
+ materialized: { no_data: true },
141
+ )
142
+
143
+ expect(Scenic.database).to have_received(:update_materialized_view)
144
+ .with(:name, definition.to_sql, no_data: true)
145
+ end
146
+
147
+ it "raises an error if not supplied a version or sql_defintion" do
148
+ expect { connection.update_view :views }.to raise_error(
149
+ ArgumentError,
150
+ /sql_definition or version must be specified/,
151
+ )
152
+ end
153
+
154
+ it "raises an error if both version and sql_defintion are provided" do
155
+ expect do
156
+ connection.update_view(
157
+ :views,
158
+ version: 1,
159
+ sql_definition: "a defintion",
160
+ )
161
+ end.to raise_error ArgumentError, /cannot both be set/
162
+ end
163
+ end
164
+
165
+ describe "replace_view" do
166
+ it "replaces the view in the database" do
167
+ definition = instance_double("Definition", to_sql: "definition")
168
+ allow(Definition).to receive(:new)
169
+ .with(:name, 3)
170
+ .and_return(definition)
171
+
172
+ connection.replace_view(:name, version: 3)
173
+
174
+ expect(Scenic.database).to have_received(:replace_view)
175
+ .with(:name, definition.to_sql)
176
+ end
177
+
178
+ it "fails to replace the materialized view in the database" do
179
+ definition = instance_double("Definition", to_sql: "definition")
180
+ allow(Definition).to receive(:new)
181
+ .with(:name, 3)
182
+ .and_return(definition)
183
+
184
+ expect do
185
+ connection.replace_view(:name, version: 3, materialized: true)
186
+ end.to raise_error(ArgumentError, /Cannot replace materialized views/)
187
+ end
188
+
189
+ it "raises an error if not supplied a version" do
190
+ expect { connection.replace_view :views }
191
+ .to raise_error(ArgumentError, /version is required/)
192
+ end
193
+ end
194
+
195
+ def connection
196
+ Class.new { extend Statements }
197
+ end
198
+ end
199
+ end