scenic 1.4.1 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.devcontainer/Dockerfile +6 -0
  3. data/.devcontainer/devcontainer.json +11 -0
  4. data/.devcontainer/docker-compose.yml +24 -0
  5. data/.github/workflows/ci.yml +71 -0
  6. data/.gitignore +1 -0
  7. data/.hound.yml +2 -4
  8. data/.rubocop.yml +129 -0
  9. data/{NEWS.md → CHANGELOG.md} +113 -15
  10. data/CODE_OF_CONDUCT.md +76 -0
  11. data/CONTRIBUTING.md +7 -9
  12. data/Gemfile +13 -1
  13. data/LICENSE.txt +1 -1
  14. data/README.md +34 -38
  15. data/Rakefile +2 -2
  16. data/SECURITY.md +14 -0
  17. data/bin/setup +9 -4
  18. data/lib/generators/scenic/materializable.rb +18 -0
  19. data/lib/generators/scenic/model/model_generator.rb +19 -8
  20. data/lib/generators/scenic/view/USAGE +1 -0
  21. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  22. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +4 -3
  23. data/lib/generators/scenic/view/view_generator.rb +18 -8
  24. data/lib/scenic/adapters/postgres/index_reapplication.rb +1 -1
  25. data/lib/scenic/adapters/postgres/refresh_dependencies.rb +21 -7
  26. data/lib/scenic/adapters/postgres/views.rb +11 -1
  27. data/lib/scenic/adapters/postgres.rb +19 -6
  28. data/lib/scenic/command_recorder/statement_arguments.rb +20 -2
  29. data/lib/scenic/command_recorder.rb +6 -1
  30. data/lib/scenic/configuration.rb +1 -1
  31. data/lib/scenic/definition.rb +4 -2
  32. data/lib/scenic/schema_dumper.rb +2 -2
  33. data/lib/scenic/statements.rb +24 -6
  34. data/lib/scenic/unaffixed_name.rb +31 -0
  35. data/lib/scenic/version.rb +1 -1
  36. data/lib/scenic/view.rb +7 -4
  37. data/lib/scenic.rb +1 -0
  38. data/scenic.gemspec +21 -23
  39. data/spec/acceptance/user_manages_views_spec.rb +2 -1
  40. data/spec/dummy/app/models/application_record.rb +5 -0
  41. data/spec/dummy/config/database.yml +5 -0
  42. data/spec/dummy/db/migrate/20220112154220_add_pg_stat_statements_extension.rb +5 -0
  43. data/spec/dummy/db/schema.rb +19 -0
  44. data/spec/generators/scenic/model/model_generator_spec.rb +1 -1
  45. data/spec/generators/scenic/view/view_generator_spec.rb +22 -4
  46. data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +66 -26
  47. data/spec/scenic/adapters/postgres_spec.rb +23 -3
  48. data/spec/scenic/command_recorder_spec.rb +15 -1
  49. data/spec/scenic/definition_spec.rb +15 -1
  50. data/spec/scenic/schema_dumper_spec.rb +48 -3
  51. data/spec/scenic/statements_spec.rb +49 -14
  52. data/spec/spec_helper.rb +5 -1
  53. data/spec/support/generator_spec_setup.rb +1 -1
  54. data/spec/support/rails_configuration_helpers.rb +10 -0
  55. metadata +32 -40
  56. data/.travis.yml +0 -44
  57. data/Appraisals +0 -33
  58. data/bin/appraisal +0 -16
  59. data/gemfiles/rails40.gemfile +0 -8
  60. data/gemfiles/rails41.gemfile +0 -8
  61. data/gemfiles/rails42.gemfile +0 -8
  62. data/gemfiles/rails50.gemfile +0 -8
  63. data/gemfiles/rails51.gemfile +0 -8
  64. data/gemfiles/rails_edge.gemfile +0 -8
