pod4 0.7.2 → 0.8.0

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