sequel-inline_schema 0.2.0 → 0.3.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
  SHA256:
3
- metadata.gz: '0586820d7a0cb25e530b431dee73a22dc205296a1cf15a945602f554ba9b3608'
4
- data.tar.gz: 4646d1e2a538da9554864bb15db471b2eb9a2c6e920b8bdcbb462883d1724517
3
+ metadata.gz: ba38d991fa938c8b49fd8d6b395609ed80191ea733dcbfc91adc539b23a1ad2c
4
+ data.tar.gz: 58cac869e2b380e53b47e17ba0aa8f8bb47e5553cf5a9ac57b499bd962d76c4e
5
5
  SHA512:
6
- metadata.gz: 06013b428fbd164f5717a815391d65723033648ef301621b7d551ebcfb8e18c6c0c64a31678a0e36885f89ee3d7e3f3e717fbeab9722f2537dbfb9e26d16c37d
7
- data.tar.gz: 92cf82542922e762fc32971cce5b16cdeebc6922940a019bc4b2e80ac8e01ae40a6cea8d2863d3da2fbd71761ee9022b5587fa6b3ac471a62a57cddf3f31129e
6
+ metadata.gz: cc518f2f4548126ce2e79c7c5fa619b4d2be33e0db757b6cd90a05bb8b5804938819352225c0ce60b9871e1b526e0d90dff743ca3304215ce33f3eef98c9ae88
7
+ data.tar.gz: f67ec3eca4fb4e65f1e66b1494d61e0c5f3b636074b88becfb3f28a70e107f3be44b67b613a50dbf63def6778b70f0befd5eb39b043b96ddc34f760ca2d16c73
Binary file
data.tar.gz.sig CHANGED
Binary file
data/History.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## v0.3.0 [2020-02-10] Michael Granger <ged@faeriemud.org>
6
+
7
+ Improvements:
8
+
9
+ - Add inline view declaration
10
+
11
+
5
12
  ## v0.2.0 [2020-01-18] Michael Granger <ged@faeriemud.org>
6
13
 
7
14
  Improvements:
data/Rakefile CHANGED
@@ -4,6 +4,6 @@ require 'rake/deveiate'
4
4
 
5
5
  Rake::DevEiate.setup( 'sequel-inline_schema' ) do |project|
6
6
  project.publish_to = 'deveiate:/usr/local/www/public/code'
7
- project.required_ruby_version = '~> 2.4'
7
+ project.required_ruby_version = '~> 2.5'
8
8
  end
9
9
 
@@ -7,7 +7,7 @@ require 'sequel/plugins/inline_schema'
7
7
  module Sequel::InlineSchema
8
8
 
9
9
  # Package version
10
- VERSION = '0.2.0'
10
+ VERSION = '0.3.0'
11
11
 
12
12
  # Version control revision
13
13
  REVISION = %q$Revision$
@@ -212,6 +212,7 @@ module Sequel::Plugins::InlineMigrations
212
212
  end
213
213
  end
214
214
 
215
+
215
216
  ### Create any new tables and run any pending migrations. If the optional +target+ is
216
217
  ### supplied, the migrations up to (and including) that one will be applied. If
217
218
  ### it has already been applied, any from the currently-applied one to it
@@ -220,6 +221,8 @@ module Sequel::Plugins::InlineMigrations
220
221
  migrator = self.migrator( target )
221
222
  classes_to_install = self.uninstalled_tables
222
223
  self.db.log_info "Classes with tables that need to be installed: %p" % [ classes_to_install ]
224
+ views_to_install = self.uninstalled_views
225
+ self.db.log_info "Views to install: %p" % [ views_to_install.map(&:table_name) ]
223
226
 
224
227
  self.db.transaction do
225
228
  self.before_migration
@@ -229,6 +232,9 @@ module Sequel::Plugins::InlineMigrations
229
232
  self.db.log_info "Running any pending migrations..."
230
233
  migrator.run
231
234
  self.after_migration
235
+
236
+ self.db.log_info "(Re)-creating any modeled views..."
237
+ views_to_install.each( &:create_view )
232
238
  end
233
239
  end
234
240
 
@@ -477,4 +483,5 @@ module Sequel::Plugins::InlineMigrations
477
483
 
478
484
  end # class Migrator
479
485
 
486
+
480
487
  end # Sequel::Plugins::InlineMigrations
@@ -100,6 +100,55 @@ module Sequel::Plugins::InlineSchema
100
100
  # Sequel plugin API -- add these methods to model classes which load the plugin.
101
101
  module ClassMethods
102
102
 
