scenic 1.0.0 → 1.2.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +14 -4
  4. data/.yardopts +0 -1
  5. data/Appraisals +26 -0
  6. data/CONTRIBUTING.md +5 -4
  7. data/LICENSE.txt +1 -1
  8. data/NEWS.md +49 -0
  9. data/README.md +71 -28
  10. data/bin/appraisal +16 -0
  11. data/bin/setup +5 -0
  12. data/bin/yard +16 -0
  13. data/gemfiles/rails40.gemfile +8 -0
  14. data/gemfiles/rails41.gemfile +8 -0
  15. data/gemfiles/rails42.gemfile +8 -0
  16. data/gemfiles/rails50.gemfile +13 -0
  17. data/lib/generators/scenic/generators.rb +1 -0
  18. data/lib/generators/scenic/model/templates/model.erb +1 -1
  19. data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +1 -1
  20. data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +2 -2
  21. data/lib/generators/scenic/view/view_generator.rb +13 -1
  22. data/lib/scenic/adapters/postgres/connection.rb +57 -0
  23. data/lib/scenic/adapters/postgres/errors.rb +26 -0
  24. data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
  25. data/lib/scenic/adapters/postgres/indexes.rb +53 -0
  26. data/lib/scenic/adapters/postgres/views.rb +51 -0
  27. data/lib/scenic/adapters/postgres.rb +144 -33
  28. data/lib/scenic/configuration.rb +2 -2
  29. data/lib/scenic/index.rb +36 -0
  30. data/lib/scenic/schema_dumper.rb +12 -5
  31. data/lib/scenic/statements.rb +7 -13
  32. data/lib/scenic/version.rb +1 -1
  33. data/lib/scenic/view.rb +1 -4
  34. data/lib/scenic.rb +1 -0
  35. data/scenic.gemspec +4 -1
  36. data/spec/dummy/config/application.rb +3 -0
  37. data/spec/generators/scenic/view/view_generator_spec.rb +12 -0
  38. data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
  39. data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
  40. data/spec/scenic/adapters/postgres_spec.rb +88 -26
  41. data/spec/scenic/statements_spec.rb +14 -15
  42. data/spec/smoke +24 -3
  43. data/spec/spec_helper.rb +4 -0
  44. metadata +64 -8
  45. data/spec/dummy/config/environments/development.rb +0 -6
  46. data/spec/dummy/config/environments/test.rb +0 -5
