pod4 0.10.6 → 1.0.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 (48) hide show
  1. checksums.yaml +5 -5
  2. data/.bugs/bugs +2 -1
  3. data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
  4. data/.hgtags +1 -0
  5. data/Gemfile +5 -5
  6. data/README.md +157 -46
  7. data/lib/pod4/basic_model.rb +9 -22
  8. data/lib/pod4/connection.rb +67 -0
  9. data/lib/pod4/connection_pool.rb +154 -0
  10. data/lib/pod4/errors.rb +20 -0
  11. data/lib/pod4/interface.rb +34 -12
  12. data/lib/pod4/model.rb +32 -27
  13. data/lib/pod4/nebulous_interface.rb +25 -30
  14. data/lib/pod4/null_interface.rb +22 -16
  15. data/lib/pod4/pg_interface.rb +84 -104
  16. data/lib/pod4/sequel_interface.rb +138 -82
  17. data/lib/pod4/tds_interface.rb +83 -70
  18. data/lib/pod4/tweaking.rb +105 -0
  19. data/lib/pod4/version.rb +1 -1
  20. data/md/breaking_changes.md +80 -0
  21. data/spec/common/basic_model_spec.rb +67 -70
  22. data/spec/common/connection_pool_parallelism_spec.rb +154 -0
  23. data/spec/common/connection_pool_spec.rb +246 -0
  24. data/spec/common/connection_spec.rb +129 -0
  25. data/spec/common/model_ai_missing_id_spec.rb +256 -0
  26. data/spec/common/model_plus_encrypting_spec.rb +16 -4
  27. data/spec/common/model_plus_tweaking_spec.rb +128 -0
  28. data/spec/common/model_plus_typecasting_spec.rb +10 -4
  29. data/spec/common/model_spec.rb +283 -363
  30. data/spec/common/nebulous_interface_spec.rb +159 -108
  31. data/spec/common/null_interface_spec.rb +88 -65
  32. data/spec/common/sequel_interface_pg_spec.rb +217 -161
  33. data/spec/common/shared_examples_for_interface.rb +50 -50
  34. data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
  35. data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
  36. data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
  37. data/spec/mri/pg_encrypting_spec.rb +1 -1
  38. data/spec/mri/pg_interface_spec.rb +311 -223
  39. data/spec/mri/sequel_encrypting_spec.rb +1 -1
  40. data/spec/mri/sequel_interface_spec.rb +177 -180
  41. data/spec/mri/tds_encrypting_spec.rb +1 -1
  42. data/spec/mri/tds_interface_spec.rb +296 -212
  43. data/tags +340 -174
  44. metadata +19 -11
  45. data/md/fixme.md +0 -3
  46. data/md/roadmap.md +0 -125
  47. data/md/typecasting.md +0 -80
  48. data/spec/common/model_new_validate_spec.rb +0 -204
@@ -1,16 +1,17 @@
1
- require 'octothorpe'
2
- require 'date'
3
- require 'time'
4
- require 'bigdecimal'
1
+ require "octothorpe"
2
+ require "date"
3
+ require "time"
4
+ require "bigdecimal"
5
5
 
6
- require_relative 'interface'
7
- require_relative 'errors'
8
- require_relative 'sql_helper'
6
+ require_relative "interface"
7
+ require_relative "connection_pool"
8
+ require_relative "errors"
9
+ require_relative "sql_helper"
9
10
 
10
11
 
11
12
  module Pod4
12
13
 
13
-
14
+
14
15
  ##
15
16
  # Pod4 Interface for requests on a SQL table via pg, the PostgresQL adapter.
16
17
  #
@@ -20,7 +21,7 @@ module Pod4
20
21
  # class CustomerInterface < SwingShift::PgInterface
21
22
  # set_schema :public # optional
22
23
  # set_table :customer
23
- # set_id_fld :id
24
+ # set_id_fld :id, autoincrement: true
24
25
  # end
25
26
  #
26
27
  class PgInterface < Interface
@@ -28,7 +29,6 @@ module Pod4
28
29
 
29
30
  attr_reader :id_fld
30
31
 
31
-
32
32
  class << self
33
33
  #--
34
34
  # These are set in the class because it keeps the model code cleaner: the definition of the
@@ -60,42 +60,48 @@ module Pod4
60
60
  ##
61
61
  # Set the name of the column that holds the unique id for the table.
62
62
  #
63
- def set_id_fld(idFld)
63
+ def set_id_fld(idFld, opts={})
64
+ ai = opts.fetch(:autoincrement) { true }
64
65
  define_class_method(:id_fld) {idFld.to_s.to_sym}
66
+ define_class_method(:id_ai) {!!ai}
65
67
  end
66
68
 
67
69
  def id_fld
68
70
  raise Pod4Error, "You need to use set_id_fld to set the ID column"
69
71
  end
70
72
 
71
- end
72
- ##
73
+ def id_ai
74
+ raise Pod4Error, "You need to use set_id_fld to set the ID column"
75
+ end
73
76
 
77
+ end # of class << self
74
78
 
75
79
  ##
76
- # Initialise the interface by passing it a Pg connection hash. For testing ONLY you can also
77
- # pass an object which pretends to be a Pg client, in which case the hash is pretty much
78
- # ignored.
80
+ # Initialise the interface by passing it a Pg connection hash, or a Pod4::ConnectionPool
81
+ # object.
79
82
  #
80
- def initialize(connectHash, testClient=nil)
81
- raise(ArgumentError, 'invalid connection hash') unless connectHash.kind_of?(Hash)
83
+ def initialize(arg)
84
+ case arg
85
+ when Hash
86
+ @connection = ConnectionPool.new(interface: self.class)
87
+ @connection.data_layer_options = arg
82
88
 
83
- @connect_hash = connectHash.dup
84
- @test_client = testClient
85
- @client = nil
89
+ when ConnectionPool
90
+ @connection = arg
91
+
92
+ else
93
+ raise ArgumentError, "Bad argument"
94
+ end
86
95
 
87
96
  rescue => e
88
97
  handle_error(e)
89
98
  end
90
99
 
91
-
92
100
  def schema; self.class.schema; end
93
101
  def table; self.class.table; end
94
102
  def id_fld; self.class.id_fld; end
103
+ def id_ai; self.class.id_ai; end
95
104
 
96
-
97
- ##
98
- #
99
105
  def list(selection=nil)
100
106
  raise(ArgumentError, 'selection parameter is not a hash') \
101
107
  unless selection.nil? || selection.respond_to?(:keys)
@@ -107,26 +113,32 @@ module Pod4
107
113
  handle_error(e)
108
114
  end
109
115
 
110
-
111
116
  ##
112
- # Record is a hash of field: value
117
+ # Record is a hash or octothorpe of field: value
113
118
  #
114
119
  # By a happy coincidence, insert returns the unique ID for the record, which is just what we
115
120
  # want to do, too.
116
121
  #
117
122
  def create(record)
118
- raise(ArgumentError, "Bad type for record parameter") \
119
- unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
123
+ raise Octothorpe::BadHash if record.nil?
124
+ ot = Octothorpe.new(record)
125
+
126
+ if id_ai
127
+ ot = ot.reject{|k,_| k == id_fld}
128
+ else
129
+ raise(ArgumentError, "ID field missing from record") if ot[id_fld].nil?
130
+ end
120
131
 
121
- sql, vals = sql_insert(record)
132
+ sql, vals = sql_insert(ot)
122
133
  x = selectp(sql, *vals)
123
134
  x.first[id_fld]
124
135
 
125
- rescue => e
126
- handle_error(e)
136
+ rescue Octothorpe::BadHash
137
+ raise ArgumentError, "Bad type for record parameter"
138
+ rescue
139
+ handle_error $!
127
140
  end
128
141
 
129
-
130
142
  ##
131
143
  # ID corresponds to whatever you set in set_id_fld
132
144
  #
@@ -146,9 +158,8 @@ module Pod4
146
158
  handle_error(e)
147
159
  end
148
160
 
149
-
150
161
  ##
151
- # ID is whatever you set in the interface using set_id_fld record should be a Hash or
162
+ # ID is whatever you set in the interface using set_id_fld; record should be a Hash or
152
163
  # Octothorpe.
153
164
  #
154
165
  def update(id, record)
@@ -166,7 +177,6 @@ module Pod4
166
177
  handle_error(e)
167
178
  end
168
179
 
169
-
170
180
  ##
171
181
  # ID is whatever you set in the interface using set_id_fld
172
182
  #
@@ -182,7 +192,6 @@ module Pod4
182
192
  handle_error(e)
183
193
  end
184
194
 
185
-
186
195
  ##
187
196
  # Run SQL code on the server. Return the results.
188
197
  #
@@ -198,12 +207,11 @@ module Pod4
198
207
  def select(sql)
199
208
  raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
200
209
 
201
- ensure_connection
202
-
210
+ client = ensure_connection
203
211
  Pod4.logger.debug(__FILE__){ "select: #{sql}" }
204
212
 
205
213
  rows = []
206
- @client.exec(sql) do |query|
214
+ client.exec(sql) do |query|
207
215
  oids = make_oid_hash(query)
208
216
 
209
217
  query.each do |r|
@@ -218,15 +226,13 @@ module Pod4
218
226
  end
219
227
  end
220
228
 
221
- @client.cancel
222
-
229
+ client.cancel
223
230
  rows
224
231
 
225
232
  rescue => e
226
233
  handle_error(e)
227
234
  end
228
235
 
229
-
230
236
  ##
231
237
  # Run SQL code on the server as per select() but with parameter insertion.
232
238
  #
@@ -236,12 +242,11 @@ module Pod4
236
242
  def selectp(sql, *vals)
237
243
  raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
238
244
 
239
- ensure_connection
240
-
245
+ client = ensure_connection
241
246
  Pod4.logger.debug(__FILE__){ "select: #{sql} #{vals.inspect}" }
242
247
 
243
248
  rows = []
244
- @client.exec_params( *parse_for_params(sql, vals) ) do |query|
249
+ client.exec_params( *parse_for_params(sql, vals) ) do |query|
245
250
  oids = make_oid_hash(query)
246
251
 
247
252
  query.each do |r|
@@ -256,30 +261,27 @@ module Pod4
256
261
  end
257
262
  end
258
263
 
259
- @client.cancel
264
+ client.cancel
260
265
  rows
261
266
 
262
267
  rescue => e
263
268
  handle_error(e)
264
269
  end
265
270
 
266
-
267
271
  ##
268
272
  # Run SQL code on the server; return true or false for success or failure
269
273
  #
270
274
  def execute(sql)
271
275
  raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
272
276
 
273
- ensure_connection
274
-
277
+ client = ensure_connection
275
278
  Pod4.logger.debug(__FILE__){ "execute: #{sql}" }
276
- @client.exec(sql)
279
+ client.exec(sql)
277
280
 
278
281
  rescue => e
279
282
  handle_error(e)
280
283
  end
281
284
 
282
-
283
285
  ##
284
286
  # Run SQL code on the server as per execute() but with parameter insertion.
285
287
  #
@@ -289,105 +291,90 @@ module Pod4
289
291
  def executep(sql, *vals)
290
292
  raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
291
293
 
292
- ensure_connection
293
-
294
+ client = ensure_connection
294
295
  Pod4.logger.debug(__FILE__){ "parameterised execute: #{sql}" }
295
- @client.exec_params( *parse_for_params(sql, vals) )
296
+ client.exec_params( *parse_for_params(sql, vals) )
296
297
 
297
298
  rescue => e
298
299
  handle_error(e)
299
300
  end
300
301
 
301
-
302
- private
303
-
304
-
305
302
  ##
306
303
  # Open the connection to the database.
307
304
  #
308
- # No parameters are needed: the option hash has everything we need.
305
+ # This is called from a Connection Object.
309
306
  #
310
- def open
307
+ def new_connection(params)
311
308
  Pod4.logger.info(__FILE__){ "Connecting to DB" }
312
309
 
313
- client = @test_Client || PG.connect(@connect_hash)
314
- raise DataBaseError, "Bad Connection" \
315
- unless client.status == PG::CONNECTION_OK
310
+ client = PG.connect(params)
311
+ raise DataBaseError, "Bad Connection" unless client.status == PG::CONNECTION_OK
316
312
 
