zermelo 1.1.0 → 1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +76 -52
- data/lib/zermelo/associations/association_data.rb +4 -3
- data/lib/zermelo/associations/class_methods.rb +37 -50
- data/lib/zermelo/associations/index.rb +3 -1
- data/lib/zermelo/associations/multiple.rb +247 -0
- data/lib/zermelo/associations/range_index.rb +44 -0
- data/lib/zermelo/associations/singular.rb +193 -0
- data/lib/zermelo/associations/unique_index.rb +4 -3
- data/lib/zermelo/backend.rb +120 -0
- data/lib/zermelo/backends/{influxdb_backend.rb → influxdb.rb} +87 -31
- data/lib/zermelo/backends/{redis_backend.rb → redis.rb} +53 -58
- data/lib/zermelo/backends/stub.rb +43 -0
- data/lib/zermelo/filter.rb +194 -0
- data/lib/zermelo/filters/index_range.rb +22 -0
- data/lib/zermelo/filters/{influxdb_filter.rb → influxdb.rb} +12 -11
- data/lib/zermelo/filters/redis.rb +173 -0
- data/lib/zermelo/filters/steps/list_step.rb +48 -30
- data/lib/zermelo/filters/steps/set_step.rb +148 -89
- data/lib/zermelo/filters/steps/sort_step.rb +2 -2
- data/lib/zermelo/record.rb +53 -0
- data/lib/zermelo/records/attributes.rb +32 -0
- data/lib/zermelo/records/class_methods.rb +12 -25
- data/lib/zermelo/records/{influxdb_record.rb → influxdb.rb} +3 -4
- data/lib/zermelo/records/instance_methods.rb +9 -8
- data/lib/zermelo/records/key.rb +3 -1
- data/lib/zermelo/records/redis.rb +17 -0
- data/lib/zermelo/records/stub.rb +17 -0
- data/lib/zermelo/version.rb +1 -1
- data/spec/lib/zermelo/associations/index_spec.rb +70 -1
- data/spec/lib/zermelo/associations/multiple_spec.rb +1084 -0
- data/spec/lib/zermelo/associations/range_index_spec.rb +77 -0
- data/spec/lib/zermelo/associations/singular_spec.rb +149 -0
- data/spec/lib/zermelo/associations/unique_index_spec.rb +58 -2
- data/spec/lib/zermelo/filter_spec.rb +363 -0
- data/spec/lib/zermelo/locks/redis_lock_spec.rb +3 -3
- data/spec/lib/zermelo/records/instance_methods_spec.rb +206 -0
- data/spec/spec_helper.rb +9 -1
- data/spec/support/mock_logger.rb +48 -0
- metadata +31 -46
- data/lib/zermelo/associations/belongs_to.rb +0 -115
- data/lib/zermelo/associations/has_and_belongs_to_many.rb +0 -128
- data/lib/zermelo/associations/has_many.rb +0 -120
- data/lib/zermelo/associations/has_one.rb +0 -109
- data/lib/zermelo/associations/has_sorted_set.rb +0 -124
- data/lib/zermelo/backends/base.rb +0 -115
- data/lib/zermelo/filters/base.rb +0 -212
- data/lib/zermelo/filters/redis_filter.rb +0 -111
- data/lib/zermelo/filters/steps/sorted_set_step.rb +0 -156
- data/lib/zermelo/records/base.rb +0 -62
- data/lib/zermelo/records/redis_record.rb +0 -27
- data/spec/lib/zermelo/associations/belongs_to_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_many_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_one_spec.rb +0 -6
- data/spec/lib/zermelo/associations/has_sorted_set.spec.rb +0 -6
- data/spec/lib/zermelo/backends/influxdb_backend_spec.rb +0 -0
- data/spec/lib/zermelo/backends/moneta_backend_spec.rb +0 -0
- data/spec/lib/zermelo/filters/influxdb_filter_spec.rb +0 -0
- data/spec/lib/zermelo/filters/redis_filter_spec.rb +0 -0
- data/spec/lib/zermelo/records/influxdb_record_spec.rb +0 -434
- data/spec/lib/zermelo/records/key_spec.rb +0 -6
- data/spec/lib/zermelo/records/redis_record_spec.rb +0 -1461
- data/spec/lib/zermelo/records/type_validator_spec.rb +0 -6
- data/spec/lib/zermelo/version_spec.rb +0 -6
- data/spec/lib/zermelo_spec.rb +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc193e2da0a7a65f2a3643520bd99663172a3d2e
|
4
|
+
data.tar.gz: ee336568cf9e99aae87ac54bbe6ce0db00500e42
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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::
|
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::
|
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::
|
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
|
146
|
-
|
147
|
-
| :list
|
148
|
-
| :set
|
149
|
-
| :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
|
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::
|
165
|
-
define_attributes :title
|
166
|
-
:score
|
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::
|
203
|
+
include Zermelo::Records::Redis
|
206
204
|
end
|
207
205
|
|
208
206
|
class Post
|
209
|
-
include Zermelo::
|
207
|
+
include Zermelo::Records::Redis
|
210
208
|
end
|
211
209
|
|
212
210
|
class Comment
|
213
|
-
include Zermelo::
|
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::
|
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::
|
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::
|
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::
|
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::
|
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
|
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
|
-
|
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
|
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::
|
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`
|
440
|
-
| union | `set`
|
441
|
-
| diff | `set`
|
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`
|
447
|
-
|
|
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
|
-
|
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
|
-
|
487
|
-
|
496
|
+
```ruby
|
497
|
+
class Comment
|
498
|
+
include Zermelo::Records::Redis
|
499
|
+
define_attributes :created_at => :timestamp
|
500
|
+
end
|
488
501
|
|
489
|
-
|
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
|
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,
|
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, :
|
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/
|
5
|
-
require 'zermelo/associations/
|
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::
|
48
|
+
associate(::Zermelo::Associations::Multiple, :has_many, name, args)
|
43
49
|
nil
|
44
50
|
end
|
45
51
|
|
46
|
-
def
|
47
|
-
associate(::Zermelo::Associations::
|
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
|
52
|
-
associate(::Zermelo::Associations::
|
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
|
57
|
-
associate(::Zermelo::Associations::
|
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::
|
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
|
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::
|
168
|
-
|
169
|
-
|
170
|
-
[:
|
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
|
190
|
-
data.sort_key
|
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::
|
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::
|
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)
|