scenic-jets 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +78 -0
- data/.gitignore +19 -0
- data/.hound.yml +2 -0
- data/.rubocop.yml +129 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +223 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/CONTRIBUTING.md +24 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +5 -0
- data/Rakefile +29 -0
- data/SECURITY.md +14 -0
- data/bin/rake +17 -0
- data/bin/rspec +17 -0
- data/bin/setup +18 -0
- data/bin/yard +16 -0
- data/lib/generators/scenic/generators.rb +12 -0
- data/lib/generators/scenic/materializable.rb +31 -0
- data/lib/generators/scenic/model/USAGE +12 -0
- data/lib/generators/scenic/model/model_generator.rb +52 -0
- data/lib/generators/scenic/model/templates/model.erb +3 -0
- data/lib/generators/scenic/view/USAGE +20 -0
- data/lib/generators/scenic/view/templates/db/migrate/create_view.erb +5 -0
- data/lib/generators/scenic/view/templates/db/migrate/update_view.erb +12 -0
- data/lib/generators/scenic/view/view_generator.rb +127 -0
- data/lib/scenic.rb +31 -0
- data/lib/scenic/adapters/postgres.rb +256 -0
- data/lib/scenic/adapters/postgres/connection.rb +57 -0
- data/lib/scenic/adapters/postgres/errors.rb +26 -0
- data/lib/scenic/adapters/postgres/index_reapplication.rb +71 -0
- data/lib/scenic/adapters/postgres/indexes.rb +53 -0
- data/lib/scenic/adapters/postgres/refresh_dependencies.rb +116 -0
- data/lib/scenic/adapters/postgres/views.rb +74 -0
- data/lib/scenic/command_recorder.rb +52 -0
- data/lib/scenic/command_recorder/statement_arguments.rb +51 -0
- data/lib/scenic/configuration.rb +37 -0
- data/lib/scenic/definition.rb +35 -0
- data/lib/scenic/index.rb +36 -0
- data/lib/scenic/schema_dumper.rb +44 -0
- data/lib/scenic/statements.rb +163 -0
- data/lib/scenic/version.rb +3 -0
- data/lib/scenic/view.rb +54 -0
- data/scenic.gemspec +36 -0
- data/spec/acceptance/user_manages_views_spec.rb +88 -0
- data/spec/acceptance_helper.rb +33 -0
- data/spec/dummy/.gitignore +16 -0
- data/spec/dummy/Rakefile +13 -0
- data/spec/dummy/app/models/application_record.rb +5 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +15 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/db/migrate/.keep +0 -0
- data/spec/dummy/db/views/.keep +0 -0
- data/spec/generators/scenic/model/model_generator_spec.rb +36 -0
- data/spec/generators/scenic/view/view_generator_spec.rb +57 -0
- data/spec/integration/revert_spec.rb +74 -0
- data/spec/scenic/adapters/postgres/connection_spec.rb +79 -0
- data/spec/scenic/adapters/postgres/refresh_dependencies_spec.rb +82 -0
- data/spec/scenic/adapters/postgres/views_spec.rb +37 -0
- data/spec/scenic/adapters/postgres_spec.rb +209 -0
- data/spec/scenic/command_recorder/statement_arguments_spec.rb +41 -0
- data/spec/scenic/command_recorder_spec.rb +111 -0
- data/spec/scenic/configuration_spec.rb +27 -0
- data/spec/scenic/definition_spec.rb +62 -0
- data/spec/scenic/schema_dumper_spec.rb +115 -0
- data/spec/scenic/statements_spec.rb +199 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/generator_spec_setup.rb +14 -0
- data/spec/support/view_definition_helpers.rb +10 -0
- metadata +307 -0
data/lib/scenic.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "scenic/configuration"
|
2
|
+
require "scenic/adapters/postgres"
|
3
|
+
require "scenic/command_recorder"
|
4
|
+
require "scenic/definition"
|
5
|
+
require "scenic/schema_dumper"
|
6
|
+
require "scenic/statements"
|
7
|
+
require "scenic/version"
|
8
|
+
require "scenic/view"
|
9
|
+
require "scenic/index"
|
10
|
+
|
11
|
+
# Scenic adds methods `ActiveRecord::Migration` to create and manage database
|
12
|
+
# views in Rails applications.
|
13
|
+
module Scenic
|
14
|
+
# Hooks Scenic into Rails.
|
15
|
+
#
|
16
|
+
# Enables scenic migration methods, migration reversability, and `schema.rb`
|
17
|
+
# dumping.
|
18
|
+
def self.load
|
19
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.include Scenic::Statements
|
20
|
+
ActiveRecord::Migration::CommandRecorder.include Scenic::CommandRecorder
|
21
|
+
ActiveRecord::SchemaDumper.prepend Scenic::SchemaDumper
|
22
|
+
end
|
23
|
+
|
24
|
+
# The current database adapter used by Scenic.
|
25
|
+
#
|
26
|
+
# This defaults to {Adapters::Postgres} but can be overridden
|
27
|
+
# via {Configuration}.
|
28
|
+
def self.database
|
29
|
+
configuration.database
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,256 @@
|
|
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
|
+
require_relative "postgres/refresh_dependencies"
|
7
|
+
|
8
|
+
module Scenic
|
9
|
+
# Scenic database adapters.
|
10
|
+
#
|
11
|
+
# Scenic ships with a Postgres adapter only but can be extended with
|
12
|
+
# additional adapters. The {Adapters::Postgres} adapter provides the
|
13
|
+
# interface.
|
14
|
+
module Adapters
|
15
|
+
# An adapter for managing Postgres views.
|
16
|
+
#
|
17
|
+
# These methods are used interally by Scenic and are not intended for direct
|
18
|
+
# use. Methods that alter database schema are intended to be called via
|
19
|
+
# {Statements}, while {#refresh_materialized_view} is called via
|
20
|
+
# {Scenic.database}.
|
21
|
+
#
|
22
|
+
# The methods are documented here for insight into specifics of how Scenic
|
23
|
+
# integrates with Postgres and the responsibilities of {Adapters}.
|
24
|
+
class Postgres
|
25
|
+
# Creates an instance of the Scenic Postgres adapter.
|
26
|
+
#
|
27
|
+
# This is the default adapter for Scenic. Configuring it via
|
28
|
+
# {Scenic.configure} is not required, but the example below shows how one
|
29
|
+
# would explicitly set it.
|
30
|
+
#
|
31
|
+
# @param [#connection] connectable An object that returns the connection
|
32
|
+
# for Scenic to use. Defaults to `ActiveRecord::Base`.
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# Scenic.configure do |config|
|
36
|
+
# config.database = Scenic::Adapters::Postgres.new
|
37
|
+
# end
|
38
|
+
def initialize(connectable = ActiveRecord::Base)
|
39
|
+
@connectable = connectable
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns an array of views in the database.
|
43
|
+
#
|
44
|
+
# This collection of views is used by the [Scenic::SchemaDumper] to
|
45
|
+
# populate the `schema.rb` file.
|
46
|
+
#
|
47
|
+
# @return [Array<Scenic::View>]
|
48
|
+
def views
|
49
|
+
Views.new(connection).all
|
50
|
+
end
|
51
|
+
|
52
|
+
# Creates a view in the database.
|
53
|
+
#
|
54
|
+
# This is typically called in a migration via {Statements#create_view}.
|
55
|
+
#
|
56
|
+
# @param name The name of the view to create
|
57
|
+
# @param sql_definition The SQL schema for the view.
|
58
|
+
#
|
59
|
+
# @return [void]
|
60
|
+
def create_view(name, sql_definition)
|
61
|
+
execute "CREATE VIEW #{quote_table_name(name)} AS #{sql_definition};"
|
62
|
+
end
|
63
|
+
|
64
|
+
# Updates a view in the database.
|
65
|
+
#
|
66
|
+
# This results in a {#drop_view} followed by a {#create_view}. The
|
67
|
+
# explicitness of that two step process is preferred to `CREATE OR
|
68
|
+
# REPLACE VIEW` because the former ensures that the view you are trying to
|
69
|
+
# update did, in fact, already exist. Additionally, `CREATE OR REPLACE
|
70
|
+
# VIEW` is allowed only to add new columns to the end of an existing
|
71
|
+
# view schema. Existing columns cannot be re-ordered, removed, or have
|
72
|
+
# their types changed. Drop and create overcomes this limitation as well.
|
73
|
+
#
|
74
|
+
# This is typically called in a migration via {Statements#update_view}.
|
75
|
+
#
|
76
|
+
# @param name The name of the view to update
|
77
|
+
# @param sql_definition The SQL schema for the updated view.
|
78
|
+
#
|
79
|
+
# @return [void]
|
80
|
+
def update_view(name, sql_definition)
|
81
|
+
drop_view(name)
|
82
|
+
create_view(name, sql_definition)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Replaces a view in the database using `CREATE OR REPLACE VIEW`.
|
86
|
+
#
|
87
|
+
# This results in a `CREATE OR REPLACE VIEW`. Most of the time the
|
88
|
+
# explicitness of the two step process used in {#update_view} is preferred
|
89
|
+
# to `CREATE OR REPLACE VIEW` because the former ensures that the view you
|
90
|
+
# are trying to update did, in fact, already exist. Additionally,
|
91
|
+
# `CREATE OR REPLACE VIEW` is allowed only to add new columns to the end
|
92
|
+
# of an existing view schema. Existing columns cannot be re-ordered,
|
93
|
+
# removed, or have their types changed. Drop and create overcomes this
|
94
|
+
# limitation as well.
|
95
|
+
#
|
96
|
+
# However, when there is a tangled dependency tree
|
97
|
+
# `CREATE OR REPLACE VIEW` can be preferable.
|
98
|
+
#
|
99
|
+
# This is typically called in a migration via
|
100
|
+
# {Statements#replace_view}.
|
101
|
+
#
|
102
|
+
# @param name The name of the view to update
|
103
|
+
# @param sql_definition The SQL schema for the updated view.
|
104
|
+
#
|
105
|
+
# @return [void]
|
106
|
+
def replace_view(name, sql_definition)
|
107
|
+
execute "CREATE OR REPLACE VIEW #{quote_table_name(name)} AS #{sql_definition};"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Drops the named view from the database
|
111
|
+
#
|
112
|
+
# This is typically called in a migration via {Statements#drop_view}.
|
113
|
+
#
|
114
|
+
# @param name The name of the view to drop
|
115
|
+
#
|
116
|
+
# @return [void]
|
117
|
+
def drop_view(name)
|
118
|
+
execute "DROP VIEW #{quote_table_name(name)};"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Creates a materialized view in the database
|
122
|
+
#
|
123
|
+
# @param name The name of the materialized view to create
|
124
|
+
# @param sql_definition The SQL schema that defines the materialized view.
|
125
|
+
# @param no_data [Boolean] Default: false. Set to true to create
|
126
|
+
# materialized view without running the associated query. You will need
|
127
|
+
# to perform a non-concurrent refresh to populate with data.
|
128
|
+
#
|
129
|
+
# This is typically called in a migration via {Statements#create_view}.
|
130
|
+
#
|
131
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
132
|
+
# in use does not support materialized views.
|
133
|
+
#
|
134
|
+
# @return [void]
|
135
|
+
def create_materialized_view(name, sql_definition, no_data: false)
|
136
|
+
raise_unless_materialized_views_supported
|
137
|
+
|
138
|
+
execute <<-SQL
|
139
|
+
CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS
|
140
|
+
#{sql_definition.rstrip.chomp(';')}
|
141
|
+
#{'WITH NO DATA' if no_data};
|
142
|
+
SQL
|
143
|
+
end
|
144
|
+
|
145
|
+
# Updates a materialized view in the database.
|
146
|
+
#
|
147
|
+
# Drops and recreates the materialized view. Attempts to maintain all
|
148
|
+
# previously existing and still applicable indexes on the materialized
|
149
|
+
# view after the view is recreated.
|
150
|
+
#
|
151
|
+
# This is typically called in a migration via {Statements#update_view}.
|
152
|
+
#
|
153
|
+
# @param name The name of the view to update
|
154
|
+
# @param sql_definition The SQL schema for the updated view.
|
155
|
+
# @param no_data [Boolean] Default: false. Set to true to create
|
156
|
+
# materialized view without running the associated query. You will need
|
157
|
+
# to perform a non-concurrent refresh to populate with data.
|
158
|
+
#
|
159
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
160
|
+
# in use does not support materialized views.
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
def update_materialized_view(name, sql_definition, no_data: false)
|
164
|
+
raise_unless_materialized_views_supported
|
165
|
+
|
166
|
+
IndexReapplication.new(connection: connection).on(name) do
|
167
|
+
drop_materialized_view(name)
|
168
|
+
create_materialized_view(name, sql_definition, no_data: no_data)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Drops a materialized view in the database
|
173
|
+
#
|
174
|
+
# This is typically called in a migration via {Statements#update_view}.
|
175
|
+
#
|
176
|
+
# @param name The name of the materialized view to drop.
|
177
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
178
|
+
# in use does not support materialized views.
|
179
|
+
#
|
180
|
+
# @return [void]
|
181
|
+
def drop_materialized_view(name)
|
182
|
+
raise_unless_materialized_views_supported
|
183
|
+
execute "DROP MATERIALIZED VIEW #{quote_table_name(name)};"
|
184
|
+
end
|
185
|
+
|
186
|
+
# Refreshes a materialized view from its SQL schema.
|
187
|
+
#
|
188
|
+
# This is typically called from application code via {Scenic.database}.
|
189
|
+
#
|
190
|
+
# @param name The name of the materialized view to refresh.
|
191
|
+
# @param concurrently [Boolean] Whether the refreshs hould happen
|
192
|
+
# concurrently or not. A concurrent refresh allows the view to be
|
193
|
+
# refreshed without locking the view for select but requires that the
|
194
|
+
# table have at least one unique index that covers all rows. Attempts to
|
195
|
+
# refresh concurrently without a unique index will raise a descriptive
|
196
|
+
# error.
|
197
|
+
#
|
198
|
+
# @raise [MaterializedViewsNotSupportedError] if the version of Postgres
|
199
|
+
# in use does not support materialized views.
|
200
|
+
# @raise [ConcurrentRefreshesNotSupportedError] when attempting a
|
201
|
+
# concurrent refresh on version of Postgres that does not support
|
202
|
+
# concurrent materialized view refreshes.
|
203
|
+
#
|
204
|
+
# @example Non-concurrent refresh
|
205
|
+
# Scenic.database.refresh_materialized_view(:search_results)
|
206
|
+
# @example Concurrent refresh
|
207
|
+
# Scenic.database.refresh_materialized_view(:posts, concurrently: true)
|
208
|
+
#
|
209
|
+
# @return [void]
|
210
|
+
def refresh_materialized_view(name, concurrently: false, cascade: false)
|
211
|
+
raise_unless_materialized_views_supported
|
212
|
+
|
213
|
+
if cascade
|
214
|
+
refresh_dependencies_for(name, concurrently: concurrently)
|
215
|
+
end
|
216
|
+
|
217
|
+
if concurrently
|
218
|
+
raise_unless_concurrent_refresh_supported
|
219
|
+
execute "REFRESH MATERIALIZED VIEW CONCURRENTLY #{quote_table_name(name)};"
|
220
|
+
else
|
221
|
+
execute "REFRESH MATERIALIZED VIEW #{quote_table_name(name)};"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
private
|
226
|
+
|
227
|
+
attr_reader :connectable
|
228
|
+
delegate :execute, :quote_table_name, to: :connection
|
229
|
+
|
230
|
+
def connection
|
231
|
+
Connection.new(connectable.connection)
|
232
|
+
end
|
233
|
+
|
234
|
+
def raise_unless_materialized_views_supported
|
235
|
+
unless connection.supports_materialized_views?
|
236
|
+
raise MaterializedViewsNotSupportedError
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def raise_unless_concurrent_refresh_supported
|
241
|
+
unless connection.supports_concurrent_refreshes?
|
242
|
+
raise ConcurrentRefreshesNotSupportedError
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def refresh_dependencies_for(name, concurrently: false)
|
247
|
+
Scenic::Adapters::Postgres::RefreshDependencies.call(
|
248
|
+
name,
|
249
|
+
self,
|
250
|
+
connection,
|
251
|
+
concurrently: concurrently,
|
252
|
+
)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Decorates an ActiveRecord connection with methods that help determine
|
5
|
+
# the connections capabilities.
|
6
|
+
#
|
7
|
+
# Every attempt is made to use the versions of these methods defined by
|
8
|
+
# Rails where they are available and public before falling back to our own
|
9
|
+
# implementations for older Rails versions.
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class Connection < SimpleDelegator
|
13
|
+
# True if the connection supports materialized views.
|
14
|
+
#
|
15
|
+
# Delegates to the method of the same name if it is already defined on
|
16
|
+
# the connection. This is the case for Rails 4.2 or higher.
|
17
|
+
#
|
18
|
+
# @return [Boolean]
|
19
|
+
def supports_materialized_views?
|
20
|
+
if undecorated_connection.respond_to?(:supports_materialized_views?)
|
21
|
+
super
|
22
|
+
else
|
23
|
+
postgresql_version >= 90300
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# True if the connection supports concurrent refreshes of materialized
|
28
|
+
# views.
|
29
|
+
#
|
30
|
+
# @return [Boolean]
|
31
|
+
def supports_concurrent_refreshes?
|
32
|
+
postgresql_version >= 90400
|
33
|
+
end
|
34
|
+
|
35
|
+
# An integer representing the version of Postgres we're connected to.
|
36
|
+
#
|
37
|
+
# postgresql_version is public in Rails 5, but protected in earlier
|
38
|
+
# versions.
|
39
|
+
#
|
40
|
+
# @return [Integer]
|
41
|
+
def postgresql_version
|
42
|
+
if undecorated_connection.respond_to?(:postgresql_version)
|
43
|
+
super
|
44
|
+
else
|
45
|
+
undecorated_connection.send(:postgresql_version)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def undecorated_connection
|
52
|
+
__getobj__
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Raised when a materialized view operation is attempted on a database
|
5
|
+
# version that does not support materialized views.
|
6
|
+
#
|
7
|
+
# Materialized views are supported on Postgres 9.3 or newer.
|
8
|
+
class MaterializedViewsNotSupportedError < StandardError
|
9
|
+
def initialize
|
10
|
+
super("Materialized views require Postgres 9.3 or newer")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Raised when attempting a concurrent materialized view refresh on a
|
15
|
+
# database version that does not support that.
|
16
|
+
#
|
17
|
+
# Concurrent materialized view refreshes are supported on Postgres 9.4 or
|
18
|
+
# newer.
|
19
|
+
class ConcurrentRefreshesNotSupportedError < StandardError
|
20
|
+
def initialize
|
21
|
+
super("Concurrent materialized view refreshes require Postgres 9.4 or newer")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Scenic
|
2
|
+
module Adapters
|
3
|
+
class Postgres
|
4
|
+
# Updating a materialized view causes the view to be dropped and
|
5
|
+
# recreated. This causes any associated indexes to be dropped as well.
|
6
|
+
# This object can be used to capture the existing indexes before the drop
|
7
|
+
# and then reapply appropriate indexes following the create.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
class IndexReapplication
|
11
|
+
# Creates the index reapplication object.
|
12
|
+
#
|
13
|
+
# @param connection [Connection] The connection to execute SQL against.
|
14
|
+
# @param speaker [#say] (ActiveRecord::Migration) The object used for
|
15
|
+
# logging the results of reapplying indexes.
|
16
|
+
def initialize(connection:, speaker: ActiveRecord::Migration)
|
17
|
+
@connection = connection
|
18
|
+
@speaker = speaker
|
19
|
+
end
|
20
|
+
|
21
|
+
# Caches indexes on the provided object before executing the block and
|
22
|
+
# then reapplying the indexes. Each recreated or skipped index is
|
23
|
+
# announced to STDOUT by default. This can be overridden in the
|
24
|
+
# constructor.
|
25
|
+
#
|
26
|
+
# @param name The name of the object we are reapplying indexes on.
|
27
|
+
# @yield Operations to perform before reapplying indexes.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def on(name)
|
31
|
+
indexes = Indexes.new(connection: connection).on(name)
|
32
|
+
|
33
|
+
yield
|
34
|
+
|
35
|
+
indexes.each(&method(:try_index_create))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :connection, :speaker
|
41
|
+
|
42
|
+
def try_index_create(index)
|
43
|
+
success = with_savepoint(index.index_name) do
|
44
|
+
connection.execute(index.definition)
|
45
|
+
end
|
46
|
+
|
47
|
+
if success
|
48
|
+
say "index '#{index.index_name}' on '#{index.object_name}' has been recreated"
|
49
|
+
else
|
50
|
+
say "index '#{index.index_name}' on '#{index.object_name}' is no longer valid and has been dropped."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def with_savepoint(name)
|
55
|
+
connection.execute("SAVEPOINT #{name}")
|
56
|
+
yield
|
57
|
+
connection.execute("RELEASE SAVEPOINT #{name}")
|
58
|
+
true
|
59
|
+
rescue
|
60
|
+
connection.execute("ROLLBACK TO SAVEPOINT #{name}")
|
61
|
+
false
|
62
|
+
end
|
63
|
+
|
64
|
+
def say(message)
|
65
|
+
subitem = true
|
66
|
+
speaker.say(message, subitem)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -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
|