317
- # This gives us type mapping for integers, floats, booleans, and dates -- but annoyingly the
318
- # PostgreSQL types 'numeric' and 'money' remain as strings... we fudge that elsewhere.
319
- #
320
- # NOTE we now deal with ALL mapping elsewhere, since pg_jruby does not support type mapping.
321
- # Also: no annoying error messages, and it seems to be a hell of a lot faster now...
322
- #
323
- # if defined?(PG::BasicTypeMapForQueries)
324
- # client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client)
325
- # end
326
- #
327
- # if defined?(PG::BasicTypeMapForResults)
328
- # client.type_map_for_results = PG::BasicTypeMapForResults.new(client)
329
- # end
330
-
331
- @client = client
332
- self
313
+ client
333
314
 
334
315
  rescue => e
335
316
  handle_error(e)
336
317
  end
337
318
 
338
-
339
319
  ##
340
320
  # Close the connection to the database.
341
321
  #
342
- # We don't actually use this, but it's here for completeness. Maybe a caller will find it
343
- # useful.
322
+ # Pod4 itself doesn't use this(?)
344
323
  #
345
- def close
324
+ def close_connection(conn)
346
325
  Pod4.logger.info(__FILE__){ "Closing connection to DB" }
347
- @client.finish unless @client.nil?
326
+ conn.finish unless conn.nil?
348
327
 
349
328
  rescue => e
350
329
  handle_error(e)
351
330
  end
352
331
 
332
+ ##
333
+ # Expose @connection, for testing only.
334
+ #
335
+ def _connection
336
+ @connection
337
+ end
338
+
339
+ private
353
340
 
354
341
  ##
355
342
  # True if we are connected to a database
356
343
  #
357
- def connected?
358
- return false if @client.nil?
359
- return false if @client.status != PG::CONNECTION_OK
344
+ def connected?(conn)
345
+ return false if conn.nil?
346
+ return false if conn.status != PG::CONNECTION_OK
360
347
 
361
348
  # pg's own examples suggest we poke the database rather than trust
362
349
  # @client.status, so...
363
- @client.exec('select 1;')
350
+ conn.exec('select 1;')
364
351
  true
365
352
  rescue PG::Error
366
353
  return false
367
354
  end
368
355
 
369
-
370
356
  ##
357
+ # Return a client from the connection pool and check it is open.
371
358
  # Since pg gives us @client.reset to reconnect, we should use it rather than just call open
372
359
  #
373
360
  def ensure_connection
361
+ client = @connection.client(self)
374
362
 
375
- if @client.nil?
363
+ if client.nil?
376
364
  open
377
- elsif ! connected?
378
- @client.reset
365
+ elsif ! connected?(client)
366
+ client.reset
379
367
  end
380
368
 
369
+ client
381
370
  end
382
371
 
383
-
384
372
  def handle_error(err, kaller=nil)
385
373
  kaller ||= caller[1..-1]
386
374
 
387
375
  Pod4.logger.error(__FILE__){ err.message }
388
376
 
389
377
  case err
390
-
391
378
  when ArgumentError, Pod4::Pod4Error, Pod4::CantContinue
392
379
  raise err.class, err.message, kaller
393
380
 
@@ -396,12 +383,10 @@ module Pod4
396
383
 
397
384
  else
398
385
  raise Pod4::Pod4Error, err.message, kaller
399
-
400
386
  end
401
387
 
402
388
  end
403
389
 
404
-
405
390
  ##
406
391
  # build a hash of column -> oid
407
392
  #
@@ -413,11 +398,10 @@ module Pod4
413
398
 
414
399
  end
415
400
 
416
-
417
401
  ##
418
402
  # Cast a query row
419
403
  #
420
- # This is to step around problems with pg type mapping There is definitely a way to tell pg to
404
+ # This is to step around problems with pg type mapping. There is definitely a way to tell pg to
421
405
  # cast money and numeric as BigDecimal, but, it's not documented...
422
406
  #
423
407
  # Also, for the pg_jruby gem, type mapping doesn't work at all?
@@ -452,7 +436,6 @@ module Pod4
452
436
  end
453
437
 
454
438
  end
455
-
456
439
 
457
440
  ##
