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/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
|
+
|