103
+ ### Extension callback -- add some class instance variables to keep track of
104
+ ### schema info.
105
+ def self::extended( model_class )
106
+ super
107
+
108
+ model_class.require_valid_table = false
109
+
110
+ # The Sequel::Dataset used to create the model's view (if it's modelling a view
111
+ # instead of a table). Setting this causes the model's schema to be ignored when
112
+ # its table is created, creating a view by the same name instead.
113
+ model_class.singleton_class.attr_accessor( :view_dataset )
114
+
115
+ # The options used when creating the view for the model.
116
+ model_class.singleton_class.attr_accessor( :view_options )
117
+ end
118
+
119
+
120
+ ### Returns the table schema created with set_schema.
121
+ def schema
122
+ if !@schema && @schema_block
123
+ self.set_dataset( self.db[@schema_name] ) if @schema_name
124
+ @schema = self.db.create_table_generator( &@schema_block )
125
+ self.set_primary_key( @schema.primary_key_name ) if @schema.primary_key_name
126
+ end
127
+ return @schema || ( superclass.schema unless superclass == Sequel::Model )
128
+ end
129
+
130
+
131
+ ### Defines a table schema (see Schema::CreateTableGenerator for more information).
132
+ ###
133
+ ### This will also set the dataset if you provide a +name+, as well as setting
134
+ ### the primary key if you define one in the passed block.
135
+ ###
136
+ ### Since this plugin allows you to declare the schema inline with the model
137
+ ### class that acts as its interface, the table will not always exist when the
138
+ ### class loads, so calling #set_schema will call require_valid_table to `false`
139
+ ### for you. You can disable this by passing `require_table: true`.
140
+ def set_schema( name=nil, require_table: false, &block )
141
+ self.require_valid_table = require_table
142
+ @schema = nil
143
+ @schema_name = name
144
+ @schema_block = block
145
+ end
146
+
147
+
148
+ #
149
+ # Table utilities
150
+ #
151
+
103
152
  ### Creates table, using the column information from set_schema.
104
153
  def create_table( *args, &block )
105
154
  self.set_schema( *args, &block ) if block
@@ -125,18 +174,6 @@ module Sequel::Plugins::InlineSchema
125
174
  end
126
175
 
127
176
 
128
- ### Called before the table is created.
129
- def before_create_table
130
- # No-op
131
- end
132
-
133
-
134
- ### Called after the table is created.
135
- def after_create_table
136
- # No-op
137
- end
138
-
139
-
140
177
  ### Drops table. If the table doesn't exist, this will probably raise an error.
141
178
  def drop_table
142
179
  self.before_drop_table
@@ -145,58 +182,96 @@ module Sequel::Plugins::InlineSchema
145
182
  end
146
183
 
147
184
 
148
- ### Drops table if it already exists, do nothing if it doesn't exist.
185
+ ### Drops table if it already exists, do nothing.
149
186
  def drop_table?
150
- self.db.drop_table?( self.table_name )
187
+ self.drop_table if self.table_exists?
151
188
  end
152
189
 
153
190
 
154
- ### Called before the table is dropped.
155
- def before_drop_table
156
- # No-op
191
+ ### Returns true if table exists, false otherwise.
192
+ def table_exists?
193
+ return self.db.table_exists?( self.table_name )
157
194
  end
158
195
 
159
196
 
160
- ### Called after the table is dropped.
161
- def after_drop_table
162
- # No-op
197
+ ### Set the dataset to use for the model to +ds+. If a +block+ is provided, it will be
198
+ ### called with the specified +ds+, and should return the modified dataset to use. Any
199
+ ### +options+ that are given will be passed to Sequel::Database#create_or_replace_view
200
+ def set_view_dataset( ds=nil, **options ) # :yield: ds
201
+ ds = yield( ds ) if block_given?
202
+
203
+ self.view_dataset = ds
204
+ self.view_options = options
163
205
  end
164
206
 
165
207
 
166
- ### Returns the table schema created with set_schema.
167
- def schema
168
- if !@schema && @schema_block
169
- self.set_dataset( self.db[@schema_name] ) if @schema_name
170
- @schema = self.db.create_table_generator( &@schema_block )
171
- self.set_primary_key( @schema.primary_key_name ) if @schema.primary_key_name
172
- end
173
- return @schema || ( superclass.schema unless superclass == Sequel::Model )
208
+ ### Create the view for this model class.
209
+ def create_view( options={} )
210
+ dataset = self.view_dataset or raise "No view declared for this model."
211
+ options = self.view_options.merge( options )
212
+
213
+ self.db.log_info "Creating view %s(%p): %s" % [ self.table_name, options, dataset.sql ]
214
+ self.db.create_or_replace_view( self.table_name, dataset, options )
174
215
  end
175
216
 
176
217
 
