hoodoo 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MTFiNjAzNDk4MDdmOTFmNWRkZTg1MmY3ODExMmY3ZGU4MTU3YjkzNg==
4
+ MDliMjQ5YTcwM2ExNTU4YzgxY2U4NTNmZTM1ODFkMTI0NzY0MmMzZQ==
5
5
  data.tar.gz: !binary |-
6
- MjMwNDRjODEzZDU5YmRmM2E0YmRlNTlmYTVkYzA3NmIxMDA3ZjE1Yg==
6
+ ZWY4NTg4ODEzOTliMGQyMTBjMmFkZGNmMmYzZmFjZjc0NDlmMWJlZQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZjEzZDYxZDJjZGI3MWU0NDFmYTYwNzQ2ZmYwZGY4MmQ2Zjc0NWJhZmRiMjli
10
- ZjQ0MTA1MzkzYTFkN2RhZTA1YWFkNjc1MDI4MDgwNzYyZDk5YmFhZTdmZDE1
11
- ZTJlYjRjZDc5N2IzYmNiNTc0M2Q0NzMzNDhlODk3OTEzZjc0YTU=
9
+ NjlkZTI4NjZhNDczZTdmZTM5ODlmNjVhNTY0MjQ1MGI3MDg4YmMxNDE3MDQw
10
+ NjMzOWY1MTE0MmM0N2E1MGYwNTEwODlmMzQ0M2I5YjEwZGY0ZWY4MTA5MGYw
11
+ NmRlZWViMWJiOGVlNDdkMTFmNjYzYjYxZTFhM2M4MzgyY2ZkYTE=
12
12
  data.tar.gz: !binary |-
13
- MzMwNGY1MGZlOTFiZjE3YmI0MDgxYjcwYTUxYTliNTQxNDFjODMwNThkMGUx
14
- ZDk5NzE0ZDcwMWY1YzZhMTIxMzgyY2ZiMWJiMTFiZjk4ZmNlMTE0N2JmZThj
15
- ZDIxZWM4MzQ0ODc0ODdmNWU5OWEzYmU3ODlkMTE4OTI1ZTNiYWM=
13
+ MzUzNmJlYTM4ZTc3ZDgyZTExMzU5ZjhhNzE2OTQxMWY1OWFlMWQwY2Q2NTgy
14
+ N2FiNmU3YmJkYjJhZDBlMjQ5NzNkZmJmYmIxY2JmMTZjYmQ0MDgzNmNmMGRj
15
+ YmRmNjJiMTY3NGU2OTRkNTc1ZGNkZDNkZjZmNjY5NTM1YmNiODI=
@@ -144,13 +144,25 @@ module Hoodoo
144
144
  model.extend( ClassMethods )
145
145
  end
146
146
 
147
- # Forms a String containing the specified +model_klass+'s attribute names
148
- # escaped and joined with commas.
147
+ # Returns a String containing the specified +model_klass+'s attribute
148
+ # names considered as column names, escaped by the in-use database
149
+ # adaptor and joined into with commas.
149
150
  #
150
- # +model_klass+ Class which responds to .attribute_names
151
+ # +model_klass+:: Class which responds to <tt>#attribute_names</tt>.
151
152
  #
152
153
  def self.sanitised_column_string( model_klass )
153
- model_klass.attribute_names.map{ | c | ActiveRecord::Base.connection.quote_column_name( c ) }.join( ',' )
154
+ self.sanitised_column_string_for( model_klass.attribute_names )
155
+ end
156
+
157
+ # As ::sanitised_column_string but takes the array of attribute or
158
+ # column names directly.
159
+ #
160
+ # +attribute_array+:: Array of column names, as Strings or Symbols.
161
+ #
162
+ def self.sanitised_column_string_for( attribute_array )
163
+ attribute_array.map do | c |
164
+ ActiveRecord::Base.connection.quote_column_name( c )
165
+ end.join( ',' )
154
166
  end
155
167
 
156
168
  # Collection of class methods that get defined on an including class via
@@ -200,9 +212,20 @@ module Hoodoo
200
212
  # the Hoodoo::Services::Implementation instance methods
201
213
  # that a resource subclass implements.
202
214
  #
203
- def dated( context )
215
+ # Additional _named_ parameters are:
216
+ #
217
+ # +unquoted_column_names+:: (Optional) An Array of Strings giving one
218
+ # or more column names to use for the query.
219
+ # If omitted, all model attribtues are used
220
+ # as columns. If the "id" column is not
221
+ # included in the Array, it will be added
222
+ # anyway as this column is mandatory. The
223
+ # effect is equivalent to an Array given in
224
+ # the ActiveRecord +select+ method.
225
+ #
226
+ def dated( context, unquoted_column_names: nil )
204
227
  date_time = context.request.dated_at || Time.now
