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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +202 -0
- data/README.rdoc +28 -0
- data/Rakefile +105 -0
- data/TODO +33 -0
- data/VERSION +1 -0
- data/bin/spqr-gen.rb +60 -0
- data/examples/codegen-schema.xml +7 -0
- data/examples/codegen/EchoAgent.rb +33 -0
- data/examples/hello.rb +44 -0
- data/examples/logservice.rb +90 -0
- data/lib/rhubarb/rhubarb.rb +504 -0
- data/lib/spqr/app.rb +299 -0
- data/lib/spqr/codegen.rb +434 -0
- data/lib/spqr/constants.rb +64 -0
- data/lib/spqr/manageable.rb +222 -0
- data/lib/spqr/spqr.rb +16 -0
- data/lib/spqr/utils.rb +88 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/spqr_spec.rb +5 -0
- data/spqr.spec.in +95 -0
- data/test/helper.rb +11 -0
- data/test/test_rhubarb.rb +608 -0
- data/test/test_spqr.rb +7 -0
- metadata +97 -0
@@ -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
|