spqr 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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