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.
@@ -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