scenic 1.3.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: