scenic 1.4.1 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +78 -0
  3. data/.hound.yml +2 -4
  4. data/.rubocop.yml +129 -0
  5. data/{NEWS.md → CHANGELOG.md} +80 -15
  6. data/CODE_OF_CONDUCT.md +76 -0
  7. data/CONTRIBUTING.md +7 -9
  8. data/Gemfile +13 -1
  9. data/LICENSE.txt +1 -1
  10. data/README.md +22 -20
  11. data/Rakefile +2 -2
  12. data/SECURITY.md +14 -0
  13. data/bin/setup +9 -4
  14. data/lib/generators/scenic/materializable.rb +9 -0
  15. data/lib/generators/scenic/model/model_generator.rb +2 -2
  16. data/lib/generators/scenic/view/USAGE +1 -0
  17. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  18. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +1 -1
  19. data/lib/generators/scenic/view/view_generator.rb +18 -8
  20. data/lib/scenic/adapters/postgres.rb +19 -6
  21. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
  22. data/lib/scenic/adapters/postgres/views.rb +10 -1
  23. data/lib/scenic/command_recorder.rb +2 -1
  24. data/lib/scenic/command_recorder/statement_arguments.rb +9 -1
  25. data/lib/scenic/configuration.rb +1 -1
  26. data/lib/scenic/definition.rb +1 -1
  27. data/lib/scenic/schema_dumper.rb +2 -2
  28. data/lib/scenic/statements.rb +24 -6
  29. data/lib/scenic/version.rb +1 -1
  30. data/lib/scenic/view.rb +1 -2
  31. data/scenic.gemspec +21 -23
  32. data/spec/acceptance/user_manages_views_spec.rb +2 -1
  33. data/spec/dummy/app/models/application_record.rb +5 -0
  34. data/spec/dummy/config/database.yml +5 -0
  35. data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
  36. data/spec/generators/scenic/view/view_generator_spec.rb +10 -4
  37. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
  38. data/spec/scenic/adapters/postgres_spec.rb +23 -3
  39. data/spec/scenic/command_recorder_spec.rb +15 -1
  40. data/spec/scenic/definition_spec.rb +7 -1
  41. data/spec/scenic/schema_dumper_spec.rb +17 -2
  42. data/spec/scenic/statements_spec.rb +48 -13
  43. data/spec/spec_helper.rb +1 -1
  44. data/spec/support/generator_spec_setup.rb +1 -1
  45. metadata +22 -40
  46. data/.travis.yml +0 -44
  47. data/Appraisals +0 -33
  48. data/bin/appraisal +0 -16
  49. data/gemfiles/rails40.gemfile +0 -8
  50. data/gemfiles/rails41.gemfile +0 -8
  51. data/gemfiles/rails42.gemfile +0 -8
  52. data/gemfiles/rails50.gemfile +0 -8
  53. data/gemfiles/rails51.gemfile +0 -8
  54. 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
- PGconn.quote_ident(name)
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
@@ -20,7 +20,8 @@ module Scenic
20
20
  end
21
21
 
22
22
  def invert_create_view(args)
23
- [:drop_view, args]
23
+ drop_view_args = StatementArguments.new(args).remove_version.to_a
24
+ [:drop_view, drop_view_args]
24
25
  end
25
26
 
26
27
  def invert_drop_view(args)
@@ -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
@@ -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 {Adapters::Postgres}
5
+ # Defaults to an instance of {Adapters::Postgres}
6
6
  # @return Scenic adapter
7
7
  attr_accessor :database
8
8
 
@@ -29,7 +29,7 @@ module Scenic
29
29
  private
30
30
 
31
31
  def filename
32
- "#{@name}_v#{version}.sql"
32
+ "#{@name.to_s.tr('.', '_')}_v#{version}.sql"
33
33
  end
34
34
  end
35
35
  end
@@ -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; remove_prefix_and_suffix(table_name) == ignored
36
- when Regexp; remove_prefix_and_suffix(table_name) =~ ignored
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
@@ -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 view.
13
- # Defaults to false.
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(name, sql_definition)
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
- # Defaults to false.
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(name, sql_definition)
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
@@ -1,3 +1,3 @@
1
1
  module Scenic