458
441
  # Given a value from the database which supposedly represents a boolean ... return one.
@@ -470,12 +453,10 @@ module Pod4
470
453
  end
471
454
  end
472
455
 
473
-
474
456
  def read_or_die(id)
475
457
  raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
476
458
  end
477
459
 
478
-
479
460
  def parse_for_params(sql, vals)
480
461
  new_params = sql.scan("%s").map.with_index{|e,i| "$#{i + 1}" }
481
462
  new_vals = vals.map{|v| v.nil? ? nil : quote(v, nil).to_s }
@@ -483,8 +464,7 @@ module Pod4
483
464
  [ sql_subst(sql, *new_params), new_vals ]
484
465
  end
485
466
 
486
-
487
- end
467
+ end # of PgInterface
488
468
 
489
469
 
490
470
  end
@@ -1,7 +1,8 @@
1
- require 'octothorpe'
1
+ require "octothorpe"
2
2
 
3
- require_relative 'interface'
4
- require_relative 'errors'
3
+ require_relative "interface"
4
+ require_relative "errors"
5
+ require_relative "connection"
5
6
 
6
7
 
7
8
  module Pod4
@@ -22,18 +23,21 @@ module Pod4
22
23
  # appropriate -- but it also depends on the underlying adapter. TinyTds maps dates to strings,
23
24
  # for example.
24
25
  #
26
+ # Connections: Because the Sequel client -- the "DB" object -- has its own connection pool and
27
+ # does most of the heavy lifting for us, the only reason we use the Connection class is to defer
28
+ # creating the DB object until the first time we need it. Most of what Connection does, we don't
29
+ # need, so our interactions with Connection are a little strange.
30
+ #
25
31
  class SequelInterface < Interface
26
32
 
27
33
  attr_reader :id_fld
28
34
 
29
-
30
35
  class << self
31
36
  #---
32
37
  # These are set in the class because it keeps the model code cleaner: the definition of the
33
38
  # interface stays in the interface, and doesn't leak out into the model.
34
39
  #+++
35
40
 
36
-
37
41
  ##
38
42
  # Use this to set the schema name (optional)
39
43
  #
@@ -43,7 +47,6 @@ module Pod4
43
47
 
44
48
  def schema; nil; end
45
49
 
46
-
47
50
  ##
48
51
  # Set the table name.
49
52
  #
@@ -55,82 +58,71 @@ module Pod4
55
58
  raise Pod4Error, "You need to use set_table to set the table name"
56
59
  end
57
60
 
58
-
59
61
  ##
60
62
  # Set the unique id field on the table.
61
63
  #
62
- def set_id_fld(idFld)
64
+ def set_id_fld(idFld, opts={})
65
+ ai = opts.fetch(:autoincrement) { true }
63
66
  define_class_method(:id_fld) {idFld.to_s.to_sym}
67
+ define_class_method(:id_ai) {!!ai}
64
68
  end
65
69
 
66
70
  def id_fld
67
71
  raise Pod4Error, "You need to use set_id_fld to set the ID column name"
68
72
  end
69
73
 
70
- end
71
- ##
74
+ def id_ai
75
+ raise Pod4Error, "You need to use set_id_fld to set the ID column name"
76
+ end
72
77
 
78
+ end # of class << self
73
79
 
74
80
  ##
75
- # Initialise the interface by passing it the Sequel DB object.
81
+ # Initialise the interface by passing it the Sequel DB object. Or a Sequel
82
+ # connection string. Or a Pod4::Connection object.
76
83
  #
77
- def initialize(db)
78
- raise(ArgumentError, "Bad database") unless db.kind_of? Sequel::Database
84
+ def initialize(arg)
79
85
  raise(Pod4Error, 'no call to set_table in the interface definition') if self.class.table.nil?
80
86
  raise(Pod4Error, 'no call to set_id_fld in the interface definition') if self.class.id_fld.nil?
81
87
 
82
- @sequel_version = Sequel.respond_to?(:qualify) ? 5 : 4
83
- @db = db # reference to the db object
84
- @id_fld = self.class.id_fld
88
+ case arg
89
+ when Sequel::Database
90
+ @connection = Connection.new(interface: self.class)
91
+ @connection.data_layer_options = arg
85
92
 
