pod4 0.7.2 → 0.8.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.
@@ -5,6 +5,7 @@ require 'bigdecimal'
5
5
 
6
6
  require_relative 'interface'
7
7
  require_relative 'errors'
8
+ require_relative 'sql_helper'
8
9
 
9
10
 
10
11
  module Pod4
@@ -22,7 +23,10 @@ module Pod4
22
23
  # set_id_fld :id
23
24
  # end
24
25
  #
26
+ # Note: TinyTDS does not appear to support parameterised queries!
27
+ #
25
28
  class TdsInterface < Interface
29
+ include SQLHelper
26
30
 
27
31
  attr_reader :id_fld
28
32
 
@@ -89,9 +93,10 @@ module Pod4
89
93
  # much ignored.
90
94
  #
91
95
  def initialize(connectHash, testClient=nil)
92
- raise(Pod4Error, 'no call to set_db in the interface definition') if self.class.db.nil?
93
- raise(Pod4Error, 'no call to set_table in the interface definition') if self.class.table.nil?
94
- raise(Pod4Error, 'no call to set_id_fld in the interface definition') if self.class.id_fld.nil?
96
+ sc = self.class
97
+ raise(Pod4Error, 'no call to set_db in the interface definition') if sc.db.nil?
98
+ raise(Pod4Error, 'no call to set_table in the interface definition') if sc.table.nil?
99
+ raise(Pod4Error, 'no call to set_id_fld in the interface definition') if sc.id_fld.nil?
95
100
  raise(ArgumentError, 'invalid connection hash') unless connectHash.kind_of?(Hash)
96
101
 
97
102
  @connect_hash = connectHash.dup