@@ -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
@@ -0,0 +1,31 @@
1
+ module Scenic
2
+ # The name of a view or table according to rails.
3
+ #
4
+ # This removes any table name prefix or suffix that is configured via
5
+ # ActiveRecord. This allows, for example, the SchemaDumper to dump a view with
6
+ # its unaffixed name, consistent with how rails handles table dumping.
7
+ class UnaffixedName
8
+ # Gets the unaffixed name for the provided string
9
+ # @return [String]
10
+ #
11
+ # @param name [String] The (potentially) affixed view name
12
+ def self.for(name)
13
+ new(name, config: ActiveRecord::Base).call
14
+ end
15
+
16
+ def initialize(name, config:)
17
+ @name = name
18
+ @config = config
19
+ end
20
+
21
+ def call
22
+ prefix = Regexp.escape(config.table_name_prefix)
23
+ suffix = Regexp.escape(config.table_name_suffix)
24
+ name.sub(/\A#{prefix}(.+)#{suffix}\z/, "\\1")
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :name, :config
30
+ end
31
+ end
@@ -1,3 +1,3 @@
1
1
  module Scenic
2
- VERSION = "1.4.1".freeze
2
+ VERSION = "1.7.0".freeze
3
3
  end
data/lib/scenic/view.rb CHANGED
@@ -26,7 +26,7 @@ module Scenic
26
26
  #
27
27
  # @param name [String] The name of the view.
28
28
  # @param definition [String] The SQL for the query that defines the view.
29
- # @param materialized [String] `true` if the view is materialized.
29
+ # @param materialized [Boolean] `true` if the view is materialized.
30
30
  def initialize(name:, definition:, materialized:)
31
31
  @name = name
32
32
  @definition = definition
@@ -45,11 +45,14 @@ module Scenic
45
45
  materialized_option = materialized ? "materialized: true, " : ""
46
46
 
47
47
  <<-DEFINITION
48
- create_view #{name.inspect}, #{materialized_option} sql_definition: <<-\SQL
49
- #{definition.indent(2)}
48
+ create_view #{UnaffixedName.for(name).inspect}, #{materialized_option}sql_definition: <<-\SQL
49
+ #{escaped_definition.indent(2)}
50
50
  SQL
51
-
52
51
  DEFINITION
53
52
  end
53
+
54
+ def escaped_definition
55
+ definition.gsub("\\", "\\\\\\")
56
+ end
54
57
  end
55
58
  end
data/lib/scenic.rb CHANGED
@@ -5,6 +5,7 @@ require "scenic/definition"
5
5
  require "scenic/railtie"
6
6
  require "scenic/schema_dumper"
7
7
  require "scenic/statements"
8
+ require "scenic/unaffixed_name"
8
9
  require "scenic/version"
9
10
  require "scenic/view"
10
11
  require "scenic/index"
data/scenic.gemspec CHANGED
@@ -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) || ENV.fetch("CODESPACES", false) %>
8
+ username: <%= ENV.fetch("POSTGRES_USER") %>
9
+ password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
10
+ <% end %>
6
11
 
7
12
  test:
8
13
  <<: *default
@@ -0,0 +1,5 @@
1
+ class AddPgStatStatementsExtension < ActiveRecord::Migration[6.1]
2
+ def change
3
+ enable_extension 'pg_stat_statements'
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 2022_01_12_154220) do
14
+
15
+ # These are extensions that must be enabled in order to support this database
16
+ enable_extension "pg_stat_statements"
17
+ enable_extension "plpgsql"
18
+
19
+ end
@@ -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,39 @@ 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
+ it "uses 'replace_view' instead of 'update_view' if replace flag is set" do
41
+ with_view_definition("aired_episodes", 1, "hello") do
42
+ allow(Dir).to receive(:entries).and_return(["aired_episodes_v01.sql"])
43
+
44
+ run_generator ["aired_episode", "--replace"]
45
+ migration = migration_file(
46
+ "db/migrate/update_aired_episodes_to_version_2.rb",
47
+ )
48
+ expect(migration).to contain "replace_view"
49
+ end
50
+ end
51
+
40
52
  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")
53
+ it "creates a view definition" do
43
54
  view_definition = file("db/views/non_public_searches_v01.sql")
44
55
 
45
56
  run_generator ["non_public.search"]
46
57
 
47
- expect(migration).to be_a_migration
48
58
  expect(view_definition).to exist
49
59
  end
60
+
61
+ it "creates a migration file" do
62
+ run_generator ["non_public.search"]
63
+
64
+ migration = migration_file("db/migrate/create_non_public_searches.rb")
65
+ expect(migration).to contain(/class CreateNonPublicSearches/)
66
+ expect(migration).to contain(/create_view "non_public.searches"/)
67
+ end
50
68
  end
51
69
  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
@@ -8,11 +8,25 @@ describe Scenic::CommandRecorder do
8
8
  expect(recorder.commands).to eq [[:create_view, [:greetings], nil]]
9
9
  end
10
10
 
11
- it "reverts to drop_view" do
11
+ it "reverts to drop_view when not passed a version" do
12
12
  recorder.revert { recorder.create_view :greetings }
13
13
 
14
14
  expect(recorder.commands).to eq [[:drop_view, [:greetings]]]
15
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
16
30
  end
17
31
 
18
32
  describe "#drop_view" do