205
- return self.dated_at( date_time )
228
+ return self.dated_at( date_time, unquoted_column_names: unquoted_column_names )
206
229
  end
207
230
 
208
231
  # Return an ActiveRecord::Relation scoping a query to include only model
@@ -215,39 +238,52 @@ module Hoodoo
215
238
  # can be converted to a DateTime instance, for which the
216
239
  # "effective dated" scope is to be constructed.
217
240
  #
218
- def dated_at( date_time = Time.now )
241
+ # Additional _named_ parameters are:
242
+ #
243
+ # +unquoted_column_names+:: (Optional) An Array of Strings giving one
244
+ # or more column names to use for the query.
245
+ # If omitted, all model attribtues are used
246
+ # as columns. If the "id" column is not
247
+ # included in the Array, it will be added
248
+ # anyway as this column is mandatory. The
249
+ # effect is equivalent to an Array given in
250
+ # the ActiveRecord +select+ method.
251
+ #
252
+ def dated_at( date_time = Time.now, unquoted_column_names: nil )
219
253
 
220
254
  dating_table_name = dated_with_table_name()
221
255
  return all() if dating_table_name.nil? # "Model.all" -> returns anonymous scope
222
256
 
223
257
  # Rationalise and convert the date time to UTC.
224
258
 
225
- date_time = Hoodoo::Utilities.rationalise_datetime( date_time ).utc
226
-
227
- # Create a string that specifies this model's attributes escaped and
228
- # joined by commas for use in a SQL query.
259
+ date_time = Hoodoo::Utilities.rationalise_datetime( date_time ).utc
260
+ safe_date_time = self.sanitize( date_time ) # ActiveRecord provides #sanitize
229
261
 
230
- formatted_model_attributes = Hoodoo::ActiveRecord::Dated.sanitised_column_string( self )
262
+ # Create strings that specify the required attributes escaped and
263
+ # joined by commas for use in a SQL query, for both main and history
264
+ # tables.
231
265
 
232
- # Convert date_time to a String suitable for an SQL query.
266
+ safe_name_string = self.quoted_column_name_string(
267
+ unquoted_column_names: unquoted_column_names
268
+ )
233
269
 
234
- string_date_time = sanitize( date_time )
270
+ safe_history_name_string = self.quoted_column_name_string_for_history(
271
+ unquoted_column_names: unquoted_column_names
272
+ )
235
273
 
236
274
  # A query that combines historical and current records which are
237
275
  # effective at the specified date time.
238
276
 
239
277
  nested_query = %{
240
278
  (
241
- SELECT #{ formatted_model_attributes } FROM (
242
- SELECT #{ formatted_model_attributes }, updated_at as effective_start, null AS effective_end
279
+ SELECT #{ safe_name_string } FROM (
280
+ SELECT #{ safe_name_string },"updated_at" AS "effective_start",NULL AS "effective_end"
243
281
  FROM #{ self.table_name }
244
-
245
282
  UNION ALL
246
-
247
- SELECT #{ self.dated_with_history_column_mapping }, effective_start, effective_end
283
+ SELECT #{ safe_history_name_string },"effective_start","effective_end"
248
284
  FROM #{ dating_table_name }
249
285
  ) AS u
250
- WHERE effective_start <= #{ string_date_time } AND (effective_end > #{ string_date_time } OR effective_end IS NULL)
286
+ WHERE "effective_start" <= #{ safe_date_time } AND ("effective_end" > #{ safe_date_time } OR "effective_end" IS NULL)
251
287
  ) AS #{ self.table_name }
252
288
  }
253
289
 
@@ -263,26 +299,42 @@ module Hoodoo
263
299
  # If historic dating hasn't been enabled via a call to #dating_enabled,
264
300
  # then the default 'all' scope is returned instead.
265
301
  #
266
- def dated_historical_and_current
302
+ # _Named_ parameters are:
303
+ #
304
+ # +unquoted_column_names+:: (Optional) An Array of Strings giving one
305
+ # or more column names to use for the query.
306
+ # If omitted, all model attribtues are used
307
+ # as columns. If the "id" column is not
308
+ # included in the Array, it will be added
309
+ # anyway as this column is mandatory. The
310
+ # effect is equivalent to an Array given in
311
+ # the ActiveRecord +select+ method.
312
+ #
313
+ def dated_historical_and_current( unquoted_column_names: nil )
267
314
 