@@ -0,0 +1,53 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Fetches indexes on objects from the Postgres connection.
5
+ #
6
+ # @api private
7
+ class Indexes
8
+ def initialize(connection:)
9
+ @connection = connection
10
+ end
11
+
12
+ # Indexes on the provided object.
13
+ #
14
+ # @param name [String] The name of the object we want indexes from.
15
+ # @return [Array<Scenic::Index>]
16
+ def on(name)
17
+ indexes_on(name).map(&method(:index_from_database))
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :connection
23
+ delegate :quote_table_name, to: :connection
24
+
25
+ def indexes_on(name)
26
+ connection.execute(<<-SQL)
27
+ SELECT
28
+ t.relname as object_name,
29
+ i.relname as index_name,
30
+ pg_get_indexdef(d.indexrelid) AS definition
31
+ FROM pg_class t
32
+ INNER JOIN pg_index d ON t.oid = d.indrelid
33
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
34
+ LEFT JOIN pg_namespace n ON n.oid = i.relnamespace
35
+ WHERE i.relkind = 'i'
36
+ AND d.indisprimary = 'f'
37
+ AND t.relname = '#{name}'
38
+ AND n.nspname = ANY (current_schemas(false))
39
+ ORDER BY i.relname
40
+ SQL
41
+ end
42
+
43
+ def index_from_database(result)
44
+ Scenic::Index.new(
45
+ object_name: result["object_name"],
46
+ index_name: result["index_name"],
47
+ definition: result["definition"],
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ module Scenic
2
+ module Adapters
3
+ class Postgres
4
+ # Fetches defined views from the postgres connection.
5
+ # @api private
6
+ class Views
7
+ def initialize(connection)
8
+ @connection = connection
9
+ end
10
+
11
+ # All of the views that this connection has defined.
12
+ #
13
+ # This will include materialized views if those are supported by the
14
+ # connection.
15
+ #
16
+ # @return [Array<Scenic::View>]
17
+ def all
18
+ views_from_postgres.map(&method(:to_scenic_view))
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :connection
24
+
25
+ def views_from_postgres
26
+ connection.execute(<<-SQL)
27
+ SELECT
28
+ c.relname as viewname,
29
+ pg_get_viewdef(c.oid) AS definition,
30
+ c.relkind AS kind
31
+ FROM pg_class c
32
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
33
+ WHERE
34
+ c.relkind IN ('m', 'v')
35
+ AND c.relname NOT IN (SELECT extname FROM pg_extension)
36
+ AND n.nspname = ANY (current_schemas(false))
37
+ ORDER BY c.oid
38
+ SQL
39
+ end
40
+
41
+ def to_scenic_view(result)
42
+ Scenic::View.new(
43
+ name: result["viewname"],
44
+ definition: result["definition"].strip,
45
+ materialized: result["kind"] == "m",
46
+ )
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,9 @@
1
+ require_relative "postgres/connection"
2
+ require_relative "postgres/errors"
3
+ require_relative "postgres/index_reapplication"
4
+ require_relative "postgres/indexes"
5
+ require_relative "postgres/views"
6
+
1
7
  module Scenic
2
8
  # Scenic database adapters.
3
9
  #
@@ -7,14 +13,31 @@ module Scenic
7
13
  module Adapters
8
14
  # An adapter for managing Postgres views.
9
15
  #
10
- # **This object is used internally by adapters and the schema dumper and is
11
- # not intended to be used by application code. It is documented here for
12
- # use by adapter gems.**
13
- #
14
- # For methods usable in migrations see {Statements}.
16
+ # These methods are used interally by Scenic and are not intended for direct
17
+ # use. Methods that alter database schema are intended to be called via
18
+ # {Statements}, while {#refresh_materialized_view} is called via
19
+ # {Scenic.database}.
15
20
  #
16
- # @api extension
21
+ # The methods are documented here for insight into specifics of how Scenic
22
+ # integrates with Postgres and the responsibilities of {Adapters}.
17
23
  class Postgres
24
+ # Creates an instance of the Scenic Postgres adapter.
25
+ #
26
+ # This is the default adapter for Scenic. Configuring it via
27
+ # {Scenic.configure} is not required, but the example below shows how one
28
+ # would explicitly set it.
29
+ #
30
+ # @param [#connection] connectable An object that returns the connection
31
+ # for Scenic to use. Defaults to `ActiveRecord::Base`.
32
+ #
33
+ # @example
34
+ # Scenic.configure do |config|
35
+ # config.adapter = Scenic::Adapters::Postgres.new
36
+ # end
37
+ def initialize(connectable = ActiveRecord::Base)
38
+ @connectable = connectable
39
+ end
40
+
18
41
  # Returns an array of views in the database.
19
42
  #
20
43
  # This collection of views is used by the [Scenic::SchemaDumper] to
@@ -22,73 +45,161 @@ module Scenic
22
45
  #
23
46
  # @return [Array<Scenic::View>]
24
47
  def views
25
- execute(<<-SQL).map { |result| view_from_database(result) }
26
- SELECT viewname, definition, FALSE AS materialized
27
- FROM pg_views
28
- WHERE schemaname = ANY (current_schemas(false))
29
- AND viewname NOT IN (SELECT extname FROM pg_extension)
30
- UNION
31
- SELECT matviewname AS viewname, definition, TRUE AS materialized
32
- FROM pg_matviews
33
- WHERE schemaname = ANY (current_schemas(false))
34
- ORDER BY viewname
35
- SQL
48
+ Views.new(connection).all
36
49
  end
37
50
 
38
51
  # Creates a view in the database.
39
52
  #
53
+ # This is typically called in a migration via {Statements#create_view}.
54
+ #
40
55
  # @param name The name of the view to create
41
- # @param sql_definition the SQL schema for the view.
56
+ # @param sql_definition The SQL schema for the view.
57
+ #
42
58
  # @return [void]
43
59
  def create_view(name, sql_definition)
44
- execute "CREATE VIEW #{name} AS #{sql_definition};"
60
+ execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
61
+ end
62
+
63
+ # Updates a view in the database.
64
+ #
65
+ # This results in a {#drop_view} followed by a {#create_view}. The
66
+ # explicitness of that two step process is preferred to `CREATE OR
67
+ # REPLACE VIEW` because the former ensures that the view you are trying to
68
+ # update did, in fact, already exist. Additionally, `CREATE OR REPLACE
69
+ # VIEW` is allowed only to add new columns to the end of an existing
70
+ # view schema. Existing columns cannot be re-ordered, removed, or have
71
+ # their types changed. Drop and create overcomes this limitation as well.
72
+ #
73
+ # This is typically called in a migration via {Statements#update_view}.
74
+ #
75
+ # @param name The name of the view to update
76
+ # @param sql_definition The SQL schema for the updated view.
77
+ #
78
+ # @return [void]
79
+ def update_view(name, sql_definition)
80
+ drop_view(name)
81
+ create_view(name, sql_definition)
45
82
  end
46
83
 
47
84
  # Drops the named view from the database
48
85
  #
86
+ # This is typically called in a migration via {Statements#drop_view}.
87
+ #
49
88
  # @param name The name of the view to drop
89
+ #
50
90
  # @return [void]
51
91
  def drop_view(name)
52
- execute "DROP VIEW #{name};"
92
+ execute "DROP VIEW #{quote_table_name(name)};"
53
93
  end
54
94
 
55
95
  # Creates a materialized view in the database
56
96
  #
57
97
  # @param name The name of the materialized view to create
58
98
  # @param sql_definition The SQL schema that defines the materialized view.
99
+ #
100
+ # This is typically called in a migration via {Statements#create_view}.
101
+ #
102
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
103
+ # in use does not support materialized views.
104
+ #
59
105
  # @return [void]
60
106
  def create_materialized_view(name, sql_definition)
61
- execute "CREATE MATERIALIZED VIEW #{name} AS #{sql_definition};"
107
+ raise_unless_materialized_views_supported
108
+ execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{sql_definition};"
109
+ end
110
+
111
+ # Updates a materialized view in the database.
112
+ #
113
+ # Drops and recreates the materialized view. Attempts to maintain all
114
+ # previously existing and still applicable indexes on the materialized
115
+ # view after the view is recreated.
116
+ #
117
+ # This is typically called in a migration via {Statements#update_view}.
118
+ #
119
+ # @param name The name of the view to update
120
+ # @param sql_definition The SQL schema for the updated view.
121
+ #
122
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
123
+ # in use does not support materialized views.
124
+ #
125
+ # @return [void]
126
+ def update_materialized_view(name, sql_definition)
127
+ raise_unless_materialized_views_supported
128
+
129
+ IndexReapplication.new(connection: connection).on(name) do
130
+ drop_materialized_view(name)
131
+ create_materialized_view(name, sql_definition)
132
+ end
62
133
  end
63
134
 
64
135
  # Drops a materialized view in the database
65
136
  #
137
+ # This is typically called in a migration via {Statements#update_view}.
138
+ #
66
139
  # @param name The name of the materialized view to drop.
140
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
141
+ # in use does not support materialized views.
142
+ #
67
143
  # @return [void]
68
144
  def drop_materialized_view(name)
69
- execute "DROP MATERIALIZED VIEW #{name};"
145
+ raise_unless_materialized_views_supported
146
+ execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};"
70
147
  end