@@ -22,13 +22,27 @@ module Scenic
22
22
  end
23
23
 
24
24
  describe "path" do
25
- it "returns a sql file in db/views with padded version and view name" do
25
+ it "returns a sql file in db/views with padded version and view name" do
26
26
  expected = "db/views/searches_v01.sql"
27
27
 
28
28
  definition = Definition.new("searches", 1)
29
29
 
30
30
  expect(definition.path).to eq expected
31
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
+
39
+ it "handles active record view prefix and suffixing" do
40
+ with_affixed_tables(prefix: "foo_", suffix: "_bar") do
41
+ definition = Definition.new("foo_searches_bar", 1)
42
+
43
+ expect(definition.path).to eq "db/views/searches_v01.sql"
44
+ end
45
+ end
32
46
  end
33
47
 
34
48
  describe "full_path" do
@@ -15,7 +15,8 @@ describe Scenic::SchemaDumper, :db do
15
15
  ActiveRecord::SchemaDumper.dump(Search.connection, stream)
16
16
 
17
17
  output = stream.string
18
- expect(output).to include 'create_view "searches"'
18
+
19
+ expect(output).to include 'create_view "searches", sql_definition: <<-SQL'
19
20
  expect(output).to include view_definition
20
21
 
21
22
  Search.connection.drop_view :searches
@@ -25,6 +26,35 @@ describe Scenic::SchemaDumper, :db do
25
26
  expect(Search.first.haystack).to eq "needle"
26
27
  end
27
28
 
29
+ it "accurately dumps create view statements with a regular expression" do
30
+ view_definition = "SELECT 'needle'::text AS haystack WHERE 'a2z' ~ '\\d+'"
31
+ Search.connection.create_view :searches, sql_definition: view_definition
32
+ stream = StringIO.new
33
+
34
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
35
+
36
+ output = stream.string
37
+ expect(output).to include "~ '\\\\d+'::text"
38
+
39
+ Search.connection.drop_view :searches
40
+ silence_stream(STDOUT) { eval(output) }
41
+
42
+ expect(Search.first.haystack).to eq "needle"
43
+ end
44
+
45
+ it "dumps a create_view for a materialized view in the database" do
46
+ view_definition = "SELECT 'needle'::text AS haystack"
47
+ Search.connection.create_view :searches, materialized: true, sql_definition: view_definition
48
+ stream = StringIO.new
49
+
50
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
51
+
52
+ output = stream.string
53
+
54
+ expect(output).to include 'create_view "searches", materialized: true, sql_definition: <<-SQL'
55
+ expect(output).to include view_definition
56
+ end
57
+
28
58
  context "with views in non public schemas" do
29
59
  it "dumps a create_view including namespace for a view in the database" do
30
60
  view_definition = "SELECT 'needle'::text AS haystack"
@@ -41,6 +71,20 @@ describe Scenic::SchemaDumper, :db do
41
71
  end
42
72
  end
43
73
 
74
+ it "handles active record table name prefixes and suffixes" do
75
+ with_affixed_tables(prefix: "a_", suffix: "_z") do
76
+ view_definition = "SELECT 'needle'::text AS haystack"
77
+ Search.connection.create_view :a_searches_z, sql_definition: view_definition
78
+ stream = StringIO.new
79
+
80
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
81
+
82
+ output = stream.string
83
+
84
+ expect(output).to include 'create_view "searches"'
85
+ end
86
+ end
87
+
44
88
  it "ignores tables internal to Rails" do
45
89
  view_definition = "SELECT 'needle'::text AS haystack"
46
90
  Search.connection.create_view :searches, sql_definition: view_definition
@@ -51,7 +95,7 @@ describe Scenic::SchemaDumper, :db do
51
95
  output = stream.string
52
96
 
53
97
  expect(output).to include 'create_view "searches"'
54
- expect(output).not_to include "ar_internal_metadata"
98
+ expect(output).not_to include "pg_stat_statements_info"
55
99
  expect(output).not_to include "schema_migrations"
56
100
  end
57
101
 
@@ -79,7 +123,8 @@ describe Scenic::SchemaDumper, :db do
79
123
  it "dumps a create_view for a view in the database" do
80
124
  view_definition = "SELECT 'needle'::text AS haystack"
81
125
  Search.connection.execute(
82
- "CREATE SCHEMA scenic; SET search_path TO scenic, public")
126
+ "CREATE SCHEMA scenic; SET search_path TO scenic, public",
127
+ )
83
128
  Search.connection.create_view 'scenic."search in a haystack"',
84
129
  sql_definition: view_definition
85
130
  stream = StringIO.new