pod4 0.6.2

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.
@@ -0,0 +1,303 @@
1
+ require 'sequel'
2
+ require 'octothorpe'
3
+
4
+ require_relative 'interface'
5
+ require_relative 'errors'
6
+
7
+
8
+ module Pod4
9
+
10
+
11
+ ##
12
+ # Pod4 Interface for a Sequel table.
13
+ #
14
+ # If your DB table is one-one with your model, you shouldn't need to override
15
+ # anything.
16
+ #
17
+ # Example:
18
+ # class CustomerInterface < SwingShift::SequelInterface
19
+ # set_table :customer
20
+ # set_id_fld :id
21
+ # end
22
+ #
23
+ # Data types: Sequel itself will translate to BigDecimal, Float, Integer,
24
+ # date, and datetime as appropriate -- but it also depends on the underlying
25
+ # adapter. TinyTds maps dates to strings, for example.
26
+ #
27
+ class SequelInterface < Interface
28
+
29
+ attr_reader :id_fld
30
+
31
+
32
+ class << self
33
+ #---
34
+ # These are set in the class because it keeps the model code cleaner: the
35
+ # definition of the interface stays in the interface, and doesn't leak
36
+ # out into the model.
37
+ #+++
38
+
39
+
40
+ ##
41
+ # Use this to set the schema name (optional)
42
+ #
43
+ def set_schema(schema)
44
+ define_class_method(:schema) {schema.to_s.to_sym}
45
+ end
46
+
47
+ def schema; nil; end
48
+
49
+
50
+ ##
51
+ # Set the table name.
52
+ #
53
+ def set_table(table)
54
+ define_class_method(:table) {table.to_s.to_sym}
55
+ end
56
+
57
+ def table
58
+ raise Pod4Error, "You need to use set_table to set the table name"
59
+ end
60
+
61
+
62
+ ##
63
+ # Set the unique id field on the table.
64
+ #
65
+ def set_id_fld(idFld)
66
+ define_class_method(:id_fld) {idFld.to_s.to_sym}
67
+ end
68
+
69
+ def id_fld
70
+ raise Pod4Error, "You need to use set_id_fld to set the ID column name"
71
+ end
72
+
73
+ end
74
+ ##
75
+
76
+
77
+ ##
78
+ # Initialise the interface by passing it the Sequel DB object.
79
+ #
80
+ def initialize(db)
81
+ raise(ArgumentError, "Bad database") unless db.kind_of? Sequel::Database
82
+
83
+ raise(Pod4Error, 'no call to set_table in the interface definition') \
84
+ if self.class.table.nil?
85
+
86
+ raise(Pod4Error, 'no call to set_id_fld in the interface definition') \
87
+ if self.class.id_fld.nil?
88
+
89
+ @db = db # referemce to the db object
90
+ @table = db[schema ? "#{schema}__#{table}".to_sym : table]
91
+ @id_fld = self.class.id_fld
92
+
93
+ rescue => e
94
+ handle_error(e)
95
+ end
96
+
97
+
98
+ def schema; self.class.schema; end
99
+ def table; self.class.table; end
100
+ def id_fld; self.class.id_fld; end
101
+
102
+ def quoted_table
103
+ if schema
104
+ %Q|#{@db.quote_identifier schema}.#{@db.quote_identifier table}|
105
+ else
106
+ @db.quote_identifier(table)
107
+ end
108
+ end
109
+
110
+
111
+
112
+ ##
113
+ # Selection is whatever Sequel's `where` supports.
114
+ #
115
+ def list(selection=nil)
116
+ sel = sanitise_hash(selection)
117
+
118
+ Pod4.logger.debug(__FILE__) do
119
+ "Listing #{self.class.table}: #{sel.inspect}"
120
+ end
121
+
122
+ (sel ? @table.where(sel) : @table.all).map {|x| Octothorpe.new(x) }
123
+ rescue => e
124
+ handle_error(e)
125
+ end
126
+
127
+
128
+ ##
129
+ # Record is a hash of field: value
130
+ # By a happy coincidence, insert returns the unique ID for the record,
131
+ # which is just what we want to do, too.
132
+ #
133
+ def create(record)
134
+ raise(ArgumentError, "Bad type for record parameter") \
135
+ unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
136
+
137
+ Pod4.logger.debug(__FILE__) do
138
+ "Creating #{self.class.table}: #{record.inspect}"
139
+ end
140
+
141
+ @table.insert( sanitise_hash(record.to_h) )
142
+
143
+ rescue => e
144
+ handle_error(e)
145
+ end
146
+
147
+
148
+ ##
149
+ # ID corresponds to whatever you set in set_id_fld
150
+ #
151
+ def read(id)
152
+ raise(ArgumentError, "ID parameter is nil") if id.nil?
153
+
154
+ Pod4.logger.debug(__FILE__) do
155
+ "Reading #{self.class.table} where #{@id_fld}=#{id}"
156
+ end
157
+
158
+ Octothorpe.new( @table[@id_fld => id] )
159
+
160
+ rescue Sequel::DatabaseError
161
+ raise CantContinue, "Problem reading record. Is '#{id}' really an ID?"
162
+
163
+ rescue => e
164
+ handle_error(e)
165
+ end
166
+
167
+
168
+ ##
169
+ # ID is whatever you set in the interface using set_id_fld
170
+ # record should be a Hash or Octothorpe.
171
+ #
172
+ def update(id, record)
173
+ read_or_die(id)
174
+
175
+ Pod4.logger.debug(__FILE__) do
176
+ "Updating #{self.class.table} where #{@id_fld}=#{id}: #{record.inspect}"
177
+ end
178
+
179
+ @table.where(@id_fld => id).update( sanitise_hash(record.to_h) )
180
+ self
181
+ rescue => e
182
+ handle_error(e)
183
+ end
184
+
185
+
186
+ ##
187
+ # ID is whatever you set in the interface using set_id_fld
188
+ #
189
+ def delete(id)
190
+ read_or_die(id)
191
+
192
+ Pod4.logger.debug(__FILE__) do
193
+ "Deleting #{self.class.table} where #{@id_fld}=#{id}"
194
+ end
195
+
196
+ @table.where(@id_fld => id).delete
197
+ self
198
+ rescue => e
199
+ handle_error(e)
200
+ end
201
+
202
+
203
+ ##
204
+ # Bonus method: execute arbitrary SQL. Returns nil.
205
+ #
206
+ def execute(sql)
207
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
208
+
209
+ Pod4.logger.debug(__FILE__) { "Execute SQL: #{sql}" }
210
+ @db.run(sql)
211
+ rescue => e
212
+ handle_error(e)
213
+ end
214
+
215
+
216
+ ##
217
+ # Bonus method: execute arbitrary SQL and return the resulting dataset as a
218
+ # Hash.
219
+ #
220
+ def select(sql)
221
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
222
+
223
+ Pod4.logger.debug(__FILE__) { "Select SQL: #{sql}" }
224
+ @db[sql].all
225
+ rescue => e
226
+ handle_error(e)
227
+ end
228
+
229
+
230
+ protected
231
+
232
+
233
+ ##
234
+ # Helper routine to handle or re-raise the right exception.
235
+ # Unless kaller is passed, we re-raise on the caller of the caller, which
236
+ # is likely the original bug
237
+ #
238
+ def handle_error(err, kaller=nil)
239
+ kaller ||= caller[1..-1]
240
+
241
+ Pod4.logger.error(__FILE__){ err.message }
242
+
243
+ case err
244
+
245
+ # Just raise the error as is
246
+ when ArgumentError,
247
+ Pod4::Pod4Error,
248
+ Pod4::CantContinue
249
+
250
+ raise err.class, err.message, kaller
251
+
252
+ # Special Case for validation
253
+ when Sequel::ValidationFailed,
254
+ Sequel::UniqueConstraintViolation,
255
+ Sequel::ForeignKeyConstraintViolation
256
+
257
+ raise Pod4::ValidationError, err.message, kaller
258
+
259
+ # This is more serious
260
+ when Sequel::DatabaseError
261
+ raise Pod4::DatabaseError, err.message, kaller
262
+
263
+ # The default is to raise a generic Pod4 error.
264
+ else
265
+ raise Pod4::Pod4Error, err.message, kaller
266
+
267
+ end
268
+
269
+ end
270
+
271
+
272
+ ##
273
+ # Sequel behaves VERY oddly if you pass a symbol as a value to the hash you
274
+ # give to a selection,etc on a dataset. (It raises an error complaining that
275
+ # the symbol does not exist as a column in the table...)
276
+ #
277
+ def sanitise_hash(sel)
278
+
279
+ case sel
280
+ when Hash
281
+ sel.each_with_object({}) do |(k,v),m|
282
+ m[k] = v.kind_of?(Symbol) ? v.to_s : v
283
+ end
284
+
285
+ else
286
+ sel
287
+
288
+ end
289
+
290
+ end
291
+
292
+
293
+ private
294
+
295
+
296
+ def read_or_die(id)
297
+ raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
298
+ end
299
+
300
+ end
301
+
302
+
303
+ end
@@ -0,0 +1,394 @@
1
+ require 'tiny_tds'
2
+ require 'octothorpe'
3
+ require 'date'
4
+ require 'time'
5
+ require 'bigdecimal'
6
+
7
+ require_relative 'interface'
8
+ require_relative 'errors'
9
+
10
+
11
+ module Pod4
12
+
13
+
14
+ ##
15
+ # Pod4 Interface for requests on a SQL table via TinyTds.
16
+ #
17
+ # If your DB table is one-one with your model, you shouldn't need to override
18
+ # anything.
19
+ #
20
+ # Example:
21
+ # class CustomerInterface < SwingShift::TdsInterface
22
+ # set_db :fred
23
+ # set_table :customer
24
+ # set_id_fld :id
25
+ # end
26
+ #
27
+ class TdsInterface < Interface
28
+
29
+ attr_reader :id_fld
30
+
31
+
32
+ class << self
33
+ #--
34
+ # These are set in the class because it keeps the model code cleaner: the
35
+ # definition of the interface stays in the interface, and doesn't leak
36
+ # out into the model.
37
+ #++
38
+
39
+
40
+ ##
41
+ # Use this to set the database name.
42
+ #
43
+ def set_db(db)
44
+ define_class_method(:db) {db.to_s.to_sym}
45
+ end
46
+
47
+ def db
48
+ raise Pod4Error, "You need to use set_db to set the database name"
49
+ end
50
+
51
+
52
+ ##
53
+ # Use this to set the schema name (optional)
54
+ #
55
+ def set_schema(schema)
56
+ define_class_method(:schema) {schema.to_s.to_sym}
57
+ end
58
+
59
+ def schema; nil; end
60
+
61
+
62
+ ##
63
+ # Use this to set the name of the table
64
+ #
65
+ def set_table(table)
66
+ define_class_method(:table) {table.to_s.to_sym}
67
+ end
68
+
69
+ def table
70
+ raise Pod4Error, "You need to use set_table to set the table name"
71
+ end
72
+
73
+
74
+ ##
75
+ # This sets the column that holds the unique id for the table
76
+ #
77
+ def set_id_fld(idFld)
78
+ define_class_method(:id_fld) {idFld.to_s.to_sym}
79
+ end
80
+
81
+ def id_fld
82
+ raise Pod4Error, "You need to use set_table to set the table name"
83
+ end
84
+
85
+ end
86
+ ##
87
+
88
+
89
+ ##
90
+ # Initialise the interface by passing it a TinyTds connection hash.
91
+ # For testing ONLY you can also pass an object which pretends to be a
92
+ # TinyTds client, in which case the hash is pretty much ignored.
93
+ #
94
+ def initialize(connectHash, testClient=nil)
95
+
96
+ raise(Pod4Error, 'no call to set_db in the interface definition') \
97
+ if self.class.db.nil?
98
+
99
+ raise(Pod4Error, 'no call to set_table in the interface definition') \
100
+ if self.class.table.nil?
101
+
102
+ raise(Pod4Error, 'no call to set_id_fld in the interface definition') \
103
+ if self.class.id_fld.nil?
104
+
105
+ raise(ArgumentError, 'invalid connection hash') \
106
+ unless connectHash.kind_of?(Hash)
107
+
108
+ @connect_hash = connectHash.dup
109
+ @test_client = testClient
110
+ @client = nil
111
+
112
+ TinyTds::Client.default_query_options[:as] = :hash
113
+ TinyTds::Client.default_query_options[:symbolize_keys] = true
114
+
115
+ rescue => e
116
+ handle_error(e)
117
+ end
118
+
119
+
120
+ def db; self.class.db; end
121
+ def schema; self.class.schema; end
122
+ def table; self.class.table; end
123
+ def id_fld; self.class.id_fld; end
124
+
125
+ def quoted_table
126
+ schema ? %Q|[#{schema}].[#{table}]| : %Q|[#{table}]|
127
+ end
128
+
129
+
130
+ ##
131
+ # Selection is a hash or something like it: keys should be field names. We
132
+ # return any records where the given fields equal the given values.
133
+ #
134
+ def list(selection=nil)
135
+
136
+ raise(Pod4::DatabaseError, 'selection parameter is not a hash') \
137
+ unless selection.nil? || selection.respond_to?(:keys)
138
+
139
+ if selection
140
+ sel = selection.map {|k,v| "[#{k}] = #{quote v}" }.join(" and ")
141
+ sql = %Q|select *
142
+ from #{quoted_table}
143
+ where #{sel};|
144
+
145
+ else
146
+ sql = %Q|select * from #{quoted_table};|
147
+ end
148
+
149
+ select(sql) {|r| Octothorpe.new(r) }
150
+
151
+ rescue => e
152
+ handle_error(e)
153
+ end
154
+
155
+
156
+ ##
157
+ # Record is a hash of field: value
158
+ # By a happy coincidence, insert returns the unique ID for the record,
159
+ # which is just what we want to do, too.
160
+ #
161
+ def create(record)
162
+ raise(ArgumentError, "Bad type for record parameter") \
163
+ unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
164
+
165
+ ks = record.keys.map {|k| "[#{k}]" }
166
+ vs = record.values.map {|v| quote v }
167
+
168
+ sql = "insert into #{quoted_table}\n"
169
+ sql << " ( " << ks.join(",") << ")\n"
170
+ sql << " output inserted.[#{id_fld}]\n"
171
+ sql << " values( " << vs.join(",") << ");"
172
+
173
+ x = select(sql)
174
+ x.first[id_fld]
175
+
176
+ rescue => e
177
+ handle_error(e)
178
+ end
179
+
180
+
181
+ ##
182
+ # ID corresponds to whatever you set in set_id_fld
183
+ #
184
+ def read(id)
185
+ raise(ArgumentError, "ID parameter is nil") if id.nil?
186
+
187
+ sql = %Q|select *
188
+ from #{quoted_table}
189
+ where [#{id_fld}] = #{quote id};|
190
+
191
+ Octothorpe.new( select(sql).first )
192
+
193
+ rescue => e
194
+ # select already wrapped any error in a Pod4::DatabaseError, but in this
195
+ # case we want to try to catch something. (Side note: TinyTds' error
196
+ # class structure is a bit poor...)
197
+ raise CantContinue, "Problem reading record. Is '#{id}' really an ID?" \
198
+ if e.cause.class == TinyTds::Error \
199
+ && e.cause.message =~ /invalid column/i
200
+
201
+ handle_error(e)
202
+ end
203
+
204
+
205
+ ##
206
+ # ID is whatever you set in the interface using set_id_fld
207
+ # record should be a Hash or Octothorpe.
208
+ #
209
+ def update(id, record)
210
+ raise(ArgumentError, "Bad type for record parameter") \
211
+ unless record.kind_of?(Hash) || record.kind_of?(Octothorpe)
212
+
213
+ read_or_die(id)
214
+
215
+ sets = record.map {|k,v| " [#{k}] = #{quote v}" }
216
+
217
+ sql = "update #{quoted_table} set\n"
218
+ sql << sets.join(",") << "\n"
219
+ sql << "where [#{id_fld}] = #{quote id};"
220
+ execute(sql)
221
+
222
+ self
223
+
224
+ rescue => e
225
+ handle_error(e)
226
+ end
227
+
228
+
229
+ ##
230
+ # ID is whatever you set in the interface using set_id_fld
231
+ #
232
+ def delete(id)
233
+ read_or_die(id)
234
+ execute( %Q|delete #{quoted_table} where [#{id_fld}] = #{quote id};| )
235
+
236
+ self
237
+
238
+ rescue => e
239
+ handle_error(e)
240
+ end
241
+
242
+
243
+ ##
244
+ # Run SQL code on the server. Return the results.
245
+ #
246
+ # Will return an array of records, or you can use it in block mode, like
247
+ # this:
248
+ #
249
+ # select("select * from customer") do |r|
250
+ # # r is a single record
251
+ # end
252
+ #
253
+ # The returned results will be an array of hashes (or if you passed a
254
+ # block, of whatever you returned from the block).
255
+ #
256
+ def select(sql)
257
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
258
+
259
+ open unless connected?
260
+
261
+ Pod4.logger.debug(__FILE__){ "select: #{sql}" }
262
+ query = @client.execute(sql)
263
+
264
+ rows = []
265
+ query.each do |r|
266
+
267
+ if block_given?
268
+ rows << yield(r)
269
+ else
270
+ rows << r
271
+ end
272
+
273
+ end
274
+
275
+ query.cancel
276
+ rows
277
+
278
+ rescue => e
279
+ handle_error(e)
280
+ end
281
+
282
+
283
+ ##
284
+ # Run SQL code on the server; return true or false for success or failure
285
+ #
286
+ def execute(sql)
287
+ raise(ArgumentError, "Bad sql parameter") unless sql.kind_of?(String)
288
+
289
+ open unless connected?
290
+
291
+ Pod4.logger.debug(__FILE__){ "execute: #{sql}" }
292
+ r = @client.execute(sql)
293
+
294
+ r.do
295
+ r
296
+
297
+ rescue => e
298
+ handle_error(e)
299
+ end
300
+
301
+
302
+ protected
303
+
304
+
305
+ ##
306
+ # Open the connection to the database.
307
+ #
308
+ # No parameters are needed: the option hash has everything we need.
309
+ #
310
+ def open
311
+ Pod4.logger.info(__FILE__){ "Connecting to DB" }
312
+ client = @test_Client || TinyTds::Client.new(@connect_hash)
313
+ raise "Bad Connection" unless client.active?
314
+
315
+ @client = client
316
+ execute("use [#{self.class.db}]")
317
+
318
+ self
319
+
320
+ rescue => e
321
+ handle_error(e)
322
+ end
323
+
324
+
325
+ ##
326
+ # Close the connection to the database.
327
+ # We don't actually use this, but it's here for completeness. Maybe a
328
+ # caller will find it useful.
329
+ #
330
+ def close
331
+ Pod4.logger.info(__FILE__){ "Closing connection to DB" }
332
+ @client.close unless @client.nil?
333
+
334
+ rescue => e
335
+ handle_error(e)
336
+ end
337
+
338
+
339
+ ##
340
+ # True if we are connected to a database
341
+ #
342
+ def connected?
343
+ @client && @client.active?
344
+ end
345
+
346
+
347
+ def handle_error(err, kaller=nil)
348
+ kaller ||= caller[1..-1]
349
+
350
+ Pod4.logger.error(__FILE__){ err.message }
351
+
352
+ case err
353
+
354
+ when ArgumentError, Pod4::Pod4Error, Pod4::CantContinue
355
+ raise err.class, err.message, kaller
356
+
357
+ when TinyTds::Error
358
+ raise Pod4::DatabaseError, err.message, kaller
359
+
360
+ else
361
+ raise Pod4::Pod4Error, err.message, kaller
362
+
363
+ end
364
+
365
+ end
366
+
367
+
368
+ def quote(fld)
369
+
370
+ case fld
371
+ when DateTime, Time
372
+ %Q|'#{fld.to_s[0..-7]}'|
373
+ when String, Date
374
+ %Q|'#{fld}'|
375
+ when BigDecimal
376
+ fld.to_f
377
+ when nil
378
+ 'NULL'
379
+ else
380
+ fld
381
+ end
382
+
383
+ end
384
+
385
+
386
+ def read_or_die(id)
387
+ raise CantContinue, "'No record found with ID '#{id}'" if read(id).empty?
388
+ end
389
+
390
+
391
+ end
392
+
393
+
394
+ end
@@ -0,0 +1,3 @@
1
+ module Pod4
2
+ VERSION = '0.6.2'
3
+ end