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