scenic 1.3.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d319d2791defb8c1025c78605b3003c0275664ba
4
- data.tar.gz: 0c79eb3cb32bf171ec085987b83ac89df8bd4ffe
3
+ metadata.gz: 5bf3b09e1e09c0e1256111963adf6345e7c48a95
4
+ data.tar.gz: e3a945e6bb156e49837cc945c120096c9664cda9
5
5
  SHA512:
6
- metadata.gz: f858662b6d60d851a0aa402401c750da7a9c482d018e49e6fcdde39cd5672bd4d8cbd5593f422afa7541c0867805f28e0a73cb1add550c0742552851f67359a3
7
- data.tar.gz: 81c7ec7accf60272ff77476c73b31ed7b332334874d150b655b5933a4546be4a009a75c9dcd4a2d93a623de702c34c6e6f80bb76d5f3f8665ff65a56dc2ca896
6
+ metadata.gz: 64fee8693cc22dd425049c19efde6f47ca19cf42a3002dd1a3b89f511e6fd9801a1f5d040988819457551e8db54e7b4d03662af373489bad2b442e3078ddaff0
7
+ data.tar.gz: bf1ca04759e648a4422289e1c2b84b13598956c5470518b354a114b802c2b8aff5788d65a34d2fefb5d5a7339ed9f06b185615efda273ba12f42902b093911df
@@ -17,15 +17,28 @@ notifications:
17
17
  - false
18
18
  sudo: false
19
19
  rvm:
20
- - 2.3.0
21
- - 2.2.4
20
+ - 2.4.1
21
+ - 2.3.4
22
+ - 2.2.7
22
23
  - 2.1.8
23
24
  gemfile:
24
25
  - gemfiles/rails40.gemfile
25
26
  - gemfiles/rails41.gemfile
26
27
  - gemfiles/rails42.gemfile
27
28
  - gemfiles/rails50.gemfile
29
+ - gemfiles/rails51.gemfile
30
+ - gemfiles/rails_edge.gemfile
28
31
  matrix:
29
32
  exclude:
33
+ - rvm: 2.4.1
34
+ gemfile: gemfiles/rails40.gemfile
35
+ - rvm: 2.4.1
36
+ gemfile: gemfiles/rails41.gemfile
30
37
  - rvm: 2.1.8
31
38
  gemfile: gemfiles/rails50.gemfile
39
+ - rvm: 2.1.8
40
+ gemfile: gemfiles/rails51.gemfile
41
+ - rvm: 2.1.8
42
+ gemfile: gemfiles/rails_edge.gemfile
43
+ allow_failures:
44
+ - gemfile: gemfiles/rails_edge.gemfile
data/Appraisals CHANGED
@@ -1,11 +1,13 @@
1
- appraise "rails40" do
2
- gem "activerecord", "~> 4.0.0"
3
- gem "railties", "~> 4.0.0"
4
- end
1
+ if RUBY_VERSION < "2.4.0"
2
+ appraise "rails40" do
3
+ gem "activerecord", "~> 4.0.0"
4
+ gem "railties", "~> 4.0.0"
5
+ end
5
6
 
6
- appraise "rails41" do
7
- gem "activerecord", "~> 4.1.0"
8
- gem "railties", "~> 4.1.0"
7
+ appraise "rails41" do
8
+ gem "activerecord", "~> 4.1.0"
9
+ gem "railties", "~> 4.1.0"
10
+ end
9
11
  end
10
12
 
11
13
  appraise "rails42" do
@@ -15,12 +17,17 @@ end
15
17
 
16
18
  if RUBY_VERSION > "2.2.0"
17
19
  appraise "rails50" do
18
- gem "rails", github: "rails/rails"
19
- gem "rspec-rails", github: "rspec/rspec-rails"
20
- gem "rspec-support", github: "rspec/rspec-support"
21
- gem "rspec-core", github: "rspec/rspec-core"
22
- gem "rspec-mocks", github: "rspec/rspec-mocks"
23
- gem "rspec-expectations", github: "rspec/rspec-expectations"
24
- gem "rspec", github: "rspec/rspec"
20
+ gem "activerecord", "~> 5.0.0"
21
+ gem "railties", "~> 5.0.0"
22
+ end
23
+
24
+ appraise "rails51" do
25
+ gem "activerecord", "~> 5.1.0"
26
+ gem "railties", "~> 5.1.0"
27
+ end
28
+
29
+ appraise "rails-edge" do
30
+ gem "rails", git: "https://github.com/rails/rails"
31
+ gem "arel", git: "https://github.com/rails/arel"
25
32
  end
