pod4 0.6.2
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 +7 -0
- data/.hgignore +18 -0
- data/.hgtags +19 -0
- data/.rspec +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +556 -0
- data/Rakefile +30 -0
- data/lib/pod4/alert.rb +87 -0
- data/lib/pod4/basic_model.rb +137 -0
- data/lib/pod4/errors.rb +80 -0
- data/lib/pod4/interface.rb +110 -0
- data/lib/pod4/metaxing.rb +66 -0
- data/lib/pod4/model.rb +347 -0
- data/lib/pod4/nebulous_interface.rb +408 -0
- data/lib/pod4/null_interface.rb +148 -0
- data/lib/pod4/param.rb +29 -0
- data/lib/pod4/pg_interface.rb +460 -0
- data/lib/pod4/sequel_interface.rb +303 -0
- data/lib/pod4/tds_interface.rb +394 -0
- data/lib/pod4/version.rb +3 -0
- data/lib/pod4.rb +54 -0
- data/md/fixme.md +32 -0
- data/md/roadmap.md +69 -0
- data/pod4.gemspec +49 -0
- data/spec/README.md +19 -0
- data/spec/alert_spec.rb +173 -0
- data/spec/basic_model_spec.rb +220 -0
- data/spec/doc_no_pending.rb +5 -0
- data/spec/fixtures/database.rb +13 -0
- data/spec/model_spec.rb +760 -0
- data/spec/nebulous_interface_spec.rb +286 -0
- data/spec/null_interface_spec.rb +153 -0
- data/spec/param_spec.rb +89 -0
- data/spec/pg_interface_spec.rb +452 -0
- data/spec/pod4_spec.rb +88 -0
- data/spec/sequel_interface_spec.rb +466 -0
- data/spec/shared_examples_for_interface.rb +160 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/tds_interface_spec.rb +494 -0
- data/tags +106 -0
- metadata +316 -0
@@ -0,0 +1,460 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require 'octothorpe'
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
5
|
+
require 'bigdecimal'
|
6
|
+
|
7
|
+
require_relative 'interface'
|
8
|
+
require_relative 'errors'
|
9
|
+
|
10
|
+
|
11
|
+
module Pod4
|
12
|
+
|
13
|
+
|
14
|
+
##
|
15
|
+
# Pod4 Interface for requests on a SQL table via pg, the PostgresQL adapter.
|
16
|
+
#
|
17
|
+
# If your DB table is one-one with your model, you shouldn't need to override
|
18
|
+
# anything.
|
19
|
+
#
|
20
|
+
# Example:
|
21
|
+
# class CustomerInterface < SwingShift::PgInterface
|
22
|
+
# set_schema :public # optional
|
23
|
+
# set_table :customer
|
24
|
+
# set_id_fld :id
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class PgInterface < Interface
|
28
|
+
|
29
|
+
attr_reader :id_fld
|
30
|
+
|
31
|
+
|
32
|
+
class << self
|
33
|
+
#--
|
34
|
+
# These are set in the class because it keeps the model code cleaner: the
|
35
|
+
# definition of the interface stays in the interface, and doesn't leak
|
36
|
+
# out into the model.
|
37
|
+
#++
|
38
|
+
|
39
|
+
##
|
40
|
+
# Set the name of the schema. This is optional.
|
41
|
+
#
|
42
|
+
def set_schema(schema)
|
43
|
+
define_class_method(:schema) {schema.to_s.to_sym}
|
44
|
+
end
|
45
|
+
|
46
|
+
def schema; nil; end
|
47
|
+
|
48
|
+
|
49
|
+
##
|
50
|
+
# Set the name of the database table
|
51
|
+
#
|
52
|
+
def set_table(table)
|
53
|
+
define_class_method(:table) {table.to_s.to_sym}
|
54
|
+
end
|
55
|
+
|
56
|
+
def table
|
57
|
+
raise Pod4Error, "You need to use set_table to set the table name"
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
##
|
62
|
+
# Set the name of the column that holds the unique id for the table.
|
63
|
+
#
|
64
|
+
def set_id_fld(idFld)
|
65
|
+
define_class_method(:id_fld) {idFld.to_s.to_sym}
|
66
|
+
end
|
67
|
+
|
68
|
+
def id_fld
|
69
|
+
raise Pod4Error, "You need to use set_id_fld to set the ID column"
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
##
|
74
|
+
|
75
|
+
|
76
|
+
##
|
77
|
+
# Initialise the interface by passing it a Pg connection hash.
|
78
|
+
# For testing ONLY you can also pass an object which pretends to be a
|
79
|
+
# Pg client, in which case the hash is pretty much ignored.
|
80
|
+
#
|
81
|
+
def initialize(connectHash, testClient=nil)
|
82
|
+
raise(ArgumentError, 'invalid connection hash') \
|
83
|
+
unless connectHash.kind_of?(Hash)
|
84
|
+
|
85
|
+
@connect_hash = connectHash.dup
|
86
|
+
@test_client = testClient
|
87
|
+
@client = nil
|
88
|
+
|
89
|
+
rescue => e
|
90
|
+
handle_error(e)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
def schema; self.class.schema; end
|
95
|
+
def table; self.class.table; end
|
96
|
+
def id_fld; self.class.id_fld; end
|
97
|
+
|
98
|
+
def quoted_table
|
99
|
+
schema ? %Q|"#{schema}"."#{table}"| : %Q|"#{table}"|
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
##
|
104
|
+
# Selection is whatever Sequel's `where` supports.
|
105
|
+
#
|
106
|
+
def list(selection=nil)
|
107
|
+
raise(ArgumentError, 'selection parameter is not a hash') \
|
108
|
+
unless selection.nil? || selection.respond_to?(:keys)
|
109
|
+
|
110
|
+
if selection
|
111
|
+
sel = selection.map {|k,v| %Q|"#{k}" = #{quote v}| }.join(" and ")
|
112
|
+
sql = %Q|select *
|
113
|
+
from #{quoted_table}
|
114
|
+
where #{sel};|
|
115
|
+
|
116
|
+
else
|
117
|
+
sql = %Q|select * from #{quoted_table};|
|
118
|
+
end
|
119
|
+
|
120
|
+
select(sql) {|r| Octothorpe.new(r) }
|
121
|
+
|
122
|
+
rescue => e
|
123
|
+
handle_error(e)
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
##
|
128
|
+
# Record is a hash of field: value
|
129
|
+
# By a happy coincidence, insert returns the unique ID for the record,
|
130
|
+
# which is just what we want to do, too.
|
131
|
+
#
|
132
|
+
def create(record)
|
133
|
+
raise(ArgumentError, "Bad type for record parameter") \
|
134
|
+
unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
|
135
|
+
|
136
|
+
ks = record.keys.map {|k| %Q|"#{k}"| }.join(',')
|
137
|
+
vs = record.values.map {|v| quote v }.join(',')
|
138
|
+
|
139
|
+
sql = %Q|insert into #{quoted_table}
|
140
|
+
( #{ks} )
|
141
|
+
values( #{vs} )
|
142
|
+
returning "#{id_fld}";|
|
143
|
+
|
144
|
+
x = select(sql)
|
145
|
+
x.first[id_fld]
|
146
|
+
|
147
|
+
rescue => e
|
148
|
+
handle_error(e)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
##
|
153
|
+
# ID corresponds to whatever you set in set_id_fld
|
154
|
+
#
|
155
|
+
def read(id)
|
156
|
+
raise(ArgumentError, "ID parameter is nil") if id.nil?
|
157
|
+
|
158
|
+
sql = %Q|select *
|
159
|
+
from #{quoted_table}
|
160
|
+
where "#{id_fld}" = #{quote id};|
|
161
|
+
|
162
|
+
Octothorpe.new( select(sql).first )
|
163
|
+
|
164
|
+
rescue => e
|
165
|
+
# Select has already wrapped the error in a Pod4Error, but in this case
|
166
|
+
# we want to catch something
|
167
|
+
raise CantContinue, "That doesn't look like an ID" \
|
168
|
+
if e.cause.class == PG::InvalidTextRepresentation
|
169
|
+
|
170
|
+
handle_error(e)
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
##
|
175
|
+
# ID is whatever you set in the interface using set_id_fld
|
176
|
+
# record should be a Hash or Octothorpe.
|
177
|
+
#
|
178
|
+
def update(id, record)
|
179
|
+
raise(ArgumentError, "Bad type for record parameter") \
|
180
|
+
unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
|
181
|
+
|
182
|
+
read_or_die(id)
|
183
|
+
sets = record.map {|k,v| %Q| "#{k}" = #{quote v}| }.join(',')
|
184
|
+
|
185
|
+
sql = %Q|update #{quoted_table} set
|
186
|
+
#{sets}
|
187
|
+
where "#{id_fld}" = #{quote id};|
|
188
|
+
|
189
|
+
execute(sql)
|
190
|
+
|
191
|
+
self
|
192
|
+
|
193
|
+
rescue => e
|
194
|
+
handle_error(e)
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
##
|
199
|
+
# ID is whatever you set in the interface using set_id_fld
|
200
|
+
#
|
201
|
+
def delete(id)
|
202
|
+
read_or_die(id)
|
203
|
+
execute( %Q|delete from #{quoted_table} where "#{id_fld}" = #{quote id};| )
|
204
|
+
|
205
|
+
self
|
206
|
+
|
207
|
+
rescue => e
|
208
|
+
handle_error(e)
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
##
|
213
|
+
# Run SQL code on the server. Return the results.
|
214
|
+
#
|
215
|
+
# Will return an array of records, or you can use it in block mode, like
|
216
|
+
# this:
|
217
|
+
#
|
218
|
+
# select("select * from customer") do |r|
|
219
|
+
# # r is a single record
|
220
|
+
# end
|
221
|
+
#
|
222
|
+
# The returned results will be an array of hashes (or if you passed a
|
223
|
+
# block, of whatever you returned from the block).
|
224
|
+
#
|
225
|
+
def select(sql)
|
226
|
+
raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
|
227
|
+
|
228
|
+
ensure_connection
|
229
|
+
|
230
|
+
Pod4.logger.debug(__FILE__){ "select: #{sql}" }
|
231
|
+
|
232
|
+
rows = []
|
233
|
+
@client.exec(sql) do |query|
|
234
|
+
oids = make_oid_hash(query)
|
235
|
+
|
236
|
+
query.each do |r|
|
237
|
+
row = cast_row_fudge(r, oids)
|
238
|
+
|
239
|
+
if block_given?
|
240
|
+
rows << yield(row)
|
241
|
+
else
|
242
|
+
rows << row
|
243
|
+
end
|
244
|
+
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
@client.cancel
|
249
|
+
|
250
|
+
rows
|
251
|
+
|
252
|
+
rescue => e
|
253
|
+
handle_error(e)
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
##
|
258
|
+
# Run SQL code on the server; return true or false for success or failure
|
259
|
+
#
|
260
|
+
def execute(sql)
|
261
|
+
raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
|
262
|
+
|
263
|
+
ensure_connection
|
264
|
+
|
265
|
+
Pod4.logger.debug(__FILE__){ "execute: #{sql}" }
|
266
|
+
@client.exec(sql)
|
267
|
+
|
268
|
+
rescue => e
|
269
|
+
handle_error(e)
|
270
|
+
end
|
271
|
+
|
272
|
+
|
273
|
+
protected
|
274
|
+
|
275
|
+
|
276
|
+
##
|
277
|
+
# Open the connection to the database.
|
278
|
+
#
|
279
|
+
# No parameters are needed: the option hash has everything we need.
|
280
|
+
#
|
281
|
+
def open
|
282
|
+
Pod4.logger.info(__FILE__){ "Connecting to DB" }
|
283
|
+
|
284
|
+
client = @test_Client || PG.connect(@connect_hash)
|
285
|
+
raise DataBaseError, "Bad Connection" \
|
286
|
+
unless client.status == PG::CONNECTION_OK
|
287
|
+
|
288
|
+
# This gives us type mapping for integers, floats, booleans, and dates
|
289
|
+
# -- but annoyingly the PostgreSQL types 'numeric' and 'money' remain as
|
290
|
+
# strings... we fudge that elsewhere.
|
291
|
+
#
|
292
|
+
# NOTE we now deal with ALL mapping elsewhere, since pg_jruby does
|
293
|
+
# not support type mapping. Also: no annoying error messages, and it
|
294
|
+
# seems to be a hell of a lot faster now...
|
295
|
+
#
|
296
|
+
# if defined?(PG::BasicTypeMapForQueries)
|
297
|
+
# client.type_map_for_queries = PG::BasicTypeMapForQueries.new(client)
|
298
|
+
# end
|
299
|
+
#
|
300
|
+
# if defined?(PG::BasicTypeMapForResults)
|
301
|
+
# client.type_map_for_results = PG::BasicTypeMapForResults.new(client)
|
302
|
+
# end
|
303
|
+
|
304
|
+
@client = client
|
305
|
+
self
|
306
|
+
|
307
|
+
rescue => e
|
308
|
+
handle_error(e)
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
##
|
313
|
+
# Close the connection to the database.
|
314
|
+
# We don't actually use this, but it's here for completeness. Maybe a
|
315
|
+
# caller will find it useful.
|
316
|
+
#
|
317
|
+
def close
|
318
|
+
Pod4.logger.info(__FILE__){ "Closing connection to DB" }
|
319
|
+
@client.finish unless @client.nil?
|
320
|
+
|
321
|
+
rescue => e
|
322
|
+
handle_error(e)
|
323
|
+
end
|
324
|
+
|
325
|
+
|
326
|
+
##
|
327
|
+
# True if we are connected to a database
|
328
|
+
#
|
329
|
+
def connected?
|
330
|
+
return false if @client.nil?
|
331
|
+
return false if @client.status != PG::CONNECTION_OK
|
332
|
+
|
333
|
+
# pg's own examples suggest we poke the database rather than trust
|
334
|
+
# @client.status, so...
|
335
|
+
@client.exec('select 1;')
|
336
|
+
true
|
337
|
+
rescue PG::Error
|
338
|
+
return false
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
##
|
343
|
+
# Since pg gives us @client.reset to reconnect, we should use it rather
|
344
|
+
# than just call open
|
345
|
+
#
|
346
|
+
def ensure_connection
|
347
|
+
|
348
|
+
if @client.nil?
|
349
|
+
open
|
350
|
+
elsif ! connected?
|
351
|
+
@client.reset
|
352
|
+
end
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
|
357
|
+
def handle_error(err, kaller=nil)
|
358
|
+
kaller ||= caller[1..-1]
|
359
|
+
|
360
|
+
Pod4.logger.error(__FILE__){ err.message }
|
361
|
+
|
362
|
+
case err
|
363
|
+
|
364
|
+
when ArgumentError, Pod4::Pod4Error, Pod4::CantContinue
|
365
|
+
raise err.class, err.message, kaller
|
366
|
+
|
367
|
+
when PG::Error
|
368
|
+
raise Pod4::DatabaseError, err.message, kaller
|
369
|
+
|
370
|
+
else
|
371
|
+
raise Pod4::Pod4Error, err.message, kaller
|
372
|
+
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
def quote(fld)
|
379
|
+
|
380
|
+
case fld
|
381
|
+
when String, Date, Time, Symbol
|
382
|
+
"'#{fld}'"
|
383
|
+
when BigDecimal
|
384
|
+
fld.to_f
|
385
|
+
when nil
|
386
|
+
'NULL'
|
387
|
+
else
|
388
|
+
fld
|
389
|
+
end
|
390
|
+
|
391
|
+
end
|
392
|
+
|
393
|
+
|
394
|
+
private
|
395
|
+
|
396
|
+
|
397
|
+
##
|
398
|
+
# build a hash of column -> oid
|
399
|
+
#
|
400
|
+
def make_oid_hash(query)
|
401
|
+
|
402
|
+
query.fields.each_with_object({}) do |f,h|
|
403
|
+
h[f.to_sym] = query.ftype( query.fnumber(f) )
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
|
409
|
+
##
|
410
|
+
# Cast a query row
|
411
|
+
# This is to step around problems with pg type mapping
|
412
|
+
# There is definitely a way to tell pg to cast money and numeric as
|
413
|
+
# BigDecimal, but, it's not documented and no one can tell me how to do it!
|
414
|
+
#
|
415
|
+
# Also, for the pg_jruby gem, type mapping doesn't work at all?
|
416
|
+
#
|
417
|
+
def cast_row_fudge(row, oids)
|
418
|
+
lBool =->(s) { s.to_i = 1 || s.upcase == 'TRUE' }
|
419
|
+
lFloat =->(s) { Float(s) rescue s }
|
420
|
+
lInt =->(s) { Integer(s,10) rescue s }
|
421
|
+
lTime =->(s) { Time.parse(s) rescue s }
|
422
|
+
lDate =->(s) { Date.parse(s) rescue s }
|
423
|
+
lBigDec =->(s) { BigDecimal.new(s) rescue s }
|
424
|
+
|
425
|
+
row.each_with_object({}) do |(k,v),h|
|
426
|
+
key = k.to_sym
|
427
|
+
oid = oids[key]
|
428
|
+
|
429
|
+
h[key] =
|
430
|
+
case
|
431
|
+
when v.class != String then v # assume already converted
|
432
|
+
|
433
|
+
when oid == 1700 then lBigDec.(v) # numeric
|
434
|
+
when oid == 790 then lBigDec.(v[1..-1]) # "£1.23"
|
435
|
+
when oid == 1082 then lDate.(v)
|
436
|
+
|
437
|
+
when [16, 1560].include?(oid) then lBool.(v)
|
438
|
+
when [700, 701].include?(oid) then lFloat.(v)
|
439
|
+
when [20, 21, 23].include?(oid) then lInt.(v)
|
440
|
+
when [1114, 1184].include?(oid) then lTime.(v)
|
441
|
+
|
442
|
+
else v
|
443
|
+
end
|
444
|
+
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|
448
|
+
|
449
|
+
|
450
|
+
def read_or_die(id)
|
451
|
+
raise CantContinue, "'No record found with ID '#{id}'" \
|
452
|
+
if read(id).empty?
|
453
|
+
|
454
|
+
end
|
455
|
+
|
456
|
+
|
457
|
+
|
458
|
+
end
|
459
|
+
|
460
|
+
end
|