86
- @table =
87
- if schema
88
- if @sequel_version == 5
89
- db[ Sequel[schema][table] ]
90
- else
91
- db[ "#{schema}__#{table}".to_sym ]
92
- end
93
- else
94
- db[table]
95
- end
96
-
97
- # Work around a problem with jdbc-postgresql where it throws an exception whenever it sees
98
- # the money type. This workaround actually allows us to return a BigDecimal, so it's better
99
- # than using postgres_pr when under jRuby!
100
- if @db.uri =~ /jdbc:postgresql/
101
- @db.conversion_procs[790] = ->(s){BigDecimal(s[1..-1]) rescue nil}
102
- c = Sequel::JDBC::Postgres::Dataset
93
+ when Hash, String
94
+ @connection = Connection.new(interface: self.class)
95
+ @connection.data_layer_options = Sequel.connect(arg)
96
+
97
+ when Connection
98
+ @connection = arg
103
99
 
104
- if @sequel_version >= 5
105
- # In Sequel 5 everything is frozen, so some hacking is required.
106
- # See https://github.com/jeremyevans/sequel/issues/1458
107
- vals = c::PG_SPECIFIC_TYPES + [Java::JavaSQL::Types::DOUBLE]
108
- c.send(:remove_const, :PG_SPECIFIC_TYPES) # We can probably get away with just const_set, but.
109
- c.send(:const_set, :PG_SPECIFIC_TYPES, vals.freeze)
110
100
  else
111
- c::PG_SPECIFIC_TYPES << Java::JavaSQL::Types::DOUBLE
112
- end
101
+ raise ArgumentError, "Bad argument"
102
+
113
103
  end
114
104
 
105
+ @sequel_version = Sequel.respond_to?(:qualify) ? 5 : 4
106
+ @id_fld = self.class.id_fld
107
+ @db = nil
108
+
115
109
  rescue => e
116
110
  handle_error(e)
117
111
  end
118
112
 
119
-
120
113
  def schema; self.class.schema; end
121
114
  def table; self.class.table; end
122
115
  def id_fld; self.class.id_fld; end
116
+ def id_ai; self.class.id_ai; end
123
117
 
124
118
  def quoted_table
125
119
  if schema
126
- %Q|#{@db.quote_identifier schema}.#{@db.quote_identifier table}|
120
+ %Q|#{db.quote_identifier schema}.#{db.quote_identifier table}|
127
121
  else
128
- @db.quote_identifier(table)
122
+ db.quote_identifier(table)
129
123
  end
130
124
  end
131
125
 
132
-
133
-
134
126
  ##
135
127
  # Selection is whatever Sequel's `where` supports.
136
128
  #
@@ -138,40 +130,42 @@ module Pod4
138
130
  sel = sanitise_hash(selection)
139
131
  Pod4.logger.debug(__FILE__) { "Listing #{self.class.table}: #{sel.inspect}" }
140
132
 
141
- (sel ? @table.where(sel) : @table.all).map {|x| Octothorpe.new(x) }
133
+ (sel ? db_table.where(sel) : db_table.all).map {|x| Octothorpe.new(x) }
142
134
  rescue => e
143
135
  handle_error(e)
144
136
  end
145
137
 
146
-
147
138
  ##
148
- # Record is a hash of field: value
139
+ # Record is a Hash or Octothorpe of field: value
149
140
  #
150
141
  def create(record)
151
- raise(ArgumentError, "Bad type for record parameter") \
152
- unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
153
-
154
- Pod4.logger.debug(__FILE__) { "Creating #{self.class.table}: #{record.inspect}" }
142
+ raise Octothorpe::BadHash if record.nil?
143
+ ot = Octothorpe.new(record)
155
144
 
156
- id = @table.insert( sanitise_hash(record.to_h) )
145
+ if id_ai
146
+ ot = ot.reject{|k,_| k == id_fld}
147
+ else
148
+ raise(ArgumentError, "ID field missing from record") if ot[id_fld].nil?
149
+ end
157
150
 
