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/tds_interface.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
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
|
@@ -30,14 +31,12 @@ module Pod4
|
|
30
31
|
|
31
32
|
attr_reader :id_fld
|
32
33
|
|
33
|
-
|
34
34
|
class << self
|
35
35
|
#--
|
36
36
|
# These are set in the class because it keeps the model code cleaner: the definition of the
|
37
37
|
# interface stays in the interface, and doesn't leak out into the model.
|
38
38
|
#++
|
39
39
|
|
40
|
-
|
41
40
|
##
|
42
41
|
# Use this to set the database name.
|
43
42
|
#
|
@@ -49,7 +48,6 @@ module Pod4
|
|
49
48
|
raise Pod4Error, "You need to use set_db to set the database name"
|
50
49
|
end
|
51
50
|
|
52
|
-
|
53
51
|
##
|
54
52
|
# Use this to set the schema name (optional)
|
55
53
|
#
|
@@ -59,7 +57,6 @@ module Pod4
|
|
59
57
|
|
60
58
|
def schema; nil; end
|
61
59
|
|
62
|
-
|
63
60
|
##
|
64
61
|
# Use this to set the name of the table
|
65
62
|
#
|
@@ -71,37 +68,41 @@ module Pod4
|
|
71
68
|
raise Pod4Error, "You need to use set_table to set the table name"
|
72
69
|
end
|
73
70
|
|
74
|
-
|
75
71
|
##
|
76
72
|
# This sets the column that holds the unique id for the table
|
77
73
|
#
|
78
|
-
def set_id_fld(idFld)
|
74
|
+
def set_id_fld(idFld, opts={})
|
75
|
+
ai = opts.fetch(:autoincrement) { true }
|
79
76
|
define_class_method(:id_fld) {idFld.to_s.to_sym}
|
77
|
+
define_class_method(:id_ai) {!!ai}
|
80
78
|
end
|
81
79
|
|
82
80
|
def id_fld
|
83
81
|
raise Pod4Error, "You need to use set_table to set the table name"
|
84
82
|
end
|
85
83
|
|
86
|
-
end
|
87
|
-
##
|
88
|
-
|
84
|
+
end # of class << self
|
89
85
|
|
90
86
|
##
|
91
|
-
# Initialise the interface by passing it a TinyTds connection hash
|
92
|
-
# also pass an object which pretends to be a TinyTds client, in which case the hash is pretty
|
93
|
-
# much ignored.
|
87
|
+
# Initialise the interface by passing it a TinyTds connection hash OR a ConnectionPool object.
|
94
88
|
#
|
95
|
-
def initialize(
|
89
|
+
def initialize(args)
|
90
|
+
case args
|
91
|
+
when Hash
|
92
|
+
@connection = ConnectionPool.new(interface: self.class)
|
93
|
+
@connection.data_layer_options = args
|
94
|
+
|
95
|
+
when ConnectionPool
|
96
|
+
@connection = args
|
97
|
+
|
98
|
+
else
|
99
|
+
raise ArgumentError, "Bad Argument"
|
100
|
+
end
|
101
|
+
|
96
102
|
sc = self.class
|
97
103
|
raise(Pod4Error, 'no call to set_db in the interface definition') if sc.db.nil?
|
98
104
|
raise(Pod4Error, 'no call to set_table in the interface definition') if sc.table.nil?
|
99
105
|
raise(Pod4Error, 'no call to set_id_fld in the interface definition') if sc.id_fld.nil?
|
100
|
-
raise(ArgumentError, 'invalid connection hash') unless connectHash.kind_of?(Hash)
|
101
|
-
|
102
|
-
@connect_hash = connectHash.dup
|
103
|
-
@test_client = testClient
|
104
|
-
@client = nil
|
105
106
|
|
106
107
|
TinyTds::Client.default_query_options[:as] = :hash
|
107
108
|
TinyTds::Client.default_query_options[:symbolize_keys] = true
|
@@ -110,11 +111,11 @@ module Pod4
|
|
110
111
|
handle_error(e)
|
111
112
|
end
|
112
113
|
|
113
|
-
|
114
114
|
def db; self.class.db; end
|
115
115
|
def schema; self.class.schema; end
|
116
116
|
def table; self.class.table; end
|
117
117
|
def id_fld; self.class.id_fld; end
|
118
|
+
def id_ai ; self.class.id_ai; end
|
118
119
|
|
119
120
|
def quoted_table
|
120
121
|
schema ? %Q|[#{schema}].[#{table}]| : %Q|[#{table}]|
|
@@ -124,13 +125,11 @@ module Pod4
|
|
124
125
|
"[#{super(fld, nil)}]"
|
125
126
|
end
|
126
127
|
|
127
|
-
|
128
128
|
##
|
129
129
|
# Selection is a hash or something like it: keys should be field names. We return any records
|
130
130
|
# where the given fields equal the given values.
|
131
131
|
#
|
132
132
|
def list(selection=nil)
|
133
|
-
|
134
133
|
raise(Pod4::DatabaseError, 'selection parameter is not a hash') \
|
135
134
|
unless selection.nil? || selection.respond_to?(:keys)
|
136
135
|
|
@@ -141,24 +140,29 @@ module Pod4
|
|
141
140
|
handle_error(e)
|
142
141
|
end
|
143
142
|
|
144
|
-
|
145
143
|
##
|
146
|
-
# Record is a
|
144
|
+
# Record is a Hash or Octothorpe of field: value
|
147
145
|
#
|
148
146
|
def create(record)
|
149
|
-
raise
|
150
|
-
|
147
|
+
raise Octothorpe::BadHash if record.nil?
|
148
|
+
ot = Octothorpe.new(record)
|
151
149
|
|
152
|
-
|
150
|
+
if id_ai
|
151
|
+
ot = ot.reject{|k,_| k == id_fld}
|
152
|
+
else
|
153
|
+
raise(ArgumentError, "ID field missing from record") if ot[id_fld].nil?
|
154
|
+
end
|
153
155
|
|
156
|
+
sql, vals = sql_insert(ot)
|
154
157
|
x = select sql_subst(sql, *vals.map{|v| quote v})
|
155
158
|
x.first[id_fld]
|
156
159
|
|
157
|
-
rescue
|
158
|
-
|
160
|
+
rescue Octothorpe::BadHash
|
161
|
+
raise ArgumentError, "Bad type for record parameter"
|
162
|
+
rescue
|
163
|
+
handle_error $!
|
159
164
|
end
|
160
165
|
|
161
|
-
|
162
166
|
##
|
163
167
|
# ID corresponds to whatever you set in set_id_fld
|
164
168
|
#
|
@@ -178,11 +182,9 @@ module Pod4
|
|
178
182
|
&& e.cause.class == TinyTds::Error \
|
179
183
|
&& e.cause.message =~ /conversion failed/i
|
180
184
|
|
181
|
-
|
182
185
|
handle_error(e)
|
183
186
|
end
|
184
187
|
|
185
|
-
|
186
188
|
##
|
187
189
|
# ID is whatever you set in the interface using set_id_fld record should be a Hash or
|
188
190
|
# Octothorpe.
|
@@ -202,7 +204,6 @@ module Pod4
|
|
202
204
|
handle_error(e)
|
203
205
|
end
|
204
206
|
|
205
|
-
|
206
207
|
##
|
207
208
|
# ID is whatever you set in the interface using set_id_fld
|
208
209
|
#
|
@@ -218,7 +219,6 @@ module Pod4
|
|
218
219
|
handle_error(e)
|
219
220
|
end
|
220
221
|
|
221
|
-
|
222
222
|
##
|
223
223
|
# Override the sql_insert method in sql_helper since our SQL is rather different
|
224
224
|
#
|
@@ -234,7 +234,6 @@ module Pod4
|
|
234
234
|
[sql, vals]
|
235
235
|
end
|
236
236
|
|
237
|
-
|
238
237
|
##
|
239
238
|
# Run SQL code on the server. Return the results.
|
240
239
|
#
|
@@ -250,10 +249,10 @@ module Pod4
|
|
250
249
|
def select(sql)
|
251
250
|
raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
|
252
251
|
|
253
|
-
|
252
|
+
client = ensure_connected
|
254
253
|
|
255
254
|
Pod4.logger.debug(__FILE__){ "select: #{sql}" }
|
256
|
-
query =
|
255
|
+
query = client.execute(sql)
|
257
256
|
|
258
257
|
rows = []
|
259
258
|
query.each do |r|
|
@@ -273,17 +272,16 @@ module Pod4
|
|
273
272
|
handle_error(e)
|
274
273
|
end
|
275
274
|
|
276
|
-
|
277
275
|
##
|
278
276
|
# Run SQL code on the server; return true or false for success or failure
|
279
277
|
#
|
280
278
|
def execute(sql)
|
281
279
|
raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
|
282
280
|
|
283
|
-
|
281
|
+
client = ensure_connected
|
284
282
|
|
285
283
|
Pod4.logger.debug(__FILE__){ "execute: #{sql}" }
|
286
|
-
r =
|
284
|
+
r = client.execute(sql)
|
287
285
|
|
288
286
|
r.do
|
289
287
|
r
|
@@ -292,62 +290,79 @@ module Pod4
|
|
292
290
|
handle_error(e)
|
293
291
|
end
|
294
292
|
|
295
|
-
|
296
293
|
##
|
297
294
|
# Wrapper for the data source library escape routine, which is all we can offer in terms of SQL
|
298
295
|
# injection protection. (Its not much.)
|
299
296
|
#
|
300
297
|
def escape(thing)
|
301
|
-
|
302
|
-
thing.kind_of?(String) ?
|
298
|
+
client = ensure_connected
|
299
|
+
thing.kind_of?(String) ? client.escape(thing) : thing
|
303
300
|
end
|
304
301
|
|
305
|
-
|
306
|
-
private
|
307
|
-
|
308
|
-
|
309
302
|
##
|
310
303
|
# Open the connection to the database.
|
311
304
|
#
|
312
|
-
#
|
305
|
+
# This is called by ConnectionPool.
|
313
306
|
#
|
314
|
-
def
|
307
|
+
def new_connection(params)
|
315
308
|
Pod4.logger.info(__FILE__){ "Connecting to DB" }
|
316
|
-
client =
|
309
|
+
client = TinyTds::Client.new(params)
|
317
310
|
raise "Bad Connection" unless client.active?
|
318
311
|
|
319
|
-
|
320
|
-
execute("use [#{self.class.db}]")
|
312
|
+
client.execute("use [#{self.class.db}]").do
|
321
313
|
|
322
|
-
|
314
|
+
client
|
323
315
|
|
324
316
|
rescue => e
|
325
317
|
handle_error(e)
|
326
318
|
end
|
327
319
|
|
328
|
-
|
329
320
|
##
|
330
321
|
# Close the connection to the database.
|
331
322
|
#
|
332
|
-
# We don't actually use this
|
333
|
-
#
|
323
|
+
# We don't actually use this. Theoretically it would be called by ConnectionPool, but we
|
324
|
+
# don't. I've left it in for completeness.
|
334
325
|
#
|
335
|
-
def
|
326
|
+
def close_connection(conn)
|
336
327
|
Pod4.logger.info(__FILE__){ "Closing connection to DB" }
|
337
|
-
|
328
|
+
conn.close unless conn.nil?
|
338
329
|
|
339
330
|
rescue => e
|
340
331
|
handle_error(e)
|
341
332
|
end
|
342
333
|
|
334
|
+
##
|
335
|
+
# Expose @connection for test purposes only
|
336
|
+
#
|
337
|
+
def _connection
|
338
|
+
@connection
|
339
|
+
end
|
340
|
+
|
341
|
+
private
|
343
342
|
|
344
343
|
##
|
345
|
-
#
|
344
|
+
# Return an open client connection from the Connection Pool, or else raise an error
|
346
345
|
#
|
347
|
-
def
|
348
|
-
|
346
|
+
def ensure_connected
|
347
|
+
client = @connection.client(self)
|
348
|
+
|
349
|
+
# If this connection has expired somehow, try to get another one.
|
350
|
+
unless connected?(client)
|
351
|
+
@connection.drop(self)
|
352
|
+
client = @connection.client(self)
|
353
|
+
end
|
354
|
+
|
355
|
+
fail "Bad Connection" unless connected?(client)
|
356
|
+
|
357
|
+
client
|
349
358
|
end
|
350
359
|
|
360
|
+
##
|
361
|
+
# True if we are connected to a database
|
362
|
+
#
|
363
|
+
def connected?(conn)
|
364
|
+
conn && conn.active?
|
365
|
+
end
|
351
366
|
|
352
367
|
def handle_error(err, kaller=nil)
|
353
368
|
kaller ||= caller[1..-1]
|
@@ -369,7 +384,6 @@ module Pod4
|
|
369
384
|
|
370
385
|
end
|
371
386
|
|
372
|
-
|
373
387
|
##
|
374
388
|
# Overrride the quote routine in sql_helper.
|
375
389
|
#
|
@@ -390,12 +404,11 @@ module Pod4
|
|
390
404
|
|
391
405
|
end
|
392
406
|
|
393
|
-
|
394
407
|
def read_or_die(id)
|
395
408
|
raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
|
396
409
|
end
|
397
410
|
|
398
|
-
end
|
411
|
+
end # of TdsInterface
|
399
412
|
|
400
413
|
|
401
414
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'pod4/errors'
|
2
|
+
require 'pod4/metaxing'
|
3
|
+
|
4
|
+
|
5
|
+
module Pod4
|
6
|
+
|
7
|
+
|
8
|
+
##
|
9
|
+
# A mixin that extends the model DSL to simplify use of custom interface methods.
|
10
|
+
#
|
11
|
+
# We add one command to the model DSL: set_custom_list. It's optional.
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# set_custom_list
|
15
|
+
# ---------------
|
16
|
+
#
|
17
|
+
# class Bar < Pod4::Model
|
18
|
+
# include Pod4::Tweaking
|
19
|
+
#
|
20
|
+
# class Interface < Pod4::PgInterface
|
21
|
+
# set table :bar
|
22
|
+
# set_id_field :id, autoincrement: true
|
23
|
+
#
|
24
|
+
# # Example custom interface method
|
25
|
+
# def list_paged(drop=0, limit=15)
|
26
|
+
# execute %Q|select * from bar offset #{drop} rows fetch next #{limit} rows only;|
|
27
|
+
# end
|
28
|
+
# end # of Interface
|
29
|
+
#
|
30
|
+
# set_interface Interface.new($conn)
|
31
|
+
# set_custom_list :list_paged
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Use this when you want to make a special version of the List action. It takes one parameter:
|
35
|
+
# the name of a custom method you have defined on the Interface. A corresponding method will be
|
36
|
+
# created on the model. Any parameters you pass to the model method will be passed on to your
|
37
|
+
# custom interface method.
|
38
|
+
#
|
39
|
+
# Your custom interface method should return an array of Octothorpes or Hashes; the
|
40
|
+
# corresponding model method will return an array of instances of the model, just as #list does.
|
41
|
+
#
|
42
|
+
# Obviously this means that the keys in your array of Hash/Octothorpe must match the column
|
43
|
+
# attributes in the model. Any missing attributes will be set to nil; any extra attributes will
|
44
|
+
# be ignored. But the ID field must be present as a key, or else an exception will be raised. if
|
45
|
+
# you want to indicate that no records were found, you must return an empty Array and not nil.
|
46
|
+
#
|
47
|
+
# Just as with List proper, the array of model instances have not had validation run against
|
48
|
+
# them, and are all status :empty.
|
49
|
+
#
|
50
|
+
module Tweaking
|
51
|
+
|
52
|
+
##
|
53
|
+
# A little bit of magic, for which I apologise.
|
54
|
+
#
|
55
|
+
# When you include this module it actually adds the methods in ClassMethods to the class as if
|
56
|
+
# you had called `extend TypeCasting:ClassMethds` *AND* (theoretically, in this case) adds the
|
57
|
+
# methods in InstanceMethods as if you had written `prepend TypeCasting::InstanceMethods`.
|
58
|
+
#
|
59
|
+
# In my defence: I didn't want to have to make you remember to do that...
|
60
|
+
#
|
61
|
+
def self.included(base)
|
62
|
+
base.extend ClassMethods
|
63
|
+
# base.send(:prepend, InstanceMethods)
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
module ClassMethods
|
68
|
+
include Metaxing
|
69
|
+
|
70
|
+
def set_custom_list(method)
|
71
|
+
raise ArgumentError, "Bad custom interface method" unless interface.respond_to?(method)
|
72
|
+
raise ArgumentError, "Method already exists on the model" \
|
73
|
+
if (self.instance_methods - self.class.instance_methods).include?(method)
|
74
|
+
|
75
|
+
define_class_method(method) do |*args|
|
76
|
+
mname = "#{interface.class.name}.#{method}"
|
77
|
+
rows = interface.send(method, *args)
|
78
|
+
|
79
|
+
raise Pod4Error, "#{mname} did not return an array" unless rows.is_a? Array
|
80
|
+
raise Pod4Error, "#{mname} did not return an array of records" \
|
81
|
+
unless rows.all?{|r| r.is_a?(Hash) || r.is_a?(Octothorpe) }
|
82
|
+
|
83
|
+
raise Pod4Error, "#{mname} returned some records with no ID" \
|
84
|
+
unless rows.all?{|r| r.has_key? interface.id_fld }
|
85
|
+
|
86
|
+
rows.map do |r|
|
87
|
+
rec = self.new r[interface.id_fld]
|
88
|
+
rec.map_to_model r
|
89
|
+
rec
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end # of ClassMethods
|
96
|
+
|
97
|
+
|
98
|
+
# module InstanceMethods
|
99
|
+
# end # of InstanceMethods
|
100
|
+
|
101
|
+
end # of Tweaking
|
102
|
+
|
103
|
+
|
104
|
+
end
|
105
|
+
|