268
315
  dating_table_name = dated_with_table_name()
269
316
  return all() if dating_table_name.nil? # "Model.all" -> returns anonymous scope
270
317
 
271
- # Create a string that specifies this model's attributes escaped and
272
- # joined by commas for use in a SQL query.
318
+ # Create strings that specify the required attributes escaped and
319
+ # joined by commas for use in a SQL query, for both main and history
320
+ # tables.
321
+
322
+ safe_name_string = self.quoted_column_name_string(
323
+ unquoted_column_names: unquoted_column_names
324
+ )
273
325
 
274
- formatted_model_attributes = Hoodoo::ActiveRecord::Dated.sanitised_column_string( self )
326
+ safe_history_name_string = self.quoted_column_name_string_for_history(
327
+ unquoted_column_names: unquoted_column_names
328
+ )
275
329
 
276
330
  # A query that combines historical and current records.
277
331
 
278
332
  nested_query = %{
279
333
  (
280
- SELECT #{ formatted_model_attributes }
334
+ SELECT #{ safe_name_string }
281
335
  FROM #{ self.table_name }
282
-
283
336
  UNION ALL
284
-
285
- SELECT #{ self.dated_with_history_column_mapping }
337
+ SELECT #{ safe_history_name_string }
286
338
  FROM #{ dating_table_name }
287
339
  ) AS #{ self.table_name }
288
340
  }
@@ -314,27 +366,64 @@ module Hoodoo
314
366
  instance.nil? ? nil : instance.table_name
315
367
  end
316
368
 
317
- protected
369
+ protected
318
370
 
319
- # Forms and returns string which maps history table column names to the
320
- # primary table column names for use in SQL queries.
371
+ # Takes an Array of unquoted column names and returns a new Array of
372
+ # names quoted by the current database adapter.
321
373
  #
322
- def dated_with_history_column_mapping
323
- desired_attributes = self.attribute_names.dup
374
+ # +unquoted_column_names+:: Optional Array of unquoted column names
375
+ # to use. Must contain only Strings.
376
+ #
377
+ def quoted_column_names( unquoted_column_names )
378
+ return unquoted_column_names.map do | c |
379
+ ActiveRecord::Base.connection.quote_column_name( c )
380
+ end
381
+ end
324
382
 
325
- # Locate the primary key field, which must be called "id".
383
+ # Returns a String of comma-separated sanitised (quoted) column names
384
+ # based on this model's attribute names, or the given array of unquoted
385
+ # column names.
386
+ #
387
+ # _Named_ parameters are:
388
+ #
389
+ # +unquoted_column_names+:: Optional Array of unquoted column names
390
+ # to use. Must contain only Strings. If column
391
+ # "id" is missing, it will be added for you.
392
+ #
393
+ def quoted_column_name_string( unquoted_column_names: nil )
394
+ unquoted_column_names ||= self.attribute_names()
395
+ unquoted_column_names << 'id' unless unquoted_column_names.include?( 'id' )
326
396
 
327
- primary_key_index = desired_attributes.index( 'id' )
397
+ return self.quoted_column_names( unquoted_column_names ).join( ',' )
398
+ end
328
399
 
329
- # Sanitise the attribute names.
400
+ # As ::quoted_column_name_string, but returns a String appropriate for
401
+ # the history table. Notably, this requires a source column of "uuid" to
402
+ # be mapped in as column name "id" and works on the assumption that the
403
+ # primary key is "id".
404
+ #
405
+ # _Named_ parameters are:
406
+ #
407
+ # +unquoted_column_names+:: Optional Array of unquoted column names
408
+ # to use. Must contain only Strings. If column
409
+ # "id" is missing, it will be added for you.
410
+ #
411
+ def quoted_column_name_string_for_history( unquoted_column_names: nil )
412
+ unquoted_column_names ||= self.attribute_names
413
+ primary_key_index = unquoted_column_names.index( 'id' )
330
414
 
331
- desired_attributes.map!{ | c | ActiveRecord::Base.connection.quote_column_name( c ) }
415
+ if primary_key_index.nil?
416
+ unquoted_column_names << 'id'
417
+ primary_key_index = unquoted_column_names.count - 1
418
+ end
332
419
 
333
- # Map the primary key.
420
+ quoted_column_names = self.quoted_column_names( unquoted_column_names )
421
+ quoted_primary_key_name = quoted_column_names[ primary_key_index ]
422
+ history_primary_key = '"uuid" AS ' << quoted_primary_key_name
334
423
 
