spqr 0.1.4 → 0.2.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.
- data/CHANGES +5 -1
- data/TODO +0 -17
- data/VERSION +1 -1
- data/examples/logservice.rb +6 -1
- data/test/helper.rb +0 -1
- metadata +2 -5
- data/lib/rhubarb/rhubarb.rb +0 -545
- data/test/test_rhubarb.rb +0 -632
data/CHANGES
CHANGED
data/TODO
CHANGED
@@ -1,10 +1,3 @@
|
|
1
|
-
To do, overall:
|
2
|
-
|
3
|
-
* packaging (partially done)
|
4
|
-
|
5
|
-
|
6
|
-
To do for spqr:
|
7
|
-
|
8
1
|
! Event support
|
9
2
|
* Support for map-, array-, and list-valued parameters and properties (QMF on Ruby itself does not support these at the moment)
|
10
3
|
* Broader query support (partially done)
|
@@ -16,16 +9,6 @@ To do for spqr:
|
|
16
9
|
- Fewer required annotations
|
17
10
|
- Automatic decoration of certain methods
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
To do for rhubarb:
|
22
|
-
|
23
|
-
* Bring code coverage up to 100%
|
24
|
-
* Documentation
|
25
|
-
* Standalone examples (i.e. not just "please see the test suite")
|
26
|
-
|
27
|
-
|
28
|
-
|
29
12
|
Legend:
|
30
13
|
|
31
14
|
! --> highest priority
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/examples/logservice.rb
CHANGED
@@ -24,7 +24,12 @@
|
|
24
24
|
|
25
25
|
require 'spqr/spqr'
|
26
26
|
require 'spqr/app'
|
27
|
-
|
27
|
+
begin
|
28
|
+
require 'rhubarb/rhubarb'
|
29
|
+
rescue LoadError
|
30
|
+
puts "You must have Rhubarb installed to run this example.\nTry \"gem install rhubarb\", and ensure that it is in your load path."
|
31
|
+
Process.exit
|
32
|
+
end
|
28
33
|
|
29
34
|
class LogService
|
30
35
|
include SPQR::Manageable
|
data/test/helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spqr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- William Benton
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-02 00:00:00 -06:00
|
13
13
|
default_executable: spqr-gen.rb
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -45,7 +45,6 @@ files:
|
|
45
45
|
- examples/codegen-schema.xml
|
46
46
|
- examples/hello.rb
|
47
47
|
- examples/logservice.rb
|
48
|
-
- lib/rhubarb/rhubarb.rb
|
49
48
|
- lib/spqr/app.rb
|
50
49
|
- lib/spqr/codegen.rb
|
51
50
|
- lib/spqr/constants.rb
|
@@ -58,7 +57,6 @@ files:
|
|
58
57
|
- spqr.spec.in
|
59
58
|
- test/example-apps.rb
|
60
59
|
- test/helper.rb
|
61
|
-
- test/test_rhubarb.rb
|
62
60
|
- test/test_spqr_clicker.rb
|
63
61
|
- test/test_spqr_dummyprop.rb
|
64
62
|
- test/test_spqr_hello.rb
|
@@ -99,7 +97,6 @@ test_files:
|
|
99
97
|
- test/helper.rb
|
100
98
|
- test/test_spqr_clicker.rb
|
101
99
|
- test/test_spqr_integerprop.rb
|
102
|
-
- test/test_rhubarb.rb
|
103
100
|
- test/test_spqr_hello.rb
|
104
101
|
- examples/agent-app.rb
|
105
102
|
- examples/examples/codegen/EchoAgent.rb
|
data/lib/rhubarb/rhubarb.rb
DELETED
@@ -1,545 +0,0 @@
|
|
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
|
-
class DbCollection < Hash
|
31
|
-
alias orig_set []=
|
32
|
-
|
33
|
-
def []=(k,v)
|
34
|
-
v.results_as_hash = true if v
|
35
|
-
v.type_translation = true if v
|
36
|
-
orig_set(k,v)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
@@dbs = DbCollection.new
|
41
|
-
|
42
|
-
def self.open(filename, which=:default)
|
43
|
-
dbs[which] = SQLite3::Database.new(filename)
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.close(which=:default)
|
47
|
-
if dbs[which]
|
48
|
-
dbs[which].close
|
49
|
-
dbs.delete(which)
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.db
|
54
|
-
dbs[:default]
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.db=(d)
|
58
|
-
dbs[:default] = d
|
59
|
-
end
|
60
|
-
|
61
|
-
def self.dbs
|
62
|
-
@@dbs
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
class Column
|
67
|
-
attr_reader :name
|
68
|
-
|
69
|
-
def initialize(name, kind, quals)
|
70
|
-
@name, @kind = name, kind
|
71
|
-
@quals = quals.map {|x| x.to_s.gsub("_", " ") if x.class == Symbol}
|
72
|
-
@quals.map
|
73
|
-
end
|
74
|
-
|
75
|
-
def to_s
|
76
|
-
qualifiers = @quals.join(" ")
|
77
|
-
if qualifiers == ""
|
78
|
-
"#@name #@kind"
|
79
|
-
else
|
80
|
-
"#@name #@kind #{qualifiers}"
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
class Reference
|
86
|
-
attr_reader :referent, :column, :options
|
87
|
-
|
88
|
-
# 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
|
89
|
-
# +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
|
90
|
-
# +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
|
91
|
-
def initialize(klass, options={})
|
92
|
-
@referent = klass
|
93
|
-
@options = options
|
94
|
-
@options[:column] ||= "row_id"
|
95
|
-
@column = options[:column]
|
96
|
-
end
|
97
|
-
|
98
|
-
def to_s
|
99
|
-
trigger = ""
|
100
|
-
trigger = " on delete cascade" if options[:on_delete] == :cascade
|
101
|
-
"references #{@referent}(#{@column})#{trigger}"
|
102
|
-
end
|
103
|
-
|
104
|
-
def managed_ref?
|
105
|
-
# XXX?
|
106
|
-
return false if referent.class == String
|
107
|
-
referent.ancestors.include? Persisting
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
|
112
|
-
# Methods mixed in to the class object of a persisting class
|
113
|
-
module PersistingClassMixins
|
114
|
-
# Returns the name of the database table modeled by this class.
|
115
|
-
# Defaults to the name of the class (sans module names)
|
116
|
-
def table_name
|
117
|
-
@table_name ||= self.name.split("::").pop.downcase
|
118
|
-
end
|
119
|
-
|
120
|
-
# Enables setting the table name to a custom name
|
121
|
-
def declare_table_name(nm)
|
122
|
-
@table_name = nm
|
123
|
-
end
|
124
|
-
|
125
|
-
# Models a foreign-key relationship. +options+ is a hash of options, which include
|
126
|
-
# +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
|
127
|
-
# +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
|
128
|
-
def references(table, options={})
|
129
|
-
Reference.new(table, options)
|
130
|
-
end
|
131
|
-
|
132
|
-
# Models a CHECK constraint.
|
133
|
-
def check(condition)
|
134
|
-
"check (#{condition})"
|
135
|
-
end
|
136
|
-
|
137
|
-
# Returns an object corresponding to the row with the given ID, or +nil+ if no such row exists.
|
138
|
-
def find(id)
|
139
|
-
tup = self.find_tuple(id)
|
140
|
-
return self.new(tup) if tup
|
141
|
-
nil
|
142
|
-
end
|
143
|
-
|
144
|
-
alias find_by_id find
|
145
|
-
|
146
|
-
def find_by(arg_hash)
|
147
|
-
arg_hash = arg_hash.dup
|
148
|
-
valid_cols = self.colnames.intersection arg_hash.keys
|
149
|
-
select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
|
150
|
-
arg_hash.each {|k,v| arg_hash[k] = v.row_id if v.respond_to? :row_id}
|
151
|
-
|
152
|
-
self.db.execute("select * from #{table_name} where #{select_criteria} order by row_id", arg_hash).map {|tup| self.new(tup) }
|
153
|
-
end
|
154
|
-
|
155
|
-
# args contains the following keys
|
156
|
-
# * :group_by maps to a list of columns to group by (mandatory)
|
157
|
-
# * :select_by maps to a hash mapping from column symbols to values (optional)
|
158
|
-
# * :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)
|
159
|
-
def find_freshest(args)
|
160
|
-
args = args.dup
|
161
|
-
|
162
|
-
args[:version] ||= SQLBUtil::timestamp
|
163
|
-
args[:select_by] ||= {}
|
164
|
-
|
165
|
-
query_params = {}
|
166
|
-
query_params[:version] = args[:version]
|
167
|
-
|
168
|
-
select_clauses = ["created <= :version"]
|
169
|
-
|
170
|
-
valid_cols = self.colnames.intersection args[:select_by].keys
|
171
|
-
|
172
|
-
valid_cols.map do |col|
|
173
|
-
select_clauses << "#{col.to_s} = #{col.inspect}"
|
174
|
-
val = args[:select_by][col]
|
175
|
-
val = val.row_id if val.respond_to? :row_id
|
176
|
-
query_params[col] = val
|
177
|
-
end
|
178
|
-
|
179
|
-
group_by_clause = "GROUP BY " + args[:group_by].join(", ")
|
180
|
-
where_clause = "WHERE " + select_clauses.join(" AND ")
|
181
|
-
projection = self.colnames - [:created]
|
182
|
-
join_clause = projection.map do |column|
|
183
|
-
"__fresh.#{column} = __freshest.#{column}"
|
184
|
-
end
|
185
|
-
|
186
|
-
projection << "MAX(created) AS __current_version"
|
187
|
-
join_clause << "__fresh.__current_version = __freshest.created"
|
188
|
-
|
189
|
-
query = "
|
190
|
-
SELECT __freshest.* FROM (
|
191
|
-
SELECT #{projection.to_a.join(', ')} FROM (
|
192
|
-
SELECT * from #{table_name} #{where_clause}
|
193
|
-
) #{group_by_clause}
|
194
|
-
) as __fresh INNER JOIN #{table_name} as __freshest ON
|
195
|
-
#{join_clause.join(' AND ')}
|
196
|
-
ORDER BY row_id
|
197
|
-
"
|
198
|
-
|
199
|
-
self.db.execute(query, query_params).map {|tup| self.new(tup) }
|
200
|
-
end
|
201
|
-
|
202
|
-
# Does what it says on the tin. Since this will allocate an object for each row, it isn't recomended for huge tables.
|
203
|
-
def find_all
|
204
|
-
self.db.execute("SELECT * from #{table_name}").map {|tup| self.new(tup)}
|
205
|
-
end
|
206
|
-
|
207
|
-
def delete_all
|
208
|
-
self.db.execute("DELETE from #{table_name}")
|
209
|
-
end
|
210
|
-
|
211
|
-
# 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.
|
212
|
-
def declare_query(name, query)
|
213
|
-
klass = (class << self; self end)
|
214
|
-
klass.class_eval do
|
215
|
-
define_method name.to_s do |*args|
|
216
|
-
# handle reference parameters
|
217
|
-
args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
|
218
|
-
|
219
|
-
res = self.db.execute("select * from #{table_name} where #{query}", args)
|
220
|
-
res.map {|row| self.new(row)}
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# 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.
|
226
|
-
def declare_custom_query(name, query)
|
227
|
-
klass = (class << self; self end)
|
228
|
-
klass.class_eval do
|
229
|
-
define_method name.to_s do |*args|
|
230
|
-
# handle reference parameters
|
231
|
-
args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
|
232
|
-
|
233
|
-
res = self.db.execute(query.gsub("__TABLE__", "#{self.table_name}"), args)
|
234
|
-
# XXX: should freshen each row?
|
235
|
-
res.map {|row| self.new(row) }
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def declare_index_on(*fields)
|
241
|
-
@creation_callbacks << Proc.new do
|
242
|
-
idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
|
243
|
-
creation_cmd = "create index #{idx_name} on #{self.table_name} (#{fields.join(', ')})"
|
244
|
-
self.db.execute(creation_cmd)
|
245
|
-
end if fields.size > 0
|
246
|
-
end
|
247
|
-
|
248
|
-
# Adds a column named +cname+ to this table declaration, and adds the following methods to the class:
|
249
|
-
# * accessors for +cname+, called +cname+ and +cname=+
|
250
|
-
# * +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
|
251
|
-
# 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)
|
252
|
-
def declare_column(cname, kind, *quals)
|
253
|
-
ensure_accessors
|
254
|
-
|
255
|
-
find_method_name = "find_by_#{cname}".to_sym
|
256
|
-
find_first_method_name = "find_first_by_#{cname}".to_sym
|
257
|
-
|
258
|
-
get_method_name = "#{cname}".to_sym
|
259
|
-
set_method_name = "#{cname}=".to_sym
|
260
|
-
|
261
|
-
# does this column reference another table?
|
262
|
-
rf = quals.find {|q| q.class == Reference}
|
263
|
-
if rf
|
264
|
-
self.refs[cname] = rf
|
265
|
-
end
|
266
|
-
|
267
|
-
# add a find for this column (a class method)
|
268
|
-
klass = (class << self; self end)
|
269
|
-
klass.class_eval do
|
270
|
-
define_method find_method_name do |arg|
|
271
|
-
res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg)
|
272
|
-
res.map {|row| self.new(row)}
|
273
|
-
end
|
274
|
-
|
275
|
-
define_method find_first_method_name do |arg|
|
276
|
-
res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg)
|
277
|
-
return self.new(res[0]) if res.size > 0
|
278
|
-
nil
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
self.colnames.merge([cname])
|
283
|
-
self.columns << Column.new(cname, kind, quals)
|
284
|
-
|
285
|
-
# add accessors
|
286
|
-
define_method get_method_name do
|
287
|
-
freshen
|
288
|
-
return @tuple["#{cname}"] if @tuple
|
289
|
-
nil
|
290
|
-
end
|
291
|
-
|
292
|
-
if not rf
|
293
|
-
define_method set_method_name do |arg|
|
294
|
-
@tuple["#{cname}"] = arg
|
295
|
-
update cname, arg
|
296
|
-
end
|
297
|
-
else
|
298
|
-
# this column references another table; create a set
|
299
|
-
# method that can handle either row objects or row IDs
|
300
|
-
define_method set_method_name do |arg|
|
301
|
-
freshen
|
302
|
-
|
303
|
-
arg_id = nil
|
304
|
-
|
305
|
-
if arg.class == Fixnum
|
306
|
-
arg_id = arg
|
307
|
-
arg = rf.referent.find arg_id
|
308
|
-
else
|
309
|
-
arg_id = arg.row_id
|
310
|
-
end
|
311
|
-
@tuple["#{cname}"] = arg
|
312
|
-
|
313
|
-
update cname, arg_id
|
314
|
-
end
|
315
|
-
|
316
|
-
# Finally, add appropriate triggers to ensure referential integrity.
|
317
|
-
# If rf has an on_delete trigger, also add the necessary
|
318
|
-
# triggers to cascade deletes.
|
319
|
-
# Note that we do not support update triggers, since the API does
|
320
|
-
# not expose the capacity to change row IDs.
|
321
|
-
|
322
|
-
self.creation_callbacks << Proc.new do
|
323
|
-
@ccount ||= 0
|
324
|
-
|
325
|
-
insert_trigger_name = "ri_insert_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}"
|
326
|
-
delete_trigger_name = "ri_delete_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}"
|
327
|
-
|
328
|
-
self.db.execute_batch("CREATE TRIGGER #{insert_trigger_name} 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 #{insert_trigger_name} (#{rf.referent.table_name} missing foreign key row) failed'); END;")
|
329
|
-
|
330
|
-
self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} 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
|
331
|
-
|
332
|
-
@ccount = @ccount + 1
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
336
|
-
|
337
|
-
# Declares a constraint. Only check constraints are supported; see
|
338
|
-
# the check method.
|
339
|
-
def declare_constraint(cname, kind, *details)
|
340
|
-
ensure_accessors
|
341
|
-
info = details.join(" ")
|
342
|
-
@constraints << "constraint #{cname} #{kind} #{info}"
|
343
|
-
end
|
344
|
-
|
345
|
-
# Creates a new row in the table with the supplied column values.
|
346
|
-
# May throw a SQLite3::SQLException.
|
347
|
-
def create(*args)
|
348
|
-
new_row = args[0]
|
349
|
-
new_row[:created] = new_row[:updated] = SQLBUtil::timestamp
|
350
|
-
|
351
|
-
cols = colnames.intersection new_row.keys
|
352
|
-
colspec = (cols.map {|col| col.to_s}).join(", ")
|
353
|
-
valspec = (cols.map {|col| col.inspect}).join(", ")
|
354
|
-
res = nil
|
355
|
-
|
356
|
-
# resolve any references in the args
|
357
|
-
new_row.each do |k,v|
|
358
|
-
new_row[k] = v.row_id if v.class.ancestors.include? Persisting
|
359
|
-
end
|
360
|
-
|
361
|
-
self.db.transaction do |db|
|
362
|
-
stmt = "insert into #{table_name} (#{colspec}) values (#{valspec})"
|
363
|
-
# p stmt
|
364
|
-
db.execute(stmt, new_row)
|
365
|
-
res = find(db.last_insert_row_id)
|
366
|
-
end
|
367
|
-
res
|
368
|
-
end
|
369
|
-
|
370
|
-
# Returns a string consisting of the DDL statement to create a table
|
371
|
-
# corresponding to this class.
|
372
|
-
def table_decl
|
373
|
-
cols = columns.join(", ")
|
374
|
-
consts = constraints.join(", ")
|
375
|
-
if consts.size > 0
|
376
|
-
"create table #{table_name} (#{cols}, #{consts});"
|
377
|
-
else
|
378
|
-
"create table #{table_name} (#{cols});"
|
379
|
-
end
|
380
|
-
end
|
381
|
-
|
382
|
-
# Creates a table in the database corresponding to this class.
|
383
|
-
def create_table(dbkey=:default)
|
384
|
-
self.db = Persistence::dbs[dbkey]
|
385
|
-
self.db.execute(table_decl)
|
386
|
-
@creation_callbacks.each {|func| func.call}
|
387
|
-
end
|
388
|
-
|
389
|
-
def db
|
390
|
-
@db || Persistence::db
|
391
|
-
end
|
392
|
-
|
393
|
-
def db=(d)
|
394
|
-
@db = d
|
395
|
-
end
|
396
|
-
|
397
|
-
# Ensure that all the necessary accessors on our class instance are defined
|
398
|
-
# and that all metaclass fields have the appropriate values
|
399
|
-
def ensure_accessors
|
400
|
-
# Define singleton accessors
|
401
|
-
if not self.respond_to? :columns
|
402
|
-
class << self
|
403
|
-
# Arrays of columns, column names, and column constraints.
|
404
|
-
# Note that colnames does not contain id, created, or updated.
|
405
|
-
# The API purposefully does not expose the ability to create a
|
406
|
-
# row with a given id, and created and updated values are
|
407
|
-
# maintained automatically by the API.
|
408
|
-
attr_accessor :columns, :colnames, :constraints, :dirtied, :refs, :creation_callbacks
|
409
|
-
end
|
410
|
-
end
|
411
|
-
|
412
|
-
# Ensure singleton fields are initialized
|
413
|
-
self.columns ||= [Column.new(:row_id, :integer, [:primary_key]), Column.new(:created, :integer, []), Column.new(:updated, :integer, [])]
|
414
|
-
self.colnames ||= Set.new [:created, :updated]
|
415
|
-
self.constraints ||= []
|
416
|
-
self.dirtied ||= {}
|
417
|
-
self.refs ||= {}
|
418
|
-
self.creation_callbacks ||= []
|
419
|
-
end
|
420
|
-
|
421
|
-
# Returns the number of rows in the table backing this class
|
422
|
-
def count
|
423
|
-
result = self.db.execute("select count(row_id) from #{table_name}")[0]
|
424
|
-
result[0].to_i
|
425
|
-
end
|
426
|
-
|
427
|
-
def find_tuple(id)
|
428
|
-
res = self.db.execute("select * from #{table_name} where row_id = ?", id)
|
429
|
-
if res.size == 0
|
430
|
-
nil
|
431
|
-
else
|
432
|
-
res[0]
|
433
|
-
end
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
module Persisting
|
438
|
-
def self.included(other)
|
439
|
-
class << other
|
440
|
-
include PersistingClassMixins
|
441
|
-
end
|
442
|
-
|
443
|
-
other.class_eval do
|
444
|
-
attr_reader :row_id
|
445
|
-
attr_reader :created
|
446
|
-
attr_reader :updated
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
def db
|
451
|
-
self.class.db
|
452
|
-
end
|
453
|
-
|
454
|
-
# Returns true if the row backing this object has been deleted from the database
|
455
|
-
def deleted?
|
456
|
-
freshen
|
457
|
-
not @tuple
|
458
|
-
end
|
459
|
-
|
460
|
-
# Initializes a new instance backed by a tuple of values. Do not call this directly.
|
461
|
-
# Create new instances with the create or find methods.
|
462
|
-
def initialize(tup)
|
463
|
-
@backed = true
|
464
|
-
@tuple = tup
|
465
|
-
mark_fresh
|
466
|
-
@row_id = @tuple["row_id"]
|
467
|
-
@created = @tuple["created"]
|
468
|
-
@updated = @tuple["updated"]
|
469
|
-
resolve_referents
|
470
|
-
self.class.dirtied[@row_id] ||= @expired_after
|
471
|
-
self
|
472
|
-
end
|
473
|
-
|
474
|
-
# Deletes the row corresponding to this object from the database;
|
475
|
-
# invalidates =self= and any other objects backed by this row
|
476
|
-
def delete
|
477
|
-
self.db.execute("delete from #{self.class.table_name} where row_id = ?", @row_id)
|
478
|
-
mark_dirty
|
479
|
-
@tuple = nil
|
480
|
-
@row_id = nil
|
481
|
-
end
|
482
|
-
|
483
|
-
## Begin private methods
|
484
|
-
|
485
|
-
private
|
486
|
-
|
487
|
-
# Fetches updated attribute values from the database if necessary
|
488
|
-
def freshen
|
489
|
-
if needs_refresh?
|
490
|
-
@tuple = self.class.find_tuple(@row_id)
|
491
|
-
if @tuple
|
492
|
-
@updated = @tuple["updated"]
|
493
|
-
else
|
494
|
-
@row_id = @updated = @created = nil
|
495
|
-
end
|
496
|
-
mark_fresh
|
497
|
-
resolve_referents
|
498
|
-
end
|
499
|
-
end
|
500
|
-
|
501
|
-
# True if the underlying row in the database is inconsistent with the state
|
502
|
-
# of this object, whether because the row has changed, or because this object has no row id
|
503
|
-
def needs_refresh?
|
504
|
-
if not @row_id
|
505
|
-
@tuple != nil
|
506
|
-
else
|
507
|
-
@expired_after < self.class.dirtied[@row_id]
|
508
|
-
end
|
509
|
-
end
|
510
|
-
|
511
|
-
# Mark this row as dirty so that any other objects backed by this row will
|
512
|
-
# update from the database before their attributes are inspected
|
513
|
-
def mark_dirty
|
514
|
-
self.class.dirtied[@row_id] = SQLBUtil::timestamp
|
515
|
-
end
|
516
|
-
|
517
|
-
# Mark this row as consistent with the underlying database as of now
|
518
|
-
def mark_fresh
|
519
|
-
@expired_after = SQLBUtil::timestamp
|
520
|
-
end
|
521
|
-
|
522
|
-
# Helper method to update the row in the database when one of our fields changes
|
523
|
-
def update(attr_name, value)
|
524
|
-
mark_dirty
|
525
|
-
self.db.execute("update #{self.class.table_name} set #{attr_name} = ?, updated = ? where row_id = ?", value, SQLBUtil::timestamp, @row_id)
|
526
|
-
end
|
527
|
-
|
528
|
-
# Resolve any fields that reference other tables, replacing row ids with referred objects
|
529
|
-
def resolve_referents
|
530
|
-
refs = self.class.refs
|
531
|
-
|
532
|
-
refs.each do |c,r|
|
533
|
-
c = c.to_s
|
534
|
-
if r.referent == self.class and @tuple[c] == row_id
|
535
|
-
@tuple[c] = self
|
536
|
-
else
|
537
|
-
row = r.referent.find @tuple[c]
|
538
|
-
@tuple[c] = row if row
|
539
|
-
end
|
540
|
-
end
|
541
|
-
end
|
542
|
-
|
543
|
-
end
|
544
|
-
|
545
|
-
end
|
data/test/test_rhubarb.rb
DELETED
@@ -1,632 +0,0 @@
|
|
1
|
-
# Test cases for Rhubarb, which is is a simple persistence layer for
|
2
|
-
# Ruby objects and SQLite.
|
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 'rhubarb/rhubarb'
|
15
|
-
require 'test/unit'
|
16
|
-
|
17
|
-
class TestClass
|
18
|
-
include Rhubarb::Persisting
|
19
|
-
declare_column :foo, :integer
|
20
|
-
declare_column :bar, :string
|
21
|
-
end
|
22
|
-
|
23
|
-
module RhubarbNamespace
|
24
|
-
class NMTC
|
25
|
-
include Rhubarb::Persisting
|
26
|
-
declare_column :foo, :integer
|
27
|
-
declare_column :bar, :string
|
28
|
-
end
|
29
|
-
|
30
|
-
class NMTC2
|
31
|
-
include Rhubarb::Persisting
|
32
|
-
declare_column :foo, :integer
|
33
|
-
declare_column :bar, :string
|
34
|
-
declare_table_name('namespacetestcase')
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
class TestClass2
|
39
|
-
include Rhubarb::Persisting
|
40
|
-
declare_column :fred, :integer
|
41
|
-
declare_column :barney, :string
|
42
|
-
declare_index_on :fred
|
43
|
-
end
|
44
|
-
|
45
|
-
class TC3
|
46
|
-
include Rhubarb::Persisting
|
47
|
-
declare_column :ugh, :datetime
|
48
|
-
declare_column :yikes, :integer
|
49
|
-
declare_constraint :yikes_pos, check("yikes >= 0")
|
50
|
-
end
|
51
|
-
|
52
|
-
class TC4
|
53
|
-
include Rhubarb::Persisting
|
54
|
-
declare_column :t1, :integer, references(TestClass)
|
55
|
-
declare_column :t2, :integer, references(TestClass2)
|
56
|
-
declare_column :enabled, :boolean, :default, :true
|
57
|
-
end
|
58
|
-
|
59
|
-
class SelfRef
|
60
|
-
include Rhubarb::Persisting
|
61
|
-
declare_column :one, :integer, references(SelfRef)
|
62
|
-
end
|
63
|
-
|
64
|
-
class CustomQueryTable
|
65
|
-
include Rhubarb::Persisting
|
66
|
-
declare_column :one, :integer
|
67
|
-
declare_column :two, :integer
|
68
|
-
declare_query :ltcols, "one < two"
|
69
|
-
declare_query :ltvars, "one < ? and two < ?"
|
70
|
-
declare_custom_query :cltvars, "select * from __TABLE__ where one < ? and two < ?"
|
71
|
-
end
|
72
|
-
|
73
|
-
class ToRef
|
74
|
-
include Rhubarb::Persisting
|
75
|
-
declare_column :foo, :string
|
76
|
-
end
|
77
|
-
|
78
|
-
class FromRef
|
79
|
-
include Rhubarb::Persisting
|
80
|
-
declare_column :t, :integer, references(ToRef, :on_delete=>:cascade)
|
81
|
-
end
|
82
|
-
|
83
|
-
class FreshTestTable
|
84
|
-
include Rhubarb::Persisting
|
85
|
-
declare_column :fee, :integer
|
86
|
-
declare_column :fie, :integer
|
87
|
-
declare_column :foe, :integer
|
88
|
-
declare_column :fum, :integer
|
89
|
-
end
|
90
|
-
|
91
|
-
class BackendBasicTests < Test::Unit::TestCase
|
92
|
-
def setup
|
93
|
-
Rhubarb::Persistence::open(":memory:")
|
94
|
-
klasses = []
|
95
|
-
klasses << TestClass
|
96
|
-
klasses << TestClass2
|
97
|
-
klasses << TC3
|
98
|
-
klasses << TC4
|
99
|
-
klasses << SelfRef
|
100
|
-
klasses << CustomQueryTable
|
101
|
-
klasses << ToRef
|
102
|
-
klasses << FromRef
|
103
|
-
klasses << FreshTestTable
|
104
|
-
klasses << RhubarbNamespace::NMTC
|
105
|
-
klasses << RhubarbNamespace::NMTC2
|
106
|
-
|
107
|
-
klasses.each { |klass| klass.create_table }
|
108
|
-
|
109
|
-
@flist = []
|
110
|
-
end
|
111
|
-
|
112
|
-
def teardown
|
113
|
-
Rhubarb::Persistence::close()
|
114
|
-
end
|
115
|
-
|
116
|
-
def test_persistence_setup
|
117
|
-
assert Rhubarb::Persistence::db.type_translation, "type translation not enabled for db"
|
118
|
-
assert Rhubarb::Persistence::db.results_as_hash, "rows-as-hashes not enabled for db"
|
119
|
-
end
|
120
|
-
|
121
|
-
def test_reference_ctor_klass
|
122
|
-
r = Rhubarb::Reference.new(TestClass)
|
123
|
-
assert(r.referent == TestClass, "Referent of managed reference instance incorrect")
|
124
|
-
assert(r.column == "row_id", "Column of managed reference instance incorrect")
|
125
|
-
assert(r.to_s == "references TestClass(row_id)", "string representation of managed reference instance incorrect")
|
126
|
-
assert(r.managed_ref?, "managed reference should return true for managed_ref?")
|
127
|
-
end
|
128
|
-
|
129
|
-
def test_namespace_table_name
|
130
|
-
assert_equal "nmtc", RhubarbNamespace::NMTC.table_name
|
131
|
-
end
|
132
|
-
|
133
|
-
def test_set_table_name
|
134
|
-
assert_equal "namespacetestcase", RhubarbNamespace::NMTC2.table_name
|
135
|
-
end
|
136
|
-
|
137
|
-
def test_reference_ctor_string
|
138
|
-
r = Rhubarb::Reference.new("TestClass")
|
139
|
-
assert(r.referent == "TestClass", "Referent of string-backed reference instance incorrect")
|
140
|
-
assert(r.column == "row_id", "Column of string-backed reference instance incorrect")
|
141
|
-
assert(r.to_s == "references TestClass(row_id)", "string representation of string-backed reference instance incorrect")
|
142
|
-
assert(r.managed_ref? == false, "unmanaged reference should return false for managed_ref?")
|
143
|
-
end
|
144
|
-
|
145
|
-
def test_instance_methods
|
146
|
-
["foo", "bar"].each do |prefix|
|
147
|
-
["#{prefix}", "#{prefix}="].each do |m|
|
148
|
-
assert TestClass.instance_methods.include?(m), "#{m} method not declared in TestClass"
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
|
153
|
-
def test_instance_methods2
|
154
|
-
["fred", "barney"].each do |prefix|
|
155
|
-
["#{prefix}", "#{prefix}="].each do |m|
|
156
|
-
assert TestClass2.instance_methods.include?(m), "#{m} method not declared in TestClass2"
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
def test_instance_methods_neg
|
162
|
-
["fred", "barney"].each do |prefix|
|
163
|
-
["#{prefix}", "#{prefix}="].each do |m|
|
164
|
-
bogus_include = TestClass.instance_methods.include? m
|
165
|
-
assert(bogus_include == false, "#{m} method declared in TestClass; shouldn't be")
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
|
170
|
-
def test_instance_methods_dont_include_class_methods
|
171
|
-
["foo", "bar"].each do |prefix|
|
172
|
-
["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
|
173
|
-
bogus_include = TestClass.instance_methods.include? m
|
174
|
-
assert(bogus_include == false, "#{m} method declared in TestClass; shouldn't be")
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def test_class_methods
|
180
|
-
["foo", "bar"].each do |prefix|
|
181
|
-
["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
|
182
|
-
klass = class << TestClass; self end
|
183
|
-
assert klass.instance_methods.include?(m), "#{m} method not declared in TestClass' eigenclass"
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
def test_class_methods2
|
189
|
-
["fred", "barney"].each do |prefix|
|
190
|
-
["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
|
191
|
-
klass = class << TestClass2; self end
|
192
|
-
assert klass.instance_methods.include?(m), "#{m} method not declared in TestClass2's eigenclass"
|
193
|
-
end
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
def test_table_class_methods_neg
|
198
|
-
["foo", "bar", "fred", "barney"].each do |prefix|
|
199
|
-
["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
|
200
|
-
klass = Class.new
|
201
|
-
|
202
|
-
klass.class_eval do
|
203
|
-
include Rhubarb::Persisting
|
204
|
-
end
|
205
|
-
|
206
|
-
bogus_include = klass.instance_methods.include?(m)
|
207
|
-
assert(bogus_include == false, "#{m} method declared in eigenclass of class including Rhubarb::Persisting; shouldn't be")
|
208
|
-
end
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def test_class_methods_neg
|
213
|
-
["fred", "barney"].each do |prefix|
|
214
|
-
["find_by_#{prefix}", "find_first_by_#{prefix}"].each do |m|
|
215
|
-
klass = class << TestClass; self end
|
216
|
-
bogus_include = klass.instance_methods.include?(m)
|
217
|
-
assert(bogus_include == false, "#{m} method declared in TestClass' eigenclass; shouldn't be")
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
def test_column_size
|
223
|
-
assert(TestClass.columns.size == 5, "TestClass has wrong number of columns")
|
224
|
-
end
|
225
|
-
|
226
|
-
def test_tc2_column_size
|
227
|
-
assert(TestClass2.columns.size == 5, "TestClass2 has wrong number of columns")
|
228
|
-
end
|
229
|
-
|
230
|
-
def test_table_column_size
|
231
|
-
klass = Class.new
|
232
|
-
klass.class_eval do
|
233
|
-
include Rhubarb::Persisting
|
234
|
-
end
|
235
|
-
if klass.respond_to? :columns
|
236
|
-
assert(Frotz.columns.size == 0, "A persisting class with no declared columns has the wrong number of columns")
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def test_constraints_size
|
241
|
-
k = Class.new
|
242
|
-
|
243
|
-
k.class_eval do
|
244
|
-
include Rhubarb::Persisting
|
245
|
-
end
|
246
|
-
|
247
|
-
{k => 0, TestClass => 0, TestClass2 => 0, TC3 => 1}.each do |klass, cts|
|
248
|
-
if klass.respond_to? :constraints
|
249
|
-
assert(klass.constraints.size == cts, "#{klass} has wrong number of constraints")
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def test_cols_and_constraints_understood
|
255
|
-
[TestClass, TestClass2, TC3, TC4].each do |klass|
|
256
|
-
assert(klass.respond_to?(:constraints), "#{klass} should have accessor for constraints")
|
257
|
-
assert(klass.respond_to?(:columns), "#{klass} should have accessor for columns")
|
258
|
-
end
|
259
|
-
end
|
260
|
-
|
261
|
-
def test_column_contents
|
262
|
-
[:row_id, :foo, :bar].each do |col|
|
263
|
-
assert(TestClass.columns.map{|c| c.name}.include?(col), "TestClass doesn't contain column #{col}")
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def test_create_proper_type
|
268
|
-
tc = TestClass.create(:foo => 1, :bar => "argh")
|
269
|
-
assert(tc.class == TestClass, "TestClass.create should return an instance of TestClass")
|
270
|
-
end
|
271
|
-
|
272
|
-
def test_create_multiples
|
273
|
-
tc_list = [nil]
|
274
|
-
1.upto(9) do |num|
|
275
|
-
tc_list.push TestClass.create(:foo => num, :bar => "argh#{num}")
|
276
|
-
end
|
277
|
-
|
278
|
-
1.upto(9) do |num|
|
279
|
-
assert(tc_list[num].foo == num, "multiple TestClass.create invocations should return records with proper foo values")
|
280
|
-
assert(tc_list[num].bar == "argh#{num}", "multiple TestClass.create invocations should return records with proper bar values")
|
281
|
-
|
282
|
-
tmp = TestClass.find(num)
|
283
|
-
|
284
|
-
assert(tmp.foo == num, "multiple TestClass.create invocations should add records with proper foo values to the db")
|
285
|
-
assert(tmp.bar == "argh#{num}", "multiple TestClass.create invocations should add records with proper bar values to the db")
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def test_delete
|
290
|
-
range = []
|
291
|
-
1.upto(9) do |num|
|
292
|
-
range << num
|
293
|
-
TestClass.create(:foo => num, :bar => "argh#{num}")
|
294
|
-
end
|
295
|
-
|
296
|
-
assert(TestClass.count == range.size, "correct number of rows inserted prior to delete")
|
297
|
-
|
298
|
-
TestClass.find(2).delete
|
299
|
-
|
300
|
-
assert(TestClass.count == range.size - 1, "correct number of rows inserted after delete")
|
301
|
-
end
|
302
|
-
|
303
|
-
def test_delete_2
|
304
|
-
TestClass.create(:foo => 42, :bar => "Wait, what was the question?")
|
305
|
-
tc1 = TestClass.find_first_by_foo(42)
|
306
|
-
tc2 = TestClass.find_first_by_foo(42)
|
307
|
-
|
308
|
-
[tc1,tc2].each do |obj|
|
309
|
-
assert obj
|
310
|
-
assert_kind_of TestClass, obj
|
311
|
-
assert_equal false, obj.instance_eval {needs_refresh?}
|
312
|
-
end
|
313
|
-
|
314
|
-
[:foo, :bar, :row_id].each do |msg|
|
315
|
-
assert_equal tc1.send(msg), tc2.send(msg)
|
316
|
-
end
|
317
|
-
|
318
|
-
tc1.delete
|
319
|
-
|
320
|
-
tc3 = TestClass.find_by_foo(42)
|
321
|
-
assert_equal [], tc3
|
322
|
-
|
323
|
-
tc3 = TestClass.find_first_by_foo(42)
|
324
|
-
assert_equal nil, tc3
|
325
|
-
|
326
|
-
[tc1, tc2].each do |obj|
|
327
|
-
assert obj.deleted?
|
328
|
-
[:foo, :bar, :row_id].each do |msg|
|
329
|
-
assert_equal nil, obj.send(msg)
|
330
|
-
end
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def test_count_base
|
335
|
-
assert(TestClass.count == 0, "a new table should have no rows")
|
336
|
-
end
|
337
|
-
|
338
|
-
def test_count_inc
|
339
|
-
1.upto(9) do |num|
|
340
|
-
TestClass.create(:foo => num, :bar => "argh#{num}")
|
341
|
-
assert(TestClass.count == num, "table row count should increment after each row create")
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
def test_create_proper_values
|
346
|
-
vals = {:foo => 1, :bar => "argh"}
|
347
|
-
tc = TestClass.create(vals)
|
348
|
-
assert(tc.foo == 1, "tc.foo (newly-created) should have the value 1")
|
349
|
-
assert(tc.bar == "argh", "tc.bar (newly-created) should have the value \"argh\"")
|
350
|
-
end
|
351
|
-
|
352
|
-
def test_create_and_find_by_id
|
353
|
-
vals = {:foo => 2, :bar => "argh"}
|
354
|
-
TestClass.create(vals)
|
355
|
-
|
356
|
-
tc = TestClass.find(1)
|
357
|
-
assert(tc.foo == 2, "tc.foo (found by id) should have the value 2")
|
358
|
-
assert(tc.bar == "argh", "tc.bar (found by id) should have the value \"argh\"")
|
359
|
-
end
|
360
|
-
|
361
|
-
def test_find_by_id_bogus
|
362
|
-
tc = TestClass.find(1)
|
363
|
-
assert(tc == nil, "TestClass table should be empty")
|
364
|
-
end
|
365
|
-
|
366
|
-
def test_create_and_find_by_foo
|
367
|
-
vals = {:foo => 2, :bar => "argh"}
|
368
|
-
TestClass.create(vals)
|
369
|
-
|
370
|
-
result = TestClass.find_by_foo(2)
|
371
|
-
tc = result[0]
|
372
|
-
assert(result.size == 1, "TestClass.find_by_foo(2) should return exactly one result")
|
373
|
-
assert(tc.foo == 2, "tc.foo (found by foo) should have the value 2")
|
374
|
-
assert(tc.bar == "argh", "tc.bar (found by foo) should have the value \"argh\"")
|
375
|
-
end
|
376
|
-
|
377
|
-
def test_create_and_find_first_by_foo
|
378
|
-
vals = {:foo => 2, :bar => "argh"}
|
379
|
-
TestClass.create(vals)
|
380
|
-
|
381
|
-
tc = (TestClass.find_first_by_foo(2))
|
382
|
-
assert(tc.foo == 2, "tc.foo (found by foo) should have the value 2")
|
383
|
-
assert(tc.bar == "argh", "tc.bar (found by foo) should have the value \"argh\"")
|
384
|
-
end
|
385
|
-
|
386
|
-
def test_create_and_find_by_bar
|
387
|
-
vals = {:foo => 2, :bar => "argh"}
|
388
|
-
TestClass.create(vals)
|
389
|
-
result = TestClass.find_by_bar("argh")
|
390
|
-
tc = result[0]
|
391
|
-
assert(result.size == 1, "TestClass.find_by_bar(\"argh\") should return exactly one result")
|
392
|
-
assert(tc.foo == 2, "tc.foo (found by bar) should have the value 2")
|
393
|
-
assert(tc.bar == "argh", "tc.bar (found by bar) should have the value \"argh\"")
|
394
|
-
end
|
395
|
-
|
396
|
-
def test_create_and_find_first_by_bar
|
397
|
-
vals = {:foo => 2, :bar => "argh"}
|
398
|
-
TestClass.create(vals)
|
399
|
-
|
400
|
-
tc = (TestClass.find_first_by_bar("argh"))
|
401
|
-
assert(tc.foo == 2, "tc.foo (found by bar) should have the value 2")
|
402
|
-
assert(tc.bar == "argh", "tc.bar (found by bar) should have the value \"argh\"")
|
403
|
-
end
|
404
|
-
|
405
|
-
def test_create_and_update_modifies_object
|
406
|
-
vals = {:foo => 1, :bar => "argh"}
|
407
|
-
TestClass.create(vals)
|
408
|
-
|
409
|
-
tc = TestClass.find(1)
|
410
|
-
tc.foo = 2
|
411
|
-
assert("#{tc.foo}" == "2", "tc.foo should have the value 2 after modifying object")
|
412
|
-
end
|
413
|
-
|
414
|
-
def test_create_and_update_modifies_db
|
415
|
-
vals = {:foo => 1, :bar => "argh"}
|
416
|
-
TestClass.create(vals)
|
417
|
-
|
418
|
-
tc = TestClass.find(1)
|
419
|
-
tc.foo = 2
|
420
|
-
|
421
|
-
tc_fresh = TestClass.find(1)
|
422
|
-
assert(tc_fresh.foo == 2, "foo value in first row of db should have the value 2 after modifying tc object")
|
423
|
-
end
|
424
|
-
|
425
|
-
def test_create_and_update_freshen
|
426
|
-
vals = {:foo => 1, :bar => "argh"}
|
427
|
-
TestClass.create(vals)
|
428
|
-
|
429
|
-
tc_fresh = TestClass.find(1)
|
430
|
-
tc = TestClass.find(1)
|
431
|
-
|
432
|
-
tc.foo = 2
|
433
|
-
|
434
|
-
assert(tc_fresh.foo == 2, "object backed by db row isn't freshened")
|
435
|
-
end
|
436
|
-
|
437
|
-
def test_reference_tables
|
438
|
-
assert(TC4.refs.size == 2, "TC4 should have 2 refs, instead has #{TC4.refs.size}")
|
439
|
-
end
|
440
|
-
|
441
|
-
def test_reference_classes
|
442
|
-
t_vals = []
|
443
|
-
t2_vals = []
|
444
|
-
|
445
|
-
1.upto(9) do |n|
|
446
|
-
t_vals.push({:foo => n, :bar => "item-#{n}"})
|
447
|
-
TestClass.create t_vals[-1]
|
448
|
-
end
|
449
|
-
|
450
|
-
9.downto(1) do |n|
|
451
|
-
t2_vals.push({:fred => n, :barney => "barney #{n}"})
|
452
|
-
TestClass2.create t2_vals[-1]
|
453
|
-
end
|
454
|
-
|
455
|
-
1.upto(9) do |n|
|
456
|
-
m = 10-n
|
457
|
-
k = TC4.create(:t1 => n, :t2 => m)
|
458
|
-
assert(k.t1.class == TestClass, "k.t1.class is #{k.t1.class}; should be TestClass")
|
459
|
-
assert(k.t2.class == TestClass2, "k.t2.class is #{k.t2.class}; should be TestClass2")
|
460
|
-
assert(k.enabled)
|
461
|
-
k.enabled = false
|
462
|
-
assert(k.enabled==false)
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
def test_references_simple
|
467
|
-
t_vals = []
|
468
|
-
t2_vals = []
|
469
|
-
|
470
|
-
1.upto(9) do |n|
|
471
|
-
t_vals.push({:foo => n, :bar => "item-#{n}"})
|
472
|
-
TestClass.create t_vals[-1]
|
473
|
-
end
|
474
|
-
|
475
|
-
9.downto(1) do |n|
|
476
|
-
t2_vals.push({:fred => n, :barney => "barney #{n}"})
|
477
|
-
TestClass2.create t2_vals[-1]
|
478
|
-
end
|
479
|
-
|
480
|
-
1.upto(9) do |n|
|
481
|
-
k = TC4.create(:t1 => n, :t2 => (10 - n))
|
482
|
-
assert(k.t1.foo == k.t2.fred, "references don't work")
|
483
|
-
end
|
484
|
-
end
|
485
|
-
|
486
|
-
def test_references_sameclass
|
487
|
-
SelfRef.create :one => nil
|
488
|
-
1.upto(3) do |num|
|
489
|
-
SelfRef.create :one => num
|
490
|
-
end
|
491
|
-
4.downto(2) do |num|
|
492
|
-
sr = SelfRef.find num
|
493
|
-
assert(sr.one.class == SelfRef, "SelfRef with row ID #{num} should have a one field of type SelfRef; is #{sr.one.class} instead")
|
494
|
-
assert(sr.one.row_id == sr.row_id - 1, "SelfRef with row ID #{num} should have a one field with a row id of #{sr.row_id - 1}; is #{sr.one.row_id} instead")
|
495
|
-
end
|
496
|
-
end
|
497
|
-
|
498
|
-
def test_references_circular_id
|
499
|
-
sr = SelfRef.create :one => nil
|
500
|
-
sr.one = sr.row_id
|
501
|
-
assert(sr == sr.one, "self-referential rows should work; instead #{sr} isn't the same as #{sr.one}")
|
502
|
-
end
|
503
|
-
|
504
|
-
def test_references_circular_obj
|
505
|
-
sr = SelfRef.create :one => nil
|
506
|
-
sr.one = sr
|
507
|
-
assert(sr == sr.one, "self-referential rows should work; instead #{sr} isn't the same as #{sr.one}")
|
508
|
-
end
|
509
|
-
|
510
|
-
def test_referential_integrity
|
511
|
-
assert_raise SQLite3::SQLException do
|
512
|
-
FromRef.create(:t => 42)
|
513
|
-
end
|
514
|
-
|
515
|
-
assert_nothing_thrown do
|
516
|
-
1.upto(20) do |x|
|
517
|
-
ToRef.create(:foo => "#{x}")
|
518
|
-
FromRef.create(:t => x)
|
519
|
-
assert_equal ToRef.count, FromRef.count
|
520
|
-
end
|
521
|
-
end
|
522
|
-
|
523
|
-
20.downto(1) do |x|
|
524
|
-
ct = ToRef.count
|
525
|
-
tr = ToRef.find(x)
|
526
|
-
tr.delete
|
527
|
-
assert_equal ToRef.count, ct - 1
|
528
|
-
assert_equal ToRef.count, FromRef.count
|
529
|
-
end
|
530
|
-
end
|
531
|
-
|
532
|
-
def test_custom_query
|
533
|
-
colresult = 0
|
534
|
-
varresult = 0
|
535
|
-
|
536
|
-
1.upto(20) do |i|
|
537
|
-
1.upto(20) do |j|
|
538
|
-
CustomQueryTable.create(:one => i, :two => j)
|
539
|
-
colresult = colresult.succ if i < j
|
540
|
-
varresult = varresult.succ if i < 5 && j < 7
|
541
|
-
end
|
542
|
-
end
|
543
|
-
|
544
|
-
f = CustomQueryTable.ltcols
|
545
|
-
assert(f.size() == colresult, "f.size() should equal colresult, but #{f.size()} != #{colresult}")
|
546
|
-
f.each {|r| assert(r.one < r.two, "#{r.one}, #{r.two} should only be in ltcols custom query if #{r.one} < #{r.two}") }
|
547
|
-
|
548
|
-
f = CustomQueryTable.ltvars 5, 7
|
549
|
-
f2 = CustomQueryTable.cltvars 5, 7
|
550
|
-
|
551
|
-
[f,f2].each do |obj|
|
552
|
-
assert(obj.size() == varresult, "query result size should equal varresult, but #{obj.size()} != #{varresult}")
|
553
|
-
obj.each {|r| assert(r.one < 5 && r.two < 7, "#{r.one}, #{r.two} should only be in ltvars/cltvars custom query if #{r.one} < 5 && #{r.two} < 7") }
|
554
|
-
end
|
555
|
-
|
556
|
-
end
|
557
|
-
|
558
|
-
def freshness_query_fixture
|
559
|
-
@flist = []
|
560
|
-
|
561
|
-
0.upto(99) do |x|
|
562
|
-
@flist << FreshTestTable.create(:fee=>x, :fie=>(x%7), :foe=>(x%11), :fum=>(x%13))
|
563
|
-
end
|
564
|
-
end
|
565
|
-
|
566
|
-
def test_freshness_query_basic
|
567
|
-
freshness_query_fixture
|
568
|
-
# basic test
|
569
|
-
basic = FreshTestTable.find_freshest(:group_by=>[:fee])
|
570
|
-
|
571
|
-
assert_equal(@flist.size, basic.size)
|
572
|
-
0.upto(99) do |x|
|
573
|
-
[:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
|
574
|
-
assert_equal(@flist[x].send(msg), basic[x].send(msg))
|
575
|
-
end
|
576
|
-
end
|
577
|
-
end
|
578
|
-
|
579
|
-
def test_freshness_query_basic_restricted
|
580
|
-
freshness_query_fixture
|
581
|
-
# basic test
|
582
|
-
|
583
|
-
basic = FreshTestTable.find_freshest(:group_by=>[:fee], :version=>@flist[30].created, :debug=>true)
|
584
|
-
|
585
|
-
assert_equal(31, basic.size)
|
586
|
-
0.upto(30) do |x|
|
587
|
-
[:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
|
588
|
-
assert_equal(@flist[x].send(msg), basic[x].send(msg))
|
589
|
-
end
|
590
|
-
end
|
591
|
-
end
|
592
|
-
|
593
|
-
def test_freshness_query_basic_select
|
594
|
-
freshness_query_fixture
|
595
|
-
# basic test
|
596
|
-
|
597
|
-
basic = FreshTestTable.find_freshest(:group_by=>[:fee], :select_by=>{:fie=>0}, :debug=>true)
|
598
|
-
|
599
|
-
expected_ct = 99/7 + 1;
|
600
|
-
|
601
|
-
assert_equal(expected_ct, basic.size)
|
602
|
-
|
603
|
-
0.upto(expected_ct - 1) do |x|
|
604
|
-
[:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
|
605
|
-
assert_equal(@flist[x*7].send(msg), basic[x].send(msg))
|
606
|
-
end
|
607
|
-
end
|
608
|
-
end
|
609
|
-
|
610
|
-
def test_freshness_query_group_single
|
611
|
-
freshness_query_fixture
|
612
|
-
# more basic tests
|
613
|
-
pairs = {:fie=>7,:foe=>11,:fum=>13}
|
614
|
-
pairs.each do |col,ct|
|
615
|
-
basic = FreshTestTable.find_freshest(:group_by=>[col])
|
616
|
-
assert_equal(ct,basic.size)
|
617
|
-
|
618
|
-
expected_objs = {}
|
619
|
-
|
620
|
-
99.downto(99-ct+1) do |x|
|
621
|
-
expected_objs[x%ct] = @flist[x]
|
622
|
-
end
|
623
|
-
|
624
|
-
basic.each do |row|
|
625
|
-
res = expected_objs[row.send(col)]
|
626
|
-
[:fee,:fie,:foe,:fum,:created,:updated,:row_id].each do |msg|
|
627
|
-
assert_equal(res.send(msg), row.send(msg))
|
628
|
-
end
|
629
|
-
end
|
630
|
-
end
|
631
|
-
end
|
632
|
-
end
|