pod4 0.10.6 → 1.0.0

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