2
- VERSION = "1.4.1".freeze
2
+ VERSION = "1.5.4".freeze
3
3
  end
@@ -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} sql_definition: <<-\SQL
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
@@ -1,38 +1,36 @@
1
- # coding: utf-8
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 'scenic/version'
3
+ require "scenic/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = 'scenic'
6
+ spec.name = "scenic"
8
7
  spec.version = Scenic::VERSION
9
- spec.authors = ['Derek Prior', 'Caleb Thompson']
10
- spec.email = ['derekprior@gmail.com', 'caleb@calebthompson.io']
11
- spec.summary = %q{Support for database views in Rails migrations}
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 = 'https://github.com/thoughtbot/scenic'
17
- spec.license = 'MIT'
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 = ['lib']
20
+ spec.require_paths = ["lib"]
22
21
 
23
- spec.add_development_dependency 'appraisal'
24
- spec.add_development_dependency 'bundler', '>= 1.5'
25
- spec.add_development_dependency 'database_cleaner'
26
- spec.add_development_dependency 'rake'
27
- spec.add_development_dependency 'rspec', '>= 3.3'
28
- spec.add_development_dependency 'pg'
29
- spec.add_development_dependency 'pry'
30
- spec.add_development_dependency 'ammeter', '>= 1.1.3'
31
- spec.add_development_dependency 'yard'
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 'activerecord', '>= 4.0.0'
35
- spec.add_dependency 'railties', '>= 4.0.0'
32
+ spec.add_dependency "activerecord", ">= 4.0.0"
33
+ spec.add_dependency "railties", ">= 4.0.0"
36
34
 
37
- spec.required_ruby_version = '~> 2.1'
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($?.exitstatus).to eq(0), "'#{command}' was unsuccessful"
60
+ expect($CHILD_STATUS.exitstatus).to eq(0), "'#{command}' was unsuccessful"
60
61
  end
61
62
 
62
63
  def write_definition(file, contents)
@@ -0,0 +1,5 @@
1
+ if Rails::VERSION::STRING >= "5.0.0"
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -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
@@ -6,7 +6,7 @@ module Scenic::Generators
6
6
  before do
7
7
  allow(ViewGenerator).to receive(:new)
8
8
  .and_return(
9
- instance_double("Scenic::Generators::ViewGenerator").as_null_object
9
+ instance_double("Scenic::Generators::ViewGenerator").as_null_object,
10
10
  )
11
11
  end
12
12
 
@@ -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 and migration files" do
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
- it "refreshes dependecies in the correct order" do
7
- adapter = Postgres.new
6
+ context "view has dependencies" do
7
+ let(:adapter) { Postgres.new }
8
8
 
9
- adapter.create_materialized_view(
10
- "first",
11
- "SELECT text 'hi' AS greeting",
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
- adapter.create_materialized_view(
15
- "second",
16
- "SELECT * from first",
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
- adapter.create_materialized_view(
20
- "third",
21
- "SELECT * from first UNION SELECT * from second",
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
- adapter.create_materialized_view(
25
- "fourth",
26
- "SELECT * from third",
27
- )
28
-
29
- expect(adapter).to receive(:refresh_materialized_view).
30
- with("public.first").ordered
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
- expect(adapter).to receive(:refresh_materialized_view).
33
- with("public.second").ordered
66
+ context "view has no dependencies" do
67
+ it "does not raise an error" do
68
+ adapter = Postgres.new
34
69
 
35
- expect(adapter).to receive(:refresh_materialized_view).
36
- with("public.third").ordered
70
+ adapter.create_materialized_view(
71
+ "first",
72
+ "SELECT text 'hi' AS greeting",
73
+ )
37
74
 
38
- described_class.call(:fourth, adapter, ActiveRecord::Base.connection)
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).with(:tests, adapter, connection)
109
- adapter.refresh_materialized_view(:tests, cascade: true)
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