71
148
 
72
149
  # Refreshes a materialized view from its SQL schema.
73
150
  #
74
- # @param name The name of the materialized view to refresh..
151
+ # This is typically called from application code via {Scenic.database}.
152
+ #
153
+ # @param name The name of the materialized view to refresh.
154
+ # @param concurrently [Boolean] Whether the refreshs hould happen
155
+ # concurrently or not. A concurrent refresh allows the view to be
156
+ # refreshed without locking the view for select but requires that the
157
+ # table have at least one unique index that covers all rows. Attempts to
158
+ # refresh concurrently without a unique index will raise a descriptive
159
+ # error.
160
+ #
161
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
162
+ # in use does not support materialized views.
163
+ # @raise [ConcurrentRefreshesNotSupportedError] when attempting a
164
+ # concurrent refresh on version of Postgres that does not support
165
+ # concurrent materialized view refreshes.
166
+ #
167
+ # @example Non-concurrent refresh
168
+ # Scenic.database.refresh_materialized_view(:search_results)
169
+ # @example Concurrent refresh
170
+ # Scenic.database.refresh_materialized_view(:posts, concurrent: true)
171
+ #
75
172
  # @return [void]
76
- def refresh_materialized_view(name)
77
- execute "REFRESH MATERIALIZED VIEW #{name};"
173
+ def refresh_materialized_view(name, concurrently: false)
174
+ raise_unless_materialized_views_supported
175
+
176
+ if concurrently
177
+ raise_unless_concurrent_refresh_supported
178
+ execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
179
+ else
180
+ execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
181
+ end
78
182
  end
