scenic 1.4.1 → 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +78 -0
- data/.hound.yml +2 -4
- data/.rubocop.yml +129 -0
- data/{NEWS.md → CHANGELOG.md} +80 -15
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +7 -9
- data/Gemfile +13 -1
- data/LICENSE.txt +1 -1
- data/README.md +22 -20
- data/Rakefile +2 -2
- data/SECURITY.md +14 -0
- data/bin/setup +9 -4
- data/lib/generators/scenic/materializable.rb +9 -0
- data/lib/generators/scenic/model/model_generator.rb +2 -2
- data/lib/generators/scenic/view/USAGE +1 -0
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
- data/lib/generators/scenic/view/view_generator.rb +18 -8
- data/lib/scenic/adapters/postgres.rb +19 -6
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
- data/lib/scenic/adapters/postgres/views.rb +10 -1
- data/lib/scenic/command_recorder.rb +2 -1
- data/lib/scenic/command_recorder/statement_arguments.rb +9 -1
- data/lib/scenic/configuration.rb +1 -1
- data/lib/scenic/definition.rb +1 -1
- data/lib/scenic/schema_dumper.rb +2 -2
- data/lib/scenic/statements.rb +24 -6
- data/lib/scenic/version.rb +1 -1
- data/lib/scenic/view.rb +1 -2
- data/scenic.gemspec +21 -23
- data/spec/acceptance/user_manages_views_spec.rb +2 -1
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/config/database.yml +5 -0
- data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
- data/spec/generators/scenic/view/view_generator_spec.rb +10 -4
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
- data/spec/scenic/adapters/postgres_spec.rb +23 -3
- data/spec/scenic/command_recorder_spec.rb +15 -1
- data/spec/scenic/definition_spec.rb +7 -1
- data/spec/scenic/schema_dumper_spec.rb +17 -2
- data/spec/scenic/statements_spec.rb +48 -13
- data/spec/spec_helper.rb +1 -1
- data/spec/support/generator_spec_setup.rb +1 -1
- metadata +22 -40
- data/.travis.yml +0 -44
- data/Appraisals +0 -33
- data/bin/appraisal +0 -16
- data/gemfiles/rails40.gemfile +0 -8
- data/gemfiles/rails41.gemfile +0 -8
- data/gemfiles/rails42.gemfile +0 -8
- data/gemfiles/rails50.gemfile +0 -8
- data/gemfiles/rails51.gemfile +0 -8
- data/gemfiles/rails_edge.gemfile +0 -8
@@ -57,7 +57,16 @@ module Scenic
|
|
57
57
|
|
58
58
|
def pg_identifier(name)
|
59
59
|
return name if name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
60
|
-
|
60
|
+
|
61
|
+
pgconn.quote_ident(name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def pgconn
|
65
|
+
if defined?(PG::Connection)
|
66
|
+
PG::Connection
|
67
|
+
else
|
68
|
+
PGconn
|
69
|
+
end
|
61
70
|
end
|
62
71
|
end
|
63
72
|
end
|
@@ -22,8 +22,12 @@ module Scenic
|
|
22
22
|
StatementArguments.new([view, options_for_revert])
|
23
23
|
end
|
24
24
|
|
25
|
+
def remove_version
|
26
|
+
StatementArguments.new([view, options_without_version])
|
27
|
+
end
|
28
|
+
|
25
29
|
def to_a
|
26
|
-
@args.to_a
|
30
|
+
@args.to_a.dup.delete_if(&:empty?)
|
27
31
|
end
|
28
32
|
|
29
33
|
private
|
@@ -38,6 +42,10 @@ module Scenic
|
|
38
42
|
revert_options.delete(:revert_to_version)
|
39
43
|
end
|
40
44
|
end
|
45
|
+
|
46
|
+
def options_without_version
|
47
|
+
options.except(:version)
|
48
|
+
end
|
41
49
|
end
|
42
50
|
end
|
43
51
|
end
|
data/lib/scenic/configuration.rb
CHANGED
data/lib/scenic/definition.rb
CHANGED
data/lib/scenic/schema_dumper.rb
CHANGED
@@ -32,8 +32,8 @@ module Scenic
|
|
32
32
|
def ignored?(table_name)
|
33
33
|
["schema_migrations", ignore_tables].flatten.any? do |ignored|
|
34
34
|
case ignored
|
35
|
-
when String
|
36
|
-
when Regexp
|
35
|
+
when String then remove_prefix_and_suffix(table_name) == ignored
|
36
|
+
when Regexp then remove_prefix_and_suffix(table_name) =~ ignored
|
37
37
|
else
|
38
38
|
raise StandardError, "ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values."
|
39
39
|
end
|
data/lib/scenic/statements.rb
CHANGED
@@ -9,8 +9,9 @@ module Scenic
|
|
9
9
|
# @param sql_definition [String] The SQL query for the view schema. An error
|
10
10
|
# will be raised if `sql_definition` and `version` are both set,
|
11
11
|
# as they are mutually exclusive.
|
12
|
-
# @param materialized [Boolean] Set to true to create a materialized
|
13
|
-
#
|
12
|
+
# @param materialized [Boolean, Hash] Set to true to create a materialized
|
13
|
+
# view. Set to { no_data: true } to create materialized view without
|
14
|
+
# loading data. Defaults to false.
|
14
15
|
# @return The database response from executing the create statement.
|
15
16
|
#
|
16
17
|
# @example Create from `db/views/searches_v02.sql`
|
@@ -36,7 +37,11 @@ module Scenic
|
|
36
37
|
sql_definition ||= definition(name, version)
|
37
38
|
|
38
39
|
if materialized
|
39
|
-
Scenic.database.create_materialized_view(
|
40
|
+
Scenic.database.create_materialized_view(
|
41
|
+
name,
|
42
|
+
sql_definition,
|
43
|
+
no_data: no_data(materialized),
|
44
|
+
)
|
40
45
|
else
|
41
46
|
Scenic.database.create_view(name, sql_definition)
|
42
47
|
end
|
@@ -75,8 +80,9 @@ module Scenic
|
|
75
80
|
# as they are mutually exclusive.
|
76
81
|
# @param revert_to_version [Fixnum] The version number to rollback to on
|
77
82
|
# `rake db rollback`
|
78
|
-
# @param materialized [Boolean] True if updating a materialized view.
|
79
|
-
#
|
83
|
+
# @param materialized [Boolean, Hash] True if updating a materialized view.
|
84
|
+
# Set to { no_data: true } to update materialized view without loading
|
85
|
+
# data. Defaults to false.
|
80
86
|
# @return The database response from executing the create statement.
|
81
87
|
#
|
82
88
|
# @example
|
@@ -100,7 +106,11 @@ module Scenic
|
|
100
106
|
sql_definition ||= definition(name, version)
|
101
107
|
|
102
108
|
if materialized
|
103
|
-
Scenic.database.update_materialized_view(
|
109
|
+
Scenic.database.update_materialized_view(
|
110
|
+
name,
|
111
|
+
sql_definition,
|
112
|
+
no_data: no_data(materialized),
|
113
|
+
)
|
104
114
|
else
|
105
115
|
Scenic.database.update_view(name, sql_definition)
|
106
116
|
end
|
@@ -141,5 +151,13 @@ module Scenic
|
|
141
151
|
def definition(name, version)
|
142
152
|
Scenic::Definition.new(name, version).to_sql
|
143
153
|
end
|
154
|
+
|
155
|
+
def no_data(materialized)
|
156
|
+
if materialized.is_a?(Hash)
|
157
|
+
materialized.fetch(:no_data, false)
|
158
|
+
else
|
159
|
+
false
|
160
|
+
end
|
161
|
+
end
|
144
162
|
end
|
145
163
|
end
|
data/lib/scenic/version.rb
CHANGED
data/lib/scenic/view.rb
CHANGED
@@ -45,10 +45,9 @@ module Scenic
|
|
45
45
|
materialized_option = materialized ? "materialized: true, " : ""
|
46
46
|
|
47
47
|
<<-DEFINITION
|
48
|
-
create_view #{name.inspect}, #{materialized_option}
|
48
|
+
create_view #{name.inspect}, #{materialized_option}sql_definition: <<-\SQL
|
49
49
|
#{definition.indent(2)}
|
50
50
|
SQL
|
51
|
-
|
52
51
|
DEFINITION
|
53
52
|
end
|
54
53
|
end
|
data/scenic.gemspec
CHANGED
@@ -1,38 +1,36 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "scenic/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
6
|
+
spec.name = "scenic"
|
8
7
|
spec.version = Scenic::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
8
|
+
spec.authors = ["Derek Prior", "Caleb Hearth"]
|
9
|
+
spec.email = ["derekprior@gmail.com", "caleb@calebhearth.com"]
|
10
|
+
spec.summary = "Support for database views in Rails migrations"
|
12
11
|
spec.description = <<-DESCRIPTION
|
13
12
|
Adds methods to ActiveRecord::Migration to create and manage database views
|
14
13
|
in Rails
|
15
14
|
DESCRIPTION
|
16
|
-
spec.homepage =
|
17
|
-
spec.license =
|
15
|
+
spec.homepage = "https://github.com/scenic-views/scenic"
|
16
|
+
spec.license = "MIT"
|
18
17
|
|
19
18
|
spec.files = `git ls-files -z`.split("\x0")
|
20
19
|
spec.test_files = spec.files.grep(%r{^spec/})
|
21
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ["lib"]
|
22
21
|
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
30
|
-
spec.add_development_dependency
|
31
|
-
spec.add_development_dependency
|
32
|
-
spec.add_development_dependency 'redcarpet'
|
22
|
+
spec.add_development_dependency "bundler", ">= 1.5"
|
23
|
+
spec.add_development_dependency "database_cleaner"
|
24
|
+
spec.add_development_dependency "rake"
|
25
|
+
spec.add_development_dependency "rspec", ">= 3.3"
|
26
|
+
spec.add_development_dependency "pg", "~> 0.19"
|
27
|
+
spec.add_development_dependency "pry"
|
28
|
+
spec.add_development_dependency "ammeter", ">= 1.1.3"
|
29
|
+
spec.add_development_dependency "yard"
|
30
|
+
spec.add_development_dependency "redcarpet"
|
33
31
|
|
34
|
-
spec.add_dependency
|
35
|
-
spec.add_dependency
|
32
|
+
spec.add_dependency "activerecord", ">= 4.0.0"
|
33
|
+
spec.add_dependency "railties", ">= 4.0.0"
|
36
34
|
|
37
|
-
spec.required_ruby_version =
|
35
|
+
spec.required_ruby_version = ">= 2.3.0"
|
38
36
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "acceptance_helper"
|
2
|
+
require "English"
|
2
3
|
|
3
4
|
describe "User manages views" do
|
4
5
|
it "handles simple views" do
|
@@ -56,7 +57,7 @@ describe "User manages views" do
|
|
56
57
|
|
57
58
|
def successfully(command)
|
58
59
|
`RAILS_ENV=test #{command}`
|
59
|
-
expect(
|
60
|
+
expect($CHILD_STATUS.exitstatus).to eq(0), "'#{command}' was unsuccessful"
|
60
61
|
end
|
61
62
|
|
62
63
|
def write_definition(file, contents)
|
@@ -2,7 +2,12 @@ development: &default
|
|
2
2
|
adapter: postgresql
|
3
3
|
database: dummy_development
|
4
4
|
encoding: unicode
|
5
|
+
host: localhost
|
5
6
|
pool: 5
|
7
|
+
<% if ENV.fetch("GITHUB_ACTIONS", false) %>
|
8
|
+
username: <%= ENV.fetch("POSTGRES_USER") %>
|
9
|
+
password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
|
10
|
+
<% end %>
|
6
11
|
|
7
12
|
test:
|
8
13
|
<<: *default
|
@@ -31,21 +31,27 @@ describe Scenic::Generators::ViewGenerator, :generator do
|
|
31
31
|
|
32
32
|
run_generator ["aired_episode", "--materialized"]
|
33
33
|
migration = migration_file(
|
34
|
-
"db/migrate/update_aired_episodes_to_version_2.rb"
|
34
|
+
"db/migrate/update_aired_episodes_to_version_2.rb",
|
35
35
|
)
|
36
36
|
expect(migration).to contain "materialized: true"
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
40
|
context "for views created in a schema other than 'public'" do
|
41
|
-
it "creates view definition
|
42
|
-
migration = file("db/migrate/create_non_public_searches.rb")
|
41
|
+
it "creates a view definition" do
|
43
42
|
view_definition = file("db/views/non_public_searches_v01.sql")
|
44
43
|
|
45
44
|
run_generator ["non_public.search"]
|
46
45
|
|
47
|
-
expect(migration).to be_a_migration
|
48
46
|
expect(view_definition).to exist
|
49
47
|
end
|
48
|
+
|
49
|
+
it "creates a migration file" do
|
50
|
+
run_generator ["non_public.search"]
|
51
|
+
|
52
|
+
migration = migration_file("db/migrate/create_non_public_searches.rb")
|
53
|
+
expect(migration).to contain(/class CreateNonPublicSearches/)
|
54
|
+
expect(migration).to contain(/create_view "non_public.searches"/)
|
55
|
+
end
|
50
56
|
end
|
51
57
|
end
|
@@ -3,39 +3,79 @@ require "spec_helper"
|
|
3
3
|
module Scenic
|
4
4
|
module Adapters
|
5
5
|
describe Postgres::RefreshDependencies, :db do
|
6
|
-
|
7
|
-
adapter
|
6
|
+
context "view has dependencies" do
|
7
|
+
let(:adapter) { Postgres.new }
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
before do
|
10
|
+
adapter.create_materialized_view(
|
11
|
+
"first",
|
12
|
+
"SELECT text 'hi' AS greeting",
|
13
|
+
)
|
14
|
+
adapter.create_materialized_view(
|
15
|
+
"second",
|
16
|
+
"SELECT * FROM first",
|
17
|
+
)
|
18
|
+
adapter.create_materialized_view(
|
19
|
+
"third",
|
20
|
+
"SELECT * FROM first UNION SELECT * FROM second",
|
21
|
+
)
|
22
|
+
adapter.create_materialized_view(
|
23
|
+
"fourth_1",
|
24
|
+
"SELECT * FROM third",
|
25
|
+
)
|
26
|
+
adapter.create_materialized_view(
|
27
|
+
"x_fourth",
|
28
|
+
"SELECT * FROM fourth_1",
|
29
|
+
)
|
30
|
+
adapter.create_materialized_view(
|
31
|
+
"fourth",
|
32
|
+
"SELECT * FROM fourth_1 UNION SELECT * FROM x_fourth",
|
33
|
+
)
|
13
34
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
35
|
+
expect(adapter).to receive(:refresh_materialized_view)
|
36
|
+
.with("public.first", concurrently: true).ordered
|
37
|
+
expect(adapter).to receive(:refresh_materialized_view)
|
38
|
+
.with("public.second", concurrently: true).ordered
|
39
|
+
expect(adapter).to receive(:refresh_materialized_view)
|
40
|
+
.with("public.third", concurrently: true).ordered
|
41
|
+
expect(adapter).to receive(:refresh_materialized_view)
|
42
|
+
.with("public.fourth_1", concurrently: true).ordered
|
43
|
+
expect(adapter).to receive(:refresh_materialized_view)
|
44
|
+
.with("public.x_fourth", concurrently: true).ordered
|
45
|
+
end
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
47
|
+
it "refreshes in the right order when called without namespace" do
|
48
|
+
described_class.call(
|
49
|
+
:fourth,
|
50
|
+
adapter,
|
51
|
+
ActiveRecord::Base.connection,
|
52
|
+
concurrently: true,
|
53
|
+
)
|
54
|
+
end
|
23
55
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
56
|
+
it "refreshes in the right order when called with namespace" do
|
57
|
+
described_class.call(
|
58
|
+
"public.fourth",
|
59
|
+
adapter,
|
60
|
+
ActiveRecord::Base.connection,
|
61
|
+
concurrently: true,
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
31
65
|
|
32
|
-
|
33
|
-
|
66
|
+
context "view has no dependencies" do
|
67
|
+
it "does not raise an error" do
|
68
|
+
adapter = Postgres.new
|
34
69
|
|
35
|
-
|
36
|
-
|
70
|
+
adapter.create_materialized_view(
|
71
|
+
"first",
|
72
|
+
"SELECT text 'hi' AS greeting",
|
73
|
+
)
|
37
74
|
|
38
|
-
|
75
|
+
expect {
|
76
|
+
described_class.call(:first, adapter, ActiveRecord::Base.connection)
|
77
|
+
}.not_to raise_error
|
78
|
+
end
|
39
79
|
end
|
40
80
|
end
|
41
81
|
end
|
@@ -27,6 +27,20 @@ module Scenic
|
|
27
27
|
expect(view.materialized).to eq true
|
28
28
|
end
|
29
29
|
|
30
|
+
it "handles semicolon in definition when using `with no data`" do
|
31
|
+
adapter = Postgres.new
|
32
|
+
|
33
|
+
adapter.create_materialized_view(
|
34
|
+
"greetings",
|
35
|
+
"SELECT text 'hi' AS greeting; \n",
|
36
|
+
no_data: true,
|
37
|
+
)
|
38
|
+
|
39
|
+
view = adapter.views.first
|
40
|
+
expect(view.name).to eq("greetings")
|
41
|
+
expect(view.materialized).to eq true
|
42
|
+
end
|
43
|
+
|
30
44
|
it "raises an exception if the version of PostgreSQL is too old" do
|
31
45
|
connection = double("Connection", supports_materialized_views?: false)
|
32
46
|
connectable = double("Connectable", connection: connection)
|
@@ -104,9 +118,15 @@ module Scenic
|
|
104
118
|
connection = double("Connection").as_null_object
|
105
119
|
connectable = double("Connectable", connection: connection)
|
106
120
|
adapter = Postgres.new(connectable)
|
107
|
-
expect(Scenic::Adapters::Postgres::RefreshDependencies)
|
108
|
-
to receive(:call)
|
109
|
-
|
121
|
+
expect(Scenic::Adapters::Postgres::RefreshDependencies)
|
122
|
+
.to receive(:call)
|
123
|
+
.with(:tests, adapter, connection, concurrently: true)
|
124
|
+
|
125
|
+
adapter.refresh_materialized_view(
|
126
|
+
:tests,
|
127
|
+
cascade: true,
|
128
|
+
concurrently: true,
|
129
|
+
)
|
110
130
|
end
|
111
131
|
|
112
132
|
context "refreshing concurrently" do
|