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.
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
+