158
- # Sequel doesn't return the key unless it is an autoincrement; otherwise it turns a row
159
- # number regardless. It probably doesn' t matter, but try to catch that anyway.
160
- # (bamf: If your non-incrementing key happens to be an integer, this won't work...)
151
+ Pod4.logger.debug(__FILE__) { "Creating #{self.class.table}: #{ot.inspect}" }
161
152
 
162
- id_val = record[id_fld] || record[id_fld.to_s]
153
+ id = db_table.insert( sanitise_hash(ot.to_h) )
163
154
 
164
- if (id.kind_of?(Fixnum) || id.nil?) && id_val && !id_val.kind_of?(Fixnum)
165
- id_val
166
- else
155
+ # Sequel doesn't return the key unless it is an autoincrement; otherwise it turns a row
156
+ # number, which isn't much use to us. We always return the key.
157
+ if id_ai
167
158
  id
159
+ else
160
+ ot[id_fld]
168
161
  end
169
-
170
- rescue => e
171
- handle_error(e)
162
+
163
+ rescue Octothorpe::BadHash
164
+ raise ArgumentError, "Bad type for record parameter"
165
+ rescue
166
+ handle_error $!
172
167
  end
173
168
 
174
-
175
169
  ##
176
170
  # ID corresponds to whatever you set in set_id_fld
177
171
  #
@@ -179,7 +173,7 @@ module Pod4
179
173
  raise(ArgumentError, "ID parameter is nil") if id.nil?
180
174
  Pod4.logger.debug(__FILE__) { "Reading #{self.class.table} where #{@id_fld}=#{id}" }
181
175
 
182
- Octothorpe.new( @table[@id_fld => id] )
176
+ Octothorpe.new( db_table[@id_fld => id] )
183
177
 
184
178
  rescue Sequel::DatabaseError
185
179
  raise CantContinue, "Problem reading record. Is '#{id}' really an ID?"
@@ -188,7 +182,6 @@ module Pod4
188
182
  handle_error(e)
189
183
  end
190
184
 
191
-
192
185
  ##
193
186
  # ID is whatever you set in the interface using set_id_fld record should be a Hash or
194
187
  # Octothorpe.
@@ -200,13 +193,12 @@ module Pod4
200
193
  "Updating #{self.class.table} where #{@id_fld}=#{id}: #{record.inspect}"
201
194
  end
202
195
 
203
- @table.where(@id_fld => id).update( sanitise_hash(record.to_h) )
196
+ db_table.where(@id_fld => id).update( sanitise_hash(record.to_h) )
204
197
  self
205
198
  rescue => e
206
199
  handle_error(e)
207
200
  end
208
201
 
209
-
210
202
  ##
211
203
  # ID is whatever you set in the interface using set_id_fld
212
204
  #
@@ -217,13 +209,12 @@ module Pod4
217
209
  "Deleting #{self.class.table} where #{@id_fld}=#{id}"
218
210
  end
219
211
 
220
- @table.where(@id_fld => id).delete
212
+ db_table.where(@id_fld => id).delete
221
213
  self
222
214
  rescue => e
223
215
  handle_error(e)
224
216
  end
225
217
 
226
-
227
218
  ##
228
219
  # Bonus method: execute arbitrary SQL. Returns nil.
229
220
  #
@@ -231,12 +222,13 @@ module Pod4
231
222
  raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
232
223
  Pod4.logger.debug(__FILE__) { "Execute SQL: #{sql}" }
233
224
 
234
- @db.run(sql)
225
+ c = @connection.client(self)
226
+ c.run(sql)
227
+
235
228
  rescue => e
236
229
  handle_error(e)
237
230
  end
238
231
 
239
-
240
232
  ##
241
233
  # Bonus method: execute SQL as per execute(), but parameterised.
242
234
  #
@@ -252,13 +244,11 @@ module Pod4
252
244
  raise(ArgumentError, "Bad mode parameter") unless %i|insert delete update|.include?(mode)
253
245
  Pod4.logger.debug(__FILE__) { "Parameterised execute #{mode} SQL: #{sql}" }
254
246
 
255
- @db[sql, *values].send(mode)
247
+ @connection.client(self)[sql, *values].send(mode)
256
248
  rescue => e
257
249
  handle_error(e)
258
250
  end
259
251
 
