mongoose 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +136 -0
- data/changes.txt +2 -0
- data/example/relation_examples.rb +117 -0
- data/example/simple_examples.rb +117 -0
- data/lib/mongoose.rb +45 -0
- data/lib/mongoose/column.rb +286 -0
- data/lib/mongoose/database.rb +113 -0
- data/lib/mongoose/linear_search.rb +114 -0
- data/lib/mongoose/skiplist.rb +412 -0
- data/lib/mongoose/table.rb +568 -0
- data/lib/mongoose/util.rb +43 -0
- data/test/tc_database.rb +27 -0
- data/test/tc_table.rb +167 -0
- data/test/ts_mongoose.rb +3 -0
- metadata +59 -0
@@ -0,0 +1,568 @@
|
|
1
|
+
module Mongoose
|
2
|
+
|
3
|
+
#-------------------------------------------------------------------------------
|
4
|
+
# Table class
|
5
|
+
#-------------------------------------------------------------------------------
|
6
|
+
class Table
|
7
|
+
#-----------------------------------------------------------------------------
|
8
|
+
# Table.db
|
9
|
+
#-----------------------------------------------------------------------------
|
10
|
+
def self.db
|
11
|
+
@@db
|
12
|
+
end
|
13
|
+
|
14
|
+
#-----------------------------------------------------------------------------
|
15
|
+
# Table.db=
|
16
|
+
#-----------------------------------------------------------------------------
|
17
|
+
def self.db=(db)
|
18
|
+
@@db = db
|
19
|
+
end
|
20
|
+
|
21
|
+
#-----------------------------------------------------------------------------
|
22
|
+
# Table.query
|
23
|
+
#-----------------------------------------------------------------------------
|
24
|
+
def self.query
|
25
|
+
self.db.tables[self][:query]
|
26
|
+
end
|
27
|
+
|
28
|
+
#-----------------------------------------------------------------------------
|
29
|
+
# Table.table_name
|
30
|
+
#-----------------------------------------------------------------------------
|
31
|
+
def self.table_name
|
32
|
+
self.db.tables[self][:table_name]
|
33
|
+
end
|
34
|
+
|
35
|
+
#-----------------------------------------------------------------------------
|
36
|
+
# Table.columns
|
37
|
+
#-----------------------------------------------------------------------------
|
38
|
+
def self.columns
|
39
|
+
self.db.tables[self][:columns]
|
40
|
+
end
|
41
|
+
|
42
|
+
#-----------------------------------------------------------------------------
|
43
|
+
# Table.column_names
|
44
|
+
#-----------------------------------------------------------------------------
|
45
|
+
def self.column_names
|
46
|
+
self.db.tables[self][:columns].collect { |c| c.name }
|
47
|
+
end
|
48
|
+
|
49
|
+
#-----------------------------------------------------------------------------
|
50
|
+
# Table.path
|
51
|
+
#-----------------------------------------------------------------------------
|
52
|
+
def self.path
|
53
|
+
self.db.path
|
54
|
+
end
|
55
|
+
|
56
|
+
#-----------------------------------------------------------------------------
|
57
|
+
# Table.validates_presence_of
|
58
|
+
#-----------------------------------------------------------------------------
|
59
|
+
def self.validates_presence_of(*col_names)
|
60
|
+
define_method(:required?) do |col_name|
|
61
|
+
col_names.include?(col_name)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#-----------------------------------------------------------------------------
|
66
|
+
# Table.has_many
|
67
|
+
#-----------------------------------------------------------------------------
|
68
|
+
def self.has_many(kind)
|
69
|
+
table_name = Util.singularize(kind.to_s).to_sym
|
70
|
+
class_name = Util.us_case_to_class_case(table_name.to_s)
|
71
|
+
col = Util.col_name_for_class(self.to_s)
|
72
|
+
|
73
|
+
define_method(kind.to_sym) do
|
74
|
+
klass = Object.const_get(class_name)
|
75
|
+
Collection.new(self, klass.find { |r|
|
76
|
+
r.send(col) == self.instance_eval { @id } })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
#-----------------------------------------------------------------------------
|
81
|
+
# Table.has_one
|
82
|
+
#-----------------------------------------------------------------------------
|
83
|
+
def self.has_one(kind)
|
84
|
+
table_name = kind.to_sym
|
85
|
+
class_name = Util.us_case_to_class_case(table_name.to_s)
|
86
|
+
col = Util.col_name_for_class(self.to_s)
|
87
|
+
|
88
|
+
define_method(kind.to_sym) do
|
89
|
+
klass = Object.const_get(class_name)
|
90
|
+
klass.find(:first) { |r| r.send(col) == self.instance_eval { @id } }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
#-----------------------------------------------------------------------------
|
95
|
+
# Table.belongs_to
|
96
|
+
#-----------------------------------------------------------------------------
|
97
|
+
def self.belongs_to(kind)
|
98
|
+
class_name = Util.us_case_to_class_case(kind.to_sym)
|
99
|
+
|
100
|
+
define_method(kind) do
|
101
|
+
klass = Object.const_get(class_name.to_s)
|
102
|
+
klass.find(send("#{kind}_id".to_sym))
|
103
|
+
end
|
104
|
+
|
105
|
+
define_method("#{kind}=".to_sym) do |other|
|
106
|
+
other.save
|
107
|
+
send("#{kind}_id=".to_sym, other.id.to_i)
|
108
|
+
save
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#-----------------------------------------------------------------------------
|
113
|
+
# Table.last_id_used
|
114
|
+
#-----------------------------------------------------------------------------
|
115
|
+
def self.last_id_used
|
116
|
+
self.db.tables[self][:last_id_used]
|
117
|
+
end
|
118
|
+
|
119
|
+
#-----------------------------------------------------------------------------
|
120
|
+
# Table.last_id_used=
|
121
|
+
#-----------------------------------------------------------------------------
|
122
|
+
def self.last_id_used=(value)
|
123
|
+
self.db.tables[self][:last_id_used] = value
|
124
|
+
end
|
125
|
+
|
126
|
+
#-----------------------------------------------------------------------------
|
127
|
+
# Table.deleted_recs_counter
|
128
|
+
#-----------------------------------------------------------------------------
|
129
|
+
def self.deleted_recs_counter
|
130
|
+
self.db.tables[self][:deleted_recs_counter]
|
131
|
+
end
|
132
|
+
|
133
|
+
#-----------------------------------------------------------------------------
|
134
|
+
# Table.deleted_recs_counter=
|
135
|
+
#-----------------------------------------------------------------------------
|
136
|
+
def self.deleted_recs_counter=(value)
|
137
|
+
self.db.tables[self][:deleted_recs_counter]=(value)
|
138
|
+
end
|
139
|
+
|
140
|
+
#-----------------------------------------------------------------------------
|
141
|
+
# Table.read_header
|
142
|
+
#-----------------------------------------------------------------------------
|
143
|
+
def self.read_header
|
144
|
+
YAML.load(File.open(File.join(self.path, self.table_name.to_s +
|
145
|
+
TBL_HDR_EXT), 'r'))
|
146
|
+
end
|
147
|
+
|
148
|
+
#-----------------------------------------------------------------------------
|
149
|
+
# Table.write_header
|
150
|
+
#-----------------------------------------------------------------------------
|
151
|
+
def self.write_header(header)
|
152
|
+
File.open(File.join(self.path, self.table_name.to_s + TBL_HDR_EXT), 'w') do |f|
|
153
|
+
YAML.dump(header, f)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
#-----------------------------------------------------------------------------
|
158
|
+
# Table.init_table
|
159
|
+
#-----------------------------------------------------------------------------
|
160
|
+
def self.init_table
|
161
|
+
|
162
|
+
tbl_header = self.read_header
|
163
|
+
|
164
|
+
self.last_id_used = tbl_header[:last_id_used]
|
165
|
+
self.deleted_recs_counter = tbl_header[:deleted_recs_counter]
|
166
|
+
|
167
|
+
self.read_header[:columns].each do |c|
|
168
|
+
self.init_column(c[:name], c[:data_type], Object.full_const_get(c[:class])
|
169
|
+
)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
#-----------------------------------------------------------------------------
|
174
|
+
# Table.init_column
|
175
|
+
#-----------------------------------------------------------------------------
|
176
|
+
def self.init_column(col_name, col_def, col_class)
|
177
|
+
col = col_class.create(self, col_name, col_def)
|
178
|
+
|
179
|
+
self.columns << col
|
180
|
+
|
181
|
+
meth = <<-END_OF_STRING
|
182
|
+
def self.#{col_name}
|
183
|
+
self.columns.detect { |c| c.name == "#{col_name}".to_sym }
|
184
|
+
end
|
185
|
+
END_OF_STRING
|
186
|
+
self.class_eval(meth)
|
187
|
+
|
188
|
+
self.class_eval do
|
189
|
+
attr_accessor col_name
|
190
|
+
end
|
191
|
+
|
192
|
+
col.init_index if col.indexed?
|
193
|
+
end
|
194
|
+
|
195
|
+
#-----------------------------------------------------------------------------
|
196
|
+
# Table.close
|
197
|
+
#-----------------------------------------------------------------------------
|
198
|
+
def self.close
|
199
|
+
self.columns.each { |c| c.close }
|
200
|
+
end
|
201
|
+
|
202
|
+
#-----------------------------------------------------------------------------
|
203
|
+
# Table.get_all_recs
|
204
|
+
#-----------------------------------------------------------------------------
|
205
|
+
def self.get_all_recs
|
206
|
+
self.with_table do |fptr|
|
207
|
+
begin
|
208
|
+
while true
|
209
|
+
fpos = fptr.tell
|
210
|
+
rec_arr = Marshal.load(fptr)
|
211
|
+
|
212
|
+
yield rec_arr[1..-1], fpos unless rec_arr[0]
|
213
|
+
end
|
214
|
+
rescue EOFError
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
#-----------------------------------------------------------------------------
|
220
|
+
# Table.add_column
|
221
|
+
#-----------------------------------------------------------------------------
|
222
|
+
def self.add_column(col_name, col_def, col_class=Column)
|
223
|
+
self.init_column(col_name, col_def, col_class)
|
224
|
+
|
225
|
+
tbl_header = self.read_header
|
226
|
+
|
227
|
+
tbl_header[:columns] << { :name => col_name, :data_type => col_def,
|
228
|
+
:class => col_class.to_s }
|
229
|
+
|
230
|
+
self.write_header(tbl_header)
|
231
|
+
end
|
232
|
+
|
233
|
+
#-----------------------------------------------------------------------------
|
234
|
+
# Table.add_indexed
|
235
|
+
#-----------------------------------------------------------------------------
|
236
|
+
def self.add_indexed_column(col_name, col_def)
|
237
|
+
self.add_column(col_name, col_def, SkipListIndexColumn)
|
238
|
+
end
|
239
|
+
|
240
|
+
#-----------------------------------------------------------------------------
|
241
|
+
# Table.find
|
242
|
+
#-----------------------------------------------------------------------------
|
243
|
+
def self.find(*args)
|
244
|
+
# If searching for just one id or a group of ids...
|
245
|
+
if args[0].is_a?(Integer)
|
246
|
+
if args.size == 1
|
247
|
+
self.get_rec(args[0])
|
248
|
+
else
|
249
|
+
args.collect { |a| self.get_rec(a) }
|
250
|
+
end
|
251
|
+
else
|
252
|
+
result = []
|
253
|
+
# If passed a query block...
|
254
|
+
if block_given?
|
255
|
+
self.query.clear
|
256
|
+
query_start = true
|
257
|
+
sub_q = false
|
258
|
+
sub_q_start = false
|
259
|
+
sub_q_result = []
|
260
|
+
|
261
|
+
# Grab the query block
|
262
|
+
yield self
|
263
|
+
|
264
|
+
# Step through the query block...
|
265
|
+
self.query.each_with_index do |q,i|
|
266
|
+
# If this is the start of an #any sub-block within the main block,
|
267
|
+
# mark it as so and start grabbing the sub-block query.
|
268
|
+
if q == :any_begin
|
269
|
+
sub_q = :true
|
270
|
+
sub_q_start = true
|
271
|
+
sub_q_result = []
|
272
|
+
next
|
273
|
+
end
|
274
|
+
# If this is the end of an #any sub-block within the main block,
|
275
|
+
# mark it as so, and add the sub-block's query results to the current
|
276
|
+
# results of the main query.
|
277
|
+
if q == :any_end
|
278
|
+
sub_q = false
|
279
|
+
if query_start
|
280
|
+
query_start = false
|
281
|
+
result = sub_q_result
|
282
|
+
else
|
283
|
+
result = result & sub_q_result
|
284
|
+
end
|
285
|
+
sub_q_result = nil
|
286
|
+
next
|
287
|
+
end
|
288
|
+
|
289
|
+
# If currently within a sub-block query, execute and add it's results
|
290
|
+
# to the current sub-block query results.
|
291
|
+
if sub_q
|
292
|
+
if sub_q_start
|
293
|
+
sub_q_start = false
|
294
|
+
sub_q_result = q[0].send(q[1], *q[2])
|
295
|
+
else
|
296
|
+
sub_q_result = sub_q_result | q[0].send(q[1], *q[2])
|
297
|
+
end
|
298
|
+
next
|
299
|
+
end
|
300
|
+
|
301
|
+
# If this is the beginning of the query start a new result set,
|
302
|
+
# otherwise, add the results of the current line of the query to
|
303
|
+
# the existing result set.
|
304
|
+
if query_start
|
305
|
+
query_start = false
|
306
|
+
result = q[0].send(q[1], *q[2])
|
307
|
+
else
|
308
|
+
result = result & q[0].send(q[1], *q[2])
|
309
|
+
end
|
310
|
+
end
|
311
|
+
# If did not pass a query block, just grab all of the ids in the table...
|
312
|
+
else
|
313
|
+
result = self.id.keys
|
314
|
+
end
|
315
|
+
|
316
|
+
# If no matching records found, return nil.
|
317
|
+
if result.nil?
|
318
|
+
nil
|
319
|
+
# If user just wants first record, return first record.
|
320
|
+
elsif args[0] == :first
|
321
|
+
self.get_rec(result.first)
|
322
|
+
# If user specified a paramaters hash, see if they specified a limit and
|
323
|
+
# return that many records.
|
324
|
+
elsif args[1].is_a?(Hash)
|
325
|
+
if args[1].has_key?(:limit)
|
326
|
+
if args[1][:limit] == 1
|
327
|
+
self.get_rec(result.first)
|
328
|
+
else
|
329
|
+
result[0...args[1][:limit]].collect { |r| self.get_rec(r) }
|
330
|
+
end
|
331
|
+
end
|
332
|
+
# Otherwise, just return the whole damn result set.
|
333
|
+
else
|
334
|
+
result.collect { |r| self.get_rec(r) }
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
#-----------------------------------------------------------------------------
|
340
|
+
# Table.any
|
341
|
+
#-----------------------------------------------------------------------------
|
342
|
+
def self.any
|
343
|
+
self.query << :any_begin
|
344
|
+
yield
|
345
|
+
self.query << :any_end
|
346
|
+
end
|
347
|
+
|
348
|
+
#-----------------------------------------------------------------------------
|
349
|
+
# Table.get_rec
|
350
|
+
#-----------------------------------------------------------------------------
|
351
|
+
def self.get_rec(id)
|
352
|
+
fpos = self.id[id]
|
353
|
+
|
354
|
+
return nil if fpos.nil?
|
355
|
+
rec_arr = []
|
356
|
+
|
357
|
+
self.with_table(File::RDONLY) do |fptr|
|
358
|
+
fptr.seek(fpos)
|
359
|
+
rec_arr = Marshal.load(fptr)
|
360
|
+
end
|
361
|
+
|
362
|
+
raise IndexCorruptError, "Index references deleted record!", caller \
|
363
|
+
if rec_arr[0]
|
364
|
+
|
365
|
+
raise IndexCorruptError, "Index ID does not match table ID!", caller \
|
366
|
+
unless rec_arr[1] == id
|
367
|
+
|
368
|
+
rec = self.new(*rec_arr[1..-1])
|
369
|
+
return rec
|
370
|
+
end
|
371
|
+
|
372
|
+
#-----------------------------------------------------------------------------
|
373
|
+
# Table.with_table
|
374
|
+
#-----------------------------------------------------------------------------
|
375
|
+
def self.with_table(access='r')
|
376
|
+
begin
|
377
|
+
yield fptr = open(File.join(self.db.path, self.table_name.to_s + TBL_EXT),
|
378
|
+
access)
|
379
|
+
ensure
|
380
|
+
fptr.close
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
#-----------------------------------------------------------------------------
|
385
|
+
# initialize
|
386
|
+
#-----------------------------------------------------------------------------
|
387
|
+
def initialize(*values)
|
388
|
+
self.class.columns.zip(values).each do |c,v|
|
389
|
+
send("#{c.name}=", v)
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
#-----------------------------------------------------------------------------
|
394
|
+
# save
|
395
|
+
#-----------------------------------------------------------------------------
|
396
|
+
def save
|
397
|
+
self.class.columns.each do |c|
|
398
|
+
raise "Value required for #{c.name}!" if respond_to?('required?') and \
|
399
|
+
required?(c.name) and send(c.name).nil?
|
400
|
+
raise "Value required for #{c.name}!" if c.required? and send(c.name).nil?
|
401
|
+
end
|
402
|
+
|
403
|
+
if @id.nil?
|
404
|
+
tbl_header = self.class.read_header
|
405
|
+
self.class.last_id_used += 1
|
406
|
+
|
407
|
+
append_record(self.class.last_id_used,
|
408
|
+
self.class.column_names[1..-1].collect { |col_name| send(col_name) })
|
409
|
+
|
410
|
+
@id = self.class.last_id_used
|
411
|
+
tbl_header[:last_id_used] = self.class.last_id_used
|
412
|
+
self.class.write_header(tbl_header)
|
413
|
+
else
|
414
|
+
update_record(@id, self.class.columns[1..-1].collect { |c| send(c.name) })
|
415
|
+
end
|
416
|
+
|
417
|
+
return true
|
418
|
+
end
|
419
|
+
|
420
|
+
#-----------------------------------------------------------------------------
|
421
|
+
# destroy
|
422
|
+
#-----------------------------------------------------------------------------
|
423
|
+
def destroy
|
424
|
+
fpos_rec_start = self.class.id[@id]
|
425
|
+
|
426
|
+
self.class.with_table(File::RDWR) do |fptr|
|
427
|
+
fptr.seek(fpos_rec_start)
|
428
|
+
|
429
|
+
rec = Marshal.load(fptr)
|
430
|
+
|
431
|
+
raise IndexCorruptError, "Index ID does not match table ID!", caller \
|
432
|
+
unless rec[1] == @id
|
433
|
+
|
434
|
+
rec[0] = true
|
435
|
+
|
436
|
+
write_record(fptr, fpos_rec_start, Marshal.dump(rec))
|
437
|
+
increment_deleted_recs_counter
|
438
|
+
end
|
439
|
+
|
440
|
+
self.class.columns.each_with_index do |c,i|
|
441
|
+
if i == 0
|
442
|
+
c.remove_index_rec(@id)
|
443
|
+
elsif c.indexed?
|
444
|
+
c.remove_index_rec(send(c.name), @id)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
freeze
|
449
|
+
end
|
450
|
+
|
451
|
+
#-----------------------------------------------------------------------------
|
452
|
+
# Private Methods
|
453
|
+
#-----------------------------------------------------------------------------
|
454
|
+
private
|
455
|
+
|
456
|
+
#-----------------------------------------------------------------------------
|
457
|
+
# append_record
|
458
|
+
#-----------------------------------------------------------------------------
|
459
|
+
def append_record(id, values)
|
460
|
+
fpos = nil
|
461
|
+
|
462
|
+
self.class.with_table(File::RDWR) do |fptr|
|
463
|
+
fptr.seek(0, IO::SEEK_END)
|
464
|
+
fpos = fptr.tell
|
465
|
+
|
466
|
+
fpos = write_record(fptr, 'end', Marshal.dump([false, id].concat(values)))
|
467
|
+
end
|
468
|
+
|
469
|
+
self.class.columns.each_with_index do |c,i|
|
470
|
+
if i == 0
|
471
|
+
c.add_index_rec(id, fpos)
|
472
|
+
elsif c.indexed?
|
473
|
+
c.add_index_rec(values[i-1], id) unless values[i-1].nil?
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
#-----------------------------------------------------------------------------
|
479
|
+
# update_record
|
480
|
+
#-----------------------------------------------------------------------------
|
481
|
+
def update_record(id, values)
|
482
|
+
temp_instance = self.class.get_rec(id)
|
483
|
+
|
484
|
+
fpos_rec_start = self.class.id[id]
|
485
|
+
|
486
|
+
self.class.with_table(File::RDWR) do |fptr|
|
487
|
+
fptr.seek(fpos_rec_start)
|
488
|
+
|
489
|
+
old_rec = Marshal.load(fptr)
|
490
|
+
|
491
|
+
new_rec = Marshal.dump(old_rec[0..1].concat(values))
|
492
|
+
|
493
|
+
raise IndexCorruptError, "Index ID does not match table ID!", caller \
|
494
|
+
unless old_rec[1] == id
|
495
|
+
|
496
|
+
old_rec_length = fptr.tell - fpos_rec_start
|
497
|
+
|
498
|
+
if new_rec.length == old_rec_length
|
499
|
+
write_record(fptr, fpos_rec_start, new_rec)
|
500
|
+
else
|
501
|
+
fpos = write_record(fptr, 'end', new_rec)
|
502
|
+
increment_deleted_recs_counter
|
503
|
+
self.class.columns[0].add_index_rec(id, fpos)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
self.class.columns[1..-1].each do |c|
|
508
|
+
unless temp_instance.send(c.name) == send(c.name)
|
509
|
+
if c.indexed?
|
510
|
+
c.remove_index_rec(temp_instance.send(c.name), id)
|
511
|
+
c.add_index_rec(send(c.name), id) unless send(c.name).nil?
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
#-----------------------------------------------------------------------------
|
518
|
+
# increment_deleted_recs_counter
|
519
|
+
#-----------------------------------------------------------------------------
|
520
|
+
def increment_deleted_recs_counter
|
521
|
+
tbl_header = self.class.read_header
|
522
|
+
self.class.deleted_recs_counter += 1
|
523
|
+
tbl_header[:deleted_recs_counter] = self.class.deleted_recs_counter
|
524
|
+
self.class.write_header(tbl_header)
|
525
|
+
end
|
526
|
+
|
527
|
+
#-----------------------------------------------------------------------------
|
528
|
+
# write_record
|
529
|
+
#-----------------------------------------------------------------------------
|
530
|
+
def write_record(fptr, pos, record)
|
531
|
+
if pos == 'end'
|
532
|
+
fptr.seek(0, IO::SEEK_END)
|
533
|
+
else
|
534
|
+
fptr.seek(pos)
|
535
|
+
end
|
536
|
+
|
537
|
+
fpos_rec_start = fptr.tell
|
538
|
+
|
539
|
+
fptr.write(record)
|
540
|
+
return fpos_rec_start
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
|
545
|
+
class Collection
|
546
|
+
include Enumerable
|
547
|
+
|
548
|
+
def initialize(owner=nil, records=[])
|
549
|
+
@owner = owner
|
550
|
+
@records = records
|
551
|
+
end
|
552
|
+
|
553
|
+
def each
|
554
|
+
records.each { |rec| yield rec }
|
555
|
+
end
|
556
|
+
|
557
|
+
def <<(rec)
|
558
|
+
col_name = (@owner.class.table_name.to_s + "_id").to_sym
|
559
|
+
rec.send("#{col_name}=".to_sym, @owner.instance_eval { @id })
|
560
|
+
rec.save
|
561
|
+
end
|
562
|
+
|
563
|
+
def append(rec)
|
564
|
+
self << rec
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
end
|