zermelo 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +76 -52
  4. data/lib/zermelo/associations/association_data.rb +4 -3
  5. data/lib/zermelo/associations/class_methods.rb +37 -50
  6. data/lib/zermelo/associations/index.rb +3 -1
  7. data/lib/zermelo/associations/multiple.rb +247 -0
  8. data/lib/zermelo/associations/range_index.rb +44 -0
  9. data/lib/zermelo/associations/singular.rb +193 -0
  10. data/lib/zermelo/associations/unique_index.rb +4 -3
  11. data/lib/zermelo/backend.rb +120 -0
  12. data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
  13. data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
  14. data/lib/zermelo/backends/stub.rb +43 -0
  15. data/lib/zermelo/filter.rb +194 -0
  16. data/lib/zermelo/filters/index_range.rb +22 -0
  17. data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
  18. data/lib/zermelo/filters/redis.rb +173 -0
  19. data/lib/zermelo/filters/steps/list_step.rb +48 -30
  20. data/lib/zermelo/filters/steps/set_step.rb +148 -89
  21. data/lib/zermelo/filters/steps/sort_step.rb +2 -2
  22. data/lib/zermelo/record.rb +53 -0
  23. data/lib/zermelo/records/attributes.rb +32 -0
  24. data/lib/zermelo/records/class_methods.rb +12 -25
  25. data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
  26. data/lib/zermelo/records/instance_methods.rb +9 -8
  27. data/lib/zermelo/records/key.rb +3 -1
  28. data/lib/zermelo/records/redis.rb +17 -0
  29. data/lib/zermelo/records/stub.rb +17 -0
  30. data/lib/zermelo/version.rb +1 -1
  31. data/spec/lib/zermelo/associations/index_spec.rb +70 -1
  32. data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
  33. data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
  34. data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
  35. data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
  36. data/spec/lib/zermelo/filter_spec.rb +363 -0
  37. data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
  38. data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
  39. data/spec/spec_helper.rb +9 -1
  40. data/spec/support/mock_logger.rb +48 -0
  41. metadata +31 -46
  42. data/lib/zermelo/associations/belongs_to.rb +0 -115
  43. data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
  44. data/lib/zermelo/associations/has_many.rb +0 -120
  45. data/lib/zermelo/associations/has_one.rb +0 -109
  46. data/lib/zermelo/associations/has_sorted_set.rb +0 -124
  47. data/lib/zermelo/backends/base.rb +0 -115
  48. data/lib/zermelo/filters/base.rb +0 -212
  49. data/lib/zermelo/filters/redis_filter.rb +0 -111
  50. data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
  51. data/lib/zermelo/records/base.rb +0 -62
  52. data/lib/zermelo/records/redis_record.rb +0 -27
  53. data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
  54. data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
  55. data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
  56. data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
  57. data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
  58. data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
  59. data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
  60. data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
  61. data/spec/lib/zermelo/records/influxdb_record_spec.rb +0 -434
  62. data/spec/lib/zermelo/records/key_spec.rb +0 -6
  63. data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
  64. data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
  65. data/spec/lib/zermelo/version_spec.rb +0 -6
  66. data/spec/lib/zermelo_spec.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9908061576a4c3d173358524eaf988a6d16bd33
4
- data.tar.gz: 843acdcd8b10b1b13407a7c1440f30f87412ac71
3
+ metadata.gz: bc193e2da0a7a65f2a3643520bd99663172a3d2e
4
+ data.tar.gz: ee336568cf9e99aae87ac54bbe6ce0db00500e42
5
5
  SHA512:
6
- metadata.gz: 5365e1259edae7641187444a6dce2f903be37ae3787f47c5d12006d8f4dd7cf308e6874e54509dabd1ac336a62c39e166839b8ffd76d8126530e43fcc6488a22
7
- data.tar.gz: c405762c072ab4f1fe5d366e9997448ba6f35d4b202a6be33535481abc20a1fdf6cbb1d14f87cd4bae37a082cbd9dbe9be3010785c453dd22622064bf5c3bea7
6
+ metadata.gz: 46fea7c0eee8e85846323a42ce4a83caee6761e48ad672c7ba70fa0a2ddd862b5d5aac849a8ce7d7d0b9c4c7deebec9bf90d1c918878c9ec73fe8b08a130cb9d
7
+ data.tar.gz: 0257355f3e92135da783b321db21850adb806758ba1089bf6a3837bf661b93925a66771ad760ad6796914fbeb269a3495720f604dad5977c1d103144143d5756
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## Zermelo Changelog
2
2
 