@@ -115,6 +120,10 @@ module Pod4
115
120
  schema ? %Q|[#{schema}].[#{table}]| : %Q|[#{table}]|
116
121
  end
117
122
 
123
+ def quote_field(fld)
124
+ "[#{super(fld, nil)}]"
125
+ end
126
+
118
127
 
119
128
  ##
120
129
  # Selection is a hash or something like it: keys should be field names. We return any records
@@ -125,17 +134,8 @@ module Pod4
125
134
  raise(Pod4::DatabaseError, 'selection parameter is not a hash') \
126
135
  unless selection.nil? || selection.respond_to?(:keys)
127
136
 
128
- if selection
129
- sel = selection.map {|k,v| "[#{k}] = #{quote v}" }.join(" and ")
130
- sql = %Q|select *
131
- from #{quoted_table}
132
- where #{sel};|
133
-
134
- else
135
- sql = %Q|select * from #{quoted_table};|
136
- end
137
-
138
- select(sql) {|r| Octothorpe.new(r) }
137
+ sql, vals = sql_select(nil, selection)
138
+ select( sql_subst(sql, *vals.map{|v| quote v}) ) {|r| Octothorpe.new(r) }
139
139
 
140
140
  rescue => e
141
141
  handle_error(e)
@@ -145,22 +145,13 @@ module Pod4
145
145
  ##
146
146
  # Record is a hash of field: value
147
147
  #
148
- # By a happy coincidence, insert returns the unique ID for the record, which is just what we
149
- # want to do, too.
150
- #
151
148
  def create(record)
152
149
  raise(ArgumentError, "Bad type for record parameter") \
153
150
  unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
154
151
 
155
- ks = record.keys.map {|k| "[#{k}]" }
156
- vs = record.values.map {|v| quote v }
157
-
158
- sql = "insert into #{quoted_table}\n"
159
- sql << " ( " << ks.join(",") << ")\n"
160
- sql << " output inserted.[#{id_fld}]\n"
161
- sql << " values( " << vs.join(",") << ");"
152
+ sql, vals = sql_insert(record)
162
153
 
163
- x = select(sql)
154
+ x = select sql_subst(sql, *vals.map{|v| quote v})
164
155
  x.first[id_fld]
165
156
 
166
157
  rescue => e
@@ -174,18 +165,17 @@ module Pod4
174
165
  def read(id)
175
166
  raise(ArgumentError, "ID parameter is nil") if id.nil?
176
167
 
177
- sql = %Q|select *
178
- from #{quoted_table}
179
- where [#{id_fld}] = #{quote id};|
180
-
181
- Octothorpe.new( select(sql).first )
168
+ sql, vals = sql_select(nil, id_fld => id)
169
+ rows = select sql_subst(sql, *vals.map{|v| quote v})
170
+ Octothorpe.new(rows.first)
182
171
 
183
172
  rescue => e
184
173
  # select already wrapped any error in a Pod4::DatabaseError, but in this case we want to try
185
174
  # to catch something. (Side note: TinyTds' error class structure is a bit poor...)
186
175
  raise CantContinue, "Problem reading record. Is '#{id}' really an ID?" \
187
176
  if e.cause.class == TinyTds::Error \
188
- && e.cause.message =~ /invalid column/i
177
+ && e.cause.message =~ /conversion failed/i
178
+
189
179
 
190
180
  handle_error(e)
191
181
  end
@@ -193,7 +183,7 @@ module Pod4
193
183
 
194
184
  ##
195
185
  # ID is whatever you set in the interface using set_id_fld record should be a Hash or
196
- # Octothorpe.
186
+ # Octothorpe.
197
187
  #
198
188
  def update(id, record)
199
189
  raise(ArgumentError, "Bad type for record parameter") \
@@ -201,12 +191,8 @@ module Pod4
201
191
 
202
192
  read_or_die(id)
203
193
 
204
- sets = record.map {|k,v| " [#{k}] = #{quote v}" }
205
-
206
- sql = "update #{quoted_table} set\n"
207
- sql << sets.join(",") << "\n"
208
- sql << "where [#{id_fld}] = #{quote id};"
209
- execute(sql)
194
+ sql, vals = sql_update(record, id_fld => id)
195
+ execute sql_subst(sql, *vals.map{|v| quote v})
210
196
 
211
197
  self
212
198
 
@@ -220,7 +206,9 @@ module Pod4
220
206
  #
221
207
  def delete(id)
222
208
  read_or_die(id)
223
- execute( %Q|delete #{quoted_table} where [#{id_fld}] = #{quote id};| )
209
+
210
+ sql, vals = sql_delete(id_fld => id)
211
+ execute sql_subst(sql, *vals.map{|v| quote v})
224
212
 
225
213
  self
226
214
 
@@ -229,6 +217,22 @@ module Pod4
229
217
  end
230
218
 
231
219
 
220
+ ##
221
+ # Override the sql_insert method in sql_helper since our SQL is rather different
222
+ #
223
+ def sql_insert(record)
224
+ flds, vals = parse_fldsvalues(record)
225
+ ph = vals.map{|x| placeholder }
226
+
227
+ sql = %Q|insert into #{quoted_table}
228
+ ( #{flds.join ','} )
229
+ output inserted.#{quote_field id_fld}
230
+ values( #{ph.join ','} );|
231
+
232
+ [sql, vals]
233
+ end
234
+
235
+
232
236
  ##
233
237
  # Run SQL code on the server. Return the results.
234
238
  #
@@ -287,6 +291,16 @@ module Pod4
287
291
  end
288
292
 
289
293
 
294
+ ##
295
+ # Wrapper for the data source library escape routine, which is all we can offer in terms of SQL
296
+ # injection protection. (Its not much.)
297
+ #
298
+ def escape(thing)
299
+ open unless connected?
300
+ thing.kind_of?(String) ? @client.escape(thing) : thing
301
+ end
302
+
303
+
290
304
  protected
291
305
 
292
306
 
@@ -354,21 +368,22 @@ module Pod4
354
368
  end
355
369
 
356
370
 
371
+ ##
372
+ # Overrride the quote routine in sql_helper.
373
+ #
374
+ # * TinyTDS doesn't cope with datetime
375
+ #
376
+ # * We might as well use it to escape strings, since that's the best we can do -- although I
377
+ # suspect that it's just turning ' into '' and nothing else...
378
+ #
357
379
  def quote(fld)
358
-
359
380
  case fld
360
381
  when DateTime, Time
361
382
  %Q|'#{fld.to_s[0..-7]}'|
362
- when Date
363
- %Q|'#{fld}'|
364
- when String
365
- %Q|'#{fld.gsub("'", "''")}'|
366
- when BigDecimal
367
- fld.to_f
368
- when nil
369
- 'NULL'
370
- else
371
- fld
383
+ when String, Symbol
384
+ %Q|'#{escape fld.to_s}'|
385
+ else
386
+ super
372
387
  end
373
388
 
374
389
  end
data/lib/pod4/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pod4
2
- VERSION = '0.7.2'
2
+ VERSION = '0.8.0'
3
3
  end
data/md/roadmap.md CHANGED
@@ -1,41 +1,95 @@
1
- Connection Object
2
- =================
1
+ Transactions
2
+ ============
3
3
 
4
- PgInterface and TdsInterface both take a connection Hash, which is all very
5
- well, but it means that we are running one database connection per model.
6
- Presumably this is a bad idea. :-)
4
+ I'd like to support basic transactions because without it we can't really claim to do optimistic
5
+ locking properly. And it would be nice to claim that.
7
6
 
8
- This actually hasn't come up in my own use of Pod4 -- for complex reasons I'm
9
- either using SequelInterface or running transient jobs which start up a couple
10
- of models, do some work, and then stop entirely -- but it _is_ rather silly.
7
+ Thought I had a solid idea of how to do it without any faffing around, but, it had some holes. Will
8
+ have to think again.
11
9
 
12
- Connection is baked into those interfaces, and interface dependant. So I'm
13
- thinking in terms of a memoising object that stores the connection hash and
14
- then gets passed to the interface. When the interface wants a connection, then
15
- it asks the connection object. If the connection object doesn't have one, then
16
- the interface connects, and gives the connection to the connection object.
10
+ But, this is top of my wish list.
17
11
 
12
+ For the record, my current thinking:
18
13
 
19
- Transactions
20
- ============
14
+ customer.new(4).read.or_die
15
+ customer.transaction do |c|
16
+ c.update(foo: 'bar')
17
+ c.orders.update(foo: 'bar')
18
+ end.or_die
19
+
20
+ * a method supports_transactions() will control whether the interface does that. sql_helper will
21
+ define it to return true.
22
+
23
+ * sql_helper will define a sql_transaction method which wraps SQL as a transaction.
21
24
 
22
- We really need this, because without it we can't even pretend to be doing
23
- proper pessimistic locking.
25
+ * interface methods create() delete() and update now accept an extra parameter, a boolean; if true,
26
+ they will return sql (and values), rather than doing anything. This is defined in Pod4::Interface.
24
27
 
25
- I've got a pretty solid idea for a nice, simple way to make this happen. It
26
- will be in place soon.
28
+ * BasicModel defines @in_transaction = false; @tx_sql = ""; @tx_vals = [].
29
+
30
+ * When a Model is @in_transaction, the create, delete and update methods pass the extra parameter
31
+ to the corresponding interface methods. The results are accumulated in @tx_sql and @tx_vals.
32
+
33
+ * the method BasicModel.transaction will:
34
+
35
+ * set interface.in_transaction = true, or raise an error if the interface doesn't support them
36
+ * yield a block passing the model instance so that the caller can run methods inside it
37
+ * set in_transaction back to false.
38
+ * call interface.executep( interface._sql_transaction( @tx_sql ), @tx_vals )
39
+
40
+ Notes:
41
+
42
+ * We will either have to standardize the execute method or check for it each time?
43
+ * Ditto with executep. Ditto with whether an interface supports parameterisation.
44
+ * trying to do a transaction across databases will fall over, but, really, no expectation there.
45
+ * You can't have a transaction that uses the result of the first half to do the last half.
46
+ * You can no longer call select() in a create, as we currently do for some interfaces? This is the
47
+ real problem I am wrestling with -- how to allow a transaction that returns a value from create().
27
48
 
28
49
 
29
50
  Migrations
30
51
  ==========
31
52
 
32
- This will almost certainly be something crude -- since we don't really control
33
- the database connection in the same way as, say, ActiveRecord -- but I honestly
34
- think it's a worthwhile feature. Just having something that you can version
35
- control and run to update a data model is enough, really.
53
+ This will almost certainly be something crude -- since we don't really control the database
54
+ connection in the same way as, say, ActiveRecord -- but I honestly think it's a worthwhile feature.
55
+ Just having something that you can version control and run to update a data model is enough,
56
+ really.
57
+
58
+ I'm not yet sure of the least useless way to implement it. Again, I favour SQL as the DSL.
59
+
60
+ We will clearly need transactions first, though.
36
61
 
37
- I'm not yet sure of the least useless way to implement it. Again, I favour SQL
38
- as the DSL.
62
+ My Current thoughts:
63
+
64
+ * a migration against a database is on a par with a model but very different. You subclass
65
+ migration and give it an interface, pointing to the table that stores the current migration
66
+ state.
67
+
68
+ * The methods in a module are exectute() up() and down() -- the last two call the first one.
69
+
70
+ * Each instance of a model is stored in a file and contains up and down SQL somehow. Each instance
71
+ has a version number.
72
+
73
+ * You run a migration by running up or down on your migration class, passing a version?
74
+
75
+
76
+ Connection Object
77
+ =================
78
+
79
+ PgInterface and TdsInterface both take a connection Hash, which is all very well, but it means that
80
+ we are running one database connection per model. Presumably this is a bad idea. :-)
81
+
82
+ This actually hasn't come up in my own use of Pod4 -- for complex reasons I'm either using
83
+ SequelInterface or running transient jobs which start up a couple of models, do some work, and then
84
+ stop entirely.
85
+
86
+ Connection is baked into those interfaces, and interface dependant. So I'm thinking in terms of a
87
+ memoising object that stores the connection hash and then gets passed to the interface. When the
88
+ interface wants a connection, then it asks the connection object. If the connection object doesn't
89
+ have one, then the interface connects, and gives the connection to the connection object.
90
+
91
+ It's looking as if we don't need this right now. One connection per model might not be as daft as
92
+ it seems.
39
93
 
40
94
 
41
95
  JDBC-SQL interface
@@ -67,3 +121,5 @@ For the jdbc-msssqlserver gem. Doable ... I *think*.
67
121
  conn.close
68
122
 
69
123
  see https://github.com/jruby/jruby/wiki/JDBC
124
+
125
+
@@ -78,6 +78,11 @@ describe 'WeirdModel' do
78
78
  expect( WeirdModel.new.alerts ).to eq([])
79
79
  end
80
80
 
81
+ it 'doesn''t freak out if the ID is not an integer' do
82
+ expect{ CustomerModel.new("france") }.not_to raise_exception
83
+ expect( CustomerModel.new("france").model_id ).to eq "france"
84
+ end
85
+
81
86
  end
82
87
  ##
83
88
 
@@ -61,6 +61,7 @@ describe 'CustomerModel' do
61
61
  {id: 20, name: 'Morticia', price: 2.34, groups: 'spanish' },
62
62
  {id: 30, name: 'Wednesday', price: 3.45, groups: 'school' },
63
63
  {id: 40, name: 'Pugsley', price: 4.56, groups: 'trains,school'} ]
64
+
64
65
  end
65
66
 
66
67
  let(:recordsx) do
@@ -83,21 +84,26 @@ describe 'CustomerModel' do
83
84
  let(:model2) do
84
85
  m = CustomerModel.new(30)
85
86
 
86
- allow( m.interface ).to receive(:read).
87
- and_return( Octothorpe.new(records[2]) )
88
-
87
+ allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[2]) )
89
88
  m.read.or_die