26
33
  end
data/NEWS.md CHANGED
@@ -5,6 +5,40 @@ changelog, see the [CHANGELOG] for each version via the version links.
5
5
 
6
6
  [CHANGELOG]: https://github.com/thoughtbot/scenic/commits/master
7
7
 
8
+ ## [1.4.0] - May 11, 2017
9
+
10
+ ### Added
11
+
12
+ - `refresh_materialized_view` now accepts a `cascade` option, which defaults to
13
+ `false`. Setting this option to `true` will refresh any materialized views the
14
+ current view depends on first, ensuring the view being refreshed has the most
15
+ up-to-date information.
16
+ - `sql_definition` argument is now supported when using `update_view`.
17
+
18
+ ### Fixed
19
+
20
+ - View migrations created under Rails 5 and newer will no longer result in
21
+ warnings.
22
+ - `ar_internal_metadata` is no longer included in the schema dump for Rails 5
23
+ and newer apps.
24
+ - Using the `scenic:model` generator will no longer create a fixture or factory.
25
+
26
+ ## [1.3.0] - May 27, 2016
27
+
28
+ ### Added
29
+ - Add `replace_view` migration statement, which issues `CREATE OR REPLACE
30
+ VIEW` rather than `CREATE VIEW` or `DROP VIEW` and `CREATE VIEW`.
31
+ - Schema-qualify views outside the 'public' namespace, such as
32
+ `scenic.searches`
33
+
34
+ ### Fixed
35
+ * Singularize generated model name when injecting into class.
36
+ Previously, pluralized names would issue a warning and Scenic would
37
+ attempt to insert model code into the pluralized model file.
38
+ * Convert shell-based smoke tests to RSpec syntax.
39
+
40
+ [1.3.0]: https://github.com/thoughtbot/scenic/compare/v1.2.0...v1.3.0
41
+
8
42
  ## [1.2.0] - February 5, 2016
9
43
 
10
44
  ### Added
data/README.md CHANGED
@@ -173,14 +173,23 @@ refreshes:
173
173
 
174
174
  ```ruby
175
175
  def self.refresh
176
- Scenic.database.refresh_materialized_view(table_name, concurrently: false)
176
+ Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
177
177
  end
178
178
  ```
179
179
 
180
180
  This will perform a non-concurrent refresh, locking the view for selects until
181
181
  the refresh is complete. You can avoid locking the view by passing
182
182
  `concurrently: true` but this requires both PostgreSQL 9.4 and your view to have
183
- at least one unique index that covers all rows.
183
+ at least one unique index that covers all rows. You can add or update indexes for
184
+ materialized views using table migration methods (e.g. `add_index table_name`)
185
+ and these will be automatically re-applied when views are updated.
186
+
187
+ The `cascade` option is to refresh materialized views that depend on other
188
+ materialized views. For example, say you have materialized view A, which selects
189
+ data from materialized view B. To get the most up to date information in view A
190
+ you would need to refresh view B first, then right after refresh view A. If you
191
+ would like this cascading refresh of materialized views, set `cascade: true`
192
+ when you refresh your materialized view.
184
193
 
185
194
  ## I don't need this view anymore. Make it go away.
186
195
 
@@ -243,7 +252,7 @@ thoughtbot, inc.
243
252
  [Derek Prior]: http://prioritized.net
244
253
  [Caleb Thompson]: http://calebthompson.io
245
254
 
