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 CHANGED
@@ -1,4 +1,8 @@
1
- version 0.1.4
1
+ version 0.2.0
2
+
3
+ * SPQR and Rhubarb are now separate projects.
4
+
5
+ version 0.1.4 (f24baeafb0fe88e09c232acac3891d11771325a9)
2
6
 
3
7
  * 0.1.3 introduced a compatibility issue with previous versions of
4
8
  Rhubarb. These should be resolved here.
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.4
1
+ 0.2.0
@@ -24,7 +24,12 @@
24
24
 
25
25
  require 'spqr/spqr'
26
26
  require 'spqr/app'
27
- require 'rhubarb/rhubarb'
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
@@ -9,7 +9,6 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
9
 
10
10
  require 'spqr/spqr'
11
11
  require 'spqr/app'
12
- require 'rhubarb/rhubarb'
13
12
 
14
13
  module QmfTestHelpers
15
14
  DEBUG = (::ENV["SPQR_TESTS_DEBUG"] and ::ENV["SPQR_TESTS_DEBUG"].downcase == "yes")
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.1.4
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-01 00:00:00 -06:00
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
@@ -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
@@ -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