260
-
261
-
262
252
  ##
263
253
  # Bonus method: execute arbitrary SQL and return the resulting dataset as a Hash.
264
254
  #
@@ -266,12 +256,11 @@ module Pod4
266
256
  raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
267
257
  Pod4.logger.debug(__FILE__) { "Select SQL: #{sql}" }
268
258
 
269
- @db[sql].all
259
+ @connection.client(self)[sql].all
270
260
  rescue => e
271
261
  handle_error(e)
272
262
  end
273
263
 
274
-
275
264
  ##
276
265
  # Bonus method: execute arbitrary SQL as per select(), but parameterised.
277
266
  #
@@ -282,14 +271,38 @@ module Pod4
282
271
  raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
283
272
  Pod4.logger.debug(__FILE__) { "Parameterised select SQL: #{sql}" }
284
273
 
285
- @db.fetch(sql, *values).all
274
+ @connection.client(self).fetch(sql, *values).all
275
+
286
276
  rescue => e
287
277
  handle_error(e)
288
278
  end
289
279
 
280
+ ##
281
+ # Called by @connection to get the DB object.
282
+ # Never called internally. Given the way Sequel works -- the data layer option object passed
283
+ # to Connection actually is the client object -- we do something a bit weird here.
284
+ #
285
+ def new_connection(options)
286
+ options
287
+ end
290
288
 
291
- private
289
+ ###
290
+ # Called by @connection to "close" the DB object.
291
+ # Never called internally, and given the way Sequel works, we implement this as a dummy
292
+ # operation
293
+ #
294
+ def close_connection
295
+ self
296
+ end
297
+
298
+ ##
299
+ # Return the connection object, for testing purposes only
300
+ #
301
+ def _connection
302
+ @connection
303
+ end
292
304
 
305
+ private
293
306
 
294
307
  ##
295
308
  # Helper routine to handle or re-raise the right exception.
@@ -329,7 +342,28 @@ module Pod4
329
342
  end
330
343
 
331
344
  end
345
+
346
+ def sequel_fudges(db)
347
+ # Work around a problem with jdbc-postgresql where it throws an exception whenever it sees
348
+ # the money type. This workaround actually allows us to return a BigDecimal, so it's better
349
+ # than using postgres_pr when under jRuby!
350
+ if db.uri =~ /jdbc:postgresql/
351
+ db.conversion_procs[790] = ->(s){BigDecimal(s[1..-1]) rescue nil}
352
+ c = Sequel::JDBC::Postgres::Dataset
332
353
 
354
+ if @sequel_version >= 5
355
+ # In Sequel 5 everything is frozen, so some hacking is required.
356
+ # See https://github.com/jeremyevans/sequel/issues/1458
357
+ vals = c::PG_SPECIFIC_TYPES + [Java::JavaSQL::Types::DOUBLE]
358
+ c.send(:remove_const, :PG_SPECIFIC_TYPES) # We can probably get away with just const_set, but.
359
+ c.send(:const_set, :PG_SPECIFIC_TYPES, vals.freeze)
360
+ else
361
+ c::PG_SPECIFIC_TYPES << Java::JavaSQL::Types::DOUBLE
362
+ end
363
+ end
364
+
365
+ db
366
+ end
333
367
 
334
368
  ##
335
369
  # Sequel behaves VERY oddly if you pass a symbol as a value to the hash you give to a
@@ -354,12 +388,34 @@ module Pod4
354
388
 
355
389
  end
356
390
 
357
-
358
391
  def read_or_die(id)
359
392
  raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
360
393
  end
361
394
 
362
- end
395
+ def db
396
+ if @db
397
+ @db
398
+ else
399
+ @db = @connection.client(self)
400
+ sequel_fudges(@db)
401
+ end
402
+ end
403
+
404
+ def db_table
405
+
406
+ if schema
407
+ if @sequel_version == 5
408
+ db[ Sequel[schema][table] ]
409
+ else
410
+ db[ "#{schema}__#{table}".to_sym ]
411
+ end
412
+ else
413
+ db[table]
414
+ end
415
+
416
+ end
417
+
418
+ end # of SequelInterface
363
419
 
364
420
 
365
421
  end