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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.bugs/bugs +2 -1
  3. data/.bugs/details/b5368c7ef19065fc597b5692314da71772660963.txt +53 -0
  4. data/.hgtags +1 -0
  5. data/Gemfile +5 -5
  6. data/README.md +157 -46
  7. data/lib/pod4/basic_model.rb +9 -22
  8. data/lib/pod4/connection.rb +67 -0
  9. data/lib/pod4/connection_pool.rb +154 -0
  10. data/lib/pod4/errors.rb +20 -0
  11. data/lib/pod4/interface.rb +34 -12
  12. data/lib/pod4/model.rb +32 -27
  13. data/lib/pod4/nebulous_interface.rb +25 -30
  14. data/lib/pod4/null_interface.rb +22 -16
  15. data/lib/pod4/pg_interface.rb +84 -104
  16. data/lib/pod4/sequel_interface.rb +138 -82
  17. data/lib/pod4/tds_interface.rb +83 -70
  18. data/lib/pod4/tweaking.rb +105 -0
  19. data/lib/pod4/version.rb +1 -1
  20. data/md/breaking_changes.md +80 -0
  21. data/spec/common/basic_model_spec.rb +67 -70
  22. data/spec/common/connection_pool_parallelism_spec.rb +154 -0
  23. data/spec/common/connection_pool_spec.rb +246 -0
  24. data/spec/common/connection_spec.rb +129 -0
  25. data/spec/common/model_ai_missing_id_spec.rb +256 -0
  26. data/spec/common/model_plus_encrypting_spec.rb +16 -4
  27. data/spec/common/model_plus_tweaking_spec.rb +128 -0
  28. data/spec/common/model_plus_typecasting_spec.rb +10 -4
  29. data/spec/common/model_spec.rb +283 -363
  30. data/spec/common/nebulous_interface_spec.rb +159 -108
  31. data/spec/common/null_interface_spec.rb +88 -65
  32. data/spec/common/sequel_interface_pg_spec.rb +217 -161
  33. data/spec/common/shared_examples_for_interface.rb +50 -50
  34. data/spec/jruby/sequel_encrypting_jdbc_pg_spec.rb +1 -1
  35. data/spec/jruby/sequel_interface_jdbc_ms_spec.rb +3 -3
  36. data/spec/jruby/sequel_interface_jdbc_pg_spec.rb +3 -23
  37. data/spec/mri/pg_encrypting_spec.rb +1 -1
  38. data/spec/mri/pg_interface_spec.rb +311 -223
  39. data/spec/mri/sequel_encrypting_spec.rb +1 -1
  40. data/spec/mri/sequel_interface_spec.rb +177 -180
  41. data/spec/mri/tds_encrypting_spec.rb +1 -1
  42. data/spec/mri/tds_interface_spec.rb +296 -212
  43. data/tags +340 -174
  44. metadata +19 -11
  45. data/md/fixme.md +0 -3
  46. data/md/roadmap.md +0 -125
  47. data/md/typecasting.md +0 -80
  48. data/spec/common/model_new_validate_spec.rb +0 -204
@@ -1,11 +1,12 @@
1
- require 'octothorpe'
2
- require 'date'
3
- require 'time'
4
- require 'bigdecimal'
1
+ require "octothorpe"
2
+ require "date"
3
+ require "time"
4
+ require "bigdecimal"
5
5
 
6
- require_relative 'interface'
7
- require_relative 'errors'
8
- require_relative 'sql_helper'
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.# For testing ONLY you can
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(connectHash, testClient=nil)
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 hash of field: value
144
+ # Record is a Hash or Octothorpe of field: value
147
145
  #
148
146
  def create(record)
149
- raise(ArgumentError, "Bad type for record parameter") \
150
- unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
147
+ raise Octothorpe::BadHash if record.nil?
148
+ ot = Octothorpe.new(record)
151
149
 
152
- sql, vals = sql_insert(record)
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 => e
158
- handle_error(e)
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
- open unless connected?
252
+ client = ensure_connected
254
253
 
255
254
  Pod4.logger.debug(__FILE__){ "select: #{sql}" }
256
- query = @client.execute(sql)
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
- open unless connected?
281
+ client = ensure_connected
284
282
 
285
283
  Pod4.logger.debug(__FILE__){ "execute: #{sql}" }
286
- r = @client.execute(sql)
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
- open unless connected?
302
- thing.kind_of?(String) ? @client.escape(thing) : thing
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
- # No parameters are needed: the option hash has everything we need.
305
+ # This is called by ConnectionPool.
313
306
  #
314
- def open
307
+ def new_connection(params)
315
308
  Pod4.logger.info(__FILE__){ "Connecting to DB" }
316
- client = @test_Client || TinyTds::Client.new(@connect_hash)
309
+ client = TinyTds::Client.new(params)
317
310
  raise "Bad Connection" unless client.active?
318
311
 
319
- @client = client
320
- execute("use [#{self.class.db}]")
312
+ client.execute("use [#{self.class.db}]").do
321
313
 
322
- self
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, but it's here for completeness. Maybe a caller will find it
333
- # useful.
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 close
326
+ def close_connection(conn)
336
327
  Pod4.logger.info(__FILE__){ "Closing connection to DB" }
337
- @client.close unless @client.nil?
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
- # True if we are connected to a database
344
+ # Return an open client connection from the Connection Pool, or else raise an error
346
345
  #
347
- def connected?
348
- @client && @client.active?
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
+