hoodoo 1.1.0 → 1.1.1

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,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