79
183
 
80
184
  private
81
185
 
82
- def execute(sql, base = ActiveRecord::Base)
83
- base.connection.execute sql
186
+ attr_reader :connectable
187
+ delegate :execute, :quote_table_name, to: :connection
188
+
189
+ def connection
190
+ Connection.new(connectable.connection)
191
+ end
192
+
193
+ def raise_unless_materialized_views_supported
194
+ unless connection.supports_materialized_views?
195
+ raise MaterializedViewsNotSupportedError
196
+ end
84
197
  end
85
198
 
86
- def view_from_database(result)
87
- Scenic::View.new(
88
- name: result["viewname"],
89
- definition: result["definition"].strip,
90
- materialized: result["materialized"] == "t",
91
- )
199
+ def raise_unless_concurrent_refresh_supported
200
+ unless connection.supports_concurrent_refreshes?
201
+ raise ConcurrentRefreshesNotSupportedError
202
+ end
92
203
  end
93
204
  end
94
205
  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 [Scenic::Adapters::Postgres]
5
+ # Defualts to an instance of {Adapters::Postgres}
6
6
  # @return Scenic adapter
7
7
  attr_accessor :database
8
8
 
@@ -28,7 +28,7 @@ module Scenic
28
28
  # @yieldparam [Scenic::Configuration] config current Scenic config
29
29
  # ```
30
30
  # Scenic.configure do |config|
31
- # config.database = Scenic::Adapters::Postgres
31
+ # config.database = Scenic::Adapters::Postgres.new
32
32
  # end
33
33
  # ```
34
34
  def self.configure
@@ -0,0 +1,36 @@
1
+ module Scenic
2
+ # The in-memory representation of a database index.
3
+ #
4
+ # **This object is used internally by adapters and the schema dumper and is
5
+ # not intended to be used by application code. It is documented here for
6
+ # use by adapter gems.**
7
+ #
8
+ # @api extension
9
+ class Index
10
+ # The name of the object that has the index
11
+ # @return [String]
12
+ attr_reader :object_name
13
+
14
+ # The name of the index
15
+ # @return [String]
16
+ attr_reader :index_name
17
+
18
+ # The SQL statement that defines the index
19
+ # @return [String]
20
+ #
21
+ # @example
22
+ # "CREATE INDEX index_users_on_email ON users USING btree (email)"
23
+ attr_reader :definition
24
+
25
+ # Returns a new instance of Index
26
+ #
27
+ # @param object_name [String] The name of the object that has the index
28
+ # @param index_name [String] The name of the index
29
+ # @param definition [String] The SQL statements that defined the index
30
+ def initialize(object_name:, index_name:, definition:)
31
+ @object_name = object_name
32
+ @index_name = index_name
33
+ @definition = definition
34
+ end
35
+ end
36
+ end
@@ -9,17 +9,24 @@ module Scenic
9
9
  end
10
10
 
11
11
  def views(stream)
12
- views_in_database.select { |view| !ignored?(view.name) }.each do |view|
13
- stream.puts(view.to_schema)
12
+ if dumpable_views_in_database.any?
13
+ stream.puts
14
14
  end
15
- end
16
15
 
17
- def views_in_database
18
- @views_in_database ||= Scenic.database.views.sort
16
+ dumpable_views_in_database.each do |view|
17
+ stream.puts(view.to_schema)
18
+ indexes(view.name, stream)
19
+ end
19
20
  end
20
21
 
21
22
  private
22
23
 
24
+ def dumpable_views_in_database
25
+ @dumpable_views_in_database ||= Scenic.database.views.reject do |view|
26
+ ignored?(view.name)
27
+ end
28
+ end
29
+
23
30
  unless ActiveRecord::SchemaDumper.instance_methods(false).include?(:ignored?)
