trix51db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/trix51db.rb +1865 -0
  2. metadata +46 -0
data/lib/trix51db.rb ADDED
@@ -0,0 +1,1865 @@
1
+ # :title:Trix51 Database Engine
2
+ # Trix51 is a simple file based database based on GDBM
3
+ # and Marshal, which provides a powerful query syntax,
4
+ # helper classes, one to one and one to many relationships,
5
+ # calculated fields, indexes and constraints. It is
6
+ # designed to replace simpler file based databases and
7
+ # provide more flexibility.
8
+ #
9
+ # * +Author:+ - April Ayres-Griffiths (aag6581@gmail.com)
10
+ # * +Copyright:+ - Copyright (c) 2012 April Ayres-Griffiths
11
+ # * +License:+ - Distributes under the Apache License
12
+
13
+ require 'gdbm'
14
+ require 'pp'
15
+ require 'time'
16
+ require 'logger'
17
+
18
+ BND = binding #:nodoc:
19
+
20
+ # This class handles top level management of the database engine
21
+ # and contains various constants and information about active
22
+ # databases. It also provides a logger based interface to the
23
+ # internals of the other database objects.
24
+ class Trix51
25
+ # Record prefix
26
+ RECORD_PREFIX = '@R@:'
27
+ # Metadata Prefix - For table structure records
28
+ META_PREFIX = '@T@:'
29
+ # Sequence Prefix - For column sequences for autoincrement fields.
30
+ SEQ_PREFIX = '@S@:'
31
+ # Used to join key values in a Record Key
32
+ KEY_JOIN = ':'
33
+
34
+ @@databases = []
35
+ @@class_to_database = {}
36
+ @@class_to_tablename = {}
37
+ @@table_to_database = {}
38
+ @@defer_classref = false
39
+ @@logger = Logger.new('trix51.log')
40
+ @@logger.level = Logger::WARN
41
+
42
+ def Trix51.add_connection( database )
43
+ @@databases.push( database )
44
+ end
45
+
46
+ def Trix51.add_helper( classname, database, table )
47
+ @@class_to_database[ classname ] = database
48
+ @@class_to_tablename[ classname ] = table
49
+ @@table_to_database[ table.tablename.to_sym ] = database
50
+ self.create_helper_code( classname )
51
+ end
52
+
53
+ # Return a list of active databases managed by the engine
54
+ def Trix51.connections
55
+ return @@databases
56
+ end
57
+
58
+ # Map a class name (String) to a Trix51::Table reference.
59
+ def Trix51.class_to_table( cn )
60
+ return @@class_to_tablename[ cn ]
61
+ end
62
+
63
+ # Map a class name (String) to a Trix51::Database reference.
64
+ def Trix51.class_to_database( cn )
65
+ return @@class_to_database[ cn ]
66
+ end
67
+
68
+ # Map a tablename to its database.
69
+ def Trix51.table_to_database( tbl )
70
+ return @@table_to_database[ tbl ]
71
+ end
72
+
73
+ # Log a debug message to the log.
74
+ def Trix51.debug( msg )
75
+ @@logger.debug( msg )
76
+ end
77
+
78
+ # Log an info level message to the log.
79
+ def Trix51.info( msg )
80
+ @@logger.info( msg )
81
+ end
82
+
83
+ # Log a warning message to the log.
84
+ def Trix51.warn( msg )
85
+ @@logger.warn( msg )
86
+ end
87
+
88
+ # Log an error to the log.
89
+ def Trix51.error( msg )
90
+ @@logger.error( msg )
91
+ end
92
+
93
+ # Log a fatal message to the log and exit.
94
+ def Trix51.fatal( msg )
95
+ @@logger.debug( msg )
96
+ end
97
+
98
+ # Generate a helper class for a table.
99
+ def Trix51.create_helper_code( classname )
100
+ return if (classname == 'Trix51::Tuple') or (@@defer_classref == true)
101
+ code = <<TEMPLATE
102
+ class #{classname} < Trix51::Tuple
103
+
104
+ def #{classname}._table
105
+ return Trix51.class_to_table( '#{classname}' )
106
+ end
107
+
108
+ def #{classname}._db
109
+ return Trix51.class_to_database( '#{classname}' )
110
+ end
111
+
112
+ def #{classname}.find_or_create( *args )
113
+
114
+ return self._table.find_or_create( *args )
115
+
116
+ end
117
+
118
+ def #{classname}.select( *args )
119
+ return self._table.select( *args )
120
+ end
121
+
122
+ def #{classname}.delete( *args )
123
+ return select._table.delete( *args )
124
+ end
125
+
126
+ def #{classname}.update( *args )
127
+ return self._table.update( *args )
128
+ end
129
+
130
+ def #{classname}.first( *args )
131
+ return self._table.first( *args )
132
+ end
133
+
134
+ def #{classname}.all
135
+ return self._table.all
136
+ end
137
+
138
+ def #{classname}.dump
139
+ self._table.dump
140
+ end
141
+
142
+ end
143
+ TEMPLATE
144
+ #puts code
145
+
146
+ eval( code, BND )
147
+ end
148
+
149
+ # Sets the defer_classref flag to true or false
150
+ def Trix51.defer_classref=( toggle )
151
+ @@defer_classref = toggle
152
+ end
153
+
154
+ # Returns the value of the defer_classref flag
155
+ def Trix51.defer_classref
156
+ return @@defer_classref
157
+ end
158
+
159
+ END {
160
+ Trix51.connections.each do |db|
161
+ db.close
162
+ end
163
+ }
164
+
165
+ end
166
+
167
+ # Class for managing a single Trix51 based database, allowing access to tables.
168
+ class Trix51::Database < Trix51
169
+
170
+ # Construct a Trix51::Database object.
171
+ #
172
+ # ==== Arguments
173
+ # * +path:+ - Path to the database file.
174
+ # * +database:+ - Name of the database file.
175
+ #
176
+ # ====Example
177
+ # require 'trix51database'
178
+ #
179
+ # db = Trix51::Database.new( path: './data', database: 'animals.t51' )
180
+ #
181
+
182
+ def initialize( *args )
183
+
184
+ @meta_prefix = Trix51::META_PREFIX
185
+ @tables = {}
186
+ @args = {
187
+ :path => './trix',
188
+ :database => 'database.db'
189
+ }
190
+
191
+ args.each { |key|
192
+ key.each_pair { |k,v|
193
+ @args[k] = v
194
+ }
195
+ }
196
+
197
+ if not Dir.exist?( @args[:path] ) then
198
+ Dir.mkdir( @args[:path] )
199
+ end
200
+
201
+ # now create / open the dbm file
202
+ begin
203
+ @dbm = GDBM.new( "#{@args[:path]}/#{@args[:database]}" )
204
+ rescue => e
205
+ raise Trix51::DatabaseError, "Unable to open #{@args[:path]}/#{@args[:database]}: #{e.to_s}"
206
+ end
207
+
208
+ Trix51.add_connection( self )
209
+
210
+ list = self.tables
211
+ list.each do |tag|
212
+ tagref = self.get_table( tag )
213
+ Trix51.add_helper( tagref.classname, self, tagref )
214
+ end
215
+
216
+ end
217
+
218
+ # Close the database, cleaning up a persisting indexes to disk.
219
+ def close
220
+ return if @dbm.closed?
221
+ @tables.each_value do |tblref|
222
+ tblref.indices_save( @args[:path] )
223
+ end
224
+ @dbm.close
225
+ end
226
+
227
+ # Returns a reference object for a given table
228
+ # Raises an exception if the table does not exist.
229
+ # ==== Arguments
230
+ # * +tablename:+ - The name of the table (symbol).
231
+ # ==== Example
232
+ # animal = db.get_table( :animal )
233
+ # Note: you can also use the alias db.animal to access it.
234
+ #
235
+ def get_table( tablename )
236
+
237
+ # do we have a cached object?
238
+ mref = @dbm[ Trix51::META_PREFIX+tablename.to_s ]
239
+
240
+ if @tables[tablename].nil? then
241
+ mref = @dbm[ Trix51::META_PREFIX+tablename.to_s ]
242
+ raise Trix51::NotFoundError, "Table #{tablename.to_s} does not exist." if mref.nil?
243
+ @tables[tablename] = Trix51::Table.new( tablename.to_s, @dbm )
244
+ @tables[tablename].indices_load( @args[:path] )
245
+ end
246
+
247
+ return @tables[tablename]
248
+
249
+ end
250
+
251
+ # Create a new table.
252
+ #
253
+ # ==== Arguments
254
+ # * +tablename:+ - The name of the table (symbol).
255
+ # * +args:+ - A hash specifying the structure of the table.
256
+ # * +classname:+ - The name of a helper class.
257
+ #
258
+ # ==== Example
259
+ # require 'trix51database'
260
+ #
261
+ # animal = db.create_table(
262
+ # :animal,
263
+ # {
264
+ # id: { datatype: 'integer', key: true, autoincrement: true },
265
+ # species_id: { datatype: 'integer', required: true, indexed: true },
266
+ # species: { datatype: 'record', source: :species_id, table: :species, dest: :id },
267
+ # name: { datatype: 'string', required: true, unique: true },
268
+ # created_date: { datatype: 'datetime', default: '#{Time.new.to_s}' },
269
+ # number_of_legs: { datatype: 'integer', default: 4 },
270
+ # number_of_eyes: { datatype: 'integer', default: 2 },
271
+ # legs_and_eyes: { datatype: 'integer', calculation: 'number_of_legs + number_of_eyes' }
272
+ # },
273
+ # 'Animal'
274
+ # )
275
+ #
276
+ # ==== Field types
277
+ # Valid field types are 'string', 'integer', 'datetime', 'float', 'boolean', 'record', 'resultset'
278
+ #
279
+ # The 'calculation:' clause fields are evaluated based on other fields in the table.
280
+ def create_table( tablename, args, classname = 'Trix51::Tuple' )
281
+
282
+ mref = @dbm[ Trix51::META_PREFIX+tablename.to_s ]
283
+
284
+ if not mref.nil? then
285
+ raise Trix51::TableExistsError, "Attempt to create a table that exists..."
286
+ end
287
+
288
+ # create it
289
+ @dbm[ Trix51::META_PREFIX+tablename.to_s ] = Marshal.dump(
290
+ tablename: tablename,
291
+ structure: args,
292
+ classname: classname
293
+ )
294
+ @tables[tablename] = Trix51::Table.new( tablename, @dbm )
295
+
296
+ Trix51.add_helper( @tables[tablename].classname, self, @tables[tablename] )
297
+
298
+ return @tables[tablename]
299
+
300
+ end
301
+
302
+ # Provides a list of the tables in the database.
303
+ def tables
304
+ list = []
305
+ @dbm.each_key do |k|
306
+ if k.match( Trix51::META_PREFIX )
307
+ list.push( k.gsub( Trix51::META_PREFIX, '' ).to_sym )
308
+ end
309
+ end
310
+ return list
311
+ end
312
+
313
+ def method_missing( methodid ) #:nodoc:
314
+ if self.tables.include?( methodid ) then
315
+ return self.get_table( methodid )
316
+ end
317
+ raise Trix51::NotFoundError, "Table #{methodid.to_s } does not exist"
318
+ end
319
+
320
+ # Return true or false if a given table exists.
321
+ def has_table?( table )
322
+ return (self.tables.include?( table ))
323
+ end
324
+
325
+ # Database accessor
326
+ attr_accessor :dbm
327
+
328
+ end
329
+
330
+ # Exception class
331
+ class Trix51::DatabaseError < StandardError
332
+ end
333
+
334
+ # Exception class
335
+ class Trix51::NotFoundError < Trix51::DatabaseError
336
+ end
337
+
338
+ # Exception class
339
+ class Trix51::TableExistsError < Trix51::DatabaseError
340
+ end
341
+
342
+ # Exception class
343
+ class Trix51::ConstraintError < Trix51::DatabaseError
344
+ end
345
+
346
+ # Exception class
347
+ class Trix51::TypeError < Trix51::DatabaseError
348
+ end
349
+
350
+ # Exception class
351
+ class Trix51::ExistsError < Trix51::DatabaseError
352
+ end
353
+
354
+ # Represents a single record from the database.
355
+ class Trix51::Tuple < Trix51
356
+
357
+ # Create a new tuple object
358
+ #
359
+ # Arguments:
360
+ # table:: A Trix51::Table object
361
+ # urid:: A unique record identifier
362
+ # anonhash:: An optional anonymous hash containing the data for the tuple.
363
+ def initialize( table, urid, anonhash=nil )
364
+ @table = table
365
+ @data = Marshal.load( @table.dbref[urid] ) if anonhash.nil?
366
+ @data = anonhash if not anonhash.nil?
367
+ @newdata = {}
368
+ @urid = urid
369
+ end
370
+
371
+ # Returns true or false if the record has been updated.
372
+ def has_updates?
373
+ return (@newdata.keys.length > 0)
374
+ end
375
+
376
+ def method_missing( methodId, *args ) #:nodoc:
377
+
378
+ name = methodId.to_s
379
+
380
+ basename = name.gsub( '=', '' ).to_sym
381
+
382
+ if not @table.structure[basename].nil? then
383
+ #return @data[name]
384
+ if name.match( '=' ) then
385
+ @newdata[basename] = args.shift
386
+ #puts "setting value of #{basename} to #{@newdata[basename]}.."
387
+ else
388
+ return @newdata[basename] || @data[basename] if ['string', 'integer', 'datetime'].include?( self.table.structure[basename][:datatype])
389
+
390
+ # derived fields
391
+ dt = self.table.structure[basename][:datatype]
392
+ "------------------------------"
393
+
394
+ if dt == 'record' then
395
+ # handle one to one
396
+ #pp self.table.structure[basename][:table]
397
+ dbref = Trix51.table_to_database( self.table.structure[basename][:table] )
398
+ #pp dbref
399
+ tblref = dbref.get_table( self.table.structure[basename][:table] )
400
+ srcref = self.table.structure[basename][:source]
401
+ dstref = self.table.structure[basename][:dest]
402
+ return tblref.first( dstref => @data[srcref] )
403
+ end
404
+
405
+ if dt == 'resultset' then
406
+ dbref = Trix51.table_to_database( self.table.structure[basename][:table] )
407
+ tblref = dbref.get_table( self.table.structure[basename][:table] )
408
+ srcref = self.table.structure[basename][:source]
409
+ dstref = self.table.structure[basename][:dest]
410
+ #puts "#{tblref.tablename}.#{dstref} = #{@data[srcref]}"
411
+ h = { dstref => @data[srcref].to_s }
412
+ return tblref.select_hash( h )
413
+ end
414
+
415
+ if dt == 'calculated' then
416
+ c = self.table.structure[basename][:calculation]
417
+ return eval( c ).to_s
418
+ end
419
+
420
+ end
421
+ else
422
+ raise Trix51::NotFoundError, "No such field #{methodId}"
423
+ end
424
+
425
+ end
426
+
427
+ # post any record updates, update any indexes and keys.
428
+ def update
429
+
430
+ ref = @data.merge( @newdata ) { |key, oldval, newval|
431
+ (not newval.nil?) ? newval : oldval
432
+ }
433
+
434
+ # ref now represents the new record
435
+ newurid = self.table.get_hash_key( ref )
436
+
437
+ @newdata.each_pair do |fieldname,fieldvalue|
438
+ raise Trix51::ConstraintError, "Update would violate unique constraint on field #{fieldname}" unless self.table.index_chk_unique_update( fieldname, fieldvalue, @urid )
439
+ end
440
+
441
+ if newurid == @urid then
442
+ self.table.index_update( @urid, newurid, @newdata )
443
+ # simple update
444
+ self.table.dbref[ @urid ] = Marshal.dump( ref )
445
+ @data = ref
446
+ @newdata = {}
447
+
448
+ #puts "Key is unchanged"
449
+
450
+ return true
451
+ end
452
+
453
+ # if we are here the key has changed
454
+ # make sure the new one does not exist
455
+ if not self.table.dbref[ newurid ].nil? then
456
+ @newdata = {}
457
+ raise Trix51::ConstraintError, "Update to record would violate unique contraints"
458
+ end
459
+
460
+ # if we are here, add the new record
461
+ self.table.index_update( @urid, newurid, @newdata )
462
+ #puts "Re-keying the record due to a change in unique key"
463
+ self.table.dbref[ newurid ] = Marshal.dump( ref )
464
+ @data = @newdata
465
+ @newdata = {}
466
+ @urid = newurid
467
+
468
+ return true
469
+
470
+ end
471
+
472
+ # Accessors for the tuple class.
473
+ attr_accessor :table, :data, :urid
474
+
475
+ end
476
+
477
+ # This class represents a set of records which can either be a table, query
478
+ # result, or the result of a group expression.
479
+ class Trix51::TupleSet
480
+
481
+ # Creates a new TupleSet object, the core class for tables / query results.
482
+ def initialize( tablename, dataref, options )
483
+ @tablename = tablename
484
+ @dbref = dataref
485
+ @options = {}
486
+ @indices = {}
487
+ @sort_order = nil
488
+ @options = options
489
+ @updateable = false
490
+ @meta = nil
491
+ if self.table? then
492
+ #pp "table"
493
+ @updateable = true
494
+ @meta = self.meta
495
+ else
496
+ @meta = {}
497
+ #pp "query or view"
498
+ @meta[:tablename] = '__query'
499
+ self.clone_structure( @options[:tableref] )
500
+ @meta[:classname] = 'Trix51::Tuple'
501
+ end
502
+
503
+ #pp @meta
504
+
505
+ @classname = @meta[:classname] || 'Trix51::Tuple'
506
+
507
+ #pp @classname
508
+
509
+ if @classname != 'Trix51::Tuple' then
510
+ eval(
511
+ "
512
+ class #{@classname} < Trix51::Tuple
513
+
514
+ end
515
+ ", BND
516
+ )
517
+ end
518
+
519
+ end
520
+
521
+ def build_temp_idx( field ) #:nodoc:
522
+
523
+ if (self.indexed?( field )) and (not self.indices[field].nil?) and (field != :random) then
524
+ return @indices[field].clone
525
+ end
526
+
527
+ res = {}
528
+ self.dbref.each_pair { |k, v|
529
+ next unless self.table_record?( k )
530
+ record = {}
531
+ if v.class.name == 'String' then
532
+ record = Marshal.load( v )
533
+ else
534
+ record = v.data
535
+ end
536
+ rv = record[field] if field != :random
537
+ rv = rand() if field == :random
538
+ rlist = res[rv] || []
539
+ rlist.push( k )
540
+ res[rv] = rlist
541
+ }
542
+ return res
543
+ end
544
+
545
+ def build_sorted_keys( field, dir='asc' ) #:nodoc:
546
+ # sort records by list of fields
547
+ idx = self.build_temp_idx( field )
548
+ sk = []
549
+ idx.keys.sort.each do |fieldvalue|
550
+ keyref = idx[fieldvalue]
551
+ keyref.each do |key|
552
+ sk.push( key )
553
+ end
554
+ end
555
+ return sk if dir == 'asc'
556
+ return sk.reverse if dir == 'desc'
557
+ end
558
+
559
+ def sorted_keys #:nodoc:
560
+ if (not @options[:sorted].nil?) and (@options[:sorted] == true) then
561
+ #return self.build_sorted_keys( @options['sortfield'].to_s, @options['sortorder'] )
562
+ if @sort_order.nil? then
563
+ @sort_order = self.build_sorted_keys( @options[:sortfield], @options[:sortorder] )
564
+ end
565
+ return @sort_order
566
+ end
567
+ return @dbref.keys
568
+ end
569
+
570
+ def record_keys #:nodoc:
571
+ list = []
572
+ @dbref.each_key do |k|
573
+ list.push( k ) if self.table_record?(k)
574
+ end
575
+ return list
576
+ end
577
+
578
+ # Returns the number of records in the database.
579
+ def size
580
+ c = 0
581
+ @dbref.each_key do |k|
582
+ c = c + 1 if k.match( self.record_prefix )
583
+ end
584
+ return c
585
+ end
586
+
587
+ def clone_structure( tupleset ) #:nodoc:
588
+ @meta[:structure] = tupleset.structure.clone
589
+ end
590
+
591
+ def structure #:nodoc:
592
+ return @meta[:structure]
593
+ end
594
+
595
+ # Returns the default for the specified field.
596
+ def default( fieldname )
597
+
598
+ unless self.exists?( fieldname) then
599
+ raise Trix51::NotFoundError, "field not found: #{fieldname} in table #{@meta[:tablename]}"
600
+ end
601
+
602
+ ref = self.structure[fieldname]
603
+
604
+ #pp ref
605
+
606
+ if (ref[:autoincrement] == true) and (ref[:datatype] == 'integer') then
607
+ return self.find_or_create_seq( fieldname )
608
+ end
609
+
610
+ if (not ref[:default].nil?) then
611
+ d = ref[:default]
612
+ if m = d.to_s.match( /^\#\{(.+)\}$/ ) then
613
+ return eval(m[1])
614
+ end
615
+ return d
616
+ end
617
+
618
+ return nil
619
+
620
+ end
621
+
622
+ # Returns true / false if the table contains field
623
+ def exists?( fieldname )
624
+ return (not self.structure[fieldname].nil?)
625
+ end
626
+
627
+ def find_or_create_seq( fieldname ) #:nodoc:
628
+
629
+ seq_name =self.seq_prefix( fieldname )
630
+ #puts "Sequence is called: #{seq_name}"
631
+
632
+ if @dbref[seq_name].nil? then
633
+ @dbref[seq_name] = '2'
634
+ return 1
635
+ else
636
+ v = @dbref[seq_name].to_i
637
+ @dbref[seq_name] = (v+1).to_s
638
+ return v
639
+ end
640
+
641
+ end
642
+
643
+ # insert record into the database using a hash
644
+ # ==== Example
645
+ # daisy = animal.insert_hash( {
646
+ # name: 'cow',
647
+ # species_id: 2
648
+ # } )
649
+ def insert_hash( hashdata ) #:nodoc:
650
+
651
+ ref = self.empty_rec.merge( hashdata ) { |key, oldval, newval|
652
+ (not newval.nil?) ? newval : oldval
653
+ }
654
+
655
+ #pp ref
656
+
657
+ flt = {}
658
+
659
+ ref.each_key { |key|
660
+ flt[key] = ref[key] if self.exists?( key )
661
+ }
662
+
663
+ #pp flt
664
+
665
+ self.check_required_fields( flt )
666
+
667
+ urid = self.get_hash_key( flt )
668
+
669
+ #puts "Record key: #{urid}"
670
+
671
+ raise Trix51::ConstraintError, "unique constraint has been violated (#{urid})" if @dbref[urid]
672
+
673
+ @dbref[urid] = Marshal.dump( flt )
674
+
675
+ #puts "INSERT "+@dbref[urid]
676
+
677
+ self.index_add( urid, flt )
678
+
679
+ return self.init_record( urid )
680
+ end
681
+
682
+ def comparison( value, operand, target ) #:nodoc:
683
+
684
+ #puts "[#{value}] #{operand.to_s} [#{target}]"
685
+
686
+ if operand.to_s == 'is' then
687
+ return (value.to_s == target.to_s)
688
+ elsif operand.to_s == 'not' then
689
+ return (value.to_s != target.to_s)
690
+ elsif operand.to_s == 'like' then
691
+ target = '^' + target.gsub( /\%/, '.*' ) + '$'
692
+ return (value.to_s.match( target ))
693
+ elsif operand.to_s == 'unlike' then
694
+ target = '^' + target.gsub( /\%/, '.*' ) + '$'
695
+ return (not value.to_s.match( target ))
696
+ elsif operand.to_s == 'under' then
697
+ return (value.to_f < target.to_f)
698
+ elsif operand.to_s == 'over' then
699
+ return (value.to_f > target.to_f)
700
+ elsif operand.to_s == 'before' then
701
+ return (value.to_s < target.to_s)
702
+ elsif operand.to_s == 'after' then
703
+ return (value.to_s > target.to_s)
704
+ elsif operand.to_s == 'between' then
705
+ raise Trix51::TypeError, "BETWEEN clause requires two values" unless (target.class.name == 'Array') and (target.length > 1)
706
+ s = target.sort
707
+ lower_bound = s.shift
708
+ upper_bound = s.pop
709
+ return ( ( value.to_s >= lower_bound.to_s ) and ( value.to_s <= upper_bound.to_s ) )
710
+ elsif operand.to_s == 'in' then
711
+ raise Trix51::TypeError, "IN clause requires an array" unless (target.class.name == 'Array') and (target.length >= 1)
712
+ return (target.include?( value ))
713
+ end
714
+
715
+ end
716
+
717
+ def eval_in_context( hashdata, expression ) #:nodoc:
718
+ inits = []
719
+ hashdata.each_pair do |k,v|
720
+ next if v.nil?
721
+ str = ''
722
+ str = "#{k} = #{v}" if v.class.name != 'String'
723
+ str = "#{k} = \"#{v}\"" if v.class.name == 'String'
724
+ inits.push( str )
725
+ end
726
+ inits.push( expression )
727
+ str = inits.join("\n")
728
+ return eval( str )
729
+ end
730
+
731
+ def hash_compare( h1, h2 ) #:nodoc:
732
+
733
+ # Comparitors: like, not_like, not, equals, lt, gt
734
+ #
735
+
736
+ h1.keys.each { |key|
737
+ #pp key
738
+ #pp self.structure[key]
739
+ if self.structure[key][:datatype] == 'calculated' then
740
+ h2[key] = self.eval_in_context( h2, self.structure[key][:calculation] )
741
+ end
742
+ v1 = h1[key]
743
+ if v1.class.name != 'Hash' then
744
+ return false if not comparison( h2[key], 'is', h1[key] )
745
+ else
746
+ v1.each_pair do |operand, target|
747
+ return false if not comparison( h2[key], operand, target )
748
+ end
749
+ end
750
+ }
751
+ return true
752
+ end
753
+
754
+ # delete records matching the specified hash
755
+ # ==== Example
756
+ # num_deleted = animal.delete_hash( {
757
+ # name: 'cow'
758
+ # } )
759
+ def delete_hash( hashdata ) #:nodoc:
760
+
761
+ matches = self.select_hash( hashdata )
762
+
763
+ matches.each_record do |record|
764
+ urid = record.urid
765
+ @dbref.delete( urid )
766
+ self.index_remove( urid )
767
+ end
768
+
769
+ return matches.length
770
+
771
+ end
772
+
773
+ # Updates any records matching the values in hash
774
+ # ==== Example
775
+ # animal.update_hash ( { name: 'cow' } ) do |record|
776
+ # record.name = 'sheep' # baaa?
777
+ # end
778
+ def update_hash( hashdata ) #:nodoc:
779
+
780
+ matches = self.select_hash( hashdata )
781
+
782
+ matches.each { |record|
783
+ yield record
784
+ record.update if record.has_updates?
785
+ }
786
+
787
+ return matches.length
788
+ end
789
+
790
+ # Updates records matching the query.
791
+ # ==== Example
792
+ # animal.update( number_of_eyes: 2, number_of_legs: 4 ) do |record|
793
+ # record.number_of_eyes = 8
794
+ # end
795
+ def update( *args )
796
+
797
+ hashdata = args_to_hash( *args )
798
+
799
+ matches = self.select_hash( hashdata )
800
+
801
+ matches.each { |record|
802
+ yield record
803
+ record.update if record.has_updates?
804
+ }
805
+
806
+ return matches.length
807
+ end
808
+
809
+ # Deletes records matching the query.
810
+ # ==== Example
811
+ # count = animal.delete( species_id: 2 )
812
+ def delete( *args )
813
+ return self.delete_hash( args_to_hash(*args) )
814
+ end
815
+
816
+ # Returns a Trix51::ResultSet containing records matching the query.
817
+ # ==== Example
818
+ # records = animal.select(
819
+ # name: { like: 'mon%' },
820
+ # created_date: { between: [ '2012-04-11', 2012-12-31' ],
821
+ # number_of_eyes: 2,
822
+ # number_of_legs: { in: [2,4] }
823
+ # )
824
+ #
825
+ # ==== Query operands
826
+ # * +is:+ - true if value matches
827
+ # * +in:+ - true if value is in specified list
828
+ # * +not:+ - true if value does not match
829
+ # * +under:+ - true if value is less than
830
+ # * +over:+ - true if value is greater than
831
+ # * +like:+ - true if value matches pattern ( '%' acts as wildcard )
832
+ # * +unlike:+ - true if value does not match pattern ( '%' acts as wildcard )
833
+ # * +between:+ - true if value is between specified values
834
+ def select( *args )
835
+ return self.select_hash( args_to_hash(*args) )
836
+ end
837
+
838
+ # Alias for select()
839
+ def having( *args )
840
+ return self.select_hash( args_to_hash(*args) )
841
+ end
842
+
843
+ # Returns the first record matching the query (Trix51::Tuple)
844
+ def first( *args )
845
+ res = self.select_hash( args_to_hash(*args) )
846
+ if res.count > 0 then
847
+ res.each_record do |x|
848
+ return x
849
+ end
850
+ return nil
851
+ end
852
+ end
853
+
854
+ # Returns all the records.
855
+ def all
856
+ return self.select_hash( {} )
857
+ end
858
+
859
+ # Deletes all records in the table.
860
+ def empty
861
+ return self.delete_hash( {} )
862
+ end
863
+
864
+ # Inserts a record into the database using named arguments.
865
+ def insert( *args )
866
+ return self.insert_hash( args_to_hash(*args) )
867
+ end
868
+
869
+ def args_to_hash( *args ) #:nodoc:
870
+ h = {}
871
+ args.each { |rec|
872
+ rec.each_pair { |k,v|
873
+ h[k] = v
874
+ }
875
+ }
876
+ #pp h
877
+ return h
878
+ end
879
+
880
+ def tuple_by_key( *args ) #:nodoc:
881
+ keys = self.key_fields
882
+ h = {}
883
+ keys.each { |key|
884
+ h[key] = args.shift
885
+ }
886
+ #pp h
887
+ urid = self.get_hash_key( h )
888
+
889
+ s = dbref[urid]
890
+
891
+ return Marshal.load( s ) if self.table? and not s.nil?
892
+ return s if not s.nil?
893
+
894
+ return nil
895
+ end
896
+
897
+ def init_record( urid ) #:nodoc:
898
+
899
+ #pp self.table?
900
+ #pp @classname
901
+
902
+ unless self.table? then
903
+ return @dbref[ urid ]
904
+ end
905
+
906
+ r = nil
907
+ if (@classname == 'Trix51::Tuple') or (Trix51.defer_classref == true) then
908
+ r = Trix51::Tuple.new( self, urid )
909
+ else
910
+ r = eval( "#{@classname}.new( self, urid )" )
911
+ end
912
+ return r
913
+ end
914
+
915
+ def check_required_fields( hashdata ) #:nodoc:
916
+
917
+ # make sure all required fields are populated
918
+ self.structure.each_key { |key|
919
+ next if (not self.structure[key][:key] == true) and (not self.structure[key][:required] == true)
920
+ raise Trix51::ConstraintError, "required or key field #{self.tablename}.#{key} is nil" if hashdata[key].nil?
921
+ raise Trix51::ConstraintError, "unique constraint violated for field #{self.tablename}.#{key} [#{hashdata[key]}]" if not self.index_chk_unique( key, hashdata[key] )
922
+ }
923
+
924
+ end
925
+
926
+ # Returns the key fields for the table
927
+ def key_fields
928
+ res = []
929
+ self.structure.keys.sort.each { |key|
930
+ #puts key
931
+ res.push(key) if self.structure[key][:key] == true
932
+ }
933
+ return res
934
+ end
935
+
936
+ def get_hash_key( hashdata ) #:nodoc:
937
+ keys = self.key_fields
938
+
939
+ v = []
940
+ keys.each { |k|
941
+ v.push( hashdata[k] )
942
+ }
943
+
944
+ return self.record_prefix + ':' + v.join( Trix51::KEY_JOIN )
945
+ end
946
+
947
+ def empty_rec #:nodoc:
948
+ ref = {}
949
+ self.structure.each_key { |key|
950
+ ref[key] = self.default( key )
951
+ }
952
+ return ref
953
+ end
954
+
955
+ def method_missing( meth, *args ) #:nodoc:
956
+ name = meth.to_s
957
+
958
+ if m = name.match( /^select_by_(.+)/ ) then
959
+
960
+ fields = m[1].split('_and_')
961
+ h = {}
962
+ while fields.length > 0 do
963
+ n = fields.shift
964
+ v = args.shift
965
+ h[n] = v
966
+ end
967
+
968
+ return self.select_hash( h )
969
+
970
+ end
971
+
972
+ if m = name.match( /^delete_by_(.+)/ ) then
973
+
974
+ fields = m[1].split('_and_')
975
+ h = {}
976
+ while fields.length > 0 do
977
+ n = fields.shift
978
+ v = args.shift
979
+ h[n] = v
980
+ end
981
+
982
+ return self.delete_hash( h )
983
+
984
+ end
985
+
986
+ if m = name.match( /^update_by_(.+)/ ) then
987
+
988
+ fields = m[1].split('_and_')
989
+ h = {}
990
+ while fields.length > 0 do
991
+ n = fields.shift
992
+ v = args.shift
993
+ h[n] = v
994
+ end
995
+
996
+ matches = self.select_hash( h )
997
+
998
+ matches.each { |record|
999
+ yield record
1000
+ record.update if record.has_updates?
1001
+ }
1002
+
1003
+ return matches.length
1004
+
1005
+ end
1006
+
1007
+ end
1008
+
1009
+ # Find (select) or create a record with the named arguments.
1010
+ def find_or_create( *args )
1011
+
1012
+ res = self.first( *args )
1013
+
1014
+ if res.nil? then
1015
+ return self.insert_hash( args_to_hash(*args) )
1016
+ end
1017
+
1018
+ return res
1019
+
1020
+ end
1021
+
1022
+ # returns true or false if a table field is indexed.
1023
+ def indexed?( field )
1024
+ return false unless self.exists?( field )
1025
+ return ( (self.key?(field)) or (self.unique?(field)) or ( (not self.structure[field][:indexed].nil?) and (self.structure[field][:indexed] == true) ) )
1026
+ end
1027
+
1028
+ # returns true or false if a table field must be unique
1029
+ def unique?( field )
1030
+ return false unless self.exists?( field )
1031
+ return ( (not self.structure[field][:unique].nil?) and (self.structure[field][:unique] == true) )
1032
+ end
1033
+
1034
+ # returns true or false if a table field forms part of the record key
1035
+ def key?( field )
1036
+ return false unless self.exists?( field )
1037
+ return ( (not self.structure[field][:key].nil?) and (self.structure[field][:key] == true) )
1038
+ end
1039
+
1040
+ # Rebuilds the index for the given field.
1041
+ def index_build( field )
1042
+ if not self.indexed?(field) then
1043
+ raise Trix51::NotFoundError, "field #{field} is not an indexed field"
1044
+ end
1045
+ #puts "Building index for field #{field}"
1046
+ res = {}
1047
+ self.dbref.each_pair { |k, v|
1048
+ next unless self.table_record?( k )
1049
+ record = Marshal.load( v )
1050
+ rv = record[field]
1051
+ rlist = res[rv] || []
1052
+ rlist.push( k )
1053
+ res[rv] = rlist
1054
+ }
1055
+ @indices[field] = res
1056
+ end
1057
+
1058
+ def index_add( urid, record ) #:nodoc:
1059
+ record.keys.each { |fieldname|
1060
+ if self.indexed?(fieldname) then
1061
+ # check index exists
1062
+ if self.indices[fieldname].nil? then
1063
+ self.index_build( fieldname )
1064
+ else
1065
+ # partial add
1066
+ res = self.indices[fieldname]
1067
+ rv = record[fieldname]
1068
+ rlist = res[rv] || []
1069
+ rlist.push( urid )
1070
+ res[rv] = rlist
1071
+ end
1072
+ end
1073
+ }
1074
+ end
1075
+
1076
+ def index_update( oldurid, newurid, record ) #:nodoc:
1077
+ record.keys.each { |fieldname|
1078
+ if self.indexed?(fieldname) then
1079
+ # check index exists
1080
+ if self.indices[fieldname].nil? then
1081
+ self.index_build( fieldname )
1082
+ else
1083
+ # partial update
1084
+ self.index_remove( oldurid )
1085
+ self.index_add( newurid, record )
1086
+ end
1087
+ end
1088
+ }
1089
+ end
1090
+
1091
+ def index_remove( urid ) #:nodoc:
1092
+ self.indices.each_pair { |fieldname, index|
1093
+ index.each_value { |rlist|
1094
+ rlist.delete( urid )
1095
+ }
1096
+ }
1097
+ end
1098
+
1099
+ def index_chk_unique( fieldname, fieldvalue )#:nodoc:
1100
+
1101
+ #puts fieldname + '=' + fieldvalue.to_s
1102
+
1103
+ if self.indexed?( fieldname ) == false then
1104
+ return true
1105
+ end
1106
+
1107
+ if self.unique?( fieldname) == false then
1108
+ return true
1109
+ end
1110
+
1111
+ if fieldvalue.nil? then
1112
+ return false
1113
+ end
1114
+
1115
+ if self.indices[fieldname].nil? then
1116
+ self.index_build( fieldname )
1117
+ end
1118
+
1119
+ if self.indices[fieldname][fieldvalue].nil? then
1120
+ return true
1121
+ end
1122
+
1123
+ if self.indices[fieldname][fieldvalue].length == 0 then
1124
+ return true
1125
+ end
1126
+
1127
+ return false
1128
+ end
1129
+
1130
+ def index_chk_unique_update( fieldname, fieldvalue, urid ) #:nodoc:
1131
+
1132
+ #puts fieldname + '=' + fieldvalue.to_s
1133
+
1134
+ if self.indexed?( fieldname ) == false then
1135
+ return true
1136
+ end
1137
+
1138
+ if self.unique?( fieldname) == false then
1139
+ return true
1140
+ end
1141
+
1142
+ if fieldvalue.nil? then
1143
+ return false
1144
+ end
1145
+
1146
+ if self.indices[fieldname].nil? then
1147
+ self.index_build( fieldname )
1148
+ end
1149
+
1150
+ if self.indices[fieldname][fieldvalue].nil? then
1151
+ return true
1152
+ end
1153
+
1154
+ if self.indices[fieldname][fieldvalue].length == 0 then
1155
+ return true
1156
+ end
1157
+
1158
+ if (self.indices[fieldname][fieldvalue].length == 1) and (self.indices[fieldname][fieldvalue][0] == urid) then
1159
+ return true
1160
+ end
1161
+
1162
+ return false
1163
+ end
1164
+
1165
+ # Add a column to the table.
1166
+ def column_add( name, spec )
1167
+
1168
+ if not @meta[:structure][name].nil? then
1169
+ raise Trix51::ExistsError, "Column #{name} already exists"
1170
+ end
1171
+
1172
+ # add in and reparse
1173
+ @meta[:structure][name] = spec
1174
+ self.meta = @meta
1175
+ @meta = self.meta
1176
+
1177
+ #pp @meta
1178
+
1179
+ need_rebuild = false
1180
+
1181
+ # now assign default
1182
+ @dbref.each_pair do |k,v|
1183
+ next unless self.table_record?( k )
1184
+
1185
+ record = Marshal.load( v )
1186
+ record[name] = self.default( name )
1187
+ v = Marshal.dump( record )
1188
+ newk = self.get_hash_key( record )
1189
+ if newk != k then
1190
+ @dbref.delete(k)
1191
+ need_rebuild = true
1192
+ end
1193
+
1194
+ @dbref[ newk ] = v
1195
+ end
1196
+
1197
+ @indices.clear if need_rebuild
1198
+
1199
+ end
1200
+
1201
+ # Remove a column from the table.
1202
+ def column_remove( name )
1203
+
1204
+ if @meta[:structure][name].nil? then
1205
+ raise Trix51::NotFoundError, "Column #{name} does not exist"
1206
+ end
1207
+
1208
+ # add in and reparse
1209
+ @meta[:structure].delete(name)
1210
+ self.meta = @meta
1211
+ @meta = self.meta
1212
+
1213
+ need_rebuild = false
1214
+
1215
+ # now assign default
1216
+ @dbref.each_pair do |k,v|
1217
+ next unless self.table_record?( k )
1218
+
1219
+ record = Marshal.load( v )
1220
+ record.delete( name ) if record.has_key?( name )
1221
+ v = Marshal.dump( record )
1222
+ newk = self.get_hash_key( record )
1223
+ if newk != k then
1224
+ @dbref.delete(k)
1225
+ need_rebuild = true
1226
+ end
1227
+
1228
+ @dbref[ newk ] = v
1229
+ end
1230
+
1231
+ @indices.clear if need_rebuild
1232
+ end
1233
+
1234
+ # Update the specification for a column.
1235
+ def column_update( name, spec )
1236
+
1237
+ if @meta[:structure][name].nil? then
1238
+ raise Trix51::NotFoundError, "Column #{name} does not exist"
1239
+ end
1240
+
1241
+ oldspec = @meta[:structure][name]
1242
+ newspec = Marshal.load( Marshal.dump( spec ) )
1243
+
1244
+ uval = {}
1245
+ ukey = {}
1246
+
1247
+ @dbref.each_pair do |k,v|
1248
+
1249
+ next unless self.table_record?( k )
1250
+
1251
+ record = Marshal.load( v )
1252
+ value = record[name]
1253
+
1254
+ # test field uniqueness
1255
+ if newspec[:unique] then
1256
+ raise Trix51::ConstraintError, "Field definition change would create a unique key violation condition" if not uval[value].nil?
1257
+ uval[value] = 1
1258
+ end
1259
+
1260
+ # type check
1261
+ nrecord = record.clone
1262
+ if newspec[:datatype] != oldspec[:datatype] then
1263
+ begin
1264
+ nrecord[name] = self.convert_type( value, newspec[:datatype] ).to_s
1265
+ #pp nrecord
1266
+ newurid = self.get_hash_key( nrecord )
1267
+ if not ukey[newurid].nil? then
1268
+ raise Trix51::ConstraintError, "Field definition change would violate uniqueness of keys"
1269
+ end
1270
+ ukey[newurid] = 1
1271
+ rescue
1272
+ raise Trix51::TypeError, "Type conversion from #{oldspec[:datatype]} to #{newspec[:datatype]} would barf..."
1273
+ end
1274
+ end
1275
+
1276
+ end
1277
+
1278
+ # update will be okay
1279
+ @meta[:structure][name] = newspec
1280
+ self.meta = @meta
1281
+ @meta = self.meta
1282
+
1283
+ @indices.delete(name) if not @indices[name].nil?
1284
+
1285
+ @dbref.each_pair do |oldurid,v|
1286
+ next unless oldurid.match( self.record_prefix )
1287
+ old_record = Marshal.load( v )
1288
+ new_record = old_record.clone
1289
+ if oldspec[name] != newspec[name] then
1290
+ new_record[name] = self.convert_type( old_record[name], newspec[:datatype] )
1291
+ end
1292
+ newurid = self.get_hash_key( new_record )
1293
+ if newurid != oldurid then
1294
+ @dbref.delete(oldurid)
1295
+ @dbref[newurid] = Marshal.dump( new_record )
1296
+ else
1297
+ @dbref[oldurid] = Marshal.dump( new_record )
1298
+ #self.index_update( oldurid, newurid, new_record[name] )
1299
+ end
1300
+ end
1301
+
1302
+ self.index_build( name ) if self.indexed?(name)
1303
+
1304
+ end
1305
+
1306
+ # Converts a value to the specified type.
1307
+ def convert_type( value, typedef )
1308
+ newvalue = nil
1309
+ if typedef == 'string' then
1310
+ return value.to_s
1311
+ end
1312
+ if typedef == 'integer' then
1313
+ return value.to_i
1314
+ end
1315
+ if typedef == 'float' then
1316
+ return value.to_f
1317
+ end
1318
+ if typedef == 'datetime' then
1319
+ return Time.parse(value.to_s).to_s
1320
+ end
1321
+ end
1322
+
1323
+ # Returns true if the given object is a table, false otherwise
1324
+ def table?
1325
+ return (@dbref.class.name == 'GDBM')
1326
+ end
1327
+
1328
+ # Interate over records in the table or resultset. Any
1329
+ # updates are automatically updated in the database.
1330
+ def each
1331
+
1332
+ self.sorted_keys.each do |k|
1333
+ next unless k.match( Trix51::RECORD_PREFIX )
1334
+ r = init_record(k)
1335
+ yield record
1336
+ r.update if r.has_updates?
1337
+ end
1338
+
1339
+ end
1340
+
1341
+ # Iterates through each key, value for the table.
1342
+ def each_pair
1343
+
1344
+ self.sorted_keys.each do |k|
1345
+ next unless self.table_record?(k)
1346
+ yield k, init_record(k)
1347
+ end
1348
+
1349
+ end
1350
+
1351
+ # Interates through each key in the dbm store.
1352
+ def each_key
1353
+ self.sorted_keys.each do |k|
1354
+ yield k if self.table_record?(k)
1355
+ end
1356
+ end
1357
+
1358
+ # Interates through each Trix51::Tuple in the table or result set.
1359
+ def each_value
1360
+ self.sorted_keys.each do |k|
1361
+ next unless k.match( self.record_prefix )
1362
+ r = init_record(k)
1363
+ yield r
1364
+ r.update if r.has_updates?
1365
+ end
1366
+ end
1367
+
1368
+ # Iterates through each Trix51::Tuple in the table or result set.
1369
+ def each_record
1370
+ self.sorted_keys.each do |k|
1371
+ next unless k.match( self.record_prefix )
1372
+ r = init_record(k)
1373
+ yield r
1374
+ r.update if r.has_updates?
1375
+ end
1376
+ end
1377
+
1378
+ def push( record ) #:nodoc:
1379
+ #puts "pushing record with urid = #{record.urid}"
1380
+ @dbref[ record.urid ] = record
1381
+ end
1382
+
1383
+ # Returns the number of records in the table or result set.
1384
+ def count
1385
+ return self.size
1386
+ end
1387
+
1388
+ # Returns the number of records in the table or result set.
1389
+ def length
1390
+ return self.size
1391
+ end
1392
+
1393
+ def record_prefix #:nodoc:
1394
+ return Trix51::RECORD_PREFIX+self.tablename.to_s
1395
+ end
1396
+
1397
+ def table_record?( key ) #:nodoc:
1398
+ return (key.match(self.record_prefix))
1399
+ end
1400
+
1401
+ def meta_record?( key ) #:nodoc:
1402
+ return (key.match(Trix51::META_PREFIX))
1403
+ end
1404
+
1405
+ def seq_record?( key ) #:nodoc:
1406
+ return (key.match(Trix51::SEQ_PREFIX))
1407
+ end
1408
+
1409
+ def meta_prefix #:nodoc:
1410
+ return Trix51::META_PREFIX+self.tablename.to_s
1411
+ end
1412
+
1413
+ def seq_prefix( fieldname ) #:nodoc:
1414
+ return Trix51::SEQ_PREFIX+self.tablename.to_s+'_'+fieldname.to_s+'_seq'
1415
+ end
1416
+
1417
+ def meta #:nodoc:
1418
+ if @meta.nil? then
1419
+ @meta = Marshal.load( @dbref[self.meta_prefix] )
1420
+ end
1421
+ return @meta
1422
+ end
1423
+
1424
+ def meta=(meta) #:nodoc:
1425
+ @dbref[self.meta_prefix ] = Marshal.dump( meta )
1426
+ end
1427
+
1428
+ # Produces a neatly formatted dump of the records (all fields)
1429
+ def dump
1430
+ fields = self.structure.keys
1431
+ self.dump_filtered( *fields )
1432
+ end
1433
+
1434
+ # Produces a neatly formatted dump of the records (selected fields)
1435
+ def dump_filtered( *fieldnames )
1436
+
1437
+ widths = {}
1438
+ #fieldnames = self.structure.keys.sort if fieldnames.length == 0
1439
+ lines = []
1440
+
1441
+ tmp = []
1442
+ fieldnames.each do |fn|
1443
+ tmp.push( fn ) if self.exists?( fn )
1444
+ end
1445
+ fieldnames = tmp
1446
+
1447
+ fieldnames.each do |field|
1448
+ widths[field] = field.length+2
1449
+
1450
+ str = ''
1451
+ (field.length+1).times do |n|
1452
+ str = str + '-'
1453
+ end
1454
+
1455
+ lines.push( str )
1456
+ end
1457
+
1458
+ @dbref.each_pair do |k, v|
1459
+ #pp v
1460
+ next unless self.table_record?(k)
1461
+ record = nil
1462
+ if self.table? then
1463
+ record = Marshal.load( v )
1464
+ else
1465
+ record = v.data
1466
+ end
1467
+ fieldnames.each do |field|
1468
+ fv = record[field] || 'nil'
1469
+ widths[field] = fv.to_s.length+2 if fv.to_s.length+2 > widths[field]
1470
+ end
1471
+ end
1472
+
1473
+ # create formatstr
1474
+ formatstr = ''
1475
+ fieldnames.each do |field|
1476
+ formatstr = formatstr + " %-#{widths[field]}s|"
1477
+ end
1478
+
1479
+ puts sprintf "#{formatstr}", *fieldnames
1480
+ puts sprintf "#{formatstr}", *lines
1481
+
1482
+ self.each_record do |v|
1483
+ record = v.data
1484
+ values = []
1485
+ fieldnames.each do |field|
1486
+ values.push( record[field] || 'nil' )
1487
+ end
1488
+ puts sprintf "#{formatstr}", *values
1489
+ end
1490
+
1491
+ #pp @dbref
1492
+
1493
+ end
1494
+
1495
+ # Returns a random record from the table or result set.
1496
+ def random
1497
+
1498
+ keys = []
1499
+ @dbref.each_key do |k|
1500
+ keys.push( k ) if self.table_record?(k)
1501
+ end
1502
+
1503
+ return nil if keys.count == 0
1504
+
1505
+ urid = keys[ rand( keys.count ) ]
1506
+
1507
+ return init_record( urid )
1508
+
1509
+ end
1510
+
1511
+ # Sorts records by specified field and ordering, returning a new result set. Special symbol :random returns
1512
+ # records in a randomized ordering.
1513
+ # ==== Example
1514
+ # animal.select( number_of_legs: 4 ).sort( :name ).dump_filtered( :name )
1515
+ #
1516
+ # name |
1517
+ # ----- |
1518
+ # aardvark |
1519
+ # bullock |
1520
+ # cow |
1521
+ def sort( field, dir='asc' )
1522
+
1523
+ res = Trix51::ResultSet.new( self.tablename, {}, { tableref: self, sorted: true, sortfield: field, sortorder: dir } )
1524
+
1525
+ self.each_key do |k|
1526
+ res.push( init_record( k ) )
1527
+ end
1528
+
1529
+ return res
1530
+
1531
+ end
1532
+
1533
+ def group_key( hashdata, keylist ) #:nodoc:
1534
+ keycode = []
1535
+ keylist.each do |key|
1536
+ if not hashdata[key].nil? then
1537
+ keycode.push( hashdata[key].to_s )
1538
+ else
1539
+ keycode.push( 'nil' )
1540
+ end
1541
+ end
1542
+ return Trix51::RECORD_PREFIX+'aggregate_'+self.tablename+':'+keycode.join(':')
1543
+ end
1544
+
1545
+ # Groups records by the array of fields specified in the *by:* clause, and returns
1546
+ # the aggregated results for the fields specified in the *calculate:* clause.
1547
+ #
1548
+ # ==== Group calculations
1549
+ # * +sum:+ - Sum of the field values
1550
+ # * +max:+ - Max of the field values
1551
+ # * +min:+ - Min of the field values
1552
+ # * +avg:+ - Average of the field values
1553
+ # * +count:+ - Count of the field values
1554
+ #
1555
+ # ==== Example
1556
+ # animal.group( by: [ :species_id ], calculate: { number_of_eyes: 'sum' } ).dump
1557
+ #
1558
+ # species_id |sum_number_of_eyes |
1559
+ # ----------- |------------------- |
1560
+ # 1 | 28 |
1561
+ # 2 | 40 |
1562
+ #
1563
+ def group( *args )
1564
+ params = args.shift
1565
+ #pp params
1566
+ keylist = params[:by]
1567
+ fields = params[:calculate]
1568
+
1569
+ collected_data = {}
1570
+ structure = {}
1571
+ agg_records = {}
1572
+
1573
+ keylist.each do |key|
1574
+ structure[key] = self.structure[key]
1575
+ structure[key][:key] = true
1576
+ end
1577
+
1578
+ fields.each_pair do |field,function|
1579
+ new_field = function.to_s + '_' + field.to_s
1580
+ structure[new_field.to_sym] = { :datatype => 'string' }
1581
+ end
1582
+
1583
+ self.each_record do |record|
1584
+
1585
+ nurid = self.group_key( record.data, keylist )
1586
+
1587
+ agg_records[nurid] = {}
1588
+ keylist.each do |key|
1589
+ key = key
1590
+ agg_records[nurid][key] = record.data[key]
1591
+ end
1592
+
1593
+
1594
+ # for this record, aggregate
1595
+ fields.each_key do |field|
1596
+ collected_data[nurid] = {} if collected_data[nurid].nil?
1597
+ collected_data[nurid][field] = [] if collected_data[nurid][field].nil?
1598
+ values = collected_data[nurid][field]
1599
+ if self.structure[field][:datatype] == 'calculated' then
1600
+ values.push( self.eval_in_context( record.data, self.structure[field][:calculation] ) )
1601
+ else
1602
+ values.push( record.data[field] )
1603
+ end
1604
+ collected_data[nurid][field] = values
1605
+ end
1606
+
1607
+
1608
+ # now collapse the data into hashes
1609
+ collected_data.each_key do |nurid|
1610
+ # by key, process each function
1611
+
1612
+ # copy keylist fields in
1613
+
1614
+
1615
+ fields.each_pair do |field,function|
1616
+ field = field
1617
+ function = function
1618
+ new_field = (function.to_s + '_' + field.to_s).to_sym
1619
+ values = collected_data[nurid][field]
1620
+
1621
+ if function == 'sum' then
1622
+ agg_records[nurid][new_field] = values.reduce(:+)
1623
+ elsif function == 'avg' then
1624
+ agg_records[nurid][new_field] = values.reduce(:+) / values.count
1625
+ elsif function == 'min' then
1626
+ agg_records[nurid][new_field] = values.min
1627
+ elsif function == 'max' then
1628
+ agg_records[nurid][new_field] = values.max
1629
+ elsif function == 'count' then
1630
+ agg_records[nurid][new_field] = values.count
1631
+ else
1632
+ raise Trix51::NotFoundError, "unknown group function #{function}"
1633
+ end
1634
+
1635
+ end
1636
+
1637
+ end
1638
+
1639
+ end
1640
+
1641
+ res = Trix51::ResultSet.new( 'aggregate_'+self.tablename, {}, { tableref: self, structure: structure } )
1642
+ meta_init = res.meta
1643
+ meta_init[:structure] = structure
1644
+ res.meta = meta_init
1645
+
1646
+ agg_records.each_pair do |k,v|
1647
+ res.dbref[k] = Trix51::Tuple.new( self, k, v )
1648
+ end
1649
+
1650
+ return res
1651
+
1652
+ end
1653
+
1654
+ # Returns table record at index.
1655
+ def [](index)
1656
+ keys = []
1657
+ self.sorted_keys.each do |k|
1658
+ keys.push( k ) if self.table_record?(k)
1659
+ end
1660
+
1661
+ return nil if (keys.count == 0) or (keys.count <= index)
1662
+
1663
+ urid = keys[ index ]
1664
+
1665
+ return init_record( urid )
1666
+ end
1667
+
1668
+ # Returns a new result set containing a list of the unique values.
1669
+ def distinct( *keylist )
1670
+
1671
+ structure = {}
1672
+ keylist.each do |key|
1673
+ key = key
1674
+ structure[key] = self.structure[key]
1675
+ structure[key][:key] = true
1676
+ end
1677
+
1678
+ agg_records = {}
1679
+
1680
+ self.each_record do |record|
1681
+
1682
+ nurid = self.group_key( record.data, keylist )
1683
+ next unless agg_records[nurid].nil?
1684
+
1685
+ agg_records[nurid] = {}
1686
+ keylist.each do |key|
1687
+ key = key
1688
+ agg_records[nurid][key] = record.data[key]
1689
+ end
1690
+
1691
+ end
1692
+
1693
+ res = Trix51::ResultSet.new( 'aggregate_'+self.tablename, {}, { tableref: self, structure: structure } )
1694
+ meta_init = res.meta
1695
+ meta_init[:structure] = structure
1696
+ res.meta = meta_init
1697
+
1698
+ agg_records.each_pair do |k,v|
1699
+ res.dbref[k] = Trix51::Tuple.new( self, k, v )
1700
+ end
1701
+
1702
+ return res
1703
+ end
1704
+
1705
+ # Returns a new result set contain only the first count records.
1706
+ # ==== Example
1707
+ # animal.select( number_of_legs: 4 ).sort( :name ).limit(2).dump_filtered( :name )
1708
+ #
1709
+ # name |
1710
+ # ----- |
1711
+ # aardvark |
1712
+ # bullock |
1713
+ def limit( count )
1714
+ res = Trix51::ResultSet.new( self.tablename, {}, { tableref: self } )
1715
+ done = 0
1716
+ self.each_record do |record|
1717
+ res.dbref[ record.urid ] = record if done < count
1718
+ done = done + 1
1719
+ end
1720
+ return res
1721
+ end
1722
+
1723
+
1724
+ def hint_matches_by_index( taglist, field, operand, target ) #:nodoc:
1725
+ newlist = []
1726
+
1727
+ # match the criteria against each key in the index, collecting any matches if they are in the original
1728
+ # taglist
1729
+
1730
+ self.index_build(field) if self.indices[field].nil?
1731
+
1732
+ self.indices[field].each_pair do |fieldvalue, urid_list|
1733
+ if comparison( fieldvalue, operand, target ) then
1734
+ matches = urid_list & taglist
1735
+ newlist.push( *matches ) if matches.length > 0
1736
+ end
1737
+ end
1738
+
1739
+ return newlist
1740
+ end
1741
+
1742
+ def hint_matches_by_recordscan( taglist, field, operand, target ) #:nodoc:
1743
+
1744
+ newlist = []
1745
+
1746
+ taglist.each do |urid|
1747
+ v = @dbref[urid]
1748
+ record = nil
1749
+ if v.class.name == 'String' then
1750
+ record = Marshal.load( v )
1751
+ else
1752
+ record = v.data
1753
+ end
1754
+ if self.structure[field][:datatype] == 'calculated' then
1755
+ record[field] = self.eval_in_context( record, self.structure[field][:calculation] )
1756
+ end
1757
+ newlist.push( urid ) if comparison( record[field], operand, target )
1758
+ end
1759
+
1760
+ return newlist
1761
+
1762
+ end
1763
+
1764
+ def hint_criteria_to_operand_target( criteria ) #:nodoc:
1765
+ if criteria.class.name != 'Hash' then
1766
+ return [ 'is', criteria ]
1767
+ else
1768
+ operand = ''
1769
+ target = ''
1770
+ criteria.each_pair do |k, v|
1771
+ operand = k.to_s
1772
+ target = v
1773
+ end
1774
+ return [ operand, target ]
1775
+ end
1776
+ end
1777
+
1778
+ # Return records based on the specified query.
1779
+ def select_hash( criteria ) #:nodoc:
1780
+ #criteria = self.args_to_hash( *args )
1781
+ taglist = self.record_keys
1782
+
1783
+ criteria.each_pair do |field, value|
1784
+ break if taglist.length == 0
1785
+
1786
+ if self.indexed?(field) then
1787
+ #puts "#{field} will use index"
1788
+ taglist = self.hint_matches_by_index( taglist, field, *hint_criteria_to_operand_target(value) )
1789
+ else
1790
+ #puts "#{field} will use record level scan (less efficient)"
1791
+ taglist = self.hint_matches_by_recordscan( taglist, field, *hint_criteria_to_operand_target(value) )
1792
+ end
1793
+
1794
+ end
1795
+
1796
+ res = Trix51::ResultSet.new( self.tablename, {}, { tableref: self } )
1797
+ taglist.each do |key|
1798
+ res.push( self.init_record( key ) )
1799
+ end
1800
+
1801
+ return res
1802
+
1803
+ end
1804
+
1805
+ # Creates an index on the specified field.
1806
+ def create_index( fieldname )
1807
+ fieldname = fieldname
1808
+ raise Trix51::NotFoundError, "Table '#{self.tablename}' does not have a field '#{fieldname}'" unless self.exists?(fieldname)
1809
+ raise Trix51::ExistsError, "Table '#{self.tablename}' already has an index on field '#{fieldname}'" if self.indexed?(fieldname)
1810
+
1811
+ @meta = self.meta
1812
+ self.structure[fieldname][:indexed] = true
1813
+ self.meta = @meta
1814
+
1815
+ self.index_build( fieldname )
1816
+ end
1817
+
1818
+ def indices_save( path ) #:nodoc:
1819
+ filename = path+'/'+self.tablename.to_s+'.idx'
1820
+ #puts "*** Save index: #{filename}"
1821
+ File.open( filename, 'w' ) do |file|
1822
+ Marshal.dump( self.indices, file )
1823
+ end
1824
+ end
1825
+
1826
+ def indices_load( path ) #:nodoc:
1827
+ filename = path+'/'+self.tablename.to_s+'.idx'
1828
+
1829
+ if File.exists?( filename ) then
1830
+ #puts "*** Load index: #{filename}"
1831
+ File.open( filename, 'r' ) do |file|
1832
+ self.indices = Marshal.load( file )
1833
+ end
1834
+ else
1835
+ #puts "*** Generating index for #{self.tablename}"
1836
+ self.structure.keys.each { |field|
1837
+ self.index_build( field ) if self.indexed?(field)
1838
+ }
1839
+ end
1840
+ #pp self.indices
1841
+ end
1842
+
1843
+ # Key accessors for the tupleset class.
1844
+ attr_accessor :dbref, :indices, :tablename, :classname
1845
+ end
1846
+
1847
+ # Class representing a table
1848
+ class Trix51::Table < Trix51::TupleSet
1849
+
1850
+ # Create an instance of the table class
1851
+ def initialize( tablename, dbref )
1852
+ super( tablename, dbref, {} )
1853
+ end
1854
+
1855
+ end
1856
+
1857
+ # Class representing a set of results
1858
+ class Trix51::ResultSet < Trix51::TupleSet
1859
+
1860
+ # Create an instance of the resultset class used in query results.
1861
+ def initialize( tablename, dbref, options )
1862
+ super( tablename, dbref, options )
1863
+ end
1864
+
1865
+ end