335
- desired_attributes[ primary_key_index ] = 'uuid as ' << desired_attributes[ primary_key_index ]
424
+ quoted_column_names[ primary_key_index ] = history_primary_key
336
425
 
337
- return desired_attributes.join( ',' )
426
+ return quoted_column_names.join( ',' )
338
427
  end
339
428
 
340
429
  end
@@ -148,7 +148,7 @@ module Hoodoo
148
148
  # around somewhere.
149
149
 
150
150
  @wrapping_session = session
151
- @wrapped_endpoint.session_id = session.session_id
151
+ @wrapped_endpoint.session_id = session.session_id unless session.nil?
152
152
  return nil
153
153
  end
154
154
  end
@@ -12,6 +12,6 @@ module Hoodoo
12
12
  # The Hoodoo gem version. If this changes, ensure that the date in
13
13
  # "hoodoo.gemspec" is correct and run "bundle install" (or "update").
14
14
  #
15
- VERSION = '1.1.0'
15
+ VERSION = '1.1.1'
16
16
 
17
17
  end
@@ -77,7 +77,7 @@ describe Hoodoo::ActiveRecord::Dated do
77
77
  @uuid_a = Hoodoo::UUID.generate
78
78
  @uuid_b = Hoodoo::UUID.generate
79
79
 
80
- @now = Time.now.utc
80
+ @now = Time.now.utc
81
81
 
82
82
  # uuid, data, created_at, effective_end, effective_start
