pod4 0.10.6 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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