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.
- checksums.yaml +5 -5
- data/.bugs/bugs +2 -1
- data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
- data/.hgtags +1 -0
- data/Gemfile +5 -5
- data/README.md +157 -46
- data/lib/pod4/basic_model.rb +9 -22
- data/lib/pod4/connection.rb +67 -0
- data/lib/pod4/connection_pool.rb +154 -0
- data/lib/pod4/errors.rb +20 -0
- data/lib/pod4/interface.rb +34 -12
- data/lib/pod4/model.rb +32 -27
- data/lib/pod4/nebulous_interface.rb +25 -30
- data/lib/pod4/null_interface.rb +22 -16
- data/lib/pod4/pg_interface.rb +84 -104
- data/lib/pod4/sequel_interface.rb +138 -82
- data/lib/pod4/tds_interface.rb +83 -70
- data/lib/pod4/tweaking.rb +105 -0
- data/lib/pod4/version.rb +1 -1
- data/md/breaking_changes.md +80 -0
- data/spec/common/basic_model_spec.rb +67 -70
- data/spec/common/connection_pool_parallelism_spec.rb +154 -0
- data/spec/common/connection_pool_spec.rb +246 -0
- data/spec/common/connection_spec.rb +129 -0
- data/spec/common/model_ai_missing_id_spec.rb +256 -0
- data/spec/common/model_plus_encrypting_spec.rb +16 -4
- data/spec/common/model_plus_tweaking_spec.rb +128 -0
- data/spec/common/model_plus_typecasting_spec.rb +10 -4
- data/spec/common/model_spec.rb +283 -363
- data/spec/common/nebulous_interface_spec.rb +159 -108
- data/spec/common/null_interface_spec.rb +88 -65
- data/spec/common/sequel_interface_pg_spec.rb +217 -161
- data/spec/common/shared_examples_for_interface.rb +50 -50
- data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
- data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
- data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
- data/spec/mri/pg_encrypting_spec.rb +1 -1
- data/spec/mri/pg_interface_spec.rb +311 -223
- data/spec/mri/sequel_encrypting_spec.rb +1 -1
- data/spec/mri/sequel_interface_spec.rb +177 -180
- data/spec/mri/tds_encrypting_spec.rb +1 -1
- data/spec/mri/tds_interface_spec.rb +296 -212
- data/tags +340 -174
- metadata +19 -11
- data/md/fixme.md +0 -3
- data/md/roadmap.md +0 -125
- data/md/typecasting.md +0 -80
- data/spec/common/model_new_validate_spec.rb +0 -204
data/lib/pod4/pg_interface.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require "octothorpe"
|
2
|
+
require "date"
|
3
|
+
require "time"
|
4
|
+
require "bigdecimal"
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
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
|
-
|
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
|
77
|
-
#
|
78
|
-
# ignored.
|
80
|
+
# Initialise the interface by passing it a Pg connection hash, or a Pod4::ConnectionPool
|
81
|
+
# object.
|
79
82
|
#
|
80
|
-
def initialize(
|
81
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
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
|
119
|
-
|
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(
|
132
|
+
sql, vals = sql_insert(ot)
|
122
133
|
x = selectp(sql, *vals)
|
123
134
|
x.first[id_fld]
|
124
135
|
|
125
|
-
rescue
|
126
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
305
|
+
# This is called from a Connection Object.
|
309
306
|
#
|
310
|
-
def
|
307
|
+
def new_connection(params)
|
311
308
|
Pod4.logger.info(__FILE__){ "Connecting to DB" }
|
312
309
|
|
313
|
-
client =
|
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
|
-
|
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
|
-
#
|
343
|
-
# useful.
|
322
|
+
# Pod4 itself doesn't use this(?)
|
344
323
|
#
|
345
|
-
def
|
324
|
+
def close_connection(conn)
|
346
325
|
Pod4.logger.info(__FILE__){ "Closing connection to DB" }
|
347
|
-
|
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
|
359
|
-
return false if
|
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
|
-
|
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
|
363
|
+
if client.nil?
|
376
364
|
open
|
377
|
-
elsif ! connected?
|
378
|
-
|
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
|
1
|
+
require "octothorpe"
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
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
|
-
|
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(
|
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
|
-
|
83
|
-
|
84
|
-
|
88
|
+
case arg
|
89
|
+
when Sequel::Database
|
90
|
+
@connection = Connection.new(interface: self.class)
|
91
|
+
@connection.data_layer_options = arg
|
85
92
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
112
|
-
|
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|#{
|
120
|
+
%Q|#{db.quote_identifier schema}.#{db.quote_identifier table}|
|
127
121
|
else
|
128
|
-
|
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 ?
|
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
|
139
|
+
# Record is a Hash or Octothorpe of field: value
|
149
140
|
#
|
150
141
|
def create(record)
|
151
|
-
raise
|
152
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
153
|
+
id = db_table.insert( sanitise_hash(ot.to_h) )
|
163
154
|
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
171
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
|
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
|