177
- ### Defines a table schema (see Schema::CreateTableGenerator for more information).
178
- ###
179
- ### This will also set the dataset if you provide a +name+, as well as setting
180
- ### the primary key if you define one in the passed block.
181
- ###
182
- ### Since this plugin allows you to declare the schema inline with the model
183
- ### class that acts as its interface, the table will not always exist when the
184
- ### class loads, so calling #set_schema will call require_valid_table to `false`
185
- ### for you. You can disable this by passing `require_table: true`.
186
- def set_schema( name=nil, require_table: false, &block )
187
- self.require_valid_table = require_table
188
- @schema = nil
189
- @schema_name = name
190
- @schema_block = block
218
+ ### Drops the view if it exists and then runs #create_view.
219
+ def create_view!( options={} )
220
+ self.drop_view?
221
+ return self.create_view
191
222
  end
192
223
 
193
224
 
194
- ### Returns true if table exists, false otherwise.
195
- def table_exists?
196
- return self.db.table_exists?( self.table_name )
225
+ ### Creates the view unless it already exists.
226
+ def create_view?( options={} )
227
+ self.create_view( options ) unless self.view_exists?
197
228
  end
198
229
 
199
230
 
231
+ ### Refresh the view for this model class. This can only
232
+ ### be called on materialized views.
233
+ def refresh_view
234
+ self.db.refresh_view( self.table_name )
235
+ end
236
+
237
+
238
+ ### Drop the view backing this model.
239
+ def drop_view
240
+ self.before_drop_view
241
+ self.db.drop_view( self.table_name )
242
+ self.after_drop_view
243
+ end
244
+
245
+
246
+ ### Drop the view if it already exists, otherwise do nothing.
247
+ def drop_view?
248
+ self.drop_view if self.view_exists?
249
+ end
250
+
251
+
252
+ ### Returns true if the view associated with this model exists, false otherwise.
253
+ def view_exists?
254
+ # Make shortcuts for fully-qualified names
255
+ class_table = Sequel[:pg_catalog][:pg_class].as( :c )
256
+ ns_table = Sequel[:pg_catalog][:pg_namespace].as( :n )
257
+ is_visible = Sequel[:pg_catalog][:pg_table_is_visible]
258
+
259
+ ds = db[ class_table ].
260
+ join( ns_table, oid: :relnamespace )
261
+ ds = ds.where( Sequel[:c][:relkind] => ['v', 'm'] ).
262
+ exclude( Sequel[:n][:nspname] => /^pg_toast/ ).
263
+ where( Sequel[:c][:relname] => self.table_name ).
264
+ where( Sequel.function(is_visible, Sequel[:c][:oid]) )
265
+
266
+ return ds.count == 1
267
+ end
268
+
269
+
270
+
271
+ #
272
+ # Hooks
273
+ #
274
+
200
275
  ### Table-creation hook; called on a model class before its table is created.
201
276
  def before_create_table
202
277
  return true
@@ -209,18 +284,71 @@ module Sequel::Plugins::InlineSchema
209
284
  end
210
285
 
211
286
 
212
- ### Return an Array of model table names that don't yet exist, in the order they
287
+ ### View-creation hook; called before the backing view is created.
288
+ def before_create_view
289
+ return true
290
+ end
291
+
292
+
293
+ ### View-creation hook; called after the backing view is created.
294
+ def after_create_view
295
+ return true
296
+ end
297
+
298
+
299
+ ### Table-drop hook; called before the table is dropped.
300
+ def before_drop_table
301
+ return true
302
+ end
303
+
304
+
305
+ ### Table-drop hook; called after the table is dropped.
306
+ def after_drop_table
307
+ return true
308
+ end
309
+
310
+
311
+ ### View-creation hook; called before the backing view is created.
312
+ def before_create_view
313
+ return true
314
+ end
315
+
316
+
317
+ ### View-creation hook; called after the backing view is created.
318
+ def after_create_view
319
+ return true
320
+ end
321
+
322
+
323
+ #
324
+ # Schema-state introspection
325
+ #
326
+
327
+ ### Return an Array of model classes whose tables don't yet exist, in the order they
213
328
  ### need to be created to satisfy foreign key constraints.
214
329
  def uninstalled_tables
215
330
  self.db.log_info " searching for unbacked model classes..."
216
331
 
217
332
  self.tsort.find_all do |modelclass|
218
- next unless modelclass.name && modelclass.name != ''
333
+ next unless modelclass.name && modelclass.name != '' && !modelclass.is_view_class?
219
334
  !modelclass.table_exists?
220
335
  end.uniq( &:table_name )
221
336
  end
222
337
 
223
338
 
339
+ ### Return an Array of model classes whose views don't yet exist, in the order
340
+ ### they need to be created.
341
+ def uninstalled_views
342
+ return self.tsort.find_all( &:is_view_class? ).reject( &:table_exists? )
343
+ end
344
+
345
+
346
+ ### Returns +true+ if the receiver is defined via a view rather than a table.
347
+ def is_view_class?
348
+ return self.respond_to?( :view_dataset ) && self.view_dataset ? true : false
349
+ end
350
+
351
+
224
352
  #########
225
353
  protected
226
354
  #########
@@ -275,7 +403,6 @@ module Sequel::Plugins::InlineSchema
275
403
  end
276
404
  end
277
405
 
278
-
279
406
  end # module ClassMethods
280
407
 
281
408
  end # module Sequel::Plugin::Schema
@@ -22,6 +22,11 @@ describe Sequel::Plugins::InlineSchema do
22
22
  name.to_sym
23
23
  end
24
24
 
25
+ let( :view ) do
26
+ name = "test_view_%s" % [ SecureRandom.hex(8) ]
27
+ name.to_sym
28
+ end
29
+
25
30
  let( :model_class ) do
26
31
  mclass = Class.new( Sequel::Model )
27
32
  mclass.set_dataset( db[table] )
@@ -29,6 +34,8 @@ describe Sequel::Plugins::InlineSchema do
29
34
  mclass
30
35
  end
31
36
 
37
+ let ( :view_dataset ) { model_class.dataset.group_and_count( :age ) }
38
+
32
39
  let( :valid_pg_attributes ) do
33
40
  [
34
41
  {
@@ -85,7 +92,7 @@ describe Sequel::Plugins::InlineSchema do
85
92
  when /CREATE TABLE/
86
93
  created = true
87
94
  else
88
- fail "Unhandled query"
95
+ fail "Unhandled query %p" % [ query ]
89
96
  end
90
97
  end
91
98
  end
@@ -130,7 +137,6 @@ describe Sequel::Plugins::InlineSchema do
130
137
  model_class.create_table!
131
138
 
132
139
  expect( db.sqls ).to include(
133
- %{DROP TABLE IF EXISTS "#{table}"},
134
140
  %{CREATE TABLE "#{table}" ("id" serial PRIMARY KEY, "name" text, "age" integer)}
135
141
  )
136
142
  end
@@ -179,6 +185,42 @@ describe Sequel::Plugins::InlineSchema do
179
185
  end
180
186
 
181
187
 
188
+
189
+ it "allows a model to create a view instead of a table" do
190
+ view_class = Class.new( Sequel::Model )
191
+ view_class.plugin( :inline_schema )
192
+ view_class.set_dataset( db[view] )
193
+ view_class.set_view_dataset { view_dataset.naked }
194
+
195
+ db.fetch = fake_db_fetcher
196
+ db.sqls.clear
197
+
198
+ view_class.create_view
199
+
200
+ expect( db.sqls ).to include(
201
+ %{CREATE OR REPLACE VIEW "#{view}" AS SELECT "age", count(*) AS "count" FROM "#{table}" GROUP BY "age"}
202
+ )
203
+ end
204
+
205
+
206
+ it "allows a model to craete a materialized view instead of a table" do
207
+ materialized_view_class = Class.new( Sequel::Model )
208
+ materialized_view_class.plugin( :inline_schema )
209
+ materialized_view_class.set_dataset( db[view] )
210
+ materialized_view_class.set_view_dataset( materialized: true ) { view_dataset.naked }
211
+
212
+ db.fetch = fake_db_fetcher
213
+ db.sqls.clear
214
+
215
+ materialized_view_class.create_view
216
+
217
+ expect( db.sqls ).to include(
218
+ %{CREATE OR REPLACE MATERIALIZED VIEW "#{view}" AS SELECT "age", count(*) AS "count" } +
219
+ %{FROM "#{table}" GROUP BY "age"}
220
+ )
221
+ end
222
+
223
+
182
224
  describe "table-creation ordering" do
183
225
 
184
226
  let( :fake_db_fetcher ) do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-inline_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Granger
@@ -34,7 +34,7 @@ cert_chain:
34
34
  jBZSA+N+xUTgUWpXjjwsLZjzJkhWATJWq+krNXcqpwXo6HsjmdUxoFMt63RBb+sI
35
35
  XrxOxp8o0uOkU7FdLSGsyqJ2LzsR4obN
36
36
  -----END CERTIFICATE-----
37
- date: 2020-01-19 00:00:00.000000000 Z
37
+ date: 2020-02-10 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: sequel
@@ -140,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
140
  requirements:
141
141
  - - "~>"
142
142
  - !ruby/object:Gem::Version
143
- version: '2.4'
143
+ version: '2.5'
144
144
  required_rubygems_version: !ruby/object:Gem::Requirement
145
145
  requirements:
146
146
  - - ">="
metadata.gz.sig CHANGED
Binary file