24
31
  # This method will be present in Rails 4.2.0 and can be removed then.
25
32
  def ignored?(table_name)
@@ -68,10 +68,8 @@ module Scenic
68
68
  # @param version [Fixnum] The version number of the view.
69
69
  # @param revert_to_version [Fixnum] The version number to rollback to on
70
70
  # `rake db rollback`
71
- # @param materialized [Boolean] Must be false. Updating a meterialized view
72
- # causes indexes on it to be dropped. For this reason you should
73
- # explicitly use {#drop_view} followed by {#create_view} and recreate
74
- # applicable indexes. Setting this to `true` will raise an error.
71
+ # @param materialized [Boolean] True if updating a materialized view.
72
+ # Defaults to false.
75
73
  # @return The database response from executing the create statement.
76
74
  #
77
75
  # @example
@@ -82,17 +80,13 @@ module Scenic
82
80
  raise ArgumentError, "version is required"
83
81
  end
84
82
 
83
+ sql_definition = definition(name, version)
84
+
85
85
  if materialized
86
- raise ArgumentError, "Updating materialized views is not supported "\
87
- "because it would cause any indexes to be dropped. Please use "\
88
- "'drop_view' followed by 'create_view', being sure to also recreate "\
89
- "any previously-existing indexes."
86
+ Scenic.database.update_materialized_view(name, sql_definition)
87
+ else
88
+ Scenic.database.update_view(name, sql_definition)
90
89
  end
91
-
92
- drop_view name,
93
- revert_to_version: revert_to_version,
94
- materialized: materialized
95
- create_view(name, version: version, materialized: materialized)
96
90
  end
97
91
 
98
92
  private
@@ -1,3 +1,3 @@
1
1
  module Scenic
2
- VERSION = "1.0.0"
2
+ VERSION = "1.2.0".freeze
3
3
  end
data/lib/scenic/view.rb CHANGED
@@ -22,9 +22,6 @@ module Scenic
22
22
  # @return [Boolean]
23
23
  attr_reader :materialized
24
24
 
25
- # @api private
26
- delegate :<=>, to: :name
27
-
28
25
  # Returns a new instance of View.
29
26
  #
30
27
  # @param name [String] The name of the view.
@@ -47,10 +44,10 @@ module Scenic
47
44
  def to_schema
48
45
  materialized_option = materialized ? "materialized: true, " : ""
49
46
  <<-DEFINITION
50
-
51
47
  create_view :#{name}, #{materialized_option} sql_definition: <<-\SQL
52
48
  #{definition.indent(2)}
53
49
  SQL
50
+
54
51
  DEFINITION
55
52
  end
56
53
  end
data/lib/scenic.rb CHANGED
@@ -7,6 +7,7 @@ require "scenic/schema_dumper"
7
7
  require "scenic/statements"
8
8
  require "scenic/version"
9
9
  require "scenic/view"
10
+ require "scenic/index"
10
11
 
11
12
  # Scenic adds methods `ActiveRecord::Migration` to create and manage database
12
13
  # views in Rails applications.