246
- ![thoughtbot](https://thoughtbot.com/logo.png)
255
+ ![thoughtbot](http://presskit.thoughtbot.com/images/thoughtbot-logo-for-readmes.svg)
247
256
 
248
257
  We love open source software! See [our other projects][community] or [hire
249
258
  us][hire] to help build your product.
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rake' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rspec' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/setup CHANGED
@@ -9,4 +9,5 @@ if [ -z "$CI" ]; then
9
9
  bundle exec appraisal install
10
10
  fi
11
11
 
12
+ bundle exec rake dummy:db:drop
12
13
  bundle exec rake dummy:db:create
@@ -0,0 +1,10 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 4.2.0"
6
+ gem "railties", "~> 4.2.0"
7
+ gem "rspec-rails"
8
+ gem "factory_girl_rails"
9
+
10
+ gemspec :path => "../"
@@ -2,12 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", :github => "rails/rails"
6
- gem "rspec-rails", :github => "rspec/rspec-rails"
7
- gem "rspec-support", :github => "rspec/rspec-support"
8
- gem "rspec-core", :github => "rspec/rspec-core"
9
- gem "rspec-mocks", :github => "rspec/rspec-mocks"
10
- gem "rspec-expectations", :github => "rspec/rspec-expectations"
11
- gem "rspec", :github => "rspec/rspec"
5
+ gem "activerecord", "~> 5.0.0"
6
+ gem "railties", "~> 5.0.0"
12
7
 
13
8
  gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.1.0"
6
+ gem "railties", "~> 5.1.0"
7
+
8
+ gemspec :path => "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", :git => "https://github.com/rails/rails"
6
+ gem "arel", :git => "https://github.com/rails/arel"
7
+
8
+ gemspec :path => "../"
@@ -11,7 +11,12 @@ module Scenic
11
11
  source_root File.expand_path("../templates", __FILE__)
12
12
 
13
13
  def invoke_rails_model_generator
14
- invoke "model", [file_path.singularize], options.merge(migration: false)
14
+ invoke "model",
15
+ [file_path.singularize],
16
+ options.merge(
17
+ fixture_replacement: false,
18
+ migration: false,
19
+ )
15
20
  end
16
21
 
17
22
  def inject_model_methods
@@ -1,3 +1,3 @@
1
1
  def self.refresh
2
- Scenic.database.refresh_materialized_view(table_name, concurrently: false)
2
+ Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
3
3
  end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < ActiveRecord::Migration
1
+ class <%= migration_class_name %> < <%= activerecord_migration_class %>
2
2
  def change
3
3
  create_view <%= formatted_plural_name %><%= ", materialized: true" if materialized? %>
4
4
  end
@@ -1,4 +1,4 @@
1
- class <%= migration_class_name %> < ActiveRecord::Migration
1
+ class <%= migration_class_name %> < <%= activerecord_migration_class %>
2
2
  def change
3
3
  <%- if materialized? -%>
4
4
  update_view <%= formatted_plural_name %>,
@@ -61,6 +61,14 @@ module Scenic
61
61
  "Update#{class_name.pluralize}ToVersion#{version}"
62
62
  end
63
63
  end
64
+
65
+ def activerecord_migration_class
66
+ if ActiveRecord::Migration.respond_to?(:current_version)
67
+ "ActiveRecord::Migration[5.0]"
68
+ else
69
+ "ActiveRecord::Migration"
70
+ end
71
+ end
64
72
  end
65
73
 
66
74
  private
@@ -3,6 +3,7 @@ require_relative "postgres/errors"
3
3
  require_relative "postgres/index_reapplication"
4
4
  require_relative "postgres/indexes"
5
5
  require_relative "postgres/views"
6
+ require_relative "postgres/refresh_dependencies"
6
7
 
7
8
  module Scenic
8
9
  # Scenic database adapters.
@@ -192,11 +193,14 @@ module Scenic
192
193
  # @example Non-concurrent refresh
193
194
  # Scenic.database.refresh_materialized_view(:search_results)
194
195
  # @example Concurrent refresh
195
- # Scenic.database.refresh_materialized_view(:posts, concurrent: true)
196
+ # Scenic.database.refresh_materialized_view(:posts, concurrently: true)
196
197
  #
197
198
  # @return [void]
198
- def refresh_materialized_view(name, concurrently: false)
199
+ def refresh_materialized_view(name, concurrently: false, cascade: false)
199
200
  raise_unless_materialized_views_supported
201
+ if cascade
202
+ refresh_dependencies_for(name)
203
+ end
200
204
 
201
205
  if concurrently
202
206
  raise_unless_concurrent_refresh_supported
@@ -226,6 +230,14 @@ module Scenic
226
230
  raise ConcurrentRefreshesNotSupportedError
227
231
  end
228
232
  end
233
+
234
+ def refresh_dependencies_for(name)
235
+ Scenic::Adapters::Postgres::RefreshDependencies.call(
236
+ name,
237
+ self,
238
+ connection,
239
+ )
240
+ end
229
241
  end
230
242
  end
231
243
  end
@@ -1,7 +1,7 @@
1
1
  module Scenic
2
2
  module Adapters
3
3
  class Postgres
4
- # Updatine a materialized view causes the view to be dropped and
4
+ # Updating a materialized view causes the view to be dropped and
5
5
  # recreated. This causes any associated indexes to be dropped as well.
6
6
  # This object can be used to capture the existing indexes before the drop
7
7
  # and then reapply appropriate indexes following the create.
@@ -0,0 +1,102 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ class RefreshDependencies
5
+ def self.call(name, adapter, connection)
6
+ new(name, adapter, connection).call
7
+ end
8
+
9
+ def initialize(name, adapter, connection)
10
+ @name = name
11
+ @adapter = adapter
12
+ @connection = connection
13
+ end
14
+
15
+ def call
16
+ dependencies.each do |dependency|
17
+ adapter.refresh_materialized_view(dependency)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :name, :adapter, :connection
24
+
25
+ class DependencyParser
26
+ def initialize(raw_dependencies, view_to_refresh)
27
+ @raw_dependencies = raw_dependencies
28
+ @view_to_refresh = view_to_refresh
29
+ end
30
+
31
+ # We're given an array from the SQL query that looks kind of like this
32
+ # [["view_name", "{'dependency_1', 'dependency_2'}"]]
33
+ #
34
+ # We need to parse that into a more easy to understand data type so we
35
+ # can use the Tsort module from the Standard Library to topologically
36
+ # sort those out so we can refresh in the correct order, so we parse
37
+ # that raw data into a hash.
38
+ #
39
+ # Then, once Tsort has worked it magic, we're given a sorted 1-D array
40
+ # ["dependency_1", "dependency_2", "view_name"]
41
+ #
42
+ # So we then need to slice off just the bit leading up to the view
43
+ # that we're refreshing, so we find where in the topologically sorted
44
+ # array our given view is, and return all the dependencies up to that
45
+ # point.
46
+ def to_sorted_array
47
+ dependency_hash = parse_to_hash(raw_dependencies)
48
+ sorted_arr = tsort(dependency_hash)
49
+ idx = sorted_arr.find_index do |dep|
50
+ dep.include?(view_to_refresh.to_s)
51
+ end
52
+ sorted_arr[0...idx]
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :raw_dependencies, :view_to_refresh
58
+
59
+ def parse_to_hash(dependency_rows)
60
+ dependency_rows.each_with_object({}) do |row, hash|
61
+ formatted_dependencies = row.last.tr("{}", "").split(",")
62
+ formatted_dependencies.each do |dependency|
63
+ hash[dependency] = [] unless hash[dependency]
64
+ end
65
+ hash[row.first] = formatted_dependencies
66
+ end
67
+ end
68
+
69
+ def tsort(hash)
70
+ each_node = lambda { |&b| hash.each_key(&b) }
71
+ each_child = lambda { |n, &b| hash[n].each(&b) }
72
+ TSort.tsort(each_node, each_child)
73
+ end
74
+ end
75
+
76
+ DEPENDENCY_SQL = <<-SQL.freeze
77
+ SELECT rewrite_namespace.nspname || '.' || class_for_rewrite.relname AS materialized_view,
78
+ array_agg(depend_namespace.nspname || '.' || class_for_depend.relname) AS depends_on
79
+ FROM pg_rewrite AS rewrite
80
+ JOIN pg_class AS class_for_rewrite ON rewrite.ev_class = class_for_rewrite.oid
81
+ JOIN pg_depend AS depend ON rewrite.oid = depend.objid
82
+ JOIN pg_class AS class_for_depend ON depend.refobjid = class_for_depend.oid
83
+ JOIN pg_namespace AS rewrite_namespace ON rewrite_namespace.oid = class_for_rewrite.relnamespace
84
+ JOIN pg_namespace AS depend_namespace ON depend_namespace.oid = class_for_depend.relnamespace
85
+ WHERE class_for_depend.relkind = 'm'
86
+ AND class_for_rewrite.relkind = 'm'
87
+ AND class_for_depend.relname != class_for_rewrite.relname
88
+ GROUP BY class_for_rewrite.relname, rewrite_namespace.nspname
89
+ ORDER BY class_for_rewrite.relname;
90
+ SQL
91
+
92
+ private_constant "DependencyParser"
93
+ private_constant "DEPENDENCY_SQL"
94
+
95
+ def dependencies
96
+ raw_dependency_info = connection.select_rows(DEPENDENCY_SQL)
97
+ DependencyParser.new(raw_dependency_info, name).to_sorted_array
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -43,9 +43,9 @@ module Scenic
43
43
  namespace, viewname = result.values_at "namespace", "viewname"
44
44
 
45
45
  if namespace != "public"
46
- namespaced_viewname = "#{namespace}.#{viewname}"
46
+ namespaced_viewname = "#{pg_identifier(namespace)}.#{pg_identifier(viewname)}"
47
47
  else
48
- namespaced_viewname = viewname
48
+ namespaced_viewname = pg_identifier(viewname)
49
49
  end
50
50
 
51
51
  Scenic::View.new(
@@ -54,6 +54,11 @@ module Scenic
54
54
  materialized: result["kind"] == "m",
55
55
  )
56
56
  end
57
+
58
+ def pg_identifier(name)
59
+ return name if name =~ /^[a-zA-Z_][a-zA-Z0-9_]*$/
60
+ PGconn.quote_ident(name)
61
+ end
57
62
  end
58
63
  end
59
64
  end
@@ -27,7 +27,7 @@ module Scenic
27
27
  end
28
28
  end
29
29
 
30
- unless ActiveRecord::SchemaDumper.instance_methods(false).include?(:ignored?)
30
+ unless ActiveRecord::SchemaDumper.private_instance_methods(false).include?(:ignored?)
31
31
  # This method will be present in Rails 4.2.0 and can be removed then.
32
32
  def ignored?(table_name)
33
33
  ["schema_migrations", ignore_tables].flatten.any? do |ignored|
@@ -6,9 +6,9 @@ module Scenic
6
6
  # @param name [String, Symbol] The name of the database view.
7
7
  # @param version [Fixnum] The version number of the view, used to find the
8
8
  # definition file in `db/views`. This defaults to `1` if not provided.
9
- # @param sql_definition [String] The SQL query for the view schema. If both
10
- # `sql_defintiion` and `version` are provided, `sql_definition` takes
11
- # prescedence.
9
+ # @param sql_definition [String] The SQL query for the view schema. An error
10
+ # will be raised if `sql_definition` and `version` are both set,
11
+ # as they are mutually exclusive.
12
12
  # @param materialized [Boolean] Set to true to create a materialized view.
13
13
  # Defaults to false.
14
14
  # @return The database response from executing the create statement.
@@ -21,14 +21,18 @@ module Scenic
21
21
  # SELECT * FROM users WHERE users.active = 't'
22
22
  # SQL
23
23
  #
24
- def create_view(name, version: 1, sql_definition: nil, materialized: false)
25
- if version.blank? && sql_definition.nil?
24
+ def create_view(name, version: nil, sql_definition: nil, materialized: false)
25
+ if version.present? && sql_definition.present?
26
26
  raise(
27
27
  ArgumentError,
28
- "view_definition or version_number must be specified"
28
+ "sql_definition and version cannot both be set",
29
29
  )
30
30
  end
31
31
 
32
+ if version.blank? && sql_definition.blank?
33
+ version = 1
34
+ end
35
+
32
36
  sql_definition ||= definition(name, version)
33
37
 
34
38
  if materialized
@@ -66,6 +70,9 @@ module Scenic
66
70
  #
67
71
  # @param name [String, Symbol] The name of the database view.
68
72
  # @param version [Fixnum] The version number of the view.
73
+ # @param sql_definition [String] The SQL query for the view schema. An error
74
+ # will be raised if `sql_definition` and `version` are both set,
75
+ # as they are mutually exclusive.
69
76
  # @param revert_to_version [Fixnum] The version number to rollback to on
70
77
  # `rake db rollback`
71
78
  # @param materialized [Boolean] True if updating a materialized view.
@@ -75,12 +82,22 @@ module Scenic
75
82
  # @example
76
83
  # update_view :engagement_reports, version: 3, revert_to_version: 2
77
84
  #
78
- def update_view(name, version: nil, revert_to_version: nil, materialized: false)
79
- if version.blank?
80
- raise ArgumentError, "version is required"
85
+ def update_view(name, version: nil, sql_definition: nil, revert_to_version: nil, materialized: false)
86
+ if version.blank? && sql_definition.blank?
87
+ raise(
88
+ ArgumentError,
89
+ "sql_definition or version must be specified",
90
+ )
81
91
  end
82
92
 
83
- sql_definition = definition(name, version)
93
+ if version.present? && sql_definition.present?
94
+ raise(
95
+ ArgumentError,
96
+ "sql_definition and version cannot both be set",
97
+ )
98
+ end
99
+
100
+ sql_definition ||= definition(name, version)
84
101
 
85
102
  if materialized
86
103
  Scenic.database.update_materialized_view(name, sql_definition)
@@ -1,3 +1,3 @@
1
1
  module Scenic
2
- VERSION = "1.3.0".freeze
2
+ VERSION = "1.4.0".freeze
3
3
  end
@@ -43,10 +43,9 @@ module Scenic
43
43
  # @api private
44
44
  def to_schema
45
45
  materialized_option = materialized ? "materialized: true, " : ""
46
- safe_to_symbolize_name = name.include?(".") ? "'#{name}'" : name
47
46
 
48
47
  <<-DEFINITION
49
- create_view :#{safe_to_symbolize_name}, #{materialized_option} sql_definition: <<-\SQL
48
+ create_view #{name.inspect}, #{materialized_option} sql_definition: <<-\SQL
50
49
  #{definition.indent(2)}
51
50
  SQL
52
51
 
@@ -51,6 +51,7 @@ describe "User manages views" do
51
51
 
52
52
  it "handles plural view names gracefully during generation" do
53
53
  successfully "rails generate scenic:model search_results --materialized"
54
+ successfully "rails destroy scenic:model search_results --materialized"
54
55
  end
55
56
 
56
57
  def successfully(command)
@@ -22,7 +22,8 @@ RSpec.configure do |config|
22
22
  config.after(:suite) do
23
23
  Dir.chdir("spec/dummy") do
24
24
  system <<-CMD
25
- rake db:drop db:create &&
25
+ echo &&
26
+ rake db:environment:set db:drop db:create &&
26
27
  git add -A &&
27
28
  git reset --hard HEAD 1>/dev/null &&
28
29
  rm -rf .git/ 1>/dev/null
@@ -4,3 +4,10 @@
4
4
  require File.expand_path('../config/application', __FILE__)
5
5
 
6
6
  Rails.application.load_tasks
7
+
8
+ unless Rake::Task.task_defined?('db:environment:set')
9
+ desc 'dummy task for rails versions where this task does not exist'
10
+ task 'db:environment:set' do
11
+ #no op
12
+ end
13
+ end
@@ -29,7 +29,7 @@ describe "Reverting scenic schema statements", :db do
29
29
  end
30
30
 
31
31
  def migration_for_create
32
- Class.new(::ActiveRecord::Migration) do
32
+ Class.new(migration_class) do
33
33
  def change
34
34
  create_view :greetings
35
35
  end
@@ -37,7 +37,7 @@ describe "Reverting scenic schema statements", :db do
37
37
  end
38
38
 
39
39
  def migration_for_drop
40
- Class.new(::ActiveRecord::Migration) do
40
+ Class.new(migration_class) do
41
41
  def change
42
42
  drop_view :greetings, revert_to_version: 1
43
43
  end
@@ -45,13 +45,21 @@ describe "Reverting scenic schema statements", :db do
45
45
  end
46
46
 
47
47
  def migration_for_update
48
- Class.new(::ActiveRecord::Migration) do
48
+ Class.new(migration_class) do
49
49
  def change
50
50
  update_view :greetings, version: 2, revert_to_version: 1
51
51
  end
52
52
  end
53
53
  end
54
54
 
55
+ def migration_class
56
+ if Rails::VERSION::MAJOR >= 5
57
+ ::ActiveRecord::Migration[5.0]
58
+ else
59
+ ::ActiveRecord::Migration
60
+ end
61
+ end
62
+
55
63
  def run_migration(migration, directions)
56
64
  silence_stream(STDOUT) do
57
65
  Array.wrap(directions).each do |direction|
@@ -0,0 +1,42 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ module Adapters
5
+ describe Postgres::RefreshDependencies, :db do
6
+ it "refreshes dependecies in the correct order" do
7
+ adapter = Postgres.new
8
+
9
+ adapter.create_materialized_view(
10
+ "first",
11
+ "SELECT text 'hi' AS greeting",
12
+ )
13
+
14
+ adapter.create_materialized_view(
15
+ "second",
16
+ "SELECT * from first",
17
+ )
18
+
19
+ adapter.create_materialized_view(
20
+ "third",
21
+ "SELECT * from first UNION SELECT * from second",
22
+ )
23
+
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
31
+
32
+ expect(adapter).to receive(:refresh_materialized_view).
33
+ with("public.second").ordered
34
+
35
+ expect(adapter).to receive(:refresh_materialized_view).
36
+ with("public.third").ordered
37
+
38
+ described_class.call(:fourth, adapter, ActiveRecord::Base.connection)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -100,6 +100,15 @@ module Scenic
100
100
  .to raise_error err
101
101
  end
102
102
 
103
+ it "can refresh the views dependencies first" do
104
+ connection = double("Connection").as_null_object
105
+ connectable = double("Connectable", connection: connection)
106
+ 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)
110
+ end
111
+
103
112
  context "refreshing concurrently" do
104
113
  it "raises descriptive error if concurrent refresh is not possible" do
105
114
  adapter = Postgres.new
@@ -2,6 +2,10 @@ require "spec_helper"
2
2
 
3
3
  class Search < ActiveRecord::Base; end
4
4
 
5
+ class SearchInAHaystack < ActiveRecord::Base
6
+ self.table_name = '"search in a haystack"'
7
+ end
8
+
5
9
  describe Scenic::SchemaDumper, :db do
6
10
  it "dumps a create_view for a view in the database" do
7
11
  view_definition = "SELECT 'needle'::text AS haystack"
@@ -11,7 +15,7 @@ describe Scenic::SchemaDumper, :db do
11
15
  ActiveRecord::SchemaDumper.dump(Search.connection, stream)
12
16
 
13
17
  output = stream.string
14
- expect(output).to include "create_view :searches"
18
+ expect(output).to include 'create_view "searches"'
15
19
  expect(output).to include view_definition
16
20
 
17
21
  Search.connection.drop_view :searches
@@ -31,9 +35,66 @@ describe Scenic::SchemaDumper, :db do
31
35
  ActiveRecord::SchemaDumper.dump(Search.connection, stream)
32
36
 
33
37
  output = stream.string
34
- expect(output).to include "create_view :'scenic.searches',"
38
+ expect(output).to include 'create_view "scenic.searches",'
35
39
 
36
40
  Search.connection.drop_view :'scenic.searches'
37
41
  end
38
42
  end
43
+
44
+ it "ignores tables internal to Rails" do
45
+ view_definition = "SELECT 'needle'::text AS haystack"
46
+ Search.connection.create_view :searches, sql_definition: view_definition
47
+ stream = StringIO.new
48
+
49
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
50
+
51
+ output = stream.string
52
+
53
+ expect(output).to include 'create_view "searches"'
54
+ expect(output).not_to include "ar_internal_metadata"
55
+ expect(output).not_to include "schema_migrations"
56
+ end
57
+
58
+ context "with views using unexpected characters in name" do
59
+ it "dumps a create_view for a view in the database" do
60
+ view_definition = "SELECT 'needle'::text AS haystack"
61
+ Search.connection.create_view '"search in a haystack"', sql_definition: view_definition
62
+ stream = StringIO.new
63
+
64
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
65
+
66
+ output = stream.string
67
+ expect(output).to include 'create_view "\"search in a haystack\"",'
68
+ expect(output).to include view_definition
69
+
70
+ Search.connection.drop_view :'"search in a haystack"'
71
+
72
+ silence_stream(STDOUT) { eval(output) }
73
+
74
+ expect(SearchInAHaystack.take.haystack).to eq "needle"
75
+ end
76
+ end
77
+
78
+ context "with views using unexpected characters, name including namespace" do
79
+ it "dumps a create_view for a view in the database" do
80
+ view_definition = "SELECT 'needle'::text AS haystack"
81
+ Search.connection.execute(
82
+ "CREATE SCHEMA scenic; SET search_path TO scenic, public")
83
+ Search.connection.create_view 'scenic."search in a haystack"',
84
+ sql_definition: view_definition
85
+ stream = StringIO.new
86
+
87
+ ActiveRecord::SchemaDumper.dump(Search.connection, stream)
88
+
89
+ output = stream.string
90
+ expect(output).to include 'create_view "scenic.\"search in a haystack\"",'
91
+ expect(output).to include view_definition
92
+
93
+ Search.connection.drop_view :'scenic."search in a haystack"'
94
+
95
+ silence_stream(STDOUT) { eval(output) }
96
+
97
+ expect(SearchInAHaystack.take.haystack).to eq "needle"
98
+ end
99
+ end
39
100
  end
@@ -30,9 +30,22 @@ module Scenic
30
30
  .with(:views, sql_definition)
31
31
  end
32
32
 
33
- it "raises an error if neither version nor sql_defintion are provided" do
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
34
47
  expect do
35
- connection.create_view :foo, version: nil, sql_definition: nil
48
+ connection.create_view :foo, version: 1, sql_definition: "a defintion"
36
49
  end.to raise_error ArgumentError
37
50
  end
38
51
  end
@@ -77,6 +90,15 @@ module Scenic
77
90
  .with(:name, definition.to_sql)
78
91
  end
79
92
 
93
+ it "updates a view from a text definition" do
94
+ sql_definition = "a defintion"
95
+
96
+ connection.update_view(:name, sql_definition: sql_definition)
97
+
98
+ expect(Scenic.database).to have_received(:update_view).
99
+ with(:name, sql_definition)
100
+ end
101
+
80
102
  it "updates the materialized view in the database" do
81
103
  definition = instance_double("Definition", to_sql: "definition")
82
104
  allow(Definition).to receive(:new)
@@ -85,13 +107,23 @@ module Scenic
85
107
 
86
108
  connection.update_view(:name, version: 3, materialized: true)
87
109
 
88
- expect(Scenic.database).to have_received(:update_materialized_view)
89
- .with(:name, definition.to_sql)
110
+ expect(Scenic.database).to have_received(:update_materialized_view).
111
+ with(:name, definition.to_sql)
90
112
  end
91
113
 
92
- it "raises an error if not supplied a version" do
93
- expect { connection.update_view :views }
94
- .to raise_error(ArgumentError, /version is required/)
114
+ it "raises an error if not supplied a version or sql_defintion" do
115
+ expect { connection.update_view :views }.to raise_error(
116
+ ArgumentError,
117
+ /sql_definition or version must be specified/)
118
+ end
119
+
120
+ it "raises an error if both version and sql_defintion are provided" do
121
+ expect do
122
+ connection.update_view(
123
+ :views,
124
+ version: 1,
125
+ sql_definition: "a defintion")
126
+ end.to raise_error ArgumentError, /cannot both be set/
95
127
  end
96
128
  end
97
129
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scenic
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Prior
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-05-27 00:00:00.000000000 Z
12
+ date: 2017-05-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: appraisal
@@ -201,12 +201,17 @@ files:
201
201
  - README.md
202
202
  - Rakefile
203
203
  - bin/appraisal
204
+ - bin/rake
205
+ - bin/rspec
204
206
  - bin/setup
205
207
  - bin/yard
206
208
  - gemfiles/rails40.gemfile
207
209
  - gemfiles/rails41.gemfile
208
210
  - gemfiles/rails42.gemfile
211
+ - gemfiles/rails42_with_fg_rails.gemfile
209
212
  - gemfiles/rails50.gemfile
213
+ - gemfiles/rails51.gemfile
214
+ - gemfiles/rails_edge.gemfile
210
215
  - lib/generators/scenic/generators.rb
211
216
  - lib/generators/scenic/materializable.rb
212
217
  - lib/generators/scenic/model/USAGE
@@ -222,6 +227,7 @@ files:
222
227
  - lib/scenic/adapters/postgres/errors.rb
223
228
  - lib/scenic/adapters/postgres/index_reapplication.rb
224
229
  - lib/scenic/adapters/postgres/indexes.rb
230
+ - lib/scenic/adapters/postgres/refresh_dependencies.rb
225
231
  - lib/scenic/adapters/postgres/views.rb
226
232
  - lib/scenic/command_recorder.rb
227
233
  - lib/scenic/command_recorder/statement_arguments.rb
@@ -252,6 +258,7 @@ files:
252
258
  - spec/generators/scenic/view/view_generator_spec.rb
253
259
  - spec/integration/revert_spec.rb
254
260
  - spec/scenic/adapters/postgres/connection_spec.rb
261
+ - spec/scenic/adapters/postgres/refresh_dependencies_spec.rb
255
262
  - spec/scenic/adapters/postgres/views_spec.rb
256
263
  - spec/scenic/adapters/postgres_spec.rb
257
264
  - spec/scenic/command_recorder/statement_arguments_spec.rb
@@ -283,7 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
283
290
  version: '0'
284
291
  requirements: []
285
292
  rubyforge_project:
286
- rubygems_version: 2.5.1
293
+ rubygems_version: 2.4.8
287
294
  signing_key:
288
295
  specification_version: 4
289
296
  summary: Support for database views in Rails migrations
@@ -306,6 +313,7 @@ test_files:
306
313
  - spec/generators/scenic/view/view_generator_spec.rb
307
314
  - spec/integration/revert_spec.rb
308
315
  - spec/scenic/adapters/postgres/connection_spec.rb
316
+ - spec/scenic/adapters/postgres/refresh_dependencies_spec.rb
309
317
  - spec/scenic/adapters/postgres/views_spec.rb
310
318
  - spec/scenic/adapters/postgres_spec.rb
311
319
  - spec/scenic/command_recorder/statement_arguments_spec.rb
@@ -317,4 +325,3 @@ test_files:
317
325
  - spec/spec_helper.rb
318
326
  - spec/support/generator_spec_setup.rb
319
327
  - spec/support/view_definition_helpers.rb
320
- has_rdoc: