spqr 0.0.1

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,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