90
89
  end
91
90
 
92
91
  let(:model3) do
93
92
  m = CustomerModel.new(40)
94
93
 
95
- allow( m.interface ).to receive(:read).
96
- and_return( Octothorpe.new(records[3]) )
97
-
94
+ allow( m.interface ).to receive(:read).and_return( Octothorpe.new(records[3]) )
98
95
  m.read.or_die
99
96
  end
100
97
 
98
+ # Model4 is for a non-integer id
99
+ let(:thing) { Octothorpe.new(id: 'eek', name: 'thing', price: 9.99, groups: 'scuttering') }
100
+
101
+ let(:model4) do
102
+ m = CustomerModel.new('eek')
103
+
104
+ allow( m.interface ).to receive(:read).and_return(thing)
105
+ m.read.or_die
106
+ end
101
107
 
102
108
  ##
103
109
 
@@ -242,6 +248,11 @@ describe 'CustomerModel' do
242
248
  expect( CustomerModel.new.alerts ).to eq([])
243
249
  end
244
250
 
251
+ it 'doesn''t freak out if the ID is not an integer' do
252
+ expect{ CustomerModel.new("france") }.not_to raise_exception
253
+ expect( CustomerModel.new("france").model_id ).to eq "france"
254
+ end
255
+
245
256
  end