data/scenic.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{^spec/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
+ spec.add_development_dependency 'appraisal'
23
24
  spec.add_development_dependency 'bundler', '>= 1.5'
24
25
  spec.add_development_dependency 'database_cleaner'
25
26
  spec.add_development_dependency 'rake'
@@ -27,9 +28,11 @@ Gem::Specification.new do |spec|
27
28
  spec.add_development_dependency 'pg'
28
29
  spec.add_development_dependency 'pry'
29
30
  spec.add_development_dependency 'ammeter', '>= 1.1.3'
31
+ spec.add_development_dependency 'yard'
32
+ spec.add_development_dependency 'redcarpet'
30
33
 
31
34
  spec.add_dependency 'activerecord', '>= 4.0.0'
32
35
  spec.add_dependency 'railties', '>= 4.0.0'
33
36
 
34
- spec.required_ruby_version = '~> 2.0'
37
+ spec.required_ruby_version = '~> 2.1'
35
38
  end
@@ -8,5 +8,8 @@ require "scenic"
8
8
 
9
9
  module Dummy
10
10
  class Application < Rails::Application
11
+ config.cache_classes = true
12
+ config.eager_load = false
13
+ config.active_support.deprecation = :stderr
11
14
  end
12
15
  end
@@ -36,4 +36,16 @@ describe Scenic::Generators::ViewGenerator, :generator do
36
36
  expect(migration).to contain "materialized: true"
37
37
  end
38
38
  end
39
+
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")
43
+ view_definition = file("db/views/non_public_searches_v01.sql")
44
+
45
+ run_generator ["non_public.search"]
46
+
47
+ expect(migration).to be_a_migration
48
+ expect(view_definition).to exist
49
+ end
50
+ end
39
51
  end
@@ -0,0 +1,79 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ module Adapters
5
+ describe Postgres::Connection do
6
+ describe "supports_materialized_views?" do
7
+ context "supports_materialized_views? was defined on connection" do
8
+ it "uses the previously defined version" do
9
+ base_response = double("response from base connection")
10
+ base_connection = double(
11
+ "Connection",
12
+ supports_materialized_views?: base_response,
13
+ )
14
+
15
+ connection = Postgres::Connection.new(base_connection)
16
+
17
+ expect(connection.supports_materialized_views?).to be base_response
18
+ end
19
+ end
20
+
21
+ context "supports_materialized_views? is not already defined" do
22
+ it "is true if postgres version is at least than 9.3.0" do
23
+ base_connection = double("Connection", postgresql_version: 90300)
24
+
25
+ connection = Postgres::Connection.new(base_connection)
26
+
27
+ expect(connection.supports_materialized_views?).to be true
28
+ end
29
+
30
+ it "is false if postgres version is less than 9.3.0" do
31
+ base_connection = double("Connection", postgresql_version: 90299)
32
+
33
+ connection = Postgres::Connection.new(base_connection)
34
+
35
+ expect(connection.supports_materialized_views?).to be false
36
+ end
37
+ end
38
+ end
39
+
40
+ describe "#postgresql_version" do
41
+ it "uses the public method on the provided connection if defined" do
42
+ base_connection = Class.new do
43
+ def postgresql_version
44
+ 123
45
+ end
46
+ end
47
+
48
+ connection = Postgres::Connection.new(base_connection.new)
49
+
50
+ expect(connection.postgresql_version).to eq 123
51
+ end
52
+
53
+ it "uses the protected method if the underlying method is not public" do
54
+ base_connection = Class.new do
55
+ protected
56
+
57
+ def postgresql_version
58
+ 123
59
+ end
60
+ end
61
+
62
+ connection = Postgres::Connection.new(base_connection.new)
63
+
64
+ expect(connection.postgresql_version).to eq 123
65
+ end
66
+ end
67
+
68
+ describe "#supports_concurrent_refresh" do
69
+ it "is true if postgres version is at least 9.4.0" do
70
+ base_connection = double("Connection", postgresql_version: 90400)
71
+
72
+ connection = Postgres::Connection.new(base_connection)
73
+
74
+ expect(connection.supports_concurrent_refreshes?).to be true
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ module Scenic
4
+ module Adapters
5
+ describe Postgres::Views, :db do
6
+ it "returns scenic view objects for plain old views" do
7
+ connection = ActiveRecord::Base.connection
8
+ connection.execute <<-SQL
9
+ CREATE VIEW children AS SELECT text 'Elliot' AS name
10
+ SQL
11
+
12
+ views = Postgres::Views.new(connection).all
13
+ first = views.first
14
+
15
+ expect(views.size).to eq 1
16
+ expect(first.name).to eq "children"
17
+ expect(first.materialized).to be false
18
+ expect(first.definition).to eq "SELECT 'Elliot'::text AS name;"
19
+ end
20
+
21
+ it "returns scenic view objects for materialized views" do
22
+ connection = ActiveRecord::Base.connection
23
+ connection.execute <<-SQL
24
+ CREATE MATERIALIZED VIEW children AS SELECT text 'Owen' AS name
25
+ SQL
26
+
27
+ views = Postgres::Views.new(connection).all
28
+ first = views.first
29
+
30
+ expect(views.size).to eq 1
31
+ expect(first.name).to eq "children"
32
+ expect(first.materialized).to be true
33
+ expect(first.definition).to eq "SELECT 'Owen'::text AS name;"
34
+ end
35
+ end
36
+ end
37
+ end