83
83
  [
@@ -208,7 +208,6 @@ describe Hoodoo::ActiveRecord::Dated do
208
208
  end
209
209
 
210
210
  context '.dated_historical_and_current' do
211
-
212
211
  it 'returns counts correctly' do
213
212
  expect( model_klass.dated_historical_and_current.count ).to be 6
214
213
  end
@@ -217,14 +216,41 @@ describe Hoodoo::ActiveRecord::Dated do
217
216
  expect( model_klass.dated_historical_and_current.pluck( :data ) ).to match_array( [ 'one', 'two', 'three', 'four', 'five', 'six' ] )
218
217
  end
219
218
 
219
+ context 'SQL' do
220
+ it 'has expected default columns' do
221
+ sql = model_klass.dated_historical_and_current.to_sql.downcase
222
+
223
+ expect( sql ).to include( 'select "id","data","created_at","updated_at"' )
224
+ expect( sql ).to include( 'select "uuid" as "id","data","created_at","updated_at"' )
225
+ end
226
+
227
+ it 'handles custom column selections' do
228
+ sql = model_klass.dated_historical_and_current(
229
+ unquoted_column_names: [ 'id', 'created_at' ]
230
+ ).to_sql.downcase
231
+
232
+ expect( sql ).to include( 'select "id","created_at"' )
233
+ expect( sql ).to include( 'select "uuid" as "id","created_at"' )
234
+ end
235
+
236
+ it 'handles custom column selections that omit "id"' do
237
+ sql = model_klass.dated_historical_and_current(
238
+ unquoted_column_names: [ 'created_at' ]
239
+ ).to_sql.downcase
240
+
241
+ expect( sql ).to include( 'select "created_at","id"' )
242
+ expect( sql ).to include( 'select "created_at","uuid" as "id"' )
243
+ end
244
+ end
220
245
  end
221
246
 
222
247
  end
223
248
 
224
249
  context "using default effective dating config" do
225
250
 
226
- # Must be defined as a method rather than using a let statement as let
227
- # statement values cannot be used in before blocks.
251
+ # Must be defined as a method rather than using a 'let' statement as
252
+ # 'let' statement values cannot be used in 'before' blocks.
253
+ #
228
254
  def model_klass
229
255
  RSpecModelEffectiveDateTest
230
256
  end
@@ -235,8 +261,9 @@ describe Hoodoo::ActiveRecord::Dated do
235
261
 
236
262
  context "overriding history table name" do
237
263
 
238
- # Must be defined as a method rather than using a let statement as let
239
- # statement values cannot be used in before blocks.
264
+ # Must be defined as a method rather than using a 'let' statement as
265
+ # 'let' statement values cannot be used in 'before' blocks.
266
+ #
240
267
  def model_klass
241
268
  RSpecModelEffectiveDateTestOverride
242
269
  end
@@ -245,4 +272,74 @@ describe Hoodoo::ActiveRecord::Dated do
245
272
 
246
273
  end
247
274
 
275
+ context "SQL and column selections" do
276
+ before :each do
277
+ @now = Time.now.utc
278
+ @safe_now = RSpecModelEffectiveDateTestOverride.sanitize( @now )
279
+
280
+ request = Hoodoo::Services::Request.new
281
+ @context = Hoodoo::Services::Context.new( nil, request, nil, nil )
282
+
283
+ @context.request.dated_at = @now
284
+ end
285
+
286
+ def run_other_expectations( sql )
287
+ expect( sql ).to include( "from r_spec_model_effective_date_history_entries" )
288
+ expect( sql ).to include( "\"effective_start\" <= #{ @safe_now }" )
289
+ expect( sql ).to include( "\"effective_end\" > #{ @safe_now }" )
290
+ expect( sql ).to include( "\"effective_end\" is null" )
291
+ end
292
+
293
+ it 'generates expected basic SQL' do
294
+ sql = RSpecModelEffectiveDateTestOverride.dated( @context ).to_sql.downcase
295
+
296
+ expect( sql ).to include( 'select "id","data","created_at","updated_at"' )
297
+ expect( sql ).to include( 'select "uuid" as "id","data","created_at","updated_at"' )
298
+ run_other_expectations( sql )
299
+ end
300
+
301
+ it 'generates expected column-selected SQL via #dated' do
302
+ sql = RSpecModelEffectiveDateTestOverride.dated(
303
+ @context,
304
+ unquoted_column_names: [ 'id', 'created_at' ]
305
+ ).to_sql.downcase
306
+
307
+ expect( sql ).to include( 'select "id","created_at"' )
308
+ expect( sql ).to include( 'select "uuid" as "id","created_at"' )
309
+ run_other_expectations( sql )
310
+ end
311
+
312
+ it 'includes "id" if omitted, via #dated' do
313
+ sql = RSpecModelEffectiveDateTestOverride.dated(
314
+ @context,
315
+ unquoted_column_names: [ 'created_at' ]
316
+ ).to_sql.downcase
317
+
318
+ expect( sql ).to include( 'select "created_at","id"' )
319
+ expect( sql ).to include( 'select "created_at","uuid" as "id"' )
320
+ run_other_expectations( sql )
321
+ end
322
+
323
+ it 'generates expected column-selected SQL via #dated_at' do
324
+ sql = RSpecModelEffectiveDateTestOverride.dated_at(
325
+ @now,
326
+ unquoted_column_names: [ 'id', 'created_at' ]
327
+ ).to_sql.downcase
328
+
329
+ expect( sql ).to include( 'select "id","created_at"' )
330
+ expect( sql ).to include( 'select "uuid" as "id","created_at"' )
331
+ run_other_expectations( sql )
332
+ end
333
+
334
+ it 'includes "id" if omitted, via #dated_at' do
335
+ sql = RSpecModelEffectiveDateTestOverride.dated_at(
336
+ @now,
337
+ unquoted_column_names: [ 'created_at' ]
338
+ ).to_sql.downcase
339
+
340
+ expect( sql ).to include( 'select "created_at","id"' )
341
+ expect( sql ).to include( 'select "created_at","uuid" as "id"' )
342
+ run_other_expectations( sql )
343
+ end
344
+ end
248
345
  end
@@ -163,7 +163,7 @@ describe Hoodoo::ActiveRecord::Support do
163
163
  manual_scope = RSpecFullScopeForTestSubclass.dated( @context ).to_sql()
164
164
 
165
165
  expect( manual_scope ).to include( "FROM #{ @thtname1 }" )
166
- expect( manual_scope ).to include( "effective_end > #{ RSpecFullScopeForTestSubclass.sanitize( @test_time_value ) }" )
166
+ expect( manual_scope ).to include( "\"effective_end\" > #{ RSpecFullScopeForTestSubclass.sanitize( @test_time_value ) }" )
167
167
  end
168
168
 
169
169
  it 'secure' do
@@ -201,7 +201,7 @@ describe Hoodoo::ActiveRecord::Support do
201
201
  manual_scope = RSpecFullScopeForTestBaseSubclassWithoutOverrides.dated( @context ).to_sql()
202
202
 
203
203
  expect( manual_scope ).to include( "FROM #{ @thtname2 }" )
204
- expect( manual_scope ).to include( "effective_end > #{ RSpecFullScopeForTestBaseSubclassWithoutOverrides.sanitize( @test_time_value ) }" )
204
+ expect( manual_scope ).to include( "\"effective_end\" > #{ RSpecFullScopeForTestBaseSubclassWithoutOverrides.sanitize( @test_time_value ) }" )
205
205
  end
206
206
 
207
207
  it 'secure' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hoodoo
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Loyalty New Zealand
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-26 00:00:00.000000000 Z
11
+ date: 2016-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools