pod4 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9c0490f9b2e4082a0d36fde99281b5ec3b18dc06
4
- data.tar.gz: 5338ef5d4695a2b0385a0093e43f4e4658c580c6
3
+ metadata.gz: 14459fc92264f22a30f5fc92078d2de5343102b0
4
+ data.tar.gz: 8595eb150b4615d49ba9447972a1dc25bca60406
5
5
  SHA512:
6
- metadata.gz: 21795d227c37d1a80d6d60a0a74f9c5111818cd80dcc696e2fc77641bbcbba9d1f089ab5ec44302825861d5aff0a6e8a3111e8c218cfd7beba5d07753ca9f7ed
7
- data.tar.gz: ec96c28be57e17385efef6725cf83e402399eeacaa55be1f089139a1a8f6b889e547f5da36f77b255c2b770a4796948698f25fbbda4d9a918951b08dcebfb308
6
+ metadata.gz: 0002b34674f809b1320cb0d47b05f8c1514eb91d2993639d5210c2ac4b9be08d57d08c7edc47112f657c326204be4301977037a2363772108a21e602a09fbde6
7
+ data.tar.gz: e983618952a89b09f28514bcf7bf299cf261f3e33347444b9a0a0a14444ce69fedde0e3c30c7972e254486471162285b34ebc33dfdd70de29e7a3aa9a15bf0c5
data/.hgignore CHANGED
@@ -16,3 +16,4 @@ mkmf.log
16
16
  .ruby-gemset
17
17
  .rbenv-gemsets
18
18
  .Project
19
+ .project
data/.hgtags CHANGED
@@ -19,3 +19,4 @@ dc22541876279b47dd366ddb44d262775ccdb933 0.6.1
19
19
  4a0e392be3499d9edc9cde66dd3b8d37136a0816 0.6.2
20
20
  e41769b310f2eaab5f37d285ce9ad8658b689916 0.7.1
21
21
  2c28b064a96d4760d124da3f1842242f52f4cc69 0.7.2
22
+ ccb4a8711560725e358f7fb87c76f4787eac5ed5 0.8.0
data/Gemfile CHANGED
@@ -24,7 +24,9 @@ group :development, :test do
24
24
 
25
25
  platforms :jruby do
26
26
  gem "jruby-lint"
27
- gem "pg_jruby"
27
+ gem "jeremyevans-postgres-pr"
28
+ gem 'jdbc-mssqlserver'
29
+ gem 'jdbc-postgres', '9.4.1200'
28
30
  end
29
31
 
30
32
 
data/Rakefile CHANGED
@@ -1,8 +1,3 @@
1
- #require "bundler/gem_tasks"
2
- #require "rspec/core/rake_task"
3
-
4
- #RSpec::Core::RakeTask.new(:spec)
5
-
6
1
  desc "Push doc to HARS"
7
2
  task :hars do
8
3
  sh "rsync -aP --delete doc/ /home/hars/hars/public/pod4"
@@ -13,6 +8,7 @@ task :retag do
13
8
  sh "ripper-tags -R"
14
9
  end
15
10
 
11
+
16
12
  namespace :rspec do
17
13
 
18
14
  desc "run tests (mri)"
@@ -25,4 +21,20 @@ namespace :rspec do
25
21
  sh "rspec spec/common spec/jruby"
26
22
  end
27
23
 
24
+ desc "run one test (pass as parameter)"
25
+ task :one do |task, args|
26
+
27
+ if args.extras.count > 0 # rake rspec[path/to/file]
28
+ sh "rspec #{args.extras.first}"
29
+
30
+ elsif ARGV.count == 2 && File.exist?(ARGV[1]) # rake rspec path/to/file
31
+ sh "rspec #{ARGV[1]}"
32
+
33
+ else
34
+ raise "You need to specify a test"
35
+ end
36
+
37
+ end
38
+
28
39
  end
40
+
@@ -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
@@ -23,6 +24,7 @@ module Pod4
23
24
  # end
24
25
  #
25
26
  class PgInterface < Interface
27
+ include SQLHelper
26
28
 
27
29
  attr_reader :id_fld
28
30
 
@@ -91,29 +93,15 @@ module Pod4
91
93
  def table; self.class.table; end
92
94
  def id_fld; self.class.id_fld; end
93
95
 
94
- def quoted_table
95
- schema ? %Q|"#{schema}"."#{table}"| : %Q|"#{table}"|
96
- end
97
-
98
96
 
99
97
  ##
100
- # Selection is whatever Sequel's `where` supports.
101
98
  #
102
99
  def list(selection=nil)
103
100
  raise(ArgumentError, 'selection parameter is not a hash') \
104
101
  unless selection.nil? || selection.respond_to?(:keys)
105
102
 
106
- if selection
107
- sel = selection.map {|k,v| %Q|"#{k}" = #{quote v}| }.join(" and ")
108
- sql = %Q|select *
109
- from #{quoted_table}
110
- where #{sel};|
111
-
112
- else
113
- sql = %Q|select * from #{quoted_table};|
114
- end
115
-
116
- select(sql) {|r| Octothorpe.new(r) }
103
+ sql, vals = sql_select(nil, selection)
104
+ selectp(sql, *vals) {|r| Octothorpe.new(r) }
117
105
 
118
106
  rescue => e
119
107
  handle_error(e)
@@ -130,15 +118,8 @@ module Pod4
130
118
  raise(ArgumentError, "Bad type for record parameter") \
131
119
  unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
132
120
 
133
- ks = record.keys.map {|k| %Q|"#{k}"| }.join(',')
134
- vs = record.values.map {|v| quote v }.join(',')
135
-
136
- sql = %Q|insert into #{quoted_table}
137
- ( #{ks} )
138
- values( #{vs} )
139
- returning "#{id_fld}";|
140
-
141
- x = select(sql)
121
+ sql, vals = sql_insert(record)
122
+ x = selectp(sql, *vals)
142
123
  x.first[id_fld]
143
124
 
144
125
  rescue => e
@@ -152,11 +133,9 @@ module Pod4
152
133
  def read(id)
153
134
  raise(ArgumentError, "ID parameter is nil") if id.nil?
154
135
 
155
- sql = %Q|select *
156
- from #{quoted_table}
157
- where "#{id_fld}" = #{quote id};|
158
-
159
- Octothorpe.new( select(sql).first )
136
+ sql, vals = sql_select(nil, id_fld => id)
137
+ rows = selectp(sql, *vals)
138
+ Octothorpe.new(rows.first)
160
139
 
161
140
  rescue => e
162
141
  # Select has already wrapped the error in a Pod4Error, but in this case we want to catch
@@ -177,13 +156,9 @@ module Pod4
177
156
  unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
178
157
 
179
158
  read_or_die(id)
180
- sets = record.map {|k,v| %Q| "#{k}" = #{quote v}| }.join(',')
181
159
 
182
- sql = %Q|update #{quoted_table} set
183
- #{sets}
184
- where "#{id_fld}" = #{quote id};|
185
-
186
- execute(sql)
160
+ sql, vals = sql_update(record, id_fld => id)
161
+ executep(sql, *vals)
187
162
 
188
163
  self
189
164
 
@@ -197,7 +172,9 @@ module Pod4
197
172
  #
198
173
  def delete(id)
199
174
  read_or_die(id)
200
- execute( %Q|delete from #{quoted_table} where "#{id_fld}" = #{quote id};| )
175
+
176
+ sql, vals = sql_delete(id_fld => id)
177
+ executep(sql, *vals)
201
178
 
202
179
  self
203
180
 
@@ -250,6 +227,44 @@ module Pod4
250
227
  end
251
228
 
252
229
 
230
+ ##
231
+ # Run SQL code on the server as per select() but with parameter insertion.
232
+ #
233
+ # Placeholders in the SQL string should all be %s as per sql_helper methods.
234
+ # Values should be as returned by sql_helper methods.
235
+ #
236
+ def selectp(sql, *vals)
237
+ raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
238
+
239
+ ensure_connection
240
+
241
+ Pod4.logger.debug(__FILE__){ "select: #{sql} #{vals.inspect}" }
242
+
243
+ rows = []
244
+ @client.exec_params( *parse_for_params(sql, vals) ) do |query|
245
+ oids = make_oid_hash(query)
246
+
247
+ query.each do |r|
248
+ row = cast_row_fudge(r, oids)
249
+
250
+ if block_given?
251
+ rows << yield(row)
252
+ else
253
+ rows << row
254
+ end
255
+
256
+ end
257
+ end
258
+
259
+ @client.cancel
260
+
261
+ rows
262
+
263
+ rescue => e
264
+ handle_error(e)
265
+ end
266
+
267
+
253
268
  ##
254
269
  # Run SQL code on the server; return true or false for success or failure
255
270
  #
@@ -266,6 +281,25 @@ module Pod4
266
281
  end
267
282
 
268
283
 
284
+ ##
285
+ # Run SQL code on the server as per execute() but with parameter insertion.
286
+ #
287
+ # Placeholders in the SQL string should all be %s as per sql_helper methods.
288
+ # Values should be as returned by sql_helper methods.
289
+ #
290
+ def executep(sql, *vals)
291
+ raise(ArgumentError, "Bad SQL parameter") unless sql.kind_of?(String)
292
+
293
+ ensure_connection
294
+
295
+ Pod4.logger.debug(__FILE__){ "parameterised execute: #{sql}" }
296
+ @client.exec_params( *parse_for_params(sql, vals) )
297
+
298
+ rescue => e
299
+ handle_error(e)
300
+ end
301
+
302
+
269
303
  protected
270
304
 
271
305
 
@@ -369,26 +403,6 @@ module Pod4
369
403
  end
370
404
 
371
405
 
372
- def quote(fld)
373
-
374
- case fld
375
- when Date, Time
376
- "'#{fld}'"
377
- when String
378
- "'#{fld.gsub("'", "''")}'"
379
- when Symbol
380
- "'#{fld.to_s.gsub("'", "''")}'"
381
- when BigDecimal
382
- fld.to_f
383
- when nil
384
- 'NULL'
385
- else
386
- fld
387
- end
388
-
389
- end
390
-
391
-
392
406
  private
393
407
 
394
408
 
@@ -404,6 +418,7 @@ module Pod4
404
418
  end
405
419
 
406
420
 
421
+
407
422
  ##
408
423
  # Cast a query row
409
424
  #
@@ -449,6 +464,15 @@ module Pod4
449
464
  raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
450
465
  end
451
466
 
467
+
468
+ def parse_for_params(sql, vals)
469
+ new_params = sql.scan("%s").map.with_index{|e,i| "$#{i + 1}" }
470
+ new_vals = vals.map{|v| v ? quote(v, nil).to_s : nil }
471
+
472
+ [ sql_subst(sql, *new_params), new_vals ]
473
+ end
474
+
475
+
452
476
  end
453
477
 
454
478
 
@@ -83,6 +83,14 @@ module Pod4
83
83
  @table = db[schema ? "#{schema}__#{table}".to_sym : table]
84
84
  @id_fld = self.class.id_fld
85
85
 
86
+ # Work around a problem with jdbc-postgresql where it throws an exception whenever it sees
87
+ # the money type. This workaround actually allows us to return a BigDecimal, so it's better
88
+ # than using postgres_pr when under jRuby!
89
+ if @db.uri =~ /jdbc:postgresql/
90
+ @db.conversion_procs[790] = ->(s){BigDecimal.new s[1..-1] rescue nil}
91
+ Sequel::JDBC::Postgres::Dataset::PG_SPECIFIC_TYPES << Java::JavaSQL::Types::DOUBLE
92
+ end
93
+
86
94
  rescue => e
87
95
  handle_error(e)
88
96
  end
@@ -118,16 +126,25 @@ module Pod4
118
126
  ##
119
127
  # Record is a hash of field: value
120
128
  #
121
- # By a happy coincidence, insert returns the unique ID for the record, which is just what we
122
- # want to do, too.
123
- #
124
129
  def create(record)
125
130
  raise(ArgumentError, "Bad type for record parameter") \
126
131
  unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
127
132
 
128
133
  Pod4.logger.debug(__FILE__) { "Creating #{self.class.table}: #{record.inspect}" }
129
134
 
130
- @table.insert( sanitise_hash(record.to_h) )
135
+ id = @table.insert( sanitise_hash(record.to_h) )
136
+
137
+ # Sequel doesn't return the key unless it is an autoincrement; otherwise it turns a row
138
+ # number regardless. It probably doesn' t matter, but try to catch that anyway.
139
+ # (bamf: If your non-incrementing key happens to be an integer, this won't work...)
140
+
141
+ id_val = record[id_fld] || record[id_fld.to_s]
142
+
143
+ if (id.kind_of?(Fixnum) || id.nil?) && id_val && !id_val.kind_of?(Fixnum)
144
+ id_val
145
+ else
146
+ id
147
+ end
131
148
 
132
149
  rescue => e
133
150
  handle_error(e)
@@ -191,27 +208,65 @@ module Pod4
191
208
  #
192
209
  def execute(sql)
193
210
  raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
194
-
195
211
  Pod4.logger.debug(__FILE__) { "Execute SQL: #{sql}" }
212
+
196
213
  @db.run(sql)
197
214
  rescue => e
198
215
  handle_error(e)
199
216
  end
200
217
 
201
218
 
219
+ ##
220
+ # Bonus method: execute SQL as per execute(), but parameterised.
221
+ #
222
+ # Use ? as a placeholder in the SQL
223
+ # mode is either :insert :update or :delete
224
+ # Please quote values for yourself, we don't.
225
+ #
226
+ # "update and delete should return the number of rows affected, and insert should return the
227
+ # autogenerated primary integer key for the row inserted (if any)"
228
+ #
229
+ def executep(sql, mode, *values)
230
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
231
+ raise(ArgumentError, "Bad mode parameter") unless %i|insert delete update|.include?(mode)
232
+ Pod4.logger.debug(__FILE__) { "Parameterised execute #{mode} SQL: #{sql}" }
233
+
234
+ @db[sql, *values].send(mode)
235
+ rescue => e
236
+ handle_error(e)
237
+ end
238
+
239
+
240
+
202
241
  ##
203
242
  # Bonus method: execute arbitrary SQL and return the resulting dataset as a Hash.
204
243
  #
205
244
  def select(sql)
206
245
  raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
207
-
208
246
  Pod4.logger.debug(__FILE__) { "Select SQL: #{sql}" }
247
+
209
248
  @db[sql].all
210
249
  rescue => e
211
250
  handle_error(e)
212
251
  end
213
252
 
214
253
 
254
+ ##
255
+ # Bonus method: execute arbitrary SQL as per select(), but parameterised.
256
+ #
257
+ # Use ? as a placeholder in the SQL
258
+ # Please quote values for yourself, we don't.
259
+ #
260
+ def selectp(sql, *values)
261
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
262
+ Pod4.logger.debug(__FILE__) { "Parameterised select SQL: #{sql}" }
263
+
264
+ @db.fetch(sql, *values).all
265
+ rescue => e
266
+ handle_error(e)
267
+ end
268
+
269
+
215
270
  protected
216
271
 
217
272
 
@@ -0,0 +1,229 @@
1
+ module Pod4
2
+
3
+
4
+ ##
5
+ # A mixin to help interfaces that need to generate SQL
6
+ #
7
+ # Most of these methods return two things: an sql string with %s where each value should be; and
8
+ # an array of values to insert.
9
+ #
10
+ # You can override placeholder() to change %s to something else. You can override quote() to
11
+ # change how values are quoted and quote_fld() to change how column names are quoted. Of course
12
+ # the SQL here won't be suitable for all data source libraries even then, but, it gives us some
13
+ # common ground to start with.
14
+ #
15
+ # You can call sql_subst() to turn the SQL and the values array into actual SQL -- but don't do
16
+ # that; you should call the parameterised query routines for the data source library instead.
17
+ #
18
+ module SQLHelper
19
+
20
+
21
+ ##
22
+ # Return the name of the table quoted as for inclusion in SQL. Might include the name of the
23
+ # schema, too, if you have set one.
24
+ #
25
+ # table() is mandatory for an Interface, and if we have a schema it will be schema().
26
+ #
27
+ def quoted_table
28
+ defined?(schema) && schema ? %Q|"#{schema}"."#{table}"| : %Q|"#{table}"|
29
+ end
30
+
31
+
32
+ private
33
+
34
+
35
+ ##
36
+ # Given a list of fields and a selection hash, return an SQL string and an array of values
37
+ # for an SQL SELECT.
38
+ #
39
+ def sql_select(fields, selection)
40
+ flds = fields ? Array(fields).flatten.map{|f| quote_field f} : ["*"]
41
+
42
+ wsql, wvals = sql_where(selection)
43
+
44
+ sql = %Q|select #{flds.join ','}
45
+ from #{quoted_table}
46
+ #{wsql};|
47
+
48
+ [sql, wvals]
49
+ end
50
+
51
+
52
+ ##
53
+ # Given a column:value hash, return an SQL string and an array of values for an SQL INSERT.
54
+ #
55
+ # Note that we get the table ID field from id_fld, which is mandatory for an Interface.
56
+ #
57
+ def sql_insert(fldsValues)
58
+ raise ArgumentError, "Needs a field:value hash" if fldsValues.nil? || fldsValues.empty?
59
+
60
+ flds, vals = parse_fldsvalues(fldsValues)
61
+ ph = Array(placeholder).flatten * flds.count
62
+
63
+ sql = %Q|insert into #{quoted_table}
64
+ ( #{flds.join ','} )
65
+ values( #{ph.join ','} )
66
+ returning #{quote_field id_fld};|
67
+
68
+ [sql, vals]
69
+ end
70
+
71
+
72
+ ##
73
+ # Given a column:value hash and a selection hash, return an SQL string and an array of values
74
+ # for an SQL UPDATE.
75
+ #
76
+ def sql_update(fldsValues, selection)
77
+ raise ArgumentError, "Needs a field:value hash" if fldsValues.nil? || fldsValues.empty?
78
+
79
+ flds, vals = parse_fldsvalues(fldsValues)
80
+ sets = flds.map {|f| %Q| #{f} = #{placeholder}| }
81
+
82
+ wsql, wvals = sql_where(selection)
83
+
84
+ sql = %Q|update #{quoted_table}
85
+ set #{sets.join ','}
86
+ #{wsql};|
87
+
88
+ [sql, vals + wvals]
89
+ end
90
+
91
+
92
+ ##
93
+ # Given a selection hash, return an SQL string and an array of values for an SQL DELETE.
94
+ #
95
+ def sql_delete(selection)
96
+ wsql, wval = sql_where(selection)
97
+ [ %Q|delete from #{quoted_table} #{wsql};|,
98
+ wval ]
99
+ end
100
+
101
+
102
+ ##
103
+ # Given a selection hash, return an SQL string and an array of values
104
+ # for an SQL where clause.
105
+ #
106
+ # This is used internally; you probably don't need it unless you are trying to override
107
+ # sql_select(), sql_update() etc.
108
+ #
109
+ def sql_where(selection)
110
+ return ["", []] if (selection.nil? || selection == {})
111
+
112
+ flds, vals = parse_fldsvalues(selection)
113
+
114
+ [ "where " + flds.map {|f| %Q|#{f} = #{placeholder}| }.join(" and "),
115
+ vals ]
116
+
117
+ end
118
+
119
+
120
+ ##
121
+ # Given a string which is supposedly the name of a column, return a string with the column name
122
+ # quoted for inclusion to SQL.
123
+ #
124
+ # Defaults to SQL standard double quotes. If you want something else, pass the new quote
125
+ # character as the optional second parameter, and/or override the method.
126
+ #
127
+ def quote_field(fld, qc=%q|"|)
128
+ raise ArgumentError, "bad field name" unless fld.kind_of?(String) || fld.kind_of?(Symbol)
129
+ %Q|#{qc}#{fld}#{qc}|
130
+ end
131
+
132
+
133
+ ##
134
+ # Given some value, quote it for inclusion in SQL.
135
+ #
136
+ # Tries to follow the generic SQL standard -- single quotes for strings, NULL for nil, etc.
137
+ # If you want something else, pass a different quote character as the second parameter, and/or
138
+ # override the method.
139
+ #
140
+ # Note that this also turns 'O'Claire' into 'O''Claire', as required by SQL.
141
+ #
142
+ def quote(fld, qc=%q|'|)
143
+
144
+ case fld
145
+ when Date, Time
146
+ %Q|#{qc}#{fld}#{qc}|
147
+ when String
148
+ %Q|#{qc}#{fld.gsub("#{qc}", "#{qc}#{qc}")}#{qc}|
149
+ when Symbol
150
+ %Q|#{qc}#{fld.to_s.gsub("#{qc}", "#{qc}#{qc}")}#{qc}|
151
+ when BigDecimal
152
+ fld.to_f
153
+ when nil
154
+ "NULL"
155
+ else
156
+ fld
157
+ end
158
+
159
+ end
160
+
161
+
162
+ ##
163
+ # Return the placeholder to use in place of values when we return SQL. Defaults to the
164
+ # Ruby-friendly %s. Override it if you want everything else.
165
+ #
166
+ def placeholder
167
+ "%s"
168
+ end
169
+
170
+
171
+ ##
172
+ # Given a string (SQL) with %s placeholders and one or more values -- substitute the values for
173
+ # the placeholders.
174
+ #
175
+ # sql_subst("foo %s bar %s", "$1", "$2") #-> "foo $1 bar $2"
176
+ # sql_subst("foo %s bar %s", "$$"] ) #-> "foo $$ bar $$"
177
+ #
178
+ # You can use this to configure your SQL ready for the parameterised query routine that comes
179
+ # with your data library. Note: this does not work if you redefine placeholder().
180
+ #
181
+ # You could also use it to turn a sql-with-placeholders string into valid SQL, by passing the
182
+ # (quoted) values array that you got from sql_select, etc.:
183
+ #
184
+ # sql, vals = sql_select(nil, id => 4)
185
+ # validSQL = sql_subst( sql, *vals.map{|v| quote v} )
186
+ #
187
+ # Note: Don't do this. Dreadful idea.
188
+ # If at all possible you should instead get the data source library to combine these two
189
+ # things. This will protect you against SQL injection (or if not, the library has screwed up).
190
+ #
191
+ def sql_subst(sql, *args)
192
+ raise ArgumentError, "bad SQL" unless sql.kind_of? String
193
+ raise ArgumentError, "missing SQL" if sql.empty?
194
+
195
+ vals = args.map(&:to_s)
196
+
197
+ case
198
+ when vals.empty? then sql
199
+ when vals.size == 1 then sql.gsub("%s", vals.first)
200
+ else
201
+ raise ArgumentError, "wrong number of values" unless sql.scan("%s").count == vals.count
202
+ sql % args
203
+ end
204
+ end
205
+
206
+
207
+ ##
208
+ # Helper routine: given a hash, quote the keys as column names and keep the values as they are
209
+ # (since we don't know whether your parameterised query routine in your data source library
210
+ # does that for you).
211
+ #
212
+ # Return the hash as two arrays, to ensure the ordering is consistent.
213
+ #
214
+ def parse_fldsvalues(hash)
215
+ flds = []; vals = []
216
+
217
+ hash.each do|f, v|
218
+ flds << quote_field(f.to_s)
219
+ vals << v
220
+ end
221
+
222
+ [flds, vals]
223
+ end
224
+
225
+ end
226
+
227
+
228
+ end
229
+