spqr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ <schema package="examples.codegen">
2
+ <class name="EchoAgent">
3
+ <method name="echo" desc="returns its argument">
4
+ <arg name="arg" dir="IO" type="lstr"/>
5
+ </method>
6
+ </class>
7
+ </schema>
@@ -0,0 +1,33 @@
1
+ module Examples
2
+ module Codegen
3
+ class EchoAgent
4
+ include SPQR::Manageable
5
+
6
+ spqr_package 'examples.codegen'
7
+ spqr_class 'EchoAgent'
8
+ # Find method (NB: you must implement this)
9
+ def EchoAgent.find_by_id(objid)
10
+ EchoAgent.new
11
+ end
12
+ # Find-all method (NB: you must implement this)
13
+ def EchoAgent.find_all
14
+ [EchoAgent.new]
15
+ end
16
+ ### Schema method declarations
17
+
18
+ # echo returns its argument
19
+ # * arg (lstr/IO)
20
+ #
21
+ def echo(args)
22
+ # Print values of in/out parameters
23
+ log.debug "arg => #{args["arg"]}" #
24
+ # Assign values to in/out parameters
25
+ args["arg"] = args["arg"]
26
+ end
27
+
28
+ spqr_expose :echo do |args|
29
+ args.declare :arg, :lstr, :inout, {}
30
+ end
31
+ end
32
+ end
33
+ end
data/examples/hello.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'spqr/spqr'
3
+ require 'spqr/app'
4
+ require 'logger'
5
+
6
+ class Hello
7
+ include SPQR::Manageable
8
+ def hello(args)
9
+ @people_greeted ||= 0
10
+ @people_greeted = @people_greeted + 1
11
+ args["result"] = "Hello, #{args['name']}!"
12
+ end
13
+
14
+ spqr_expose :hello do |args|
15
+ args.declare :name, :lstr, :in
16
+ args.declare :result, :lstr, :out
17
+ end
18
+
19
+ # This is for the service_name property
20
+ def service_name
21
+ @service_name = "HelloAgent"
22
+ end
23
+
24
+ spqr_package :hello
25
+ spqr_class :Hello
26
+ spqr_statistic :people_greeted, :int
27
+ spqr_property :service_name, :lstr
28
+
29
+ # These should return the same object for the lifetime of the agent
30
+ # app, since this example has no persistent objects.
31
+ def Hello.find_all
32
+ @@hellos ||= [Hello.new]
33
+ end
34
+
35
+ def Hello.find_by_id(id)
36
+ @@hellos ||= [Hello.new]
37
+ @@hellos[0]
38
+ end
39
+ end
40
+
41
+ app = SPQR::App.new(:loglevel => :debug)
42
+ app.register Hello
43
+
44
+ app.main
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This is a simple logging service that operates over QMF. The API is
4
+ # pretty basic:
5
+ # LogService is a singleton and supports the following methods:
6
+ # * debug(msg)
7
+ # * warn(msg)
8
+ # * info(msg)
9
+ # * error(msg)
10
+ # each of which creates a log record of the given severity,
11
+ # timestamped with the current time, and with msg as the log
12
+ # message.
13
+ #
14
+ # LogRecord corresponds to an individual log entry, and exposes the
15
+ # following (read-only) properties:
16
+ # * l_when (unsigned int), seconds since the epoch corresponding to
17
+ # this log record's creation date
18
+ # * severity (long string), a string representation of the severity
19
+ # * msg (long string), the log message
20
+ #
21
+ # If you invoke logservice.rb with an argument, it will place the
22
+ # generated log records in that file, and they will persist between
23
+ # invocations.
24
+
25
+ require 'rubygems'
26
+ require 'spqr/spqr'
27
+ require 'spqr/app'
28
+ require 'rhubarb/rhubarb'
29
+
30
+ class LogService
31
+ include SPQR::Manageable
32
+
33
+ [:debug, :warn, :info, :error].each do |name|
34
+ define_method name do |args|
35
+ args['result'] = LogRecord.create(:l_when=>Time.now.to_i, :severity=>"#{name.to_s.upcase}", :msg=>args['msg'].dup)
36
+ end
37
+
38
+ spqr_expose name do |args|
39
+ args.declare :msg, :lstr, :in
40
+ args.declare :result, :objId, :out
41
+ end
42
+ end
43
+
44
+ def self.find_all
45
+ @@singleton ||= LogService.new
46
+ [@@singleton]
47
+ end
48
+
49
+ def self.find_by_id(i)
50
+ @@singleton ||= LogService.new
51
+ end
52
+
53
+ spqr_package :examples
54
+ spqr_class :LogService
55
+ end
56
+
57
+ class LogRecord
58
+ include SPQR::Manageable
59
+ include Rhubarb::Persisting
60
+
61
+ declare_column :l_when, :integer
62
+ declare_column :severity, :string
63
+ declare_column :msg, :string
64
+
65
+ # XXX: rhubarb should create a find_all by default
66
+ declare_query :find_all, "1"
67
+
68
+ spqr_property :l_when, :uint
69
+ spqr_property :severity, :lstr
70
+ spqr_property :msg, :lstr
71
+
72
+ spqr_package :examples
73
+ spqr_class :LogRecord
74
+
75
+ def spqr_object_id
76
+ row_id
77
+ end
78
+ end
79
+
80
+ TABLE = ARGV[0] or ":memory:"
81
+ DO_CREATE = (TABLE == ":memory:" or not File.exist?(TABLE))
82
+
83
+ Rhubarb::Persistence::open(TABLE)
84
+
85
+ LogRecord.create_table if DO_CREATE
86
+
87
+ app = SPQR::App.new(:loglevel => :debug)
88
+ app.register LogService, LogRecord
89
+
90
+ app.main
@@ -0,0 +1,504 @@
1
+ # Rhubarb is a simple persistence layer for Ruby objects and SQLite.
2
+ # For now, see the test cases for example usage.
3
+ #
4
+ # Copyright (c) 2009 Red Hat, Inc.
5
+ #
6
+ # Author: William Benton (willb@redhat.com)
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ require 'rubygems'
15
+ require 'set'
16
+ require 'time'
17
+ require 'sqlite3'
18
+
19
+ module Rhubarb
20
+
21
+ module SQLBUtil
22
+ def self.timestamp(tm=nil)
23
+ tm ||= Time.now.utc
24
+ (tm.tv_sec * 1000000) + tm.tv_usec
25
+ end
26
+ end
27
+
28
+
29
+ module Persistence
30
+ @@backend = nil
31
+
32
+ def self.open(filename)
33
+ self.db = SQLite3::Database.new(filename)
34
+ end
35
+
36
+ def self.close
37
+ self.db.close
38
+ self.db = nil
39
+ end
40
+
41
+ def self.db
42
+ @@backend
43
+ end
44
+
45
+ def self.db=(d)
46
+ @@backend = d
47
+ self.db.results_as_hash = true if d != nil
48
+ self.db.type_translation = true if d != nil
49
+ end
50
+
51
+ def self.execute(*query)
52
+ db.execute(*query) if db != nil
53
+ end
54
+ end
55
+
56
+ class Column
57
+ attr_reader :name
58
+
59
+ def initialize(name, kind, quals)
60
+ @name, @kind = name, kind
61
+ @quals = quals.map {|x| x.to_s.gsub("_", " ") if x.class == Symbol}
62
+ @quals.map
63
+ end
64
+
65
+ def to_s
66
+ qualifiers = @quals.join(" ")
67
+ if qualifiers == ""
68
+ "#@name #@kind"
69
+ else
70
+ "#@name #@kind #{qualifiers}"
71
+ end
72
+ end
73
+ end
74
+
75
+ class Reference
76
+ attr_reader :referent, :column, :options
77
+
78
+ # Creates a new Reference object, modeling a foreign-key relationship to another table. +klass+ is a class that includes Persisting; +options+ is a hash of options, which include
79
+ # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
80
+ # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
81
+ def initialize(klass, options={})
82
+ @referent = klass
83
+ @options = options
84
+ @options[:column] ||= "row_id"
85
+ @column = options[:column]
86
+ end
87
+
88
+ def to_s
89
+ trigger = ""
90
+ trigger = " on delete cascade" if options[:on_delete] == :cascade
91
+ "references #{@referent}(#{@column})#{trigger}"
92
+ end
93
+
94
+ def managed_ref?
95
+ # XXX?
96
+ return false if referent.class == String
97
+ referent.ancestors.include? Persisting
98
+ end
99
+ end
100
+
101
+
102
+ # Methods mixed in to the class object of a persisting class
103
+ module PersistingClassMixins
104
+ # Returns the name of the database table modeled by this class.
105
+ def table_name
106
+ self.name.downcase
107
+ end
108
+
109
+ # Models a foreign-key relationship. +options+ is a hash of options, which include
110
+ # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
111
+ # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
112
+ def references(table, options={})
113
+ Reference.new(table, options)
114
+ end
115
+
116
+ # Models a CHECK constraint.
117
+ def check(condition)
118
+ "check (#{condition})"
119
+ end
120
+
121
+ # Returns an object corresponding to the row with the given ID, or +nil+ if no such row exists.
122
+ def find(id)
123
+ tup = self.find_tuple(id)
124
+ return self.new(tup) if tup
125
+ nil
126
+ end
127
+
128
+ def find_by(arg_hash)
129
+ arg_hash = arg_hash.dup
130
+ valid_cols = self.colnames.intersection arg_hash.keys
131
+ select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
132
+ arg_hash.each {|k,v| arg_hash[k] = v.row_id if v.respond_to? :row_id}
133
+
134
+ Persistence::execute("select * from #{table_name} where #{select_criteria}", arg_hash).map {|tup| self.new(tup) }
135
+ end
136
+
137
+ # args contains the following keys
138
+ # * :group_by maps to a list of columns to group by (mandatory)
139
+ # * :select_by maps to a hash mapping from column symbols to values (optional)
140
+ # * :version maps to some version to be considered "current" for the purposes of this query; that is, all rows later than the "current" version will be disregarded (optional, defaults to latest version)
141
+ def find_freshest(args)
142
+ args = args.dup
143
+
144
+ args[:version] ||= SQLBUtil::timestamp
145
+ args[:select_by] ||= {}
146
+
147
+ query_params = {}
148
+ query_params[:version] = args[:version]
149
+
150
+ select_clauses = ["created <= :version"]
151
+
152
+ valid_cols = self.colnames.intersection args[:select_by].keys
153
+
154
+ valid_cols.map do |col|
155
+ select_clauses << "#{col.to_s} = #{col.inspect}"
156
+ val = args[:select_by][col]
157
+ val = val.row_id if val.respond_to? :row_id
158
+ query_params[col] = val
159
+ end
160
+
161
+ group_by_clause = "GROUP BY " + args[:group_by].join(", ")
162
+ where_clause = "WHERE " + select_clauses.join(" AND ")
163
+ projection = self.colnames - [:created]
164
+ join_clause = projection.map do |column|
165
+ "__fresh.#{column} = __freshest.#{column}"
166
+ end
167
+
168
+ projection << "MAX(created) AS __current_version"
169
+ join_clause << "__fresh.__current_version = __freshest.created"
170
+
171
+ query = "
172
+ SELECT __freshest.* FROM (
173
+ SELECT #{projection.to_a.join(', ')} FROM (
174
+ SELECT * from #{table_name} #{where_clause}
175
+ ) #{group_by_clause}
176
+ ) as __fresh INNER JOIN #{table_name} as __freshest ON
177
+ #{join_clause.join(' AND ')}
178
+ ORDER BY row_id
179
+ "
180
+
181
+ Persistence::execute(query, query_params).map {|tup| self.new(tup) }
182
+ end
183
+
184
+ # Does what it says on the tin. Since this will allocate an object for each row, it isn't recomended for huge tables.
185
+ def find_all
186
+ Persistence::execute("SELECT * from #{table_name}").map {|tup| self.new(tup)}
187
+ end
188
+
189
+ # Declares a query method named +name+ and adds it to this class. The query method returns a list of objects corresponding to the rows returned by executing "+SELECT * FROM+ _table_ +WHERE+ _query_" on the database.
190
+ def declare_query(name, query)
191
+ klass = (class << self; self end)
192
+ klass.class_eval do
193
+ define_method name.to_s do |*args|
194
+ # handle reference parameters
195
+ args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
196
+
197
+ res = Persistence::execute("select * from #{table_name} where #{query}", args)
198
+ res.map {|row| self.new(row)}
199
+ end
200
+ end
201
+ end
202
+
203
+ # Declares a custom query method named +name+, and adds it to this class. The custom query method returns a list of objects corresponding to the rows returned by executing +query+ on the database. +query+ should select all fields (with +SELECT *+). If +query+ includes the string +\_\_TABLE\_\_+, it will be expanded to the table name. Typically, you will want to use +declare\_query+ instead; this method is most useful for self-joins.
204
+ def declare_custom_query(name, query)
205
+ klass = (class << self; self end)
206
+ klass.class_eval do
207
+ define_method name.to_s do |*args|
208
+ # handle reference parameters
209
+ args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
210
+
211
+ res = Persistence::execute(query.gsub("__TABLE__", "#{self.table_name}"), args)
212
+ # XXX: should freshen each row?
213
+ res.map {|row| self.new(row) }
214
+ end
215
+ end
216
+ end
217
+
218
+ def declare_index_on(*fields)
219
+ @creation_callbacks << Proc.new do
220
+ idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
221
+ creation_cmd = "create index #{idx_name} on #{self.table_name} (#{fields.join(', ')})"
222
+ Persistence.execute(creation_cmd)
223
+ end if fields.size > 0
224
+ end
225
+
226
+ # Adds a column named +cname+ to this table declaration, and adds the following methods to the class:
227
+ # * accessors for +cname+, called +cname+ and +cname=+
228
+ # * +find\_by\_cname+ and +find\_first\_by\_cname+ methods, which return a list of rows and the first row that have the given value for +cname+, respectively
229
+ # If the column references a column in another table (given via a +references(...)+ argument to +quals+), then add triggers to the database to ensure referential integrity and cascade-on-delete (if specified)
230
+ def declare_column(cname, kind, *quals)
231
+ ensure_accessors
232
+
233
+ find_method_name = "find_by_#{cname}".to_sym
234
+ find_first_method_name = "find_first_by_#{cname}".to_sym
235
+
236
+ get_method_name = "#{cname}".to_sym
237
+ set_method_name = "#{cname}=".to_sym
238
+
239
+ # does this column reference another table?
240
+ rf = quals.find {|q| q.class == Reference}
241
+ if rf
242
+ self.refs[cname] = rf
243
+ end
244
+
245
+ # add a find for this column (a class method)
246
+ klass = (class << self; self end)
247
+ klass.class_eval do
248
+ define_method find_method_name do |arg|
249
+ res = Persistence::execute("select * from #{table_name} where #{cname} = ?", arg)
250
+ res.map {|row| self.new(row)}
251
+ end
252
+
253
+ define_method find_first_method_name do |arg|
254
+ res = Persistence::execute("select * from #{table_name} where #{cname} = ?", arg)
255
+ return self.new(res[0]) if res.size > 0
256
+ nil
257
+ end
258
+ end
259
+
260
+ self.colnames.merge([cname])
261
+ self.columns << Column.new(cname, kind, quals)
262
+
263
+ # add accessors
264
+ define_method get_method_name do
265
+ freshen
266
+ return @tuple["#{cname}"] if @tuple
267
+ nil
268
+ end
269
+
270
+ if not rf
271
+ define_method set_method_name do |arg|
272
+ @tuple["#{cname}"] = arg
273
+ update cname, arg
274
+ end
275
+ else
276
+ # this column references another table; create a set
277
+ # method that can handle either row objects or row IDs
278
+ define_method set_method_name do |arg|
279
+ freshen
280
+
281
+ arg_id = nil
282
+
283
+ if arg.class == Fixnum
284
+ arg_id = arg
285
+ arg = rf.referent.find arg_id
286
+ else
287
+ arg_id = arg.row_id
288
+ end
289
+ @tuple["#{cname}"] = arg
290
+
291
+ update cname, arg_id
292
+ end
293
+
294
+ # Finally, add appropriate triggers to ensure referential integrity.
295
+ # If rf has an on_delete trigger, also add the necessary
296
+ # triggers to cascade deletes.
297
+ # Note that we do not support update triggers, since the API does
298
+ # not expose the capacity to change row IDs.
299
+
300
+ self.creation_callbacks << Proc.new do
301
+ Persistence::db.execute_batch("CREATE TRIGGER refint_insert_#{self.table_name}_#{rf.referent.table_name}_#{self.creation_callbacks.size} BEFORE INSERT ON \"#{self.table_name}\" WHEN new.\"#{cname}\" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM \"#{rf.referent.table_name}\" WHERE new.\"#{cname}\" == \"#{rf.column}\") BEGIN SELECT RAISE(ABORT, 'constraint failed'); END;")
302
+
303
+ Persistence::db.execute_batch("CREATE TRIGGER refint_delete_#{self.table_name}_#{rf.referent.table_name}_#{self.creation_callbacks.size} BEFORE DELETE ON \"#{rf.referent.table_name}\" WHEN EXISTS (SELECT 1 FROM \"#{self.table_name}\" WHERE old.\"#{rf.column}\" == \"#{cname}\") BEGIN DELETE FROM \"#{self.table_name}\" WHERE \"#{cname}\" = old.\"#{rf.column}\"; END;") if rf.options[:on_delete] == :cascade
304
+ end
305
+ end
306
+ end
307
+
308
+ # Declares a constraint. Only check constraints are supported; see
309
+ # the check method.
310
+ def declare_constraint(cname, kind, *details)
311
+ ensure_accessors
312
+ info = details.join(" ")
313
+ @constraints << "constraint #{cname} #{kind} #{info}"
314
+ end
315
+
316
+ # Creates a new row in the table with the supplied column values.
317
+ # May throw a SQLite3::SQLException.
318
+ def create(*args)
319
+ new_row = args[0]
320
+ new_row[:created] = new_row[:updated] = SQLBUtil::timestamp
321
+
322
+ cols = colnames.intersection new_row.keys
323
+ colspec = (cols.map {|col| col.to_s}).join(", ")
324
+ valspec = (cols.map {|col| col.inspect}).join(", ")
325
+ res = nil
326
+
327
+ # resolve any references in the args
328
+ new_row.each do |k,v|
329
+ new_row[k] = v.row_id if v.class.ancestors.include? Persisting
330
+ end
331
+
332
+ Persistence::db.transaction do |db|
333
+ stmt = "insert into #{table_name} (#{colspec}) values (#{valspec})"
334
+ # p stmt
335
+ db.execute(stmt, new_row)
336
+ res = find(db.last_insert_row_id)
337
+ end
338
+ res
339
+ end
340
+
341
+ # Returns a string consisting of the DDL statement to create a table
342
+ # corresponding to this class.
343
+ def table_decl
344
+ cols = columns.join(", ")
345
+ consts = constraints.join(", ")
346
+ if consts.size > 0
347
+ "create table #{table_name} (#{cols}, #{consts});"
348
+ else
349
+ "create table #{table_name} (#{cols});"
350
+ end
351
+ end
352
+
353
+ # Creates a table in the database corresponding to this class.
354
+ def create_table
355
+ Persistence::execute(table_decl)
356
+ @creation_callbacks.each {|func| func.call}
357
+ end
358
+
359
+ # Ensure that all the necessary accessors on our class instance are defined
360
+ # and that all metaclass fields have the appropriate values
361
+ def ensure_accessors
362
+ # Define singleton accessors
363
+ if not self.respond_to? :columns
364
+ class << self
365
+ # Arrays of columns, column names, and column constraints.
366
+ # Note that colnames does not contain id, created, or updated.
367
+ # The API purposefully does not expose the ability to create a
368
+ # row with a given id, and created and updated values are
369
+ # maintained automatically by the API.
370
+ attr_accessor :columns, :colnames, :constraints, :dirtied, :refs, :creation_callbacks
371
+ end
372
+ end
373
+
374
+ # Ensure singleton fields are initialized
375
+ self.columns ||= [Column.new(:row_id, :integer, [:primary_key]), Column.new(:created, :integer, []), Column.new(:updated, :integer, [])]
376
+ self.colnames ||= Set.new [:created, :updated]
377
+ self.constraints ||= []
378
+ self.dirtied ||= {}
379
+ self.refs ||= {}
380
+ self.creation_callbacks ||= []
381
+ end
382
+
383
+ # Returns the number of rows in the table backing this class
384
+ def count
385
+ result = Persistence::execute("select count(row_id) from #{table_name}")[0]
386
+ result[0].to_i
387
+ end
388
+
389
+ def find_tuple(id)
390
+ res = Persistence::execute("select * from #{table_name} where row_id = ?", id)
391
+ if res.size == 0
392
+ nil
393
+ else
394
+ res[0]
395
+ end
396
+ end
397
+ end
398
+
399
+ module Persisting
400
+ def self.included(other)
401
+ class << other
402
+ include PersistingClassMixins
403
+ end
404
+
405
+ other.class_eval do
406
+ attr_reader :row_id
407
+ attr_reader :created
408
+ attr_reader :updated
409
+ end
410
+ end
411
+
412
+
413
+ # Returns true if the row backing this object has been deleted from the database
414
+ def deleted?
415
+ freshen
416
+ not @tuple
417
+ end
418
+
419
+ # Initializes a new instance backed by a tuple of values. Do not call this directly.
420
+ # Create new instances with the create or find methods.
421
+ def initialize(tup)
422
+ @backed = true
423
+ @tuple = tup
424
+ mark_fresh
425
+ @row_id = @tuple["row_id"]
426
+ @created = @tuple["created"]
427
+ @updated = @tuple["updated"]
428
+ resolve_referents
429
+ self.class.dirtied[@row_id] ||= @expired_after
430
+ self
431
+ end
432
+
433
+ # Deletes the row corresponding to this object from the database;
434
+ # invalidates =self= and any other objects backed by this row
435
+ def delete
436
+ Persistence::execute("delete from #{self.class.table_name} where row_id = ?", @row_id)
437
+ mark_dirty
438
+ @tuple = nil
439
+ @row_id = nil
440
+ end
441
+
442
+ ## Begin private methods
443
+
444
+ private
445
+
446
+ # Fetches updated attribute values from the database if necessary
447
+ def freshen
448
+ if needs_refresh?
449
+ @tuple = self.class.find_tuple(@row_id)
450
+ if @tuple
451
+ @updated = @tuple["updated"]
452
+ else
453
+ @row_id = @updated = @created = nil
454
+ end
455
+ mark_fresh
456
+ resolve_referents
457
+ end
458
+ end
459
+
460
+ # True if the underlying row in the database is inconsistent with the state
461
+ # of this object, whether because the row has changed, or because this object has no row id
462
+ def needs_refresh?
463
+ if not @row_id
464
+ @tuple != nil
465
+ else
466
+ @expired_after < self.class.dirtied[@row_id]
467
+ end
468
+ end
469
+
470
+ # Mark this row as dirty so that any other objects backed by this row will
471
+ # update from the database before their attributes are inspected
472
+ def mark_dirty
473
+ self.class.dirtied[@row_id] = SQLBUtil::timestamp
474
+ end
475
+
476
+ # Mark this row as consistent with the underlying database as of now
477
+ def mark_fresh
478
+ @expired_after = SQLBUtil::timestamp
479
+ end
480
+
481
+ # Helper method to update the row in the database when one of our fields changes
482
+ def update(attr_name, value)
483
+ mark_dirty
484
+ Persistence::execute("update #{self.class.table_name} set #{attr_name} = ?, updated = ? where row_id = ?", value, SQLBUtil::timestamp, @row_id)
485
+ end
486
+
487
+ # Resolve any fields that reference other tables, replacing row ids with referred objects
488
+ def resolve_referents
489
+ refs = self.class.refs
490
+
491
+ refs.each do |c,r|
492
+ c = c.to_s
493
+ if r.referent == self.class and @tuple[c] == row_id
494
+ @tuple[c] = self
495
+ else
496
+ row = r.referent.find @tuple[c]
497
+ @tuple[c] = row if row
498
+ end
499
+ end
500
+ end
501
+
502
+ end
503
+
504
+ end