rhubarb 0.2.7 → 0.3.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/VERSION +1 -1
- data/lib/rhubarb/classmixins.rb +34 -18
- data/lib/rhubarb/column.rb +2 -2
- data/lib/rhubarb/mixins/freshness.rb +2 -2
- data/lib/rhubarb/persisting.rb +6 -2
- data/ruby-rhubarb.spec.in +5 -1
- data/test/test_rhubarb.rb +93 -0
- metadata +37 -13
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/lib/rhubarb/classmixins.rb
CHANGED
@@ -19,8 +19,14 @@ module Rhubarb
|
|
19
19
|
PCM_OUTPUT_TRANSFORMERS = {:object=>Util.deswizzle_object_proc, :zblob=>Util.dezblobify_proc, }
|
20
20
|
# Returns the name of the database table modeled by this class.
|
21
21
|
# Defaults to the name of the class (sans module names)
|
22
|
-
def table_name
|
22
|
+
def table_name(quoted=false)
|
23
23
|
@table_name ||= self.name.split("::").pop.downcase
|
24
|
+
@quoted_table_name ||= "'#{@table_name}'"
|
25
|
+
quoted ? @quoted_table_name : @table_name
|
26
|
+
end
|
27
|
+
|
28
|
+
def quoted_table_name
|
29
|
+
table_name(true)
|
24
30
|
end
|
25
31
|
|
26
32
|
# Enables setting the table name to a custom name
|
@@ -56,19 +62,19 @@ module Rhubarb
|
|
56
62
|
valid_cols = self.colnames.intersection arg_hash.keys
|
57
63
|
select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
|
58
64
|
arg_hash.each {|key,val| arg_hash[key] = val.row_id if val.respond_to? :row_id}
|
59
|
-
db.do_query("select * from #{
|
65
|
+
db.do_query("select * from #{quoted_table_name} where #{select_criteria} order by row_id", arg_hash) {|tup| results << self.new(tup) }
|
60
66
|
results
|
61
67
|
end
|
62
68
|
|
63
69
|
# Does what it says on the tin. Since this will allocate an object for each row, it isn't recomended for huge tables.
|
64
70
|
def find_all
|
65
71
|
results = []
|
66
|
-
db.do_query("SELECT * from #{
|
72
|
+
db.do_query("SELECT * from #{quoted_table_name}") {|tup| results << self.new(tup)}
|
67
73
|
results
|
68
74
|
end
|
69
75
|
|
70
76
|
def delete_all
|
71
|
-
db.do_query("DELETE from #{
|
77
|
+
db.do_query("DELETE from #{quoted_table_name}")
|
72
78
|
end
|
73
79
|
|
74
80
|
def delete_where(arg_hash)
|
@@ -76,7 +82,7 @@ module Rhubarb
|
|
76
82
|
valid_cols = self.colnames.intersection arg_hash.keys
|
77
83
|
select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
|
78
84
|
arg_hash.each {|key,val| arg_hash[key] = val.row_id if val.respond_to? :row_id}
|
79
|
-
db.do_query("DELETE FROM #{
|
85
|
+
db.do_query("DELETE FROM #{quoted_table_name} WHERE #{select_criteria}", arg_hash)
|
80
86
|
end
|
81
87
|
|
82
88
|
# 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.
|
@@ -87,7 +93,7 @@ module Rhubarb
|
|
87
93
|
# 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.
|
88
94
|
def declare_custom_query(name, query)
|
89
95
|
klass = (class << self; self end)
|
90
|
-
processed_query = query.gsub("__TABLE__", "#{self.
|
96
|
+
processed_query = query.gsub("__TABLE__", "#{self.quoted_table_name}")
|
91
97
|
|
92
98
|
klass.class_eval do
|
93
99
|
define_method name.to_s do |*args|
|
@@ -113,7 +119,7 @@ module Rhubarb
|
|
113
119
|
def declare_index_on(*fields)
|
114
120
|
@creation_callbacks << Proc.new do
|
115
121
|
idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
|
116
|
-
creation_cmd = "create index #{idx_name} on #{self.
|
122
|
+
creation_cmd = "create index #{idx_name} on #{self.quoted_table_name} (#{fields.join(', ')})"
|
117
123
|
self.db.execute(creation_cmd)
|
118
124
|
end if fields.size > 0
|
119
125
|
end
|
@@ -127,7 +133,7 @@ module Rhubarb
|
|
127
133
|
|
128
134
|
find_method_name = "find_by_#{cname}".to_sym
|
129
135
|
find_first_method_name = "find_first_by_#{cname}".to_sym
|
130
|
-
find_query = "select * from #{
|
136
|
+
find_query = "select * from #{quoted_table_name} where \"#{cname}\" = ? order by row_id"
|
131
137
|
|
132
138
|
get_method_name = "#{cname}".to_sym
|
133
139
|
set_method_name = "#{cname}=".to_sym
|
@@ -173,8 +179,9 @@ module Rhubarb
|
|
173
179
|
xform = PCM_INPUT_TRANSFORMERS[kind]
|
174
180
|
|
175
181
|
define_method set_method_name do |arg|
|
176
|
-
|
177
|
-
|
182
|
+
new_val = xform ? xform.call(arg) : arg
|
183
|
+
@tuple["#{cname}"] = new_val
|
184
|
+
update cname, new_val
|
178
185
|
end
|
179
186
|
else
|
180
187
|
# this column references another table; create a set
|
@@ -202,15 +209,16 @@ module Rhubarb
|
|
202
209
|
# not expose the capacity to change row IDs.
|
203
210
|
|
204
211
|
rtable = rf.referent.table_name
|
212
|
+
qrtable = rf.referent.quoted_table_name
|
205
213
|
|
206
214
|
self.creation_callbacks << Proc.new do
|
207
215
|
@ccount ||= 0
|
208
216
|
|
209
217
|
insert_trigger_name, delete_trigger_name = %w{insert delete}.map {|op| "ri_#{op}_#{self.table_name}_#{@ccount}_#{rtable}" }
|
210
218
|
|
211
|
-
self.db.execute_batch("CREATE TRIGGER #{insert_trigger_name} BEFORE INSERT ON
|
219
|
+
self.db.execute_batch("CREATE TRIGGER #{insert_trigger_name} BEFORE INSERT ON #{quoted_table_name} WHEN new.\"#{cname}\" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM #{qrtable} WHERE new.\"#{cname}\" == \"#{rf.column}\") BEGIN SELECT RAISE(ABORT, 'constraint #{insert_trigger_name} (#{rtable} missing foreign key row) failed'); END;")
|
212
220
|
|
213
|
-
self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} BEFORE DELETE ON
|
221
|
+
self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} BEFORE DELETE ON #{qrtable} WHEN EXISTS (SELECT 1 FROM #{quoted_table_name} WHERE old.\"#{rf.column}\" == \"#{cname}\") BEGIN DELETE FROM #{quoted_table_name} WHERE \"#{cname}\" = old.\"#{rf.column}\"; END;") if rf.options[:on_delete] == :cascade
|
214
222
|
|
215
223
|
@ccount = @ccount + 1
|
216
224
|
end
|
@@ -230,8 +238,8 @@ module Rhubarb
|
|
230
238
|
new_row = args[0]
|
231
239
|
new_row[:created] = new_row[:updated] = Util::timestamp
|
232
240
|
|
233
|
-
|
234
|
-
|
241
|
+
colspec, valspec = cvspecs[new_row.keys]
|
242
|
+
|
235
243
|
res = nil
|
236
244
|
|
237
245
|
# resolve any references in the args
|
@@ -240,7 +248,7 @@ module Rhubarb
|
|
240
248
|
new_row[column] = xform ? xform.call(value) : Util::rhubarb_fk_identity(value)
|
241
249
|
end
|
242
250
|
|
243
|
-
create_text = "insert into #{
|
251
|
+
create_text = "insert into #{quoted_table_name} (#{colspec}) values (#{valspec})"
|
244
252
|
db.do_query(create_text, new_row)
|
245
253
|
|
246
254
|
res = find(db.last_insert_row_id)
|
@@ -252,11 +260,12 @@ module Rhubarb
|
|
252
260
|
# corresponding to this class.
|
253
261
|
def table_decl
|
254
262
|
ddlspecs = [columns.join(", "), constraints.join(", ")].reject {|str| str.size==0}.join(", ")
|
255
|
-
"create table #{
|
263
|
+
"create table #{quoted_table_name} (#{ddlspecs});"
|
256
264
|
end
|
257
265
|
|
258
266
|
# Creates a table in the database corresponding to this class.
|
259
267
|
def create_table(dbkey=:default)
|
268
|
+
ensure_accessors
|
260
269
|
self.db ||= Persistence::dbs[dbkey] unless @explicitdb
|
261
270
|
self.db.execute(table_decl)
|
262
271
|
@creation_callbacks.each {|func| func.call}
|
@@ -298,16 +307,23 @@ module Rhubarb
|
|
298
307
|
|
299
308
|
# Returns the number of rows in the table backing this class
|
300
309
|
def count
|
301
|
-
self.db.get_first_value("select count(row_id) from #{
|
310
|
+
self.db.get_first_value("select count(row_id) from #{quoted_table_name}").to_i
|
302
311
|
end
|
303
312
|
|
304
313
|
def find_tuple(tid)
|
305
|
-
ft_text = "select * from #{
|
314
|
+
ft_text = "select * from #{quoted_table_name} where row_id = ?"
|
306
315
|
result = nil
|
307
316
|
db.do_query(ft_text, tid) {|tup| result = tup; break}
|
308
317
|
result
|
309
318
|
end
|
310
319
|
|
320
|
+
def cvspecs
|
321
|
+
@cvspecs ||= Hash.new do |h,new_row_keys|
|
322
|
+
cols = colnames.intersection new_row_keys
|
323
|
+
h[new_row_keys] = [Proc.new {|f| f.to_s.inspect}, Proc.new {|f| f.inspect}].map {|p| (cols.map {|col| p.call(col)}).join(", ")}
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
311
327
|
include FindFreshest
|
312
328
|
|
313
329
|
end
|
data/lib/rhubarb/column.rb
CHANGED
@@ -51,9 +51,9 @@ module Rhubarb
|
|
51
51
|
query = "
|
52
52
|
SELECT __freshest.* FROM (
|
53
53
|
SELECT #{projection.to_a.join(', ')} FROM (
|
54
|
-
SELECT * from #{
|
54
|
+
SELECT * from #{quoted_table_name} #{where_clause}
|
55
55
|
) #{group_by_clause}
|
56
|
-
) as __fresh INNER JOIN #{
|
56
|
+
) as __fresh INNER JOIN #{quoted_table_name} as __freshest ON #{join_clause} ORDER BY row_id"
|
57
57
|
self.db.execute(query, query_params).map {|tup| self.new(tup) }
|
58
58
|
end
|
59
59
|
end
|
data/lib/rhubarb/persisting.rb
CHANGED
@@ -23,6 +23,10 @@ module Rhubarb
|
|
23
23
|
attr_reader :created
|
24
24
|
attr_reader :updated
|
25
25
|
end
|
26
|
+
|
27
|
+
other.instance_eval do
|
28
|
+
private_class_method :ensure_accessors
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
32
|
def db
|
@@ -62,7 +66,7 @@ module Rhubarb
|
|
62
66
|
# Deletes the row corresponding to this object from the database;
|
63
67
|
# invalidates =self= and any other objects backed by this row
|
64
68
|
def delete
|
65
|
-
db.do_query("delete from #{self.class.
|
69
|
+
db.do_query("delete from #{self.class.quoted_table_name} where row_id = ?", @row_id)
|
66
70
|
mark_dirty
|
67
71
|
@tuple = nil
|
68
72
|
@row_id = nil
|
@@ -119,7 +123,7 @@ module Rhubarb
|
|
119
123
|
def update(attr_name, value)
|
120
124
|
mark_dirty
|
121
125
|
|
122
|
-
db.do_query("update #{self.class.
|
126
|
+
db.do_query("update #{self.class.quoted_table_name} set '#{attr_name}' = ?, updated = ? where row_id = ?", value, Util::timestamp, @row_id)
|
123
127
|
end
|
124
128
|
|
125
129
|
# Resolve any fields that reference other tables, replacing row ids with referred objects
|
data/ruby-rhubarb.spec.in
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
%{!?ruby_sitelib: %global ruby_sitelib %(ruby -rrbconfig -e 'puts Config::CONFIG["sitelibdir"] ')}
|
2
|
-
%define rel 1
|
2
|
+
%define rel 1
|
3
3
|
|
4
4
|
Summary: Simple versioned object-graph persistence layer
|
5
5
|
Name: ruby-rhubarb
|
@@ -48,6 +48,10 @@ rm -rf %{buildroot}
|
|
48
48
|
%{ruby_sitelib}/rhubarb/persistence.rb
|
49
49
|
|
50
50
|
%changelog
|
51
|
+
* Wed Sep 15 2010 willb <willb@redhat> - 0.2.7-1
|
52
|
+
- Updated to version 0.2.7
|
53
|
+
- fixed problems with hash-valued params in custom queries
|
54
|
+
|
51
55
|
* Wed Apr 28 2010 willb <willb@redhat> - 0.2.6-1.0
|
52
56
|
- Updated to version 0.2.6
|
53
57
|
|
data/test/test_rhubarb.rb
CHANGED
@@ -119,6 +119,25 @@ class ObjectTestTable
|
|
119
119
|
declare_column :obj, :object
|
120
120
|
end
|
121
121
|
|
122
|
+
class NoColumnsTestTable
|
123
|
+
include Rhubarb::Persisting
|
124
|
+
end
|
125
|
+
|
126
|
+
class Create
|
127
|
+
include Rhubarb::Persisting
|
128
|
+
declare_column :name, :string
|
129
|
+
end
|
130
|
+
|
131
|
+
class Group
|
132
|
+
include Rhubarb::Persisting
|
133
|
+
declare_column :other, :int, references(Create, :on_delete=>:cascade)
|
134
|
+
end
|
135
|
+
|
136
|
+
class Order
|
137
|
+
include Rhubarb::Persisting
|
138
|
+
declare_column :group, :int
|
139
|
+
end
|
140
|
+
|
122
141
|
class PreparedStmtBackendTests < Test::Unit::TestCase
|
123
142
|
def dbfile
|
124
143
|
ENV['RHUBARB_TEST_DB'] || ":memory:"
|
@@ -159,6 +178,44 @@ class PreparedStmtBackendTests < Test::Unit::TestCase
|
|
159
178
|
Rhubarb::Persistence::close()
|
160
179
|
end
|
161
180
|
|
181
|
+
def test_reserved_word_table
|
182
|
+
assert_nothing_raised do
|
183
|
+
Create.create_table
|
184
|
+
Create.create(:name=>"bar")
|
185
|
+
c = Create.find_first_by_name("bar")
|
186
|
+
c.name = "blah"
|
187
|
+
c.name = "bar"
|
188
|
+
assert(c.name == "bar")
|
189
|
+
c.delete
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_reserved_word_column
|
194
|
+
assert_nothing_raised do
|
195
|
+
Order.create_table
|
196
|
+
Order.create(:group=>42)
|
197
|
+
o = Order.find_first_by_group(42)
|
198
|
+
o.group = 37
|
199
|
+
assert(o.group == 37)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_reserved_word_fk_table
|
204
|
+
assert_nothing_raised do
|
205
|
+
Create.create_table
|
206
|
+
Group.create_table
|
207
|
+
Create.create(:name=>"bar")
|
208
|
+
Group.create(:other=>Create.create(:name=>"blah"))
|
209
|
+
assert(Group.find_all[0].other.name=="blah")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_no_columns_table
|
214
|
+
assert_nothing_raised do
|
215
|
+
NoColumnsTestTable.create_table
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
162
219
|
def test_persistence_setup
|
163
220
|
assert Rhubarb::Persistence::db.type_translation, "type translation not enabled for db"
|
164
221
|
assert Rhubarb::Persistence::db.results_as_hash, "rows-as-hashes not enabled for db"
|
@@ -833,6 +890,42 @@ class PreparedStmtBackendTests < Test::Unit::TestCase
|
|
833
890
|
assert_equal(text, Zlib::Inflate.inflate(cbtrow.info))
|
834
891
|
end
|
835
892
|
|
893
|
+
def test_mutable_object_data
|
894
|
+
things = []
|
895
|
+
|
896
|
+
words = %w{It is a truth universally acknowledged that a single man in possession of a good fortune must be in want of a wife}
|
897
|
+
|
898
|
+
100.times do
|
899
|
+
things << words.sort_by {rand}
|
900
|
+
end
|
901
|
+
|
902
|
+
things.each_with_index do |thing, idx|
|
903
|
+
ObjectTestTable.create(:otype=>thing.class.name.to_s, :obj=>thing)
|
904
|
+
end
|
905
|
+
|
906
|
+
otts = ObjectTestTable.find_all
|
907
|
+
|
908
|
+
assert_equal otts.size, things.size
|
909
|
+
|
910
|
+
things.zip(otts) do |thing, ott|
|
911
|
+
assert_equal thing.class.name.to_s, ott.otype
|
912
|
+
assert_equal thing, ott.obj
|
913
|
+
end
|
914
|
+
|
915
|
+
otts.each_with_index do |ott, i|
|
916
|
+
obj = ott.obj.reverse
|
917
|
+
ott.obj = obj.dup
|
918
|
+
assert_equal obj, ott.obj
|
919
|
+
assert_equal obj, ObjectTestTable.find(ott.row_id).obj
|
920
|
+
things[i].reverse!
|
921
|
+
end
|
922
|
+
|
923
|
+
things.zip(otts) do |thing, ott|
|
924
|
+
assert_equal thing.class.name.to_s, ott.otype
|
925
|
+
assert_equal thing, ott.obj
|
926
|
+
end
|
927
|
+
end
|
928
|
+
|
836
929
|
|
837
930
|
def test_object_data
|
838
931
|
things = []
|
metadata
CHANGED
@@ -1,7 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rhubarb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
5
11
|
platform: ruby
|
6
12
|
authors:
|
7
13
|
- William Benton
|
@@ -9,29 +15,41 @@ autorequire:
|
|
9
15
|
bindir: bin
|
10
16
|
cert_chain: []
|
11
17
|
|
12
|
-
date:
|
18
|
+
date: 2011-02-15 00:00:00 -06:00
|
13
19
|
default_executable:
|
14
20
|
dependencies:
|
15
21
|
- !ruby/object:Gem::Dependency
|
16
22
|
name: rspec
|
17
|
-
|
18
|
-
|
19
|
-
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
20
26
|
requirements:
|
21
27
|
- - ">="
|
22
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 13
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 2
|
33
|
+
- 9
|
23
34
|
version: 1.2.9
|
24
|
-
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
25
37
|
- !ruby/object:Gem::Dependency
|
26
38
|
name: sqlite3-ruby
|
27
|
-
|
28
|
-
|
29
|
-
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
30
42
|
requirements:
|
31
43
|
- - ">="
|
32
44
|
- !ruby/object:Gem::Version
|
45
|
+
hash: 27
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 2
|
49
|
+
- 2
|
33
50
|
version: 1.2.2
|
34
|
-
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
35
53
|
description: Rhubarb is a simple object-graph persistence library implemented as a mixin. It also works with the SPQR library for straightforward object publishing over QMF.
|
36
54
|
email: willb@redhat.com
|
37
55
|
executables: []
|
@@ -72,21 +90,27 @@ rdoc_options:
|
|
72
90
|
require_paths:
|
73
91
|
- lib
|
74
92
|
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
75
94
|
requirements:
|
76
95
|
- - ">="
|
77
96
|
- !ruby/object:Gem::Version
|
97
|
+
hash: 3
|
98
|
+
segments:
|
99
|
+
- 0
|
78
100
|
version: "0"
|
79
|
-
version:
|
80
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
81
103
|
requirements:
|
82
104
|
- - ">="
|
83
105
|
- !ruby/object:Gem::Version
|
106
|
+
hash: 3
|
107
|
+
segments:
|
108
|
+
- 0
|
84
109
|
version: "0"
|
85
|
-
version:
|
86
110
|
requirements: []
|
87
111
|
|
88
112
|
rubyforge_project:
|
89
|
-
rubygems_version: 1.3.
|
113
|
+
rubygems_version: 1.3.7
|
90
114
|
signing_key:
|
91
115
|
specification_version: 3
|
92
116
|
summary: "Rhubarb: object graph persistence, easy as pie"
|