246
257
  ##
247
258
 
@@ -532,6 +543,14 @@ describe 'CustomerModel' do
532
543
  new_model.create
533
544
  end
534
545
 
546
+ it 'doesn\'t freak out if the model is not an integer' do
547
+ expect( new_model.interface ).to receive(:create)
548
+ new_model.id = "handy"
549
+ new_model.name = "Thing"
550
+
551
+ expect{ new_model.create }.not_to raise_error
552
+ end
553
+
535
554
  end
536
555
  ##
537
556
 
@@ -597,6 +616,12 @@ describe 'CustomerModel' do
597
616
  expect( model.model_status ).to eq :warning
598
617
  end
599
618
 
619
+ it 'doesn\'t freak out if the model is non-integer' do
620
+ allow( model.interface ).to receive(:read).and_return( thing )
621
+
622
+ expect{ CustomerModel.new('eek').read }.not_to raise_error
623
+ end
624
+
600
625
  context 'if the interface.read returns an empty Octothorpe' do
601
626
  let(:missing) { CustomerModel.new(99) }
602
627
 
@@ -658,7 +683,7 @@ describe 'CustomerModel' do
658
683
  model3.update
659
684
  end
660
685
 
661
- it 'doesnt call update on the interface if the validation fails' do
686
+ it 'doesn\'t call update on the interface if the validation fails' do
662
687
  expect( model3.interface ).not_to receive(:update)
663
688
 
664
689
  model3.name = "fall over" # triggers validation
@@ -670,6 +695,14 @@ describe 'CustomerModel' do
670
695
  model3.update
671
696
  end
672
697
 
698
+ it 'doesn\'t freak out if the model is non-integer' do
699
+ expect( model4.interface ).
700
+ to receive(:update).
701
+ and_return( model4.interface )
702
+
703
+ model4.update
704
+ end
705
+
673
706
  context 'when the record already has error alerts' do
674
707
 
675
708
  it 'passes if there is no longer anything wrong' do
@@ -753,6 +786,15 @@ describe 'CustomerModel' do
753
786
  expect( model2.model_status ).to eq :deleted
754
787
  end
755
788
 
789
+ it 'doesn\'t freak out if the model is non-integer' do
790
+ expect( model4.interface ).
791
+ to receive(:delete).
792
+ and_return( model4.interface )
793
+
794
+ model4.delete
795
+ end
796
+
797
+
756
798
  end
757
799
  ##
758
800
 
@@ -88,6 +88,8 @@ class FakeRequester
88
88
  end
89
89
 
90
90
  req = NebulousStomp::NebRequestNull.new('faketarget', verb, paramStr)
91
+ hash2[:inReplyTo] = req.replyID
92
+
91
93
  mess = NebulousStomp::Message.from_cache( hash1.merge(hash2).to_json )
92
94
  req.insert_fake_stomp(mess)
93
95
  req