3
+ # 1.2.0 - 2015-06-24
4
+
5
+ * spec cleanup, apply to multiple backends if not backend-specific
6
+ * fix sorted_sets to compose with other queries
7
+ * range queries against sorted sets
8
+ * improve setting association from its inverse side; all callbacks now called properly
9
+ * renamed .delete on associations to .remove
10
+ * also support .remove_ids on multiple associations, so record need not be loaded
11
+ * stub record type
12
+
3
13
  # 1.1.0 - 2015-04-09
4
14
 
5
15
  * refactored query builder to improve composability
data/README.md CHANGED
@@ -38,11 +38,11 @@ You can optionally set `Zermelo.logger` to an instance of a Ruby `Logger` class,
38
38
 
39
39
  ### Class ids
40
40
 
41
- Include **zermelo**'s Record module in the class you want to persist data from:
41
+ Include **zermelo**'s `Zermelo::Records::Redis` module in the class you want to persist data from:
42
42
 
43
43
  ```ruby
44
44
  class Post
45
- include Zermelo::Record
45
+ include Zermelo::Records::Redis
46
46
  end
47
47
  ```
48
48
 
@@ -67,7 +67,7 @@ A data record without any actual data isn't very useful, so let's add a few simp
67
67
 
68
68
  ```ruby
69
69
  class Post
70
- include Zermelo::Record
70
+ include Zermelo::Records::Redis
71
71
  define_attributes :title => :string,
72
72
  :score => :integer,
73
73
  :timestamp => :timestamp,
@@ -96,8 +96,7 @@ which can then be verified by inspection of the object's attributes, e.g.:
96
96
  post.attributes.inpsect # == {:id => '03c839ac-24af-432e-aa58-fd1d4bf73f24', :title => 'Introduction to Zermelo', :score => 100, :timestamp => '2000-01-01 00:00:00 UTC', :published => false}
97
97
  ```
98
98
 
99
- Zermelo supports the following simple attribute types, and automatically
100
- validates that the values are of the correct class, casting if possible:
99
+ Zermelo supports the following simple attribute types, and automatically validates that the values are of the correct class, casting if possible:
101
100
 
102
101
  | Type | Ruby class | Notes |
103
102
  |------------|-------------------------------|-------|
@@ -116,7 +115,7 @@ So if we add tags to the Post data definition:
116
115
 
117
116
  ```ruby
118
117
  class Post
119
- include Zermelo::Record
118
+ include Zermelo::Records::Redis
120
119
  define_attributes :title => :string,
121
120
  :score => :integer,
122
121
  :timestamp => :timestamp,
@@ -125,7 +124,7 @@ class Post
125
124
  end
126
125
  ```
127
126
 
128
- and then create another
127
+ and then create another `Post` instance:
129
128
 
130
129
  ```ruby
131
130
  post = Post.new(:id => 1, :tags => Set.new(['database', 'ORM']))
@@ -139,19 +138,18 @@ SADD post:1:attrs:tags 'database' 'ORM'
139
138
  SADD post::attrs:ids 1
140
139
  ```
141
140
 
142
- Zermelo supports the following complex attribute types, and automatically
143
- validates that the values are of the correct class, casting if possible:
141
+ Zermelo supports the following complex attribute types, and automatically validates that the values are of the correct class, casting if possible:
144
142
 
145
- | Type | Ruby class | Notes |
146
- |------------|---------------|---------------------------------------------------------|
147
- | :list | Enumerable | Stored as a Redis [LIST](http://redis.io/commands#list) |
148
- | :set | Array or Set | Stored as a Redis [SET](http://redis.io/commands#set) |
149
- | :hash | Hash | Stored as a Redis [HASH](http://redis.io/commands#hash) |
143
+ | Type | Ruby class | Notes |
144
+ |-------------|---------------|---------------------------------------------------------|
145
+ | :list | Enumerable | Stored as a Redis [LIST](http://redis.io/commands#list) |
146
+ | :set | Array or Set | Stored as a Redis [SET](http://redis.io/commands#set) |
147
+ | :hash | Hash | Stored as a Redis [HASH](http://redis.io/commands#hash) |
148
+ | :sorted_set | Enumerable | Stored as a Redis [ZSET](http://redis.io/commands#zset) |
150
149
 
151
- Structure data members must be primitives that will cast OK to and from Redis via the
152
- driver, thus String, Integer and Float.
150
+ Structure data members must be primitives that will cast OK to and from Redis via the driver, thus String, Integer and Float.
153
151
 
154
- Redis [sorted sets](http://redis.io/commands#sorted_set) are only supported through associations, for which see later on.
152
+ Redis [sorted sets](http://redis.io/commands#sorted_set) are also supported through **zermelo**'s associations (recommended due to the fact that queries can be constructed against them).
155
153
 
156
154
  ### Validations
157
155
 
@@ -161,9 +159,9 @@ So an attribute which should be present:
161
159
 
162
160
  ```ruby
163
161
  class Post
164
- include Zermelo::Record
165
- define_attributes :title => :string,
166
- :score => :integer
162
+ include Zermelo::Records::Redis
163
+ define_attributes :title => :string,
164
+ :score => :integer
167
165
  validates :title, :presence => true
168
166
  end
169
167
  ```
@@ -202,15 +200,15 @@ Another feature added by ActiveModel is the ability to detect changed data in re
202
200
 
203
201
  ```ruby
204
202
  class Author
205
- include Zermelo::Record
203
+ include Zermelo::Records::Redis
206
204
  end
207
205
 
208
206
  class Post
209
- include Zermelo::Record
207
+ include Zermelo::Records::Redis
210
208
  end
211
209
 
212
210
  class Comment
213
- include Zermelo::Record
211
+ include Zermelo::Records::Redis
214
212
  end
215
213
 
216
214
  Author.lock(Post, Comment) do
@@ -224,7 +222,7 @@ Assuming a saved `Post` instance has been created:
224
222
 
225
223
  ```ruby
226
224
  class Post
227
- include Zermelo::Record
225
+ include Zermelo::Records::Redis
228
226
  define_attributes :title => :string,
229
227
  :score => :integer,
230
228
  :timestamp => :timestamp,
@@ -296,19 +294,19 @@ Instances also have attribute accessors and the various methods included from th
296
294
  |Name | Type | Redis data structure | Notes |
297
295
  |---------------------------|---------------------------|----------------------|-------|
298
296
  | `has_many` | one-to-many | [SET](http://redis.io/commands#set) | |
299
- | `has_sorted_set` | one-to-many | [ZSET](http://redis.io/commands#sorted_set) | |
297
+ | `has_sorted_set` | one-to-many | [ZSET](http://redis.io/commands#sorted_set) | Arguments: `:key` (required), `:order` (optional, `:asc` or `:desc`) |
300
298
  | `has_one` | one-to-one | [HASH](http://redis.io/commands#hash) | |
301
299
  | `belongs_to` | many-to-one or one-to-one | [HASH](http://redis.io/commands#hash) or [STRING](http://redis.io/commands#string) | Inverse of any of the above three |
302
300
  | `has_and_belongs_to_many` | many-to-many | 2 [SET](http://redis.io/commands#set)s | Mirrored by an inverse HaBtM association on the other side. |
303
301
 
304
302
  ```ruby
305
303
  class Post
306
- include Zermelo::Record
304
+ include Zermelo::Records::Redis
307
305
  has_many :comments, :class_name => 'Comment', :inverse_of => :post
308
306
  end
309
307
 
310
308
  class Comment
311
- include Zermelo::Record
309
+ include Zermelo::Records::Redis
312
310
  belongs_to :post, :class_name => 'Post', :inverse_of => :comments
313
311
  end
314
312
  ```
@@ -319,24 +317,35 @@ Records are added and removed from their parent one-to-many or many-to-many asso
319
317
 
320
318
  ```ruby
321
319
  post.comments.add(comment) # or post.comments << comment
320
+ post.comments.remove(comment)
322
321
  ```
323
322
 
324
- Associations' `.add` can also take more than one argument:
323
+ Associations' `.add`/`.remove` can also take more than one argument:
325
324
 
326
325
  ```ruby
327
326
  post.comments.add(comment1, comment2, comment3)
327
+ post.comments.remove(comment1, comment2, comment3)
328
+ ```
329
+
330
+ If you only have ids available, you don't need to `.load` the respective objects, you can instead use `.add_ids`/`.remove_ids`:
331
+
332
+ ```ruby
333
+ post.comments.add_ids("comment_id")
334
+ post.comments.remove_ids("comment_id")
335
+ post.comments.add_ids("comment1_id", "comment2_id", "comment3_id")
336
+ post.comments.remove_ids("comment1_id", "comment2_id", "comment3_id")
328
337
  ```
329
338
 
330
339
  `has_one` associations are simply set with an `=` method on the association:
331
340
 
332
341
  ```ruby
333
342
  class User
334
- include Zermelo::Record
343
+ include Zermelo::Records::Redis
335
344
  has_one :preferences, :class_name => 'Preferences', :inverse_of => :user
336
345
  end
337
346
 
338
347
  class Preferences
339
- include Zermelo::Record
348
+ include Zermelo::Records::Redis
340
349
  belongs_to :user, :class_name => 'User', :inverse_of => :preferences
341
350
  end
342
351
 
@@ -348,10 +357,16 @@ prefs.save
348
357
  user.preferences = prefs
349
358
  ```
350
359
 
360
+ and cleared by assigning the association to nil:
361
+
362
+ ```ruby
363
+ user.preferences = nil
364
+ ```
365
+
351
366
  The class methods defined above can be applied to associations references as well, so the resulting data will be filtered by the data relationships applying in the association, e.g.
352
367
 
353
368
  ```ruby
354
- post = Post.new(:id => 'a')
369
+ post = Post.new(:id => 'a')
355
370
  post.save
356
371
  comment1 = Comment.new(:id => '1')
357
372
  comment1.save
@@ -364,12 +379,12 @@ post.comments << comment1
364
379
  p post.comments.ids # == [1]
365
380
  ```
366
381
 
367
- `associated_ids_for` is somewhat of a special case; it uses the smallest/simplest queries possible to get the ids of the associated records of a set of records, e.g. for the data directly above:
382
+ `.associated_ids_for` is somewhat of a special case; it uses the simplest queries possible to get the ids of the associated records of a set of records, e.g. for the data directly above:
368
383
 
369
384
  ```ruby
370
385
  Post.associated_ids_for(:comments) # => {'a' => ['1']}
371
386
 
372
- post_b = Post.new(:id => 'b')
387
+ post_b = Post.new(:id => 'b')
373
388
  post_b.save
374
389
  post_b.comments << comment2
375
390
  comment3 = Comment.new(:id => '3')
@@ -377,7 +392,6 @@ comment3.save
377
392
  post.comments << comment3
378
393
 
379
394
  Post.associated_ids_for(:comments) # => {'a' => ['1', '3'], 'b' => ['2']}
380
- Post.intersect(:id => 'a').associated_ids_for(:comments) # => {'a' => ['1', '3']}
381
395
  ```
382
396
 
383
397
  For `belongs to` associations, you may pass an extra option to `associated_ids_for`, `:inversed => true`, and you'll get the data back as if it were applied from the inverse side; however the data will only cover that used as the query root. Again, assuming the data from the last two code blocks, e.g.
@@ -385,9 +399,6 @@ For `belongs to` associations, you may pass an extra option to `associated_ids_f
385
399
  ```ruby
386
400
  Comment.associated_ids_for(:post) # => {'1' => 'a', '2' => 'b', '3' => 'a'}
387
401
  Comment.associated_ids_for(:post, :inversed => true) # => {'a' => ['1', '3'], 'b' => ['2']}
388
-
389
- Comment.intersect(:id => ['1', '2']).associated_ids_for(:post) # => {'1' => 'a', '2' => 'b'}
390
- Comment.intersect(:id => ['1', '2']).associated_ids_for(:post, :inversed => true) # => {'a' => ['1'], 'b' => ['2']}
391
402
  ```
392
403
 
393
404
  ### Class data indexing
@@ -398,7 +409,7 @@ Using the code from the instance attributes section, and adding indexing:
398
409
 
399
410
  ```ruby
400
411
  class Post
401
- include Zermelo::Record
412
+ include Zermelo::Records::Redis
402
413
  define_attributes :title => :string,
403
414
  :score => :integer,
404
415
  :timestamp => :timestamp,
@@ -436,15 +447,12 @@ SADD post::indices:by_published:boolean:false 03c839ac-24af-432e-aa58-fd1d4bf73f
436
447
 
437
448
  | Name | Input | Output | Arguments | Options |
438
449
  |-----------------|-----------------------|--------------|---------------------------------------|------------------------------------------|
439
- | intersect | `set` or `sorted_set` | `set` | Query hash | |
440
- | union | `set` or `sorted_set` | `set` | Query hash | |
441
- | diff | `set` or `sorted_set` | `set` | Query hash | |
442
- | intersect_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
443
- | union_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
444
- | diff_range | `sorted_set` | `sorted_set` | start (`Integer`), finish (`Integer`) | :desc (`Boolean`), :by_score (`Boolean`) |
450
+ | intersect | `set` / `sorted_set` | (as input) | Query hash | |
451
+ | union | `set` / `sorted_set` | (as input) | Query hash | |
452
+ | diff | `set` / `sorted_set` | (as input) | Query hash | |
445
453
  | sort | `set` or `sorted_set` | `list` | keys (Symbol or Array of Symbols) | :limit (`Integer`), :offset (`Integer`) |
446
- | offset | `list` | `list` | amount (`Integer`) | |
447
- | limit | `list` | `list` | amount (`Integer`) | |
454
+ | offset | `list` / `sorted_set` | `list` | amount (`Integer`) | :limit (`Integer`) |
455
+ | page | `list` / `sorted_set` | `list` | page_number (`Integer`) | :per_page (`Integer`) |
448
456
 
449
457
  These queries can be applied against all instances of a class, or against associations belonging to an instance, e.g.
450
458
 
@@ -481,21 +489,37 @@ DEL comment::tmp:fe8dd59e4a1197f62d19c8aa942c4ff9
481
489
 
482
490
  (where the name of the temporary Redis `SET` will of course change every time)
483
491
 
484
- The current implementation of the filtering is somewhat ad-hoc, and has these limitations:
492
+ ---
493
+
494
+ `has_sorted_set` queries can take exact values, or a range bounded in no, one or both directions. (Regular Ruby `Range` objects can't be used as they don't easily support timestamps, so there's a `Zermelo::Filters::IndexRange` class which can be used as a query value instead.)
485
495
 
486
- * no conversion of `list`s back into `set`s is allowed
487
- * `sort`/`offset`/`limit` can only be used once in a filter chain
496
+ ```ruby
497
+ class Comment
498
+ include Zermelo::Records::Redis
499
+ define_attributes :created_at => :timestamp
500
+ end
488
501
 
489
- I plan to fix these as soon as I possibly can.
502
+ t = Time.now
503
+
504
+ comment1 = Comment.new(:id => '1', :created_at => t - 120)
505
+ comment1.save
506
+ comment2 = Comment.new(:id => '2', :created_at => t - 60)
507
+ comment2.save
508
+
509
+ range = Zermelo::Filters::IndexRange.new(t - 90, t, :by_score => true)
510
+ Comment.ids # ['1', '2']
511
+ Comment.intersect(:created_at => range).ids # ['2']
512
+
513
+ ```
490
514
 
491
515
  ### Future
492
516
 
493
517
  Some possible changes:
494
518
 
495
- * pluggable key naming strategies
496
519
  * pluggable id generation strategies
520
+ * pluggable key naming strategies
497
521
  * instrumentation for benchmarking etc.
498
- * multiple data backends; there's currently an experimental InfluxDB backend, and more are planned.
522
+ * multiple data backends; there's currently an experimental InfluxDB 0.8 backend (waiting for the Ruby driver to update for 0.9 support).
499
523
 
500
524
  ## License
501
525
 
@@ -2,11 +2,12 @@ module Zermelo
2
2
  module Associations
3
3
  class AssociationData
4
4
  attr_writer :data_klass_name, :related_klass_names
5
- attr_accessor :name, :type_klass, :inverse, :sort_key, :callbacks
5
+ attr_accessor :name, :type_klass, :data_type, :inverse, :sort_key,
6
+ :sort_order, :callbacks
6
7
 
7
8
  def initialize(opts = {})
8
- [:name, :type_klass, :inverse, :sort_key, :callbacks, :data_klass_name,
9
- :related_klass_names].each do |a|
9
+ [:name, :type_klass, :data_type, :inverse, :sort_key, :sort_order,
10
+ :callbacks, :data_klass_name, :related_klass_names].each do |a|
10
11
 
11
12
  send("#{a}=".to_sym, opts[a])
12
13
  end
@@ -1,12 +1,10 @@
1
1
  require 'zermelo/associations/association_data'
2
2
  require 'zermelo/associations/index_data'
3
3
 
4
- require 'zermelo/associations/belongs_to'
5
- require 'zermelo/associations/has_and_belongs_to_many'
6
- require 'zermelo/associations/has_many'
7
- require 'zermelo/associations/has_one'
8
- require 'zermelo/associations/has_sorted_set'
4
+ require 'zermelo/associations/singular'
5
+ require 'zermelo/associations/multiple'
9
6
  require 'zermelo/associations/index'
7
+ require 'zermelo/associations/range_index'
10
8
  require 'zermelo/associations/unique_index'
11
9
 
12
10
  # NB: this module gets mixed in to Zermelo::Record as class methods
@@ -30,6 +28,14 @@ module Zermelo
30
28
  nil
31
29
  end
32
30
 
31
+ def range_index_by(*args)
32
+ att_types = attribute_types
33
+ args.each do |arg|
34
+ index(::Zermelo::Associations::RangeIndex, arg, :type => att_types[arg])
35
+ end
36
+ nil
37
+ end
38
+
33
39
  def unique_index_by(*args)
34
40
  att_types = attribute_types
35
41
  args.each do |arg|
@@ -39,31 +45,33 @@ module Zermelo
39
45
  end
40
46
 
41
47
  def has_many(name, args = {})
42
- associate(::Zermelo::Associations::HasMany, name, args)
48
+ associate(::Zermelo::Associations::Multiple, :has_many, name, args)
43
49
  nil
44
50
  end
45
51
 
46
- def has_one(name, args = {})
47
- associate(::Zermelo::Associations::HasOne, name, args)
52
+ def has_sorted_set(name, args = {})
53
+ associate(::Zermelo::Associations::Multiple, :has_sorted_set, name, args)
48
54
  nil
49
55
  end
50
56
 
51
- def has_sorted_set(name, args = {})
52
- associate(::Zermelo::Associations::HasSortedSet, name, args)
57
+ def has_and_belongs_to_many(name, args = {})
58
+ associate(::Zermelo::Associations::Multiple, :has_and_belongs_to_many, name, args)
53
59
  nil
54
60
  end
55
61
 
56
- def has_and_belongs_to_many(name, args = {})
57
- associate(::Zermelo::Associations::HasAndBelongsToMany, name, args)
62
+ def has_one(name, args = {})
63
+ associate(::Zermelo::Associations::Singular, :has_one, name, args)
58
64
  nil
59
65
  end
60
66
 
61
67
  def belongs_to(name, args = {})
62
- associate(::Zermelo::Associations::BelongsTo, name, args)
68
+ associate(::Zermelo::Associations::Singular, :belongs_to, name, args)
63
69
  nil
64
70
  end
65
71
  # end used by client classes
66
72
 
73
+ private
74
+
67
75
  # used internally by other parts of Zermelo to implement the above
68
76
  # configuration
69
77
 
@@ -77,15 +85,12 @@ module Zermelo
77
85
  @association_data.values.each do |data|
78
86
  klass = data.data_klass
79
87
  next if visited.include?(klass)
80
- visited |= klass.associated_classes(visited, false)
88
+ visited |= klass.send(:associated_classes, visited, false)
81
89
  end
82
90
  end
83
91
  visited
84
92
  end
85
93
 
86
- # TODO for each association: check whether it has changed
87
- # would need an instance-level hash with association name as key,
88
- # boolean 'changed' value
89
94
  def with_associations(record)
90
95
  @lock.synchronize do
91
96
  @association_data ||= {}
@@ -112,14 +117,6 @@ module Zermelo
112
117
  end
113
118
  # end used internally within Zermelo
114
119
 
115
- # # TODO can remove need for some of the inverse mapping
116
- # # was inverse_of(source, klass)
117
- # with_association_data do |d|
118
- # d.detect {|name, data| data.klass == klass && data.inverse == source}
119
- # end
120
-
121
- private
122
-
123
120
  def add_index_data(klass, name, args = {})
124
121
  return if name.nil?
125
122
 
@@ -151,12 +148,7 @@ module Zermelo
151
148
  instance_eval idx, __FILE__, __LINE__
152
149
  end
153
150
 
154
- def add_association_data(klass, name, args = {})
155
-
156
- # TODO have inverse be a reference (or copy?) of the association data
157
- # record for that inverse association; would need to defer lookup until
158
- # all data in place for all assocs, so might be best if looked up and
159
- # cached on first use
151
+ def add_association_data(klass, type, name, args = {})
160
152
  inverse = if args[:inverse_of].nil? || args[:inverse_of].to_s.empty?
161
153
  nil
162
154
  else
@@ -164,13 +156,10 @@ module Zermelo
164
156
  end
165
157
 
166
158
  callbacks = case klass.name
167
- when ::Zermelo::Associations::HasMany.name,
168
- ::Zermelo::Associations::HasSortedSet.name,
169
- ::Zermelo::Associations::HasAndBelongsToMany.name
170
- [:before_add, :after_add, :before_remove, :after_remove]
171
- when ::Zermelo::Associations::HasOne.name,
172
- ::Zermelo::Associations::BelongsTo.name
173
- [:before_set, :after_set, :before_clear, :after_clear]
159
+ when ::Zermelo::Associations::Multiple.name
160
+ [:before_add, :after_add, :before_remove, :after_remove, :before_read, :after_read]
161
+ when ::Zermelo::Associations::Singular.name
162
+ [:before_set, :after_set, :before_clear, :after_clear, :before_read, :after_read]
174
163
  else
175
164
  []
176
165
  end
@@ -178,6 +167,7 @@ module Zermelo
178
167
  data = Zermelo::Associations::AssociationData.new(
179
168
  :name => name,
180
169
  :data_klass_name => args[:class_name],
170
+ :data_type => type,
181
171
  :type_klass => klass,
182
172
  :inverse => inverse,
183
173
  :related_klass_names => args[:related_class_names],
@@ -186,8 +176,10 @@ module Zermelo
186
176
  }
187
177
  )
188
178
 
189
- if klass.name == Zermelo::Associations::HasSortedSet.name
190
- data.sort_key = (args[:key] || :id)
179
+ if :has_sorted_set.eql?(type)
180
+ data.sort_key = args[:key]
181
+ data.sort_order =
182
+ !args[:order].nil? && :desc.eql?(args[:order].to_sym) ? :desc : :asc
191
183
  end
192
184
 
193
185
  @lock.synchronize do
@@ -196,25 +188,20 @@ module Zermelo
196
188
  end
197
189
  end
198
190
 
199
- def associate(klass, name, args = {})
191
+ def associate(klass, type, name, args = {})
200
192
  return if name.nil?
201
193
 
202
- add_association_data(klass, name, args)
194
+ add_association_data(klass, type, name, args)
203
195
 
204
196
  assoc = case klass.name
205
- when ::Zermelo::Associations::HasMany.name,
206
- ::Zermelo::Associations::HasSortedSet.name,
207
- ::Zermelo::Associations::HasAndBelongsToMany.name
208
-
197
+ when ::Zermelo::Associations::Multiple.name
209
198
  %Q{
210
199
  def #{name}
211
200
  #{name}_proxy
212
201
  end
213
202
  }
214
203
 
215
- when ::Zermelo::Associations::HasOne.name,
216
- ::Zermelo::Associations::BelongsTo.name
217
-
204
+ when ::Zermelo::Associations::Singular.name
218
205
  %Q{
219
206
  def #{name}
220
207
  #{name}_proxy.value
@@ -232,7 +219,7 @@ module Zermelo
232
219
  def #{name}_proxy
233
220
  raise "Associations cannot be invoked for records without an id" if self.id.nil?
234
221
 
235
- @#{name}_proxy ||= #{klass.name}.new(self, '#{name}')
222
+ @#{name}_proxy ||= #{klass.name}.new(:#{type}, self.class, self.id, '#{name}')
236
223
  end
237
224
  private :#{name}_proxy
238
225
  }
@@ -1,5 +1,7 @@
1
1
  # NB index instances are all internal to zermelo, not user-accessible
2
2
 
3
+ require 'zermelo/records/key'
4
+
3
5
  module Zermelo
4
6
  module Associations
5
7
  class Index
@@ -29,7 +31,7 @@ module Zermelo
29
31
 
30
32
  def move_id(id, value_from, indexer_to, value_to)
31
33
  return unless indexer = key(value_from)
32
- @backend.move(indexer, id, indexer_to.key(value_to))
34
+ @backend.move(indexer, id, indexer_to.key(value_to), id)
33
35
  end
34
36
 
35
37
  def key(value)