og 0.23.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/ProjectInfo +58 -0
  2. data/README +5 -4
  3. data/Rakefile +2 -2
  4. data/doc/AUTHORS +10 -7
  5. data/doc/RELEASES +108 -0
  6. data/lib/og.rb +1 -3
  7. data/lib/og/collection.rb +4 -4
  8. data/lib/og/entity.rb +96 -27
  9. data/lib/og/evolution.rb +78 -0
  10. data/lib/og/manager.rb +29 -32
  11. data/lib/og/mixin/hierarchical.rb +1 -1
  12. data/lib/og/mixin/optimistic_locking.rb +5 -8
  13. data/lib/og/mixin/orderable.rb +15 -2
  14. data/lib/og/mixin/schema_inheritance_base.rb +12 -0
  15. data/lib/og/mixin/taggable.rb +29 -25
  16. data/lib/og/mixin/timestamped.rb +4 -2
  17. data/lib/og/mixin/tree.rb +0 -1
  18. data/lib/og/relation.rb +161 -116
  19. data/lib/og/relation/all.rb +6 -0
  20. data/lib/og/relation/belongs_to.rb +4 -1
  21. data/lib/og/relation/has_many.rb +6 -5
  22. data/lib/og/relation/joins_many.rb +13 -12
  23. data/lib/og/relation/refers_to.rb +3 -3
  24. data/lib/og/store.rb +9 -9
  25. data/lib/og/store/{filesys.rb → alpha/filesys.rb} +0 -0
  26. data/lib/og/store/alpha/kirby.rb +284 -0
  27. data/lib/og/store/{memory.rb → alpha/memory.rb} +2 -0
  28. data/lib/og/store/{sqlserver.rb → alpha/sqlserver.rb} +6 -6
  29. data/lib/og/store/kirby.rb +145 -162
  30. data/lib/og/store/mysql.rb +58 -27
  31. data/lib/og/store/psql.rb +15 -13
  32. data/lib/og/store/sql.rb +136 -135
  33. data/lib/og/store/sqlite.rb +13 -12
  34. data/lib/og/validation.rb +2 -2
  35. data/lib/vendor/kbserver.rb +20 -0
  36. data/lib/vendor/kirbybase.rb +2790 -1601
  37. data/test/og/CONFIG.rb +79 -0
  38. data/test/og/mixin/tc_hierarchical.rb +1 -1
  39. data/test/og/mixin/tc_optimistic_locking.rb +1 -3
  40. data/test/og/mixin/tc_orderable.rb +42 -1
  41. data/test/og/mixin/tc_taggable.rb +1 -1
  42. data/test/og/mixin/tc_timestamped.rb +1 -1
  43. data/test/og/store/tc_filesys.rb +1 -2
  44. data/test/og/tc_delete_all.rb +45 -0
  45. data/test/og/tc_inheritance.rb +10 -38
  46. data/test/og/tc_join.rb +2 -11
  47. data/test/og/tc_multiple.rb +3 -16
  48. data/test/og/tc_override.rb +3 -3
  49. data/test/og/tc_polymorphic.rb +3 -13
  50. data/test/og/tc_relation.rb +8 -6
  51. data/test/og/tc_reverse.rb +2 -11
  52. data/test/og/tc_select.rb +2 -15
  53. data/test/og/tc_store.rb +4 -63
  54. data/test/og/tc_types.rb +1 -2
  55. metadata +80 -77
@@ -64,11 +64,12 @@ class SqliteStore < SqlStore
64
64
  end
65
65
 
66
66
  def enchant(klass, manager)
67
- if klass.metadata.primary_key.flatten.first == :oid
68
- unless klass.properties.find { |p| p.symbol == :oid }
67
+ if klass.ann.this.primary_key.symbol == :oid
68
+ unless klass.properties.include? :oid
69
69
  klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
70
70
  end
71
71
  end
72
+
72
73
  super
73
74
  end
74
75
 
@@ -117,17 +118,17 @@ private
117
118
 
118
119
  sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
119
120
 
120
- # Create table constrains.
121
+ # Create table constraints.
121
122
 
122
- if klass.__meta and constrains = klass.__meta[:sql_constrain]
123
- sql << ", #{constrains.join(', ')}"
123
+ if constraints = klass.ann.this[:sql_constraint]
124
+ sql << ", #{constraints.join(', ')}"
124
125
  end
125
126
 
126
127
  sql << ");"
127
128
 
128
129
  # Create indices.
129
130
 
130
- if klass.__meta and indices = klass.__meta[:index]
131
+ if indices = klass.ann.this[:index]
131
132
  for data in indices
132
133
  idx, options = *data
133
134
  idx = idx.to_s
@@ -153,7 +154,7 @@ private
153
154
  # Create join tables if needed. Join tables are used in
154
155
  # 'many_to_many' relations.
155
156
 
156
- if klass.__meta and join_tables = klass.__meta[:join_tables]
157
+ if join_tables = klass.ann.this[:join_tables]
157
158
  for info in join_tables
158
159
  begin
159
160
  create_join_table_sql(info).each do |sql|
@@ -172,7 +173,7 @@ private
172
173
  end
173
174
  end
174
175
 
175
- def create_field_map(klass)
176
+ def create_field_map(klass)
176
177
  res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
177
178
  map = {}
178
179
 
@@ -188,12 +189,12 @@ private
188
189
  end
189
190
 
190
191
  def eval_og_insert(klass)
191
- pk = klass.primary_key.first
192
- props = klass.properties.dup
192
+ pk = klass.primary_key.symbol
193
+ props = klass.properties.values.dup
193
194
  values = props.collect { |p| write_prop(p) }.join(',')
194
195
 
195
- if klass.metadata.superclass or klass.metadata.subclasses
196
- props << Property.new(:ogtype, String)
196
+ if klass.schema_inheritance?
197
+ props << Property.new(:symbol => :ogtype, :klass => String)
197
198
  values << ", '#{klass}'"
198
199
  end
199
200
 
@@ -44,7 +44,7 @@ module Validation
44
44
  end;
45
45
  }
46
46
 
47
- __meta[:validations] << [code, c[:on]]
47
+ validations! << [code, c[:on]]
48
48
  end
49
49
  end
50
50
 
@@ -65,7 +65,7 @@ module Validation
65
65
  end;
66
66
  }
67
67
 
68
- __meta[:validations] << [code, c[:on]]
68
+ validations! << [code, c[:on]]
69
69
  end
70
70
  end
71
71
  alias_method :validate_associated, :validate_related
@@ -0,0 +1,20 @@
1
+ # Multi-user server script for KirbyBase.
2
+
3
+ require 'kirbybase'
4
+ require 'drb'
5
+ require 'benchmark'
6
+ include Benchmark
7
+
8
+ host = ''
9
+ port = 44444
10
+
11
+ puts 'Initializing database server and indexes...'
12
+
13
+ # Create an instance of the database.
14
+ db = KirbyBase.new(:server)
15
+
16
+ DRb.start_service('druby://:44444', db)
17
+
18
+ puts 'Server ready to receive connections...'
19
+
20
+ DRb.thread.join
@@ -1,1601 +1,2790 @@
1
- require 'date'
2
- require 'drb'
3
- require 'csv'
4
- require 'fileutils'
5
-
6
- # KirbyBase is a class that allows you to create and manipulate simple,
7
- # plain-text databases. You can use it in either a single-user or
8
- # client-server mode. You can select records for retrieval/updating using
9
- # code blocks.
10
- #
11
- # Author:: Jamey Cribbs (mailto:jcribbs@twmi.rr.com)
12
- # Homepage:: http://www.netpromi.com/kirbybase.html
13
- # Copyright:: Copyright (c) 2005 NetPro Technologies, LLC
14
- # License:: Distributes under the same terms as Ruby
15
- # History:
16
- # 2005-03-28:: Version 2.0
17
- # * This is a completely new version. The interface has changed
18
- # dramatically.
19
- # 2005-04-11:: Version 2.1
20
- # * Changed the interface to KirbyBase#new and KirbyBase#create_table. You
21
- # now specify arguments via a code block or as part of the argument list.
22
- # * Added the ability to specify a class at table creation time.
23
- # Thereafter, whenever you do a #select, the result set will be an array
24
- # of instances of that class, instead of instances of Struct. You can
25
- # also use instances of this class as the argument to KBTable#insert,
26
- # KBTable#update, and KBTable#set.
27
- # * Added the ability to encrypt a table so that it is no longer stored as
28
- # a plain-text file.
29
- # * Added the ability to explicity specify that you want a result set to be
30
- # sorted in ascending order.
31
- # * Added the ability to import a csv file into an existing table.
32
- # * Added the ability to select a record as if the table were a Hash with
33
- # it's key being the recno field.
34
- # * Added the ability to update a record as if the table were a Hash with
35
- # it's key being the recno field.
36
- # 2005-05-02:: Version 2.2
37
- # * By far the biggest change in this version is that I have completely
38
- # redesigned the internal structure of the database code. Because the
39
- # KirbyBase and KBTable classes were too tightly coupled, I have created
40
- # a KBEngine class and moved all low-level I/O logic and locking logic
41
- # to this class. This allowed me to restructure the KirbyBase class to
42
- # remove all of the methods that should have been private, but couldn't be
43
- # because of the coupling to KBTable. In addition, it has allowed me to
44
- # take all of the low-level code that should not have been in the KBTable
45
- # class and put it where it belongs, as part of the underlying engine. I
46
- # feel that the design of KirbyBase is much cleaner now. No changes were
47
- # made to the class interfaces, so you should not have to change any of
48
- # your code.
49
- # * Changed str_to_date and str_to_datetime to use Date#parse method.
50
- # * Changed #pack method so that it no longer reads the whole file into
51
- # memory while packing it.
52
- # * Changed code so that special character sequences like &linefeed; can be
53
- # part of input data and KirbyBase will not interpret it as special
54
- # characters.
55
- #
56
- #---------------------------------------------------------------------------
57
- # KirbyBase
58
- #---------------------------------------------------------------------------
59
- class KirbyBase
60
- include DRb::DRbUndumped
61
-
62
- attr_reader :engine
63
-
64
- attr_accessor :connect_type, :host, :port, :path, :ext
65
-
66
- #-----------------------------------------------------------------------
67
- # initialize
68
- #-----------------------------------------------------------------------
69
- #++
70
- # Create a new database instance.
71
- #
72
- # *connect_type*:: Symbol (:local, :client, :server) specifying role to
73
- # play.
74
- # *host*:: String containing IP address or DNS name of server hosting
75
- # database. (Only valid if connect_type is :client.)
76
- # *port*:: Integer specifying port database server is listening on.
77
- # (Only valid if connect_type is :client.)
78
- # *path*:: String specifying path to location of database tables.
79
- # *ext*:: String specifying extension of table files.
80
- #
81
- def initialize(connect_type=:local, host=nil, port=nil, path='./',
82
- ext='.tbl')
83
- @connect_type = connect_type
84
- @host = host
85
- @port = port
86
- @path = path
87
- @ext = ext
88
-
89
- # See if user specified any method arguments via a code block.
90
- yield self if block_given?
91
-
92
- # After the yield, make sure the user doesn't change any of these
93
- # instance variables.
94
- class << self
95
- private :connect_type=, :host=, :path=, :ext=
96
- end
97
-
98
- # Did user supply full and correct arguments to method?
99
- raise ArgumentError, 'Invalid connection type specified' unless (
100
- [:local, :client, :server].include?(@connect_type))
101
- raise "Must specify hostname or IP address!" if \
102
- @connect_type == :client and @host.nil?
103
- raise "Must specify port number!" if @connect_type == :client and \
104
- @port.nil?
105
- raise "Invalid path!" if @path.nil?
106
- raise "Invalid extension!" if @ext.nil?
107
-
108
- # If running as a client, start druby and connect to server.
109
- if client?
110
- DRb.start_service()
111
- @server = DRbObject.new(nil, 'druby://%s:%d' % [@host, @port])
112
- @engine = @server.engine
113
- @path = @server.path
114
- @ext = @server.ext
115
- else
116
- @engine = KBEngine.create_called_from_database_instance(self)
117
- end
118
- end
119
-
120
- #-----------------------------------------------------------------------
121
- # server?
122
- #-----------------------------------------------------------------------
123
- #++
124
- # Is this running as a server?
125
- #
126
- def server?
127
- @connect_type == :server
128
- end
129
-
130
- #-----------------------------------------------------------------------
131
- # client?
132
- #-----------------------------------------------------------------------
133
- #++
134
- # Is this running as a client?
135
- #
136
- def client?
137
- @connect_type == :client
138
- end
139
-
140
- #-----------------------------------------------------------------------
141
- # local?
142
- #-----------------------------------------------------------------------
143
- #++
144
- # Is this running in single-user, embedded mode?
145
- #
146
- def local?
147
- @connect_type == :local
148
- end
149
-
150
- #-----------------------------------------------------------------------
151
- # tables
152
- #-----------------------------------------------------------------------
153
- #++
154
- # Return an array containing the names of all tables in this database.
155
- #
156
- def tables
157
- return @engine.tables
158
- end
159
-
160
- #-----------------------------------------------------------------------
161
- # get_table
162
- #-----------------------------------------------------------------------
163
- #++
164
- # Return a reference to the requested table.
165
- # *name*:: Symbol or string of table name.
166
- #
167
- def get_table(name)
168
- raise('Do not call this method from a server instance!') if server?
169
- raise('Table not found!') unless table_exists?(name)
170
-
171
- return KBTable.create_called_from_database_instance(self,
172
- name.to_sym, File.join(@path, name.to_s + @ext))
173
- end
174
-
175
- #-----------------------------------------------------------------------
176
- # create_table
177
- #-----------------------------------------------------------------------
178
- #++
179
- # Create new table and return a reference to the new table.
180
- # *name*:: Symbol or string of table name.
181
- # *field_defs*:: List of field names (Symbols) and field types (Symbols)
182
- # *Block*:: Optional code block allowing you to set the following:
183
- # *encrypt*:: true/false specifying whether table should be encrypted.
184
- # *record_class*:: Class or String specifying the user create class that
185
- # will be associated with table records.
186
- #
187
- def create_table(name=nil, *field_defs)
188
- raise "Can't call #create_table from server!" if server?
189
-
190
- t_struct = Struct.new(:name, :field_defs, :encrypt, :record_class)
191
- t = t_struct.new
192
- t.name = name
193
- t.field_defs = field_defs
194
- t.encrypt = false
195
- t.record_class = 'Struct'
196
-
197
- yield t if block_given?
198
-
199
- raise "No table name specified!" if t.name.nil?
200
- raise "No table field definitions specified!" if t.field_defs.nil?
201
-
202
- @engine.new_table(t.name, t.field_defs, t.encrypt,
203
- t.record_class.to_s)
204
- return get_table(t.name)
205
- end
206
-
207
- #-----------------------------------------------------------------------
208
- # drop_table
209
- #-----------------------------------------------------------------------
210
- #++
211
- # Delete a table.
212
- #
213
- # *tablename*:: Symbol or string of table name.
214
- #
215
- def drop_table(tablename)
216
- return @engine.delete_table(tablename)
217
- end
218
-
219
- #-----------------------------------------------------------------------
220
- # table_exists?
221
- #-----------------------------------------------------------------------
222
- #++
223
- # Return true if table exists.
224
- #
225
- # *tablename*:: Symbol or string of table name.
226
- #
227
- def table_exists?(tablename)
228
- return @engine.table_exists?(tablename)
229
- end
230
- end
231
-
232
- #---------------------------------------------------------------------------
233
- # KBEngine
234
- #---------------------------------------------------------------------------
235
- class KBEngine
236
- include DRb::DRbUndumped
237
-
238
- EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
239
- '0123456789.+-,$:|&;_ '
240
- EN_STR_LEN = EN_STR.length
241
- EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
242
- EN_KEY = EN_KEY1.unpack("u")[0]
243
- EN_KEY_LEN = EN_KEY.length
244
-
245
- # Regular expression used to determine if field needs to be
246
- # encoded.
247
- ENCODE_RE = /&|\n|\r|\032|\|/
248
-
249
- # Make constructor private.
250
- private_class_method :new
251
-
252
- def KBEngine.create_called_from_database_instance(db)
253
- return new(db)
254
- end
255
-
256
- def initialize(db)
257
- @db = db
258
- # This hash will hold the table locks if in client/server mode.
259
- @mutex_hash = {} if @db.server?
260
- end
261
-
262
- #-----------------------------------------------------------------------
263
- # table_exists?
264
- #-----------------------------------------------------------------------
265
- #++
266
- # Return true if table exists.
267
- #
268
- # *tablename*:: Symbol or string of table name.
269
- #
270
- def table_exists?(tablename)
271
- return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
272
- end
273
-
274
- #-----------------------------------------------------------------------
275
- # tables
276
- #-----------------------------------------------------------------------
277
- #++
278
- # Return an array containing the names of all tables in this database.
279
- #
280
- def tables
281
- list = []
282
- Dir.foreach(@db.path) { |filename|
283
- list << File.basename(filename, '.*').to_sym if \
284
- filename =~ Regexp.new(@db.ext)
285
- }
286
- return list
287
- end
288
-
289
- #-----------------------------------------------------------------------
290
- # new_table
291
- #-----------------------------------------------------------------------
292
- #++
293
- # Create physical file holding table. This table should not be directly
294
- # called in your application, but only called by #create_table.
295
- #
296
- # *name*:: Symbol or string of table name.
297
- # *field_defs*:: List of field names (Symbols) and field types (Symbols)
298
- # *encrypt*:: true/false specifying whether table should be encrypted.
299
- # *record_class*:: Class or String specifying the user create class that
300
- #
301
- def new_table(name, field_defs, encrypt, record_class)
302
- # Can't create a table that already exists!
303
- raise "Table #{name.to_s} already exists!" if table_exists?(name)
304
-
305
- raise "Must have a field type for each field name" \
306
- unless field_defs.size.remainder(2) == 0
307
-
308
- temp_field_defs = []
309
- (0...field_defs.size).step(2) { |x|
310
- raise "Invalid field type: #{field_defs[x+1]}" unless \
311
- KBTable.valid_field_type?(field_defs[x+1])
312
- temp_field_defs << \
313
- "#{field_defs[x].to_s}:#{field_defs[x+1]}"
314
- }
315
-
316
- # Header rec consists of last record no. used, delete count, and
317
- # all field names/types. Here, I am inserting the 'recno' field
318
- # at the beginning of the fields.
319
- header_rec = ['000000', '000000', record_class, 'recno:Integer',
320
- temp_field_defs].join('|')
321
-
322
- header_rec = 'Z' + encrypt_str(header_rec) if encrypt
323
-
324
- begin
325
- fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
326
- fptr.write(header_rec + "\n")
327
- ensure
328
- fptr.close
329
- end
330
- end
331
-
332
- #-----------------------------------------------------------------------
333
- # delete_table
334
- #-----------------------------------------------------------------------
335
- #++
336
- # Delete a table.
337
- #
338
- # *tablename*:: Symbol or string of table name.
339
- #
340
- def delete_table(tablename)
341
- with_write_lock(tablename) do
342
- File.delete(File.join(@db.path, tablename.to_s + @db.ext))
343
- return true
344
- end
345
- end
346
-
347
- #----------------------------------------------------------------------
348
- # get_total_recs
349
- #----------------------------------------------------------------------
350
- #++
351
- # Return total number of non-deleted records in table.
352
- #
353
- # *table*:: Table instance.
354
- #
355
- def get_total_recs(table)
356
- return get_recs(table).size
357
- end
358
-
359
- #-----------------------------------------------------------------------
360
- # get_header_vars
361
- #-----------------------------------------------------------------------
362
- #++
363
- # Returns array containing first line of file.
364
- #
365
- # *table*:: Table instance.
366
- #
367
- def get_header_vars(table)
368
- with_table(table) do |fptr|
369
- line = get_header_record(table, fptr)
370
-
371
- last_rec_no, del_ctr, record_class, *flds = line.split('|')
372
- field_names = flds.collect { |x| x.split(':')[0].to_sym }
373
- field_types = flds.collect { |x| x.split(':')[1].to_sym }
374
-
375
- return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
376
- record_class, field_names, field_types]
377
- end
378
- end
379
-
380
- #-----------------------------------------------------------------------
381
- # get_recs
382
- #-----------------------------------------------------------------------
383
- #++
384
- # Return array of all table records (arrays).
385
- #
386
- # *table*:: Table instance.
387
- #
388
- def get_recs(table)
389
- encrypted = table.encrypted?
390
- recs = []
391
-
392
- with_table(table) do |fptr|
393
- begin
394
- # Skip header rec.
395
- fptr.readline
396
-
397
- # Loop through table.
398
- while true
399
- # Record current position in table. Then read first
400
- # detail record.
401
- fpos = fptr.tell
402
- line = fptr.readline
403
-
404
- line = unencrypt_str(line) if encrypted
405
- line.strip!
406
-
407
- # If blank line (i.e. 'deleted'), skip it.
408
- next if line == ''
409
-
410
- # Split the line up into fields.
411
- rec = line.split('|', -1)
412
- rec << fpos << line.length
413
- recs << rec
414
- end
415
- # Here's how we break out of the loop...
416
- rescue EOFError
417
- end
418
- return recs
419
- end
420
- end
421
-
422
- #-----------------------------------------------------------------------
423
- # insert_record
424
- #-----------------------------------------------------------------------
425
- #
426
- def insert_record(table, rec)
427
- with_write_locked_table(table) do |fptr|
428
- # Auto-increment the record number field.
429
- rec_no = incr_rec_no_ctr(table, fptr)
430
-
431
- # Insert the newly created record number value at the beginning
432
- # of the field values.
433
- rec[0] = rec_no
434
-
435
- # Encode any special characters (like newlines) before writing
436
- # out the record.
437
- write_record(table, fptr, 'end', rec.collect { |r| encode_str(r)
438
- }.join('|'))
439
-
440
- # Return the record number of the newly created record.
441
- return rec_no
442
- end
443
- end
444
-
445
- #-----------------------------------------------------------------------
446
- # update_records
447
- #-----------------------------------------------------------------------
448
- #
449
- def update_records(table, recs)
450
- with_write_locked_table(table) do |fptr|
451
- recs.each do |rec|
452
- line = rec[:rec].collect { |r| encode_str(r) }.join('|')
453
-
454
- # This doesn't actually 'delete' the line, it just
455
- # makes it all spaces. That way, if the updated
456
- # record is the same or less length than the old
457
- # record, we can write the record back into the
458
- # same spot. If the updated record is greater than
459
- # the old record, we will leave the now spaced-out
460
- # line and write the updated record at the end of
461
- # the file.
462
- write_record(table, fptr, rec[:fpos],
463
- ' ' * rec[:line_length])
464
- if line.length > rec[:line_length]
465
- write_record(table, fptr, 'end', line)
466
- incr_del_ctr(table, fptr)
467
- else
468
- write_record(table, fptr, rec[:fpos], line)
469
- end
470
- end
471
- # Return the number of records updated.
472
- return recs.size
473
- end
474
- end
475
-
476
- #-----------------------------------------------------------------------
477
- # delete_records
478
- #-----------------------------------------------------------------------
479
- #
480
- def delete_records(table, recs)
481
- with_write_locked_table(table) do |fptr|
482
- recs.each do |rec|
483
- # Go to offset within the file where the record is and
484
- # replace it with all spaces.
485
- write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
486
- incr_del_ctr(table, fptr)
487
- end
488
-
489
- # Return the number of records deleted.
490
- return recs.size
491
- end
492
- end
493
-
494
- #-----------------------------------------------------------------------
495
- # pack_table
496
- #-----------------------------------------------------------------------
497
- #
498
- def pack_table(table)
499
- with_write_lock(table) do
500
- fptr = open(table.filename, 'r')
501
- new_fptr = open(table.filename+'temp', 'w')
502
-
503
- line = fptr.readline.chomp
504
- # Reset the delete counter in the header rec to 0.
505
- if line[0..0] == 'Z'
506
- header_rec = unencrypt_str(line[1..-1]).split('|')
507
- header_rec[1] = '000000'
508
- new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
509
- "\n")
510
- else
511
- header_rec = line.split('|')
512
- header_rec[1] = '000000'
513
- new_fptr.write(header_rec.join('|') + "\n")
514
- end
515
-
516
- lines_deleted = 0
517
-
518
- begin
519
- while true
520
- line = fptr.readline
521
-
522
- if table.encrypted?
523
- temp_line = unencrypt_str(line)
524
- else
525
- temp_line = line
526
- end
527
-
528
- if temp_line.strip == ''
529
- lines_deleted += 1
530
- else
531
- new_fptr.write(line)
532
- end
533
- end
534
- # Here's how we break out of the loop...
535
- rescue EOFError
536
- end
537
-
538
- # Close the table and release the write lock.
539
- fptr.close
540
- new_fptr.close
541
- File.delete(table.filename)
542
- FileUtils.mv(table.filename+'temp', table.filename)
543
-
544
- # Return the number of deleted records that were removed.
545
- return lines_deleted
546
- end
547
- end
548
-
549
- #-----------------------------------------------------------------------
550
- # PRIVATE METHODS
551
- #-----------------------------------------------------------------------
552
- private
553
-
554
- #-----------------------------------------------------------------------
555
- # with_table
556
- #-----------------------------------------------------------------------
557
- #
558
- def with_table(table, access='r')
559
- begin
560
- yield fptr = open(table.filename, access)
561
- ensure
562
- fptr.close
563
- end
564
- end
565
-
566
- #-----------------------------------------------------------------------
567
- # with_write_lock
568
- #-----------------------------------------------------------------------
569
- #
570
- def with_write_lock(table)
571
- begin
572
- write_lock(table.name) if @db.server?
573
- yield
574
- ensure
575
- write_unlock(table.name) if @db.server?
576
- end
577
- end
578
-
579
- #-----------------------------------------------------------------------
580
- # with_write_locked_table
581
- #-----------------------------------------------------------------------
582
- #
583
- def with_write_locked_table(table, access='r+')
584
- begin
585
- write_lock(table.name) if @db.server?
586
- yield fptr = open(table.filename, access)
587
- ensure
588
- fptr.close
589
- write_unlock(table.name) if @db.server?
590
- end
591
- end
592
-
593
- #-----------------------------------------------------------------------
594
- # write_lock
595
- #-----------------------------------------------------------------------
596
- #
597
- def write_lock(tablename)
598
- # Unless an key already exists in the hash holding mutex records
599
- # for this table, create a write key for this table in the mutex
600
- # hash. Then, place a lock on that mutex.
601
- @mutex_hash[tablename] = Mutex.new unless (
602
- @mutex_hash.has_key?(tablename))
603
- @mutex_hash[tablename].lock
604
-
605
- return true
606
- end
607
-
608
- #----------------------------------------------------------------------
609
- # write_unlock
610
- #----------------------------------------------------------------------
611
- #
612
- def write_unlock(tablename)
613
- # Unlock the write mutex for this table.
614
- @mutex_hash[tablename].unlock
615
-
616
- return true
617
- end
618
-
619
- #----------------------------------------------------------------------
620
- # write_record
621
- #----------------------------------------------------------------------
622
- #
623
- def write_record(table, fptr, pos, record)
624
- if table.encrypted?
625
- temp_rec = encrypt_str(record)
626
- else
627
- temp_rec = record
628
- end
629
-
630
- # If record is to be appended, go to end of table and write
631
- # record, adding newline character.
632
- if pos == 'end'
633
- fptr.seek(0, IO::SEEK_END)
634
- fptr.write(temp_rec + "\n")
635
- else
636
- # Otherwise, overwrite another record (that's why we don't
637
- # add the newline character).
638
- fptr.seek(pos)
639
- fptr.write(temp_rec)
640
- end
641
- end
642
-
643
- #----------------------------------------------------------------------
644
- # write_header_record
645
- #----------------------------------------------------------------------
646
- #
647
- def write_header_record(table, fptr, record)
648
- fptr.seek(0)
649
-
650
- if table.encrypted?
651
- fptr.write('Z' + encrypt_str(record) + "\n")
652
- else
653
- fptr.write(record + "\n")
654
- end
655
- end
656
-
657
- #----------------------------------------------------------------------
658
- # get_header_record
659
- #----------------------------------------------------------------------
660
- #
661
- def get_header_record(table, fptr)
662
- fptr.seek(0)
663
-
664
- if table.encrypted?
665
- return unencrypt_str(fptr.readline[1..-1].chomp)
666
- else
667
- return fptr.readline.chomp
668
- end
669
- end
670
-
671
- #-----------------------------------------------------------------------
672
- # reset_rec_no_ctr
673
- #-----------------------------------------------------------------------
674
- #
675
- def reset_rec_no_ctr(table, fptr)
676
- last_rec_no, rest_of_line = get_header_record(table, fptr).split(
677
- '|', 2)
678
- write_header_record(table, fptr, ['%06d' % 0, rest_of_line].join(
679
- '|'))
680
- return true
681
- end
682
-
683
- #-----------------------------------------------------------------------
684
- # incr_rec_no_ctr
685
- #-----------------------------------------------------------------------
686
- #
687
- def incr_rec_no_ctr(table, fptr)
688
- last_rec_no, rest_of_line = get_header_record(table, fptr).split(
689
- '|', 2)
690
- last_rec_no = last_rec_no.to_i + 1
691
-
692
- write_header_record(table, fptr, ['%06d' % last_rec_no,
693
- rest_of_line].join('|'))
694
-
695
- # Return the new recno.
696
- return last_rec_no
697
- end
698
-
699
- #-----------------------------------------------------------------------
700
- # incr_del_ctr
701
- #-----------------------------------------------------------------------
702
- #
703
- def incr_del_ctr(table, fptr)
704
- last_rec_no, del_ctr, rest_of_line = get_header_record(table,
705
- fptr).split('|', 3)
706
- del_ctr = del_ctr.to_i + 1
707
-
708
- write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
709
- rest_of_line].join('|'))
710
-
711
- return true
712
- end
713
-
714
- #-----------------------------------------------------------------------
715
- # encrypt_str
716
- #-----------------------------------------------------------------------
717
- #++
718
- # Returns an encrypted string, using the Vignere Cipher.
719
- #
720
- # *s*:: String to encrypt.
721
- #
722
- def encrypt_str(s)
723
- new_str = ''
724
- i_key = -1
725
- s.each_byte do |c|
726
- if i_key < EN_KEY_LEN - 1
727
- i_key += 1
728
- else
729
- i_key = 0
730
- end
731
-
732
- if EN_STR.index(c.chr).nil?
733
- new_str << c.chr
734
- next
735
- end
736
-
737
- i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
738
- i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
739
- new_str << EN_STR[i_from_str]
740
- end
741
- return new_str
742
- end
743
-
744
- #-----------------------------------------------------------------------
745
- # unencrypt_str
746
- #-----------------------------------------------------------------------
747
- #++
748
- # Returns an unencrypted string, using the Vignere Cipher.
749
- #
750
- # *s*:: String to unencrypt.
751
- #
752
- def unencrypt_str(s)
753
- new_str = ''
754
- i_key = -1
755
- s.each_byte do |c|
756
- if i_key < EN_KEY_LEN - 1
757
- i_key += 1
758
- else
759
- i_key = 0
760
- end
761
-
762
- if EN_STR.index(c.chr).nil?
763
- new_str << c.chr
764
- next
765
- end
766
-
767
- i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
768
- i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
769
- new_str << EN_STR[i_from_str]
770
- end
771
- return new_str
772
- end
773
-
774
- #-----------------------------------------------------------------------
775
- # encode_str
776
- #-----------------------------------------------------------------------
777
- #++
778
- # Replace characters in string that can cause problems when storing.
779
- #
780
- # *s*:: String to be encoded.
781
- #
782
- def encode_str(s)
783
- if s =~ ENCODE_RE
784
- return s.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub(
785
- "\r", '&carriage_return;').gsub("\032", '&substitute;').gsub(
786
- "|", '&pipe;')
787
- else
788
- return s
789
- end
790
- end
791
- end
792
-
793
- #---------------------------------------------------------------------------
794
- # KBTable
795
- #---------------------------------------------------------------------------
796
- class KBTable
797
- include DRb::DRbUndumped
798
-
799
- # Make constructor private. KBTable instances should only be created
800
- # from KirbyBase#get_table.
801
- private_class_method :new
802
-
803
- # Regular expression used to determine if field needs to be
804
- # un-encoded.
805
- UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
806
-
807
- TYPE_CONV = { :Integer => :Integer, :Float => :Float,
808
- :String => :unencode_str, :Boolean => :str_to_bool,
809
- :Date => :str_to_date, :DateTime => :str_to_datetime }
810
-
811
- attr_reader :filename, :name
812
-
813
- #-----------------------------------------------------------------------
814
- # KBTable.valid_field_type
815
- #-----------------------------------------------------------------------
816
- #++
817
- # Return true if valid field type.
818
- #
819
- # *field_type*:: Symbol specifying field type.
820
- #
821
- def KBTable.valid_field_type?(field_type)
822
- TYPE_CONV.key?(field_type)
823
- end
824
-
825
- #-----------------------------------------------------------------------
826
- # create_called_from_database_instance
827
- #-----------------------------------------------------------------------
828
- #++
829
- # Return a new instance of KBTable. Should never be called directly by
830
- # your application. Should only be called from KirbyBase#get_table.
831
- #
832
- # *db*:: KirbyBase instance.
833
- # *name*:: Symbol specifying table name.
834
- # *filename*:: String specifying filename of physical file that holds
835
- # table.
836
- #
837
- def KBTable.create_called_from_database_instance(db, name, filename)
838
- return new(db, name, filename)
839
- end
840
-
841
- # This has been declared private so user's cannot create new instances
842
- # of KBTable from their application. A user gets a handle to a KBTable
843
- # instance by calling KirbyBase#get_table for an existing table or
844
- # KirbyBase#create_table for a new table.
845
- def initialize(db, name, filename)
846
- @db = db
847
- @name = name
848
- @filename = filename
849
- @encrypted = false
850
-
851
- # Alias delete_all to clear method.
852
- alias delete_all clear
853
-
854
- update_header_vars
855
- end
856
-
857
- #-----------------------------------------------------------------------
858
- # encrypted?
859
- #-----------------------------------------------------------------------
860
- #++
861
- # Returns true if table is encrypted.
862
- #
863
- def encrypted?
864
- if @encrypted
865
- return true
866
- else
867
- return false
868
- end
869
- end
870
-
871
- #-----------------------------------------------------------------------
872
- # field_names
873
- #-----------------------------------------------------------------------
874
- #++
875
- # Return array containing table field names.
876
- #
877
- def field_names
878
- update_header_vars
879
- return @field_names
880
- end
881
-
882
- #-----------------------------------------------------------------------
883
- # field_types
884
- #-----------------------------------------------------------------------
885
- #++
886
- # Return array containing table field types.
887
- #
888
- def field_types
889
- update_header_vars
890
- return @field_types
891
- end
892
-
893
- #-----------------------------------------------------------------------
894
- # insert
895
- #-----------------------------------------------------------------------
896
- #++
897
- # Insert a new record into a table, return unique record number.
898
- #
899
- # *data*:: Array, Hash, Struct instance containing field values of
900
- # new record.
901
- # *insert_proc*:: Proc instance containing insert code. This and the
902
- # data parameter are mutually exclusive.
903
- #
904
- def insert(*data, &insert_proc)
905
- raise 'Cannot specify both a hash/array/struct and a ' + \
906
- 'proc for method #insert!' unless data.empty? or insert_proc.nil?
907
-
908
- raise 'Must specify either hash/array/struct or insert ' + \
909
- 'proc for method #insert!' if data.empty? and insert_proc.nil?
910
-
911
- # Update the header variables.
912
- update_header_vars
913
-
914
- # Convert input, which could be an array, a hash, or a Struct
915
- # into a common format (i.e. hash).
916
- if data.empty?
917
- input_rec = convert_input_data(insert_proc)
918
- else
919
- input_rec = convert_input_data(data)
920
- end
921
-
922
- # Check the field values to make sure they are proper types.
923
- validate_input(input_rec)
924
-
925
- return @db.engine.insert_record(self, @field_names.collect { |f|
926
- input_rec.fetch(f, '')
927
- })
928
- end
929
-
930
- #-----------------------------------------------------------------------
931
- # update_all
932
- #-----------------------------------------------------------------------
933
- #++
934
- # Return array of records (Structs) to be updated, in this case all
935
- # records.
936
- #
937
- # *updates*:: Hash or Struct containing updates.
938
- #
939
- def update_all(*updates)
940
- update(*updates) { true }
941
- end
942
-
943
- #-----------------------------------------------------------------------
944
- # update
945
- #-----------------------------------------------------------------------
946
- #++
947
- # Return array of records (Structs) to be updated based on select cond.
948
- #
949
- # *updates*:: Hash or Struct containing updates.
950
- # *select_cond*:: Proc containing code to select records to update.
951
- #
952
- def update(*updates, &select_cond)
953
- raise ArgumentError, "Must specify select condition code " + \
954
- "block. To update all records, use #update_all instead." if \
955
- select_cond.nil?
956
-
957
- # Update the header variables.
958
- update_header_vars
959
-
960
- # Get all records that match the selection criteria and
961
- # return them in an array.
962
- result_set = get_matches(:update, @field_names, select_cond)
963
-
964
- return result_set if updates.empty?
965
-
966
- set(result_set, updates)
967
- end
968
-
969
- #-----------------------------------------------------------------------
970
- # []=
971
- #-----------------------------------------------------------------------
972
- #++
973
- # Update record whose recno field equals index.
974
- #
975
- # *index*:: Integer specifying recno you wish to select.
976
- # *updates*:: Hash, Struct, or Array containing updates.
977
- #
978
- def []=(index, updates)
979
- return update(updates) { |r| r.recno == index }
980
- end
981
-
982
- #-----------------------------------------------------------------------
983
- # set
984
- #-----------------------------------------------------------------------
985
- #++
986
- # Set fields of records to updated values. Returns number of records
987
- # updated.
988
- #
989
- # *recs*:: Array of records (Structs) that will be updated.
990
- # *data*:: Hash, Struct, Proc containing updates.
991
- #
992
- def set(recs, data)
993
- # Update the header variables.
994
- update_header_vars
995
-
996
- # Convert updates, which could be an array, a hash, or a Struct
997
- # into a common format (i.e. hash).
998
- update_rec = convert_input_data(data)
999
-
1000
- validate_input(update_rec)
1001
-
1002
- updated_recs = []
1003
- recs.each do |rec|
1004
- updated_rec = {}
1005
- updated_rec[:rec] = @field_names.collect { |f|
1006
- if update_rec.has_key?(f)
1007
- update_rec[f]
1008
- else
1009
- rec.send(f)
1010
- end
1011
- }
1012
- updated_rec[:fpos] = rec.fpos
1013
- updated_rec[:line_length] = rec.line_length
1014
- updated_recs << updated_rec
1015
- end
1016
- @db.engine.update_records(self, updated_recs)
1017
-
1018
- # Return the number of records updated.
1019
- return recs.size
1020
- end
1021
-
1022
- #-----------------------------------------------------------------------
1023
- # delete
1024
- #-----------------------------------------------------------------------
1025
- #++
1026
- # Delete records from table and return # deleted.
1027
- #
1028
- # *select_cond*:: Proc containing code to select records.
1029
- #
1030
- def delete(&select_cond)
1031
- raise ArgumentError, "Must specify select condition code " + \
1032
- "block. To delete all records, use #clear instead." if \
1033
- select_cond.nil?
1034
-
1035
- # Update the header variables.
1036
- update_header_vars
1037
-
1038
- # Get all records that match the selection criteria and
1039
- # return them in an array.
1040
- result_set = get_matches(:delete, [], select_cond)
1041
-
1042
- @db.engine.delete_records(self, result_set)
1043
-
1044
- # Return the number of records deleted.
1045
- return result_set.size
1046
- end
1047
-
1048
- #-----------------------------------------------------------------------
1049
- # clear
1050
- #-----------------------------------------------------------------------
1051
- #++
1052
- # Delete all records from table. You can also use #delete_all.
1053
- #
1054
- # *reset_recno_ctr*:: true/false specifying whether recno counter should
1055
- # be reset to 0.
1056
- #
1057
- def clear(reset_recno_ctr=true)
1058
- delete { true }
1059
- pack
1060
-
1061
- @db.engine.reset_recno_ctr if reset_recno_ctr
1062
- end
1063
-
1064
- #-----------------------------------------------------------------------
1065
- # []
1066
- #-----------------------------------------------------------------------
1067
- #++
1068
- # Return the record(s) whose recno field is included in index.
1069
- #
1070
- # *index*:: Array of Integer(s) specifying recno(s) you wish to select.
1071
- #
1072
- def [](*index)
1073
- recs = select { |r| index.include?(r.recno) }
1074
- if recs.size == 1
1075
- return recs[0]
1076
- else
1077
- return recs
1078
- end
1079
- end
1080
-
1081
- #-----------------------------------------------------------------------
1082
- # select
1083
- #-----------------------------------------------------------------------
1084
- #++
1085
- # Return array of records (Structs) matching select conditions.
1086
- #
1087
- # *filter*:: List of field names (Symbols) to include in result set.
1088
- # *select_cond*:: Proc containing select code.
1089
- #
1090
- def select(*filter, &select_cond)
1091
- # Declare these variables before the code block so they don't go
1092
- # after the code block is done.
1093
- result_set = []
1094
-
1095
- # Update the header variables.
1096
- update_header_vars
1097
-
1098
- # Validate that all names in filter are valid field names.
1099
- validate_filter(filter)
1100
-
1101
- filter = @field_names if filter.empty?
1102
-
1103
- # Get all records that match the selection criteria and
1104
- # return them in an array of Struct instances.
1105
- return get_matches(:select, filter, select_cond)
1106
- end
1107
-
1108
- #-----------------------------------------------------------------------
1109
- # pack
1110
- #-----------------------------------------------------------------------
1111
- #++
1112
- # Remove blank records from table, return total removed.
1113
- #
1114
- def pack
1115
- return @db.engine.pack_table(self)
1116
- end
1117
-
1118
- #-----------------------------------------------------------------------
1119
- # total_recs
1120
- #-----------------------------------------------------------------------
1121
- #++
1122
- # Return total number of undeleted (blank) records in table.
1123
- #
1124
- def total_recs
1125
- return @db.engine.get_total_recs(self)
1126
- end
1127
-
1128
- #-----------------------------------------------------------------------
1129
- # import_csv
1130
- #-----------------------------------------------------------------------
1131
- #++
1132
- # Import csv file into table.
1133
- #
1134
- # *csv_filename*:: filename of csv file to import.
1135
- #
1136
- def import_csv(csv_filename)
1137
- type_convs = @field_types.collect { |x|
1138
- TYPE_CONV[x]
1139
- }
1140
-
1141
- CSV.open(csv_filename, 'r') do |row|
1142
- temp_row = []
1143
- (0...@field_names.size-1).each { |x|
1144
- if row[x].to_s == ''
1145
- temp_row << nil
1146
- else
1147
- temp_row << send(type_convs[x+1], row[x].to_s)
1148
- end
1149
- }
1150
- insert(*temp_row)
1151
- end
1152
- end
1153
-
1154
- #-----------------------------------------------------------------------
1155
- # PRIVATE METHODS
1156
- #-----------------------------------------------------------------------
1157
- private
1158
-
1159
- #-----------------------------------------------------------------------
1160
- # str_to_date
1161
- #-----------------------------------------------------------------------
1162
- #++
1163
- # Convert a String to a Date.
1164
- #
1165
- # *s*:: String to be converted.
1166
- #
1167
- def str_to_date(s)
1168
- # Convert a string to a date object. NOTE: THIS IS SLOW!!!!
1169
- # If you can, just define any date fields in the database as
1170
- # string fields. Queries will be much faster if you do.
1171
- return Date.parse(s)
1172
- end
1173
-
1174
- #-----------------------------------------------------------------------
1175
- # str_to_datetime
1176
- #-----------------------------------------------------------------------
1177
- #++
1178
- # Convert a String to a DateTime.
1179
- #
1180
- # *s*:: String to be converted.
1181
- #
1182
- def str_to_datetime(s)
1183
- # Convert a string to a datetime object. NOTE: THIS IS SLOW!!!!
1184
- # If you can, just define any datetime fields in the database as
1185
- # string fields. Queries will be much faster if you do.
1186
- return DateTime.parse(s)
1187
- end
1188
-
1189
- #-----------------------------------------------------------------------
1190
- # str_to_bool
1191
- #-----------------------------------------------------------------------
1192
- #++
1193
- # Convert a String to a TrueClass or FalseClass.
1194
- #
1195
- # *s*:: String to be converted.
1196
- #
1197
- def str_to_bool(s)
1198
- if s == 'false' or s.nil?
1199
- return false
1200
- else
1201
- return true
1202
- end
1203
- end
1204
-
1205
- #-----------------------------------------------------------------------
1206
- # validate_filter
1207
- #-----------------------------------------------------------------------
1208
- #++
1209
- # Check that filter contains valid field names.
1210
- #
1211
- # *filter*:: Array holding field names to include in result set.
1212
- #
1213
- def validate_filter(filter)
1214
- # Each field in the filter array must be a valid fieldname in the
1215
- # table.
1216
- filter.each { |x|
1217
- raise "Invalid field name: #{x}" unless (
1218
- @field_names.include?(x))
1219
- }
1220
- end
1221
-
1222
- #-----------------------------------------------------------------------
1223
- # convert_input_data
1224
- #-----------------------------------------------------------------------
1225
- #++
1226
- # Convert data passed to #input, #update, or #set to a common format.
1227
- #
1228
- # *values*:: Proc, user class, hash, or array holding input values.
1229
- #
1230
- def convert_input_data(values)
1231
- if values.class == Proc
1232
- tbl_struct = Struct.new(*@field_names[1..-1])
1233
- tbl_rec = tbl_struct.new
1234
- begin
1235
- values.call(tbl_rec)
1236
- rescue NoMethodError
1237
- raise "Invalid field name in code block: %s" % $!
1238
- end
1239
- temp_hash = {}
1240
- @field_names[1..-1].collect { |f|
1241
- temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
1242
- }
1243
- return temp_hash
1244
- elsif values[0].class.to_s == @record_class
1245
- temp_hash = {}
1246
- @field_names[1..-1].collect { |f|
1247
- temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
1248
- }
1249
- return temp_hash
1250
- elsif values[0].class == Hash
1251
- return values[0].dup
1252
- elsif values[0].kind_of?(Struct)
1253
- temp_hash = {}
1254
- @field_names[1..-1].collect { |f|
1255
- temp_hash[f] = values[0][f] if values[0].members.include?(
1256
- f.to_s)
1257
- }
1258
- return temp_hash
1259
- elsif values[0].class == Array
1260
- raise ArgumentError, "Must specify all fields in input " + \
1261
- "array." unless values[0].size == @field_names[1..-1].size
1262
- temp_hash = {}
1263
- @field_names[1..-1].collect { |f|
1264
- temp_hash[f] = values[0][@field_names.index(f)-1]
1265
- }
1266
- return temp_hash
1267
- elsif values.class == Array
1268
- raise ArgumentError, "Must specify all fields in input " + \
1269
- "array." unless values.size == @field_names[1..-1].size
1270
- temp_hash = {}
1271
- @field_names[1..-1].collect { |f|
1272
- temp_hash[f] = values[@field_names.index(f)-1]
1273
- }
1274
- return temp_hash
1275
- else
1276
- raise(ArgumentError, 'Invalid type for values container.')
1277
- end
1278
- end
1279
-
1280
- #-----------------------------------------------------------------------
1281
- # validate_input
1282
- #-----------------------------------------------------------------------
1283
- #++
1284
- # Check input data to ensure proper data types.
1285
- #
1286
- # *data*:: Hash of data values.
1287
- #
1288
- def validate_input(data)
1289
- raise "Cannot insert/update recno field!" if data.has_key?(:recno)
1290
-
1291
- @field_names[1..-1].each do |f|
1292
- next unless data.has_key?(f)
1293
-
1294
- next if data[f].nil?
1295
- case @field_types[@field_names.index(f)]
1296
- when :String
1297
- raise "Invalid String value for: %s" % f unless \
1298
- data[f].respond_to?(:to_str)
1299
- when :Boolean
1300
- raise "Invalid Boolean value for: %s" % f unless \
1301
- data[f].kind_of?(TrueClass) or data[f].kind_of?(FalseClass)
1302
- when :Integer
1303
- raise "Invalid Integer value for: %s" % f unless \
1304
- data[f].respond_to?(:to_int)
1305
- when :Float
1306
- raise "Invalid Float value for: %s" % f unless \
1307
- data[f].class == Float
1308
- when :Date
1309
- raise "Invalid Date value for: %s" % f unless \
1310
- data[f].class == Date
1311
- when :DateTime
1312
- raise "Invalid DateTime value for: %s" % f unless \
1313
- data[f].class == DateTime
1314
- end
1315
- end
1316
- end
1317
-
1318
- #-----------------------------------------------------------------------
1319
- # update_header_vars
1320
- #-----------------------------------------------------------------------
1321
- #++
1322
- # Read header record and update instance variables.
1323
- #
1324
- def update_header_vars
1325
- @encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \
1326
- @field_types = @db.engine.get_header_vars(self)
1327
- end
1328
-
1329
- #-----------------------------------------------------------------------
1330
- # get_matches
1331
- #-----------------------------------------------------------------------
1332
- #++
1333
- # Return records from table that match select condition.
1334
- #
1335
- # *query_type*:: Symbol specifying type of query (:select, :update,
1336
- # or :delete).
1337
- # *filter*:: Array of field names to include in each record of result
1338
- # set.
1339
- # *select_cond*:: Proc containing select condition.
1340
- #
1341
- def get_matches(query_type, filter, select_cond)
1342
- tbl_struct = Struct.new(*@field_names)
1343
- case query_type
1344
- when :select
1345
- if @record_class == 'Struct'
1346
- result_struct = Struct.new(*filter)
1347
- end
1348
- when :update
1349
- result_struct = Struct.new(*(filter + [:fpos, :line_length]))
1350
- when :delete
1351
- result_struct = Struct.new(:fpos, :line_length)
1352
- end
1353
-
1354
- # Create an empty array to hold any matches found.
1355
- matchList = KBResultSet.new(self, filter,
1356
- filter.collect { |f| @field_types[@field_names.index(f)] })
1357
-
1358
- type_convs = @field_types.collect { |x| TYPE_CONV[x] }
1359
-
1360
- # Loop through table.
1361
- @db.engine.get_recs(self).each do |rec|
1362
- tbl_rec = tbl_struct.new(*(0...@field_names.size).collect do |i|
1363
- if rec[i] == ''
1364
- nil
1365
- else
1366
- send(type_convs[i], rec[i])
1367
- end
1368
- end)
1369
-
1370
- next unless select_cond.call(tbl_rec) unless select_cond.nil?
1371
-
1372
- if query_type != :select or @record_class == 'Struct'
1373
- result_rec = result_struct.new(*filter.collect { |f|
1374
- tbl_rec.send(f) })
1375
- else
1376
- if Object.const_get(@record_class).respond_to?(:kb_defaults)
1377
- result_rec = Object.const_get(@record_class).new(
1378
- *@field_names.collect { |f|
1379
- tbl_rec.send(f) || Object.const_get(@record_class
1380
- ).kb_defaults[@field_names.index(f)]
1381
- }
1382
- )
1383
- else
1384
- result_rec = Object.const_get(@record_class).kb_create(
1385
- *@field_names.collect { |f|
1386
- # Just a warning here: If you specify a filter on
1387
- # a select, you are only going to get those fields
1388
- # you specified in the result set, EVEN IF
1389
- # record_class is a custom class instead of Struct.
1390
- if filter.include?(f)
1391
- tbl_rec.send(f)
1392
- else
1393
- nil
1394
- end
1395
- }
1396
- )
1397
- end
1398
- end
1399
-
1400
- unless query_type == :select
1401
- result_rec.fpos = rec[-2]
1402
- result_rec.line_length = rec[-1]
1403
- end
1404
- matchList << result_rec
1405
- end
1406
- return matchList
1407
- end
1408
-
1409
- #-----------------------------------------------------------------------
1410
- # unencode_str
1411
- #-----------------------------------------------------------------------
1412
- #++
1413
- # Return string to unencoded format.
1414
- #
1415
- # *s*:: String to be unencoded.
1416
- #
1417
- def unencode_str(s)
1418
- if s =~ UNENCODE_RE
1419
- return s.gsub('&linefeed;', "\n").gsub(
1420
- '&carriage_return;', "\r").gsub('&substitute;', "\032").gsub(
1421
- '&pipe;', "|").gsub('&amp;', "&")
1422
- else
1423
- return s
1424
- end
1425
- end
1426
- end
1427
-
1428
-
1429
- #---------------------------------------------------------------------------
1430
- # KBResult
1431
- #---------------------------------------------------------------------------
1432
- class KBResultSet < Array
1433
- #-----------------------------------------------------------------------
1434
- # KBResultSet.reverse
1435
- #-----------------------------------------------------------------------
1436
- #
1437
- def KBResultSet.reverse(sort_field)
1438
- return [sort_field, :desc]
1439
- end
1440
-
1441
- #-----------------------------------------------------------------------
1442
- # initialize
1443
- #-----------------------------------------------------------------------
1444
- #
1445
- def initialize(table, filter, filter_types, *args)
1446
- @table = table
1447
- @filter = filter
1448
- @filter_types = filter_types
1449
- super(*args)
1450
- end
1451
-
1452
- #-----------------------------------------------------------------------
1453
- # to_ary
1454
- #-----------------------------------------------------------------------
1455
- #
1456
- def to_ary
1457
- to_a
1458
- end
1459
-
1460
- #-----------------------------------------------------------------------
1461
- # set
1462
- #-----------------------------------------------------------------------
1463
- #++
1464
- # Update record(s) in table, return number of records updated.
1465
- #
1466
- def set(*updates, &update_cond)
1467
- raise 'Cannot specify both a hash and a proc for method #set!' \
1468
- unless updates.empty? or update_cond.nil?
1469
-
1470
- raise 'Must specify update proc or hash for method #set!' if \
1471
- updates.empty? and update_cond.nil?
1472
-
1473
- if updates.empty?
1474
- @table.set(self, update_cond)
1475
- else
1476
- @table.set(self, updates)
1477
- end
1478
- end
1479
-
1480
- #-----------------------------------------------------------------------
1481
- # sort
1482
- #-----------------------------------------------------------------------
1483
- #
1484
- def sort(*sort_fields)
1485
- sort_fields_arrs = []
1486
- sort_fields.each do |f|
1487
- if f.to_s[0..0] == '-'
1488
- sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
1489
- elsif f.to_s[0..0] == '+'
1490
- sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
1491
- else
1492
- sort_fields_arrs << [f, :asc]
1493
- end
1494
- end
1495
-
1496
- sort_fields_arrs.each do |f|
1497
- raise "Invalid sort field" unless @filter.include?(f[0])
1498
- end
1499
-
1500
- super() { |a,b|
1501
- x = []
1502
- y = []
1503
- sort_fields_arrs.each do |s|
1504
- if [:Integer, :Float].include?(
1505
- @filter_types[@filter.index(s[0])])
1506
- a_value = a.send(s[0]) || 0
1507
- b_value = b.send(s[0]) || 0
1508
- else
1509
- a_value = a.send(s[0])
1510
- b_value = b.send(s[0])
1511
- end
1512
- if s[1] == :desc
1513
- x << b_value
1514
- y << a_value
1515
- else
1516
- x << a_value
1517
- y << b_value
1518
- end
1519
- end
1520
- x <=> y
1521
- }
1522
- end
1523
-
1524
- #-----------------------------------------------------------------------
1525
- # to_report
1526
- #-----------------------------------------------------------------------
1527
- #
1528
- def to_report(recs_per_page=0, print_rec_sep=false)
1529
- result = collect { |r| @filter.collect {|f| r.send(f)} }
1530
-
1531
- # How many records before a formfeed.
1532
- delim = ' | '
1533
-
1534
- # columns of physical rows
1535
- columns = [@filter].concat(result).transpose
1536
-
1537
- max_widths = columns.collect { |c|
1538
- c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
1539
- }
1540
-
1541
- row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
1542
- delim.length * (max_widths.size - 1))
1543
-
1544
- justify_hash = { :String => :ljust, :Integer => :rjust,
1545
- :Float => :rjust, :Boolean => :ljust, :Date => :ljust,
1546
- :DateTime => :ljust }
1547
-
1548
- header_line = @filter.zip(max_widths, @filter.collect { |f|
1549
- @filter_types[@filter.index(f)] }).collect { |x,y,z|
1550
- x.to_s.send(justify_hash[z], y) }.join(delim)
1551
-
1552
- output = ''
1553
- recs_on_page_cnt = 0
1554
-
1555
- result.each do |row|
1556
- if recs_on_page_cnt == 0
1557
- output << header_line + "\n" << row_dashes + "\n"
1558
- end
1559
-
1560
- output << row.zip(max_widths, @filter.collect { |f|
1561
- @filter_types[@filter.index(f)] }).collect { |x,y,z|
1562
- x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
1563
-
1564
- output << row_dashes + '\n' if print_rec_sep
1565
- recs_on_page_cnt += 1
1566
-
1567
- if recs_per_page > 0 and (recs_on_page_cnt ==
1568
- num_recs_per_page)
1569
- output << '\f'
1570
- recs_on_page_count = 0
1571
- end
1572
- end
1573
- return output
1574
- end
1575
- end
1576
-
1577
-
1578
- #---------------------------------------------------------------------------
1579
- # NilClass
1580
- #---------------------------------------------------------------------------
1581
- #
1582
- class NilClass
1583
- def method_missing(method_id, stuff)
1584
- return false
1585
- end
1586
- end
1587
-
1588
- #---------------------------------------------------------------------------
1589
- # Symbol
1590
- #---------------------------------------------------------------------------
1591
- #
1592
- class Symbol
1593
- def -@
1594
- ("-"+self.to_s).to_sym
1595
- end
1596
-
1597
- def +@
1598
- ("+"+self.to_s).to_sym
1599
- end
1600
- end
1601
-
1
+ require 'date'
2
+ require 'drb'
3
+ require 'csv'
4
+ require 'fileutils'
5
+ require 'yaml'
6
+
7
+ #
8
+ # :main:KirbyBase
9
+ # :title:KirbyBase Class Documentation
10
+ # KirbyBase is a class that allows you to create and manipulate simple,
11
+ # plain-text databases. You can use it in either a single-user or
12
+ # client-server mode. You can select records for retrieval/updating using
13
+ # code blocks.
14
+ #
15
+ # Author:: Jamey Cribbs (mailto:jcribbs@twmi.rr.com)
16
+ # Homepage:: http://www.netpromi.com/kirbybase.html
17
+ # Copyright:: Copyright (c) 2005 NetPro Technologies, LLC
18
+ # License:: Distributes under the same terms as Ruby
19
+ # History:
20
+ # 2005-03-28:: Version 2.0
21
+ # * This is a completely new version. The interface has changed
22
+ # dramatically.
23
+ # 2005-04-11:: Version 2.1
24
+ # * Changed the interface to KirbyBase#new and KirbyBase#create_table. You
25
+ # now specify arguments via a code block or as part of the argument list.
26
+ # * Added the ability to specify a class at table creation time.
27
+ # Thereafter, whenever you do a #select, the result set will be an array
28
+ # of instances of that class, instead of instances of Struct. You can
29
+ # also use instances of this class as the argument to KBTable#insert,
30
+ # KBTable#update, and KBTable#set.
31
+ # * Added the ability to encrypt a table so that it is no longer stored as
32
+ # a plain-text file.
33
+ # * Added the ability to explicity specify that you want a result set to be
34
+ # sorted in ascending order.
35
+ # * Added the ability to import a csv file into an existing table.
36
+ # * Added the ability to select a record as if the table were a Hash with
37
+ # it's key being the recno field.
38
+ # * Added the ability to update a record as if the table were a Hash with
39
+ # it's key being the recno field.
40
+ # 2005-05-02:: Version 2.2
41
+ # * By far the biggest change in this version is that I have completely
42
+ # redesigned the internal structure of the database code. Because the
43
+ # KirbyBase and KBTable classes were too tightly coupled, I have created
44
+ # a KBEngine class and moved all low-level I/O logic and locking logic
45
+ # to this class. This allowed me to restructure the KirbyBase class to
46
+ # remove all of the methods that should have been private, but couldn't be
47
+ # because of the coupling to KBTable. In addition, it has allowed me to
48
+ # take all of the low-level code that should not have been in the KBTable
49
+ # class and put it where it belongs, as part of the underlying engine. I
50
+ # feel that the design of KirbyBase is much cleaner now. No changes were
51
+ # made to the class interfaces, so you should not have to change any of
52
+ # your code.
53
+ # * Changed str_to_date and str_to_datetime to use Date#parse method.
54
+ # * Changed #pack method so that it no longer reads the whole file into
55
+ # memory while packing it.
56
+ # * Changed code so that special character sequences like &linefeed; can be
57
+ # part of input data and KirbyBase will not interpret it as special
58
+ # characters.
59
+ # 2005-08-09:: Version 2.2.1
60
+ # * Fixed a bug in with_write_lock.
61
+ # * Fixed a bug that occurred if @record_class was a nested class.
62
+ # 2005-09-08:: Version 2.3 Beta 1
63
+ # * Added ability to specify one-to-one links between tables.
64
+ # * Added ability to specify one-to-many links between tables.
65
+ # * Added ability to specify calculated fields in tables.
66
+ # * Added Memo and Blob field types.
67
+ # * Added indexing to speed up queries.
68
+ # 2005-10-03:: Version 2.3 Beta 2
69
+ # * New column type: :YAML. Many thanks to Logan Capaldo for this idea!
70
+ # * Two new methods: #add_table_column and #drop_table_column.
71
+ # * I have refined the select code so that, when you are doing a one-to-one
72
+ # or one-to-many select, if an appropriate index exists for the child
73
+ # table, KirbyBase automatically uses it.
74
+ # * I have changed the designation for a one-to-one link from Link-> to
75
+ # Lookup-> after googling helped me see that this is a more correct term
76
+ # for what I am trying to convey with this link type.
77
+ # 2005-10-10:: Version 2.3 Production
78
+ # * Added the ability to designate a table field as the "key" field, for
79
+ # Lookup purposes. This simply makes it easier to define Lookup fields.
80
+ # * This led me to finally give in and add "the Hal Fulton Feature" as I am
81
+ # forever going to designate it. You can now specify a Lookup field
82
+ # simply by specifying it's field type as a table, for example:
83
+ # :manager, :person (where :manager is the field name, and :person is the
84
+ # name of a table). See the docs for the specifics or ask Hal. :)
85
+ #
86
+ #---------------------------------------------------------------------------
87
+ # KirbyBase
88
+ #---------------------------------------------------------------------------
89
+ class KirbyBase
90
+ include DRb::DRbUndumped
91
+
92
+ attr_reader :engine
93
+
94
+ attr_accessor(:connect_type, :host, :port, :path, :ext)
95
+
96
+ #-----------------------------------------------------------------------
97
+ # initialize
98
+ #-----------------------------------------------------------------------
99
+ #++
100
+ # Create a new database instance.
101
+ #
102
+ # *connect_type*:: Symbol (:local, :client, :server) specifying role to
103
+ # play.
104
+ # *host*:: String containing IP address or DNS name of server hosting
105
+ # database. (Only valid if connect_type is :client.)
106
+ # *port*:: Integer specifying port database server is listening on.
107
+ # (Only valid if connect_type is :client.)
108
+ # *path*:: String specifying path to location of database tables.
109
+ # *ext*:: String specifying extension of table files.
110
+ #
111
+ def initialize(connect_type=:local, host=nil, port=nil, path='./',
112
+ ext='.tbl')
113
+ @connect_type = connect_type
114
+ @host = host
115
+ @port = port
116
+ @path = path
117
+ @ext = ext
118
+
119
+ # See if user specified any method arguments via a code block.
120
+ yield self if block_given?
121
+
122
+ # After the yield, make sure the user doesn't change any of these
123
+ # instance variables.
124
+ class << self
125
+ private(:connect_type=, :host=, :path=, :ext=)
126
+ end
127
+
128
+ # Did user supply full and correct arguments to method?
129
+ raise ArgumentError, 'Invalid connection type specified' unless (
130
+ [:local, :client, :server].include?(@connect_type))
131
+ raise "Must specify hostname or IP address!" if \
132
+ @connect_type == :client and @host.nil?
133
+ raise "Must specify port number!" if @connect_type == :client and \
134
+ @port.nil?
135
+ raise "Invalid path!" if @path.nil?
136
+ raise "Invalid extension!" if @ext.nil?
137
+
138
+ @table_hash = {}
139
+
140
+ # If running as a client, start druby and connect to server.
141
+ if client?
142
+ DRb.start_service()
143
+ @server = DRbObject.new(nil, 'druby://%s:%d' % [@host, @port])
144
+ @engine = @server.engine
145
+ @path = @server.path
146
+ @ext = @server.ext
147
+ else
148
+ @engine = KBEngine.create_called_from_database_instance(self)
149
+ end
150
+
151
+ # The reason why I create all the table instances here is two
152
+ # reasons: (1) I want all of the tables ready to go when a user
153
+ # does a #get_table, so they don't have to wait for the instance
154
+ # to be created, and (2) I want all of the table indexes to get
155
+ # created at the beginning during database initialization so that
156
+ # they are ready for the user to use. Since index creation
157
+ # happens when the table instance is first created, I go ahead and
158
+ # create table instances right off the bat.
159
+ #
160
+ # Also, I use to only execute the code below if this was either a
161
+ # single-user instance of KirbyBase or if client-server, I would
162
+ # only let the client-side KirbyBase instance create the table
163
+ # instances, since there was no need for the server-side KirbyBase
164
+ # instance to create table instances. But, since I want indexes
165
+ # created at db initialization and the server's db instance might
166
+ # be initialized long before any client's db is initialized, I now
167
+ # let the server create table instances also. This is strictly to
168
+ # get the indexes created, there is no other use for the table
169
+ # instances on the server side as they will never be used.
170
+ # Everything should and does go through the table instances created
171
+ # on the client-side.
172
+ @engine.tables.each do |tbl|
173
+ @table_hash[tbl] = KBTable.create_called_from_database_instance(
174
+ self, tbl, File.join(@path, tbl.to_s + @ext))
175
+ end
176
+ end
177
+
178
+ #-----------------------------------------------------------------------
179
+ # server?
180
+ #-----------------------------------------------------------------------
181
+ #++
182
+ # Is this running as a server?
183
+ #
184
+ def server?
185
+ @connect_type == :server
186
+ end
187
+
188
+ #-----------------------------------------------------------------------
189
+ # client?
190
+ #-----------------------------------------------------------------------
191
+ #++
192
+ # Is this running as a client?
193
+ #
194
+ def client?
195
+ @connect_type == :client
196
+ end
197
+
198
+ #-----------------------------------------------------------------------
199
+ # local?
200
+ #-----------------------------------------------------------------------
201
+ #++
202
+ # Is this running in single-user, embedded mode?
203
+ #
204
+ def local?
205
+ @connect_type == :local
206
+ end
207
+
208
+ #-----------------------------------------------------------------------
209
+ # tables
210
+ #-----------------------------------------------------------------------
211
+ #++
212
+ # Return an array containing the names of all tables in this database.
213
+ #
214
+ def tables
215
+ return @engine.tables
216
+ end
217
+
218
+ #-----------------------------------------------------------------------
219
+ # get_table
220
+ #-----------------------------------------------------------------------
221
+ #++
222
+ # Return a reference to the requested table.
223
+ # *name*:: Symbol of table name.
224
+ #
225
+ def get_table(name)
226
+ raise('Do not call this method from a server instance!') if server?
227
+ raise('Table not found!') unless table_exists?(name)
228
+
229
+ if @table_hash.has_key?(name)
230
+ return @table_hash[name]
231
+ else
232
+ @table_hash[name] = \
233
+ KBTable.create_called_from_database_instance(self,
234
+ name, File.join(@path, name.to_s + @ext))
235
+ return @table_hash[name]
236
+ end
237
+ end
238
+
239
+ #-----------------------------------------------------------------------
240
+ # create_table
241
+ #-----------------------------------------------------------------------
242
+ #++
243
+ # Create new table and return a reference to the new table.
244
+ # *name*:: Symbol of table name.
245
+ # *field_defs*:: List of field names (Symbols), field types (Symbols),
246
+ # field indexes, and field extras (Indexes, Lookups,
247
+ # Link_manys, Calculateds, etc.)
248
+ # *Block*:: Optional code block allowing you to set the following:
249
+ # *encrypt*:: true/false specifying whether table should be encrypted.
250
+ # *record_class*:: Class or String specifying the user create class that
251
+ # will be associated with table records.
252
+ #
253
+ def create_table(name=nil, *field_defs)
254
+ raise "Can't call #create_table from server!" if server?
255
+
256
+ t_struct = Struct.new(:name, :field_defs, :encrypt, :record_class)
257
+ t = t_struct.new
258
+ t.name = name
259
+ t.field_defs = field_defs
260
+ t.encrypt = false
261
+ t.record_class = 'Struct'
262
+
263
+ yield t if block_given?
264
+
265
+ raise "Name must be a symbol!" unless t.name.is_a?(Symbol)
266
+ raise "No table name specified!" if t.name.nil?
267
+ raise "No table field definitions specified!" if t.field_defs.nil?
268
+
269
+ @engine.new_table(t.name, t.field_defs, t.encrypt,
270
+ t.record_class.to_s)
271
+
272
+ return get_table(t.name)
273
+ end
274
+
275
+ #-----------------------------------------------------------------------
276
+ # drop_table
277
+ #-----------------------------------------------------------------------
278
+ #++
279
+ # Delete a table.
280
+ #
281
+ # *tablename*:: Symbol of table name.
282
+ #
283
+ def drop_table(tablename)
284
+ raise "Table does not exist!" unless table_exists?(tablename)
285
+ @table_hash.delete(tablename)
286
+ return @engine.delete_table(tablename)
287
+ end
288
+
289
+ #-----------------------------------------------------------------------
290
+ # table_exists?
291
+ #-----------------------------------------------------------------------
292
+ #++
293
+ # Return true if table exists.
294
+ #
295
+ # *tablename*:: Symbol of table name.
296
+ #
297
+ def table_exists?(tablename)
298
+ return @engine.table_exists?(tablename)
299
+ end
300
+
301
+ #-----------------------------------------------------------------------
302
+ # add_table_column
303
+ #-----------------------------------------------------------------------
304
+ #++
305
+ # Add a column to a table.
306
+ #
307
+ # Make sure you are executing this method while in single-user mode
308
+ # (i.e. not running in client/server mode). After you run it, it is
309
+ # probably a good idea to release your handle on the db and
310
+ # re-initialize KirbyBase, as this method changes the table structure.
311
+ #
312
+ # *tablename*:: Symbol of table name.
313
+ # *col_name*:: Symbol of column name to add.
314
+ # *col_type*:: Symbol (or Hash if includes field extras) of column type
315
+ # to add.
316
+ # *after*:: Symbol of column name that you want to add this column
317
+ # after.
318
+ #
319
+ def add_table_column(tablename, col_name, col_type, after=nil)
320
+ raise "Do not execute this method from the server!!!" if server?
321
+
322
+ raise "Invalid table name!" unless table_exists?(tablename)
323
+
324
+ raise "Invalid field name in 'after': #{after}" unless after.nil? \
325
+ or @table_hash[tablename].field_names.include?(after)
326
+
327
+ # Does this new column have field extras (i.e. Index, Lookup, etc.)
328
+ if col_type.is_a?(Hash)
329
+ temp_type = col_type[:DataType]
330
+ else
331
+ temp_type = col_type
332
+ end
333
+
334
+ raise 'Invalid field type: %s' % temp_type unless \
335
+ KBTable.valid_field_type?(temp_type)
336
+
337
+ @engine.add_column(@table_hash[tablename], col_name, col_type,
338
+ after)
339
+
340
+ # Need to reinitialize the table instance and associated indexes.
341
+ @engine.remove_recno_index(tablename)
342
+ @engine.remove_indexes(tablename)
343
+ @table_hash.delete(tablename)
344
+ @table_hash[tablename] = \
345
+ KBTable.create_called_from_database_instance(self, tablename,
346
+ File.join(@path, tablename.to_s + @ext))
347
+ end
348
+
349
+ #-----------------------------------------------------------------------
350
+ # drop_table_column
351
+ #-----------------------------------------------------------------------
352
+ #++
353
+ # Drop a column from a table.
354
+ #
355
+ # Make sure you are executing this method while in single-user mode
356
+ # (i.e. not running in client/server mode). After you run it, it is
357
+ # probably a good idea to release your handle on the db and
358
+ # re-initialize KirbyBase, as this method changes the table structure.
359
+ #
360
+ # *tablename*:: Symbol of table name.
361
+ # *col_name*:: Symbol of column name to add.
362
+ #
363
+ def drop_table_column(tablename, col_name)
364
+ raise "Do not execute this method from the server!!!" if server?
365
+
366
+ raise "Invalid table name!" unless table_exists?(tablename)
367
+
368
+ raise 'Invalid column name: ' % col_name unless \
369
+ @table_hash[tablename].field_names.include?(col_name)
370
+
371
+ raise "Cannot drop :recno column!" if col_name == :recno
372
+
373
+ @engine.drop_column(@table_hash[tablename], col_name)
374
+
375
+ # Need to reinitialize the table instance and associated indexes.
376
+ @engine.remove_recno_index(tablename)
377
+ @engine.remove_indexes(tablename)
378
+ @table_hash.delete(tablename)
379
+ @table_hash[tablename] = \
380
+ KBTable.create_called_from_database_instance(self, tablename,
381
+ File.join(@path, tablename.to_s + @ext))
382
+ end
383
+ end
384
+
385
+ #---------------------------------------------------------------------------
386
+ # KBEngine
387
+ #---------------------------------------------------------------------------
388
+ class KBEngine
389
+ include DRb::DRbUndumped
390
+
391
+ EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
392
+ '0123456789.+-,$:|&;_ '
393
+ EN_STR_LEN = EN_STR.length
394
+ EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
395
+ EN_KEY = EN_KEY1.unpack("u")[0]
396
+ EN_KEY_LEN = EN_KEY.length
397
+
398
+ # Make constructor private.
399
+ private_class_method :new
400
+
401
+ #-----------------------------------------------------------------------
402
+ # KBEngine.create_called_from_database_instance
403
+ #-----------------------------------------------------------------------
404
+ def KBEngine.create_called_from_database_instance(db)
405
+ return new(db)
406
+ end
407
+
408
+ #-----------------------------------------------------------------------
409
+ # initialize
410
+ #-----------------------------------------------------------------------
411
+ def initialize(db)
412
+ @db = db
413
+ @recno_indexes = {}
414
+ @indexes = {}
415
+
416
+ # This hash will hold the table locks if in client/server mode.
417
+ @mutex_hash = {} if @db.server?
418
+ end
419
+
420
+ #-----------------------------------------------------------------------
421
+ # init_recno_index
422
+ #-----------------------------------------------------------------------
423
+ def init_recno_index(table)
424
+ return if recno_index_exists?(table)
425
+
426
+ with_write_locked_table(table) do |fptr|
427
+ @recno_indexes[table.name] = KBRecnoIndex.new(table)
428
+ @recno_indexes[table.name].rebuild(fptr)
429
+ end
430
+ end
431
+
432
+ #-----------------------------------------------------------------------
433
+ # rebuild_recno_index
434
+ #-----------------------------------------------------------------------
435
+ def rebuild_recno_index(table)
436
+ with_write_locked_table(table) do |fptr|
437
+ @recno_indexes[table.name].rebuild(fptr)
438
+ end
439
+ end
440
+
441
+ #-----------------------------------------------------------------------
442
+ # remove_recno_index
443
+ #-----------------------------------------------------------------------
444
+ def remove_recno_index(tablename)
445
+ @recno_indexes.delete(tablename)
446
+ end
447
+
448
+ #-----------------------------------------------------------------------
449
+ # recno_index_exists?
450
+ #-----------------------------------------------------------------------
451
+ def recno_index_exists?(table)
452
+ @recno_indexes.include?(table.name)
453
+ end
454
+
455
+ #-----------------------------------------------------------------------
456
+ # init_index
457
+ #-----------------------------------------------------------------------
458
+ def init_index(table, index_fields)
459
+ return if index_exists?(table, index_fields)
460
+
461
+ with_write_locked_table(table) do |fptr|
462
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym] = \
463
+ KBIndex.new(table, index_fields)
464
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
465
+ ].rebuild(fptr)
466
+ end
467
+ end
468
+
469
+ #-----------------------------------------------------------------------
470
+ # rebuild_index
471
+ #-----------------------------------------------------------------------
472
+ def rebuild_index(table, index_fields)
473
+ with_write_locked_table(table) do |fptr|
474
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
475
+ ].rebuild(fptr)
476
+ end
477
+ end
478
+
479
+ #-----------------------------------------------------------------------
480
+ # remove_indexes
481
+ #-----------------------------------------------------------------------
482
+ def remove_indexes(tablename)
483
+ re_table_name = Regexp.new(tablename.to_s)
484
+ @indexes.delete_if { |k,v| k.to_s =~ re_table_name }
485
+ end
486
+
487
+ #-----------------------------------------------------------------------
488
+ # add_to_indexes
489
+ #-----------------------------------------------------------------------
490
+ def add_to_indexes(table, rec, fpos)
491
+ @recno_indexes[table.name].add_index_rec(rec.first, fpos)
492
+
493
+ re_table_name = Regexp.new(table.name.to_s)
494
+ @indexes.each_pair do |key, index|
495
+ index.add_index_rec(rec) if key.to_s =~ re_table_name
496
+ end
497
+ end
498
+
499
+ #-----------------------------------------------------------------------
500
+ # delete_from_indexes
501
+ #-----------------------------------------------------------------------
502
+ def delete_from_indexes(table, rec, fpos)
503
+ @recno_indexes[table.name].delete_index_rec(rec.recno)
504
+
505
+ re_table_name = Regexp.new(table.name.to_s)
506
+ @indexes.each_pair do |key, index|
507
+ index.delete_index_rec(rec.recno) if key.to_s =~ re_table_name
508
+ end
509
+ end
510
+
511
+ #-----------------------------------------------------------------------
512
+ # index_exists?
513
+ #-----------------------------------------------------------------------
514
+ def index_exists?(table, index_fields)
515
+ @indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
516
+ end
517
+
518
+ #-----------------------------------------------------------------------
519
+ # update_recno_index
520
+ #-----------------------------------------------------------------------
521
+ def update_recno_index(table, recno, fpos)
522
+ @recno_indexes[table.name].update_index_rec(recno, fpos)
523
+ end
524
+
525
+ #-----------------------------------------------------------------------
526
+ # update_to_indexes
527
+ #-----------------------------------------------------------------------
528
+ def update_to_indexes(table, rec)
529
+ re_table_name = Regexp.new(table.name.to_s)
530
+ @indexes.each_pair do |key, index|
531
+ index.update_index_rec(rec) if key.to_s =~ re_table_name
532
+ end
533
+ end
534
+
535
+ #-----------------------------------------------------------------------
536
+ # get_index
537
+ #-----------------------------------------------------------------------
538
+ def get_index(table, index_name)
539
+ return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
540
+ end
541
+
542
+ #-----------------------------------------------------------------------
543
+ # get_recno_index
544
+ #-----------------------------------------------------------------------
545
+ def get_recno_index(table)
546
+ return @recno_indexes[table.name].get_idx
547
+ end
548
+
549
+ #-----------------------------------------------------------------------
550
+ # table_exists?
551
+ #-----------------------------------------------------------------------
552
+ def table_exists?(tablename)
553
+ return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
554
+ end
555
+
556
+ #-----------------------------------------------------------------------
557
+ # tables
558
+ #-----------------------------------------------------------------------
559
+ def tables
560
+ list = []
561
+ Dir.foreach(@db.path) { |filename|
562
+ list << File.basename(filename, '.*').to_sym if \
563
+ File.extname(filename) == @db.ext
564
+ }
565
+ return list
566
+ end
567
+
568
+ #-----------------------------------------------------------------------
569
+ # build_header_field_string
570
+ #-----------------------------------------------------------------------
571
+ def build_header_field_string(field_name_def, field_type_def)
572
+ # Put field name at start of string definition.
573
+ temp_field_def = field_name_def.to_s + ':'
574
+
575
+ # if field type is a hash, that means that it is not just a
576
+ # simple field. Either is is being used in an index, it is a
577
+ # Lookup field, it is a Link_many field, or it is a Calculated
578
+ # field. This next bit of code is to piece together a proper
579
+ # string so that it can be written out to the header rec.
580
+ if field_type_def.is_a?(Hash)
581
+ raise 'Missing :DataType key in field type hash!' unless \
582
+ field_type_def.has_key?(:DataType)
583
+
584
+ temp_type = field_type_def[:DataType]
585
+
586
+ raise 'Invalid field type: %s' % temp_type unless \
587
+ KBTable.valid_field_type?(temp_type)
588
+
589
+ temp_field_def += field_type_def[:DataType].to_s
590
+
591
+ if field_type_def.has_key?(:Key)
592
+ temp_field_def += ':Key->true'
593
+ end
594
+ if field_type_def.has_key?(:Index)
595
+ raise 'Invalid field type for index: %s' % temp_type \
596
+ unless KBTable.valid_index_type?(temp_type)
597
+
598
+ temp_field_def += ':Index->' + field_type_def[:Index].to_s
599
+ end
600
+ if field_type_def.has_key?(:Lookup)
601
+ if field_type_def[:Lookup].is_a?(Array)
602
+ temp_field_def += \
603
+ ':Lookup->%s.%s' % field_type_def[:Lookup]
604
+ else
605
+ tbl = @db.get_table(field_type_def[:Lookup])
606
+ temp_field_def += \
607
+ ':Lookup->%s.%s' % [field_type_def[:Lookup],
608
+ tbl.lookup_key]
609
+ end
610
+ elsif field_type_def.has_key?(:Link_many)
611
+ raise 'Field type for Link_many field must be :ResultSet' \
612
+ unless temp_type == :ResultSet
613
+ temp_field_def += \
614
+ ':Link_many->%s=%s.%s' % field_type_def[:Link_many]
615
+ elsif field_type_def.has_key?(:Calculated)
616
+ temp_field_def += \
617
+ ':Calculated->%s' % field_type_def[:Calculated]
618
+ end
619
+ else
620
+ if KBTable.valid_field_type?(field_type_def)
621
+ temp_field_def += field_type_def.to_s
622
+ elsif @db.table_exists?(field_type_def)
623
+ tbl = @db.get_table(field_type_def)
624
+ temp_field_def += \
625
+ '%s:Lookup->%s.%s' % [tbl.field_types[
626
+ tbl.field_names.index(tbl.lookup_key)], field_type_def,
627
+ tbl.lookup_key]
628
+ else
629
+ raise 'Invalid field type: %s' % field_type_def
630
+ end
631
+ end
632
+ return temp_field_def
633
+ end
634
+
635
+ #-----------------------------------------------------------------------
636
+ # new_table
637
+ #-----------------------------------------------------------------------
638
+ #++
639
+ # Create physical file holding table. This table should not be directly
640
+ # called in your application, but only called by #create_table.
641
+ #
642
+ def new_table(name, field_defs, encrypt, record_class)
643
+ # Can't create a table that already exists!
644
+ raise "Table already exists!" if table_exists?(name)
645
+
646
+ raise 'Must have a field type for each field name' \
647
+ unless field_defs.size.remainder(2) == 0
648
+ temp_field_defs = []
649
+ (0...field_defs.size).step(2) do |x|
650
+ temp_field_defs << build_header_field_string(field_defs[x],
651
+ field_defs[x+1])
652
+ end
653
+
654
+ # Header rec consists of last record no. used, delete count, and
655
+ # all field names/types. Here, I am inserting the 'recno' field
656
+ # at the beginning of the fields.
657
+ header_rec = ['000000', '000000', record_class, 'recno:Integer',
658
+ temp_field_defs].join('|')
659
+
660
+ header_rec = 'Z' + encrypt_str(header_rec) if encrypt
661
+
662
+ begin
663
+ fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
664
+ fptr.write(header_rec + "\n")
665
+ ensure
666
+ fptr.close
667
+ end
668
+ end
669
+
670
+ #-----------------------------------------------------------------------
671
+ # delete_table
672
+ #-----------------------------------------------------------------------
673
+ def delete_table(tablename)
674
+ with_write_lock(tablename) do
675
+ remove_indexes(tablename)
676
+ remove_recno_index(tablename)
677
+ File.delete(File.join(@db.path, tablename.to_s + @db.ext))
678
+ return true
679
+ end
680
+ end
681
+
682
+ #----------------------------------------------------------------------
683
+ # get_total_recs
684
+ #----------------------------------------------------------------------
685
+ def get_total_recs(table)
686
+ return get_recs(table).size
687
+ end
688
+
689
+ #-----------------------------------------------------------------------
690
+ # get_header_vars
691
+ #-----------------------------------------------------------------------
692
+ def get_header_vars(table)
693
+ with_table(table) do |fptr|
694
+ line = get_header_record(table, fptr)
695
+
696
+ last_rec_no, del_ctr, record_class, *flds = line.split('|')
697
+ field_names = flds.collect { |x| x.split(':')[0].to_sym }
698
+ field_types = flds.collect { |x| x.split(':')[1].to_sym }
699
+ field_indexes = [nil] * field_names.size
700
+ field_extras = [nil] * field_names.size
701
+
702
+ flds.each_with_index do |x,i|
703
+ field_extras[i] = {}
704
+ if x.split(':').size > 2
705
+ x.split(':')[2..-1].each do |y|
706
+ if y =~ /Index/
707
+ field_indexes[i] = y
708
+ else
709
+ field_extras[i][y.split('->')[0]] = \
710
+ y.split('->')[1]
711
+ end
712
+ end
713
+ end
714
+ end
715
+ return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
716
+ record_class, field_names, field_types, field_indexes,
717
+ field_extras]
718
+ end
719
+ end
720
+
721
+ #-----------------------------------------------------------------------
722
+ # get_recs
723
+ #-----------------------------------------------------------------------
724
+ def get_recs(table)
725
+ encrypted = table.encrypted?
726
+ recs = []
727
+
728
+ with_table(table) do |fptr|
729
+ begin
730
+ # Skip header rec.
731
+ fptr.readline
732
+
733
+ # Loop through table.
734
+ while true
735
+ # Record current position in table. Then read first
736
+ # detail record.
737
+ fpos = fptr.tell
738
+ line = fptr.readline
739
+ line.chomp!
740
+ line_length = line.length
741
+
742
+ line = unencrypt_str(line) if encrypted
743
+ line.strip!
744
+
745
+ # If blank line (i.e. 'deleted'), skip it.
746
+ next if line == ''
747
+
748
+ # Split the line up into fields.
749
+ rec = line.split('|', -1)
750
+ rec << fpos << line_length
751
+ recs << rec
752
+ end
753
+ # Here's how we break out of the loop...
754
+ rescue EOFError
755
+ end
756
+ return recs
757
+ end
758
+ end
759
+
760
+ #-----------------------------------------------------------------------
761
+ # get_recs_by_recno
762
+ #-----------------------------------------------------------------------
763
+ def get_recs_by_recno(table, recnos)
764
+ encrypted = table.encrypted?
765
+ recs = []
766
+ recno_idx = get_recno_index(table)
767
+
768
+ with_table(table) do |fptr|
769
+ # Skip header rec.
770
+ fptr.readline
771
+
772
+ recnos.collect { |r| [recno_idx[r], r]
773
+ }.sort.each do |r|
774
+ fptr.seek(r[0])
775
+ line = fptr.readline
776
+ line.chomp!
777
+ line_length = line.length
778
+
779
+ line = unencrypt_str(line) if encrypted
780
+ line.strip!
781
+
782
+ # If blank line (i.e. 'deleted'), skip it.
783
+ next if line == ''
784
+
785
+ # Split the line up into fields.
786
+ rec = line.split('|', -1)
787
+ raise "Index Corrupt!" unless rec[0].to_i == r[1]
788
+ rec << r[0] << line_length
789
+ recs << rec
790
+ end
791
+ return recs
792
+ end
793
+ end
794
+
795
+ #-----------------------------------------------------------------------
796
+ # get_rec_by_recno
797
+ #-----------------------------------------------------------------------
798
+ def get_rec_by_recno(table, recno)
799
+ encrypted = table.encrypted?
800
+ recno_idx = get_recno_index(table)
801
+
802
+ return nil unless recno_idx.has_key?(recno)
803
+
804
+ with_table(table) do |fptr|
805
+ fptr.seek(recno_idx[recno])
806
+ line = fptr.readline
807
+ line.chomp!
808
+ line_length = line.length
809
+
810
+ line = unencrypt_str(line) if encrypted
811
+ line.strip!
812
+
813
+ return nil if line == ''
814
+
815
+ # Split the line up into fields.
816
+ rec = line.split('|', -1)
817
+
818
+ raise "Index Corrupt!" unless rec[0].to_i == recno
819
+ rec << recno_idx[recno] << line_length
820
+ return rec
821
+ end
822
+ end
823
+
824
+ #-----------------------------------------------------------------------
825
+ # insert_record
826
+ #-----------------------------------------------------------------------
827
+ def insert_record(table, rec)
828
+ with_write_locked_table(table) do |fptr|
829
+ # Auto-increment the record number field.
830
+ rec_no = incr_rec_no_ctr(table, fptr)
831
+
832
+ # Insert the newly created record number value at the beginning
833
+ # of the field values.
834
+ rec[0] = rec_no
835
+
836
+ fptr.seek(0, IO::SEEK_END)
837
+ fpos = fptr.tell
838
+
839
+ write_record(table, fptr, 'end', rec.join('|'))
840
+
841
+ add_to_indexes(table, rec, fpos)
842
+
843
+ # Return the record number of the newly created record.
844
+ return rec_no
845
+ end
846
+ end
847
+
848
+ #-----------------------------------------------------------------------
849
+ # update_records
850
+ #-----------------------------------------------------------------------
851
+ def update_records(table, recs)
852
+ with_write_locked_table(table) do |fptr|
853
+ recs.each do |rec|
854
+ line = rec[:rec].join('|')
855
+
856
+ # This doesn't actually 'delete' the line, it just
857
+ # makes it all spaces. That way, if the updated
858
+ # record is the same or less length than the old
859
+ # record, we can write the record back into the
860
+ # same spot. If the updated record is greater than
861
+ # the old record, we will leave the now spaced-out
862
+ # line and write the updated record at the end of
863
+ # the file.
864
+ write_record(table, fptr, rec[:fpos],
865
+ ' ' * rec[:line_length])
866
+ if line.length > rec[:line_length]
867
+ fptr.seek(0, IO::SEEK_END)
868
+ new_fpos = fptr.tell
869
+ write_record(table, fptr, 'end', line)
870
+ incr_del_ctr(table, fptr)
871
+
872
+ update_recno_index(table, rec[:rec].first, new_fpos)
873
+ else
874
+ write_record(table, fptr, rec[:fpos], line)
875
+ end
876
+ update_to_indexes(table, rec[:rec])
877
+ end
878
+ # Return the number of records updated.
879
+ return recs.size
880
+ end
881
+ end
882
+
883
+ #-----------------------------------------------------------------------
884
+ # delete_records
885
+ #-----------------------------------------------------------------------
886
+ def delete_records(table, recs)
887
+ with_write_locked_table(table) do |fptr|
888
+ recs.each do |rec|
889
+ # Go to offset within the file where the record is and
890
+ # replace it with all spaces.
891
+ write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
892
+ incr_del_ctr(table, fptr)
893
+
894
+ delete_from_indexes(table, rec, rec.fpos)
895
+ end
896
+
897
+ # Return the number of records deleted.
898
+ return recs.size
899
+ end
900
+ end
901
+
902
+ #-----------------------------------------------------------------------
903
+ # add_column
904
+ #-----------------------------------------------------------------------
905
+ def add_column(table, col_name, col_type, after)
906
+ temp_field_def = build_header_field_string(col_name, col_type)
907
+
908
+ if after.nil?
909
+ insert_after = -1
910
+ else
911
+ if table.field_names.last == after
912
+ insert_after = -1
913
+ else
914
+ insert_after = table.field_names.index(after)+1
915
+ end
916
+ end
917
+
918
+ with_write_lock(table.name) do
919
+ fptr = open(table.filename, 'r')
920
+ new_fptr = open(table.filename+'temp', 'w')
921
+
922
+ line = fptr.readline.chomp
923
+
924
+ if line[0..0] == 'Z'
925
+ header_rec = unencrypt_str(line[1..-1]).split('|')
926
+ if insert_after == -1
927
+ header_rec.insert(insert_after, temp_field_def)
928
+ else
929
+ header_rec.insert(insert_after+3, temp_field_def)
930
+ end
931
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
932
+ "\n")
933
+ else
934
+ header_rec = line.split('|')
935
+ if insert_after == -1
936
+ header_rec.insert(insert_after, temp_field_def)
937
+ else
938
+ header_rec.insert(insert_after+3, temp_field_def)
939
+ end
940
+ new_fptr.write(header_rec.join('|') + "\n")
941
+ end
942
+
943
+ begin
944
+ while true
945
+ line = fptr.readline.chomp
946
+
947
+ if table.encrypted?
948
+ temp_line = unencrypt_str(line)
949
+ else
950
+ temp_line = line
951
+ end
952
+
953
+ rec = temp_line.split('|')
954
+ rec.insert(insert_after, '')
955
+
956
+ if table.encrypted?
957
+ new_fptr.write(encrypt_str(rec.join('|')) + "\n")
958
+ else
959
+ new_fptr.write(rec.join('|') + "\n")
960
+ end
961
+ end
962
+ # Here's how we break out of the loop...
963
+ rescue EOFError
964
+ end
965
+
966
+ # Close the table and release the write lock.
967
+ fptr.close
968
+ new_fptr.close
969
+ File.delete(table.filename)
970
+ FileUtils.mv(table.filename+'temp', table.filename)
971
+ end
972
+ end
973
+
974
+ #-----------------------------------------------------------------------
975
+ # drop_column
976
+ #-----------------------------------------------------------------------
977
+ def drop_column(table, col_name)
978
+ col_index = table.field_names.index(col_name)
979
+ with_write_lock(table.name) do
980
+ fptr = open(table.filename, 'r')
981
+ new_fptr = open(table.filename+'temp', 'w')
982
+
983
+ line = fptr.readline.chomp
984
+
985
+ if line[0..0] == 'Z'
986
+ header_rec = unencrypt_str(line[1..-1]).split('|')
987
+ header_rec.delete_at(col_index+3)
988
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
989
+ "\n")
990
+ else
991
+ header_rec = line.split('|')
992
+ header_rec.delete_at(col_index+3)
993
+ new_fptr.write(header_rec.join('|') + "\n")
994
+ end
995
+
996
+ begin
997
+ while true
998
+ line = fptr.readline.chomp
999
+
1000
+ if table.encrypted?
1001
+ temp_line = unencrypt_str(line)
1002
+ else
1003
+ temp_line = line
1004
+ end
1005
+
1006
+ rec = temp_line.split('|')
1007
+ rec.delete_at(col_index)
1008
+
1009
+ if table.encrypted?
1010
+ new_fptr.write(encrypt_str(rec.join('|')) + "\n")
1011
+ else
1012
+ new_fptr.write(rec.join('|') + "\n")
1013
+ end
1014
+ end
1015
+ # Here's how we break out of the loop...
1016
+ rescue EOFError
1017
+ end
1018
+
1019
+ # Close the table and release the write lock.
1020
+ fptr.close
1021
+ new_fptr.close
1022
+ File.delete(table.filename)
1023
+ FileUtils.mv(table.filename+'temp', table.filename)
1024
+ end
1025
+ end
1026
+
1027
+ #-----------------------------------------------------------------------
1028
+ # pack_table
1029
+ #-----------------------------------------------------------------------
1030
+ def pack_table(table)
1031
+ with_write_lock(table.name) do
1032
+ fptr = open(table.filename, 'r')
1033
+ new_fptr = open(table.filename+'temp', 'w')
1034
+
1035
+ line = fptr.readline.chomp
1036
+ # Reset the delete counter in the header rec to 0.
1037
+ if line[0..0] == 'Z'
1038
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1039
+ header_rec[1] = '000000'
1040
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1041
+ "\n")
1042
+ else
1043
+ header_rec = line.split('|')
1044
+ header_rec[1] = '000000'
1045
+ new_fptr.write(header_rec.join('|') + "\n")
1046
+ end
1047
+
1048
+ lines_deleted = 0
1049
+
1050
+ begin
1051
+ while true
1052
+ line = fptr.readline
1053
+
1054
+ if table.encrypted?
1055
+ temp_line = unencrypt_str(line)
1056
+ else
1057
+ temp_line = line
1058
+ end
1059
+
1060
+ if temp_line.strip == ''
1061
+ lines_deleted += 1
1062
+ else
1063
+ new_fptr.write(line)
1064
+ end
1065
+ end
1066
+ # Here's how we break out of the loop...
1067
+ rescue EOFError
1068
+ end
1069
+
1070
+ # Close the table and release the write lock.
1071
+ fptr.close
1072
+ new_fptr.close
1073
+ File.delete(table.filename)
1074
+ FileUtils.mv(table.filename+'temp', table.filename)
1075
+
1076
+ # Return the number of deleted records that were removed.
1077
+ return lines_deleted
1078
+ end
1079
+ end
1080
+
1081
+ #-----------------------------------------------------------------------
1082
+ # get_memo
1083
+ #-----------------------------------------------------------------------
1084
+ def get_memo(filepath)
1085
+ begin
1086
+ f = File.new(filepath)
1087
+ return f.readlines
1088
+ ensure
1089
+ f.close
1090
+ end
1091
+ end
1092
+
1093
+ #-----------------------------------------------------------------------
1094
+ # get_blob
1095
+ #-----------------------------------------------------------------------
1096
+ def get_blob(filepath)
1097
+ begin
1098
+ f = File.new(filepath, 'rb')
1099
+ return f.read
1100
+ ensure
1101
+ f.close
1102
+ end
1103
+ end
1104
+
1105
+
1106
+ #-----------------------------------------------------------------------
1107
+ # PRIVATE METHODS
1108
+ #-----------------------------------------------------------------------
1109
+ private
1110
+
1111
+ #-----------------------------------------------------------------------
1112
+ # with_table
1113
+ #-----------------------------------------------------------------------
1114
+ def with_table(table, access='r')
1115
+ begin
1116
+ yield fptr = open(table.filename, access)
1117
+ ensure
1118
+ fptr.close
1119
+ end
1120
+ end
1121
+
1122
+ #-----------------------------------------------------------------------
1123
+ # with_write_lock
1124
+ #-----------------------------------------------------------------------
1125
+ def with_write_lock(tablename)
1126
+ begin
1127
+ write_lock(tablename) if @db.server?
1128
+ yield
1129
+ ensure
1130
+ write_unlock(tablename) if @db.server?
1131
+ end
1132
+ end
1133
+
1134
+ #-----------------------------------------------------------------------
1135
+ # with_write_locked_table
1136
+ #-----------------------------------------------------------------------
1137
+ def with_write_locked_table(table, access='r+')
1138
+ begin
1139
+ write_lock(table.name) if @db.server?
1140
+ yield fptr = open(table.filename, access)
1141
+ ensure
1142
+ fptr.close
1143
+ write_unlock(table.name) if @db.server?
1144
+ end
1145
+ end
1146
+
1147
+ #-----------------------------------------------------------------------
1148
+ # write_lock
1149
+ #-----------------------------------------------------------------------
1150
+ def write_lock(tablename)
1151
+ # Unless an key already exists in the hash holding mutex records
1152
+ # for this table, create a write key for this table in the mutex
1153
+ # hash. Then, place a lock on that mutex.
1154
+ @mutex_hash[tablename] = Mutex.new unless (
1155
+ @mutex_hash.has_key?(tablename))
1156
+ @mutex_hash[tablename].lock
1157
+
1158
+ return true
1159
+ end
1160
+
1161
+ #----------------------------------------------------------------------
1162
+ # write_unlock
1163
+ #----------------------------------------------------------------------
1164
+ def write_unlock(tablename)
1165
+ # Unlock the write mutex for this table.
1166
+ @mutex_hash[tablename].unlock
1167
+
1168
+ return true
1169
+ end
1170
+
1171
+ #----------------------------------------------------------------------
1172
+ # write_record
1173
+ #----------------------------------------------------------------------
1174
+ def write_record(table, fptr, pos, record)
1175
+ if table.encrypted?
1176
+ temp_rec = encrypt_str(record)
1177
+ else
1178
+ temp_rec = record
1179
+ end
1180
+
1181
+ # If record is to be appended, go to end of table and write
1182
+ # record, adding newline character.
1183
+ if pos == 'end'
1184
+ fptr.seek(0, IO::SEEK_END)
1185
+ fptr.write(temp_rec + "\n")
1186
+ else
1187
+ # Otherwise, overwrite another record (that's why we don't
1188
+ # add the newline character).
1189
+ fptr.seek(pos)
1190
+ fptr.write(temp_rec)
1191
+ end
1192
+ end
1193
+
1194
+ #----------------------------------------------------------------------
1195
+ # write_header_record
1196
+ #----------------------------------------------------------------------
1197
+ def write_header_record(table, fptr, record)
1198
+ fptr.seek(0)
1199
+
1200
+ if table.encrypted?
1201
+ fptr.write('Z' + encrypt_str(record) + "\n")
1202
+ else
1203
+ fptr.write(record + "\n")
1204
+ end
1205
+ end
1206
+
1207
+ #----------------------------------------------------------------------
1208
+ # get_header_record
1209
+ #----------------------------------------------------------------------
1210
+ def get_header_record(table, fptr)
1211
+ fptr.seek(0)
1212
+
1213
+ if table.encrypted?
1214
+ return unencrypt_str(fptr.readline[1..-1].chomp)
1215
+ else
1216
+ return fptr.readline.chomp
1217
+ end
1218
+ end
1219
+
1220
+ #-----------------------------------------------------------------------
1221
+ # reset_rec_no_ctr
1222
+ #-----------------------------------------------------------------------
1223
+ def reset_rec_no_ctr(table, fptr)
1224
+ last_rec_no, rest_of_line = get_header_record(table, fptr).split(
1225
+ '|', 2)
1226
+ write_header_record(table, fptr, ['%06d' % 0, rest_of_line].join(
1227
+ '|'))
1228
+ return true
1229
+ end
1230
+
1231
+ #-----------------------------------------------------------------------
1232
+ # incr_rec_no_ctr
1233
+ #-----------------------------------------------------------------------
1234
+ def incr_rec_no_ctr(table, fptr)
1235
+ last_rec_no, rest_of_line = get_header_record(table, fptr).split(
1236
+ '|', 2)
1237
+ last_rec_no = last_rec_no.to_i + 1
1238
+
1239
+ write_header_record(table, fptr, ['%06d' % last_rec_no,
1240
+ rest_of_line].join('|'))
1241
+
1242
+ # Return the new recno.
1243
+ return last_rec_no
1244
+ end
1245
+
1246
+ #-----------------------------------------------------------------------
1247
+ # incr_del_ctr
1248
+ #-----------------------------------------------------------------------
1249
+ def incr_del_ctr(table, fptr)
1250
+ last_rec_no, del_ctr, rest_of_line = get_header_record(table,
1251
+ fptr).split('|', 3)
1252
+ del_ctr = del_ctr.to_i + 1
1253
+
1254
+ write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
1255
+ rest_of_line].join('|'))
1256
+
1257
+ return true
1258
+ end
1259
+
1260
+ #-----------------------------------------------------------------------
1261
+ # encrypt_str
1262
+ #-----------------------------------------------------------------------
1263
+ def encrypt_str(s)
1264
+ # Returns an encrypted string, using the Vignere Cipher.
1265
+
1266
+ new_str = ''
1267
+ i_key = -1
1268
+ s.each_byte do |c|
1269
+ if i_key < EN_KEY_LEN - 1
1270
+ i_key += 1
1271
+ else
1272
+ i_key = 0
1273
+ end
1274
+
1275
+ if EN_STR.index(c.chr).nil?
1276
+ new_str << c.chr
1277
+ next
1278
+ end
1279
+
1280
+ i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
1281
+ i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
1282
+ new_str << EN_STR[i_from_str]
1283
+ end
1284
+ return new_str
1285
+ end
1286
+
1287
+ #-----------------------------------------------------------------------
1288
+ # unencrypt_str
1289
+ #-----------------------------------------------------------------------
1290
+ def unencrypt_str(s)
1291
+ # Returns an unencrypted string, using the Vignere Cipher.
1292
+
1293
+ new_str = ''
1294
+ i_key = -1
1295
+ s.each_byte do |c|
1296
+ if i_key < EN_KEY_LEN - 1
1297
+ i_key += 1
1298
+ else
1299
+ i_key = 0
1300
+ end
1301
+
1302
+ if EN_STR.index(c.chr).nil?
1303
+ new_str << c.chr
1304
+ next
1305
+ end
1306
+
1307
+ i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
1308
+ i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
1309
+ new_str << EN_STR[i_from_str]
1310
+ end
1311
+ return new_str
1312
+ end
1313
+ end
1314
+
1315
+
1316
+ #---------------------------------------------------------------------------
1317
+ # KBTypeConversionsMixin
1318
+ #---------------------------------------------------------------------------
1319
+ module KBTypeConversionsMixin
1320
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
1321
+
1322
+ #-----------------------------------------------------------------------
1323
+ # convert_to
1324
+ #-----------------------------------------------------------------------
1325
+ def convert_to(data_type, s)
1326
+ return nil if s.empty? or s.nil?
1327
+
1328
+ case data_type
1329
+ when :String
1330
+ if s =~ UNENCODE_RE
1331
+ return s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
1332
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
1333
+ ).gsub('&amp;', "&")
1334
+ else
1335
+ return s
1336
+ end
1337
+ when :Integer
1338
+ return s.to_i
1339
+ when :Float
1340
+ return s.to_f
1341
+ when :Boolean
1342
+ if ['false', 'False', nil, false].include?(s)
1343
+ return false
1344
+ else
1345
+ return true
1346
+ end
1347
+ when :Date
1348
+ return Date.parse(s)
1349
+ when :Time
1350
+ return Time.parse(s)
1351
+ when :DateTime
1352
+ return DateTime.parse(s)
1353
+ when :YAML
1354
+ # This code is here in case the YAML field is the last
1355
+ # field in the record. Because YAML normall defines a
1356
+ # nil value as "--- ", but KirbyBase strips trailing
1357
+ # spaces off the end of the record, so if this is the
1358
+ # last field in the record, KirbyBase will strip the
1359
+ # trailing space off and make it "---". When KirbyBase
1360
+ # attempts to convert this value back using to_yaml,
1361
+ # you get an exception.
1362
+ if s == "---"
1363
+ return nil
1364
+ elsif s =~ UNENCODE_RE
1365
+ y = s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
1366
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
1367
+ ).gsub('&amp;', "&")
1368
+ return YAML.load(y)
1369
+ else
1370
+ return YAML.load(s)
1371
+ end
1372
+ when :Memo
1373
+ return KBMemo.new(@tbl.db, s)
1374
+ when :Blob
1375
+ return KBBlob.new(@tbl.db, s)
1376
+ else
1377
+ raise "Invalid field type: %s" % data_type
1378
+ end
1379
+ end
1380
+ end
1381
+
1382
+
1383
+ #---------------------------------------------------------------------------
1384
+ # KBTable
1385
+ #---------------------------------------------------------------------------
1386
+ class KBTable
1387
+ include DRb::DRbUndumped
1388
+
1389
+ # Make constructor private. KBTable instances should only be created
1390
+ # from KirbyBase#get_table.
1391
+ private_class_method :new
1392
+
1393
+ VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1394
+ :DateTime, :Memo, :ResultSet, :YAML]
1395
+
1396
+ VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1397
+ :DateTime]
1398
+
1399
+ # Regular expression used to determine if field needs to be
1400
+ # encoded.
1401
+ ENCODE_RE = /&|\n|\r|\032|\|/
1402
+
1403
+ attr_reader :filename, :name, :table_class, :db, :lookup_key
1404
+
1405
+ #-----------------------------------------------------------------------
1406
+ # KBTable.valid_field_type
1407
+ #-----------------------------------------------------------------------
1408
+ #++
1409
+ # Return true if valid field type.
1410
+ #
1411
+ # *field_type*:: Symbol specifying field type.
1412
+ #
1413
+ def KBTable.valid_field_type?(field_type)
1414
+ VALID_FIELD_TYPES.include?(field_type)
1415
+ end
1416
+
1417
+ #-----------------------------------------------------------------------
1418
+ # KBTable.valid_index_type
1419
+ #-----------------------------------------------------------------------
1420
+ #++
1421
+ # Return true if valid index type.
1422
+ #
1423
+ # *field_type*:: Symbol specifying field type.
1424
+ #
1425
+ def KBTable.valid_index_type?(field_type)
1426
+ VALID_INDEX_TYPES.include?(field_type)
1427
+ end
1428
+
1429
+ #-----------------------------------------------------------------------
1430
+ # create_called_from_database_instance
1431
+ #-----------------------------------------------------------------------
1432
+ #++
1433
+ # Return a new instance of KBTable. Should never be called directly by
1434
+ # your application. Should only be called from KirbyBase#get_table.
1435
+ #
1436
+ def KBTable.create_called_from_database_instance(db, name, filename)
1437
+ return new(db, name, filename)
1438
+ end
1439
+
1440
+ #-----------------------------------------------------------------------
1441
+ # initialize
1442
+ #-----------------------------------------------------------------------
1443
+ #++
1444
+ # This has been declared private so user's cannot create new instances
1445
+ # of KBTable from their application. A user gets a handle to a KBTable
1446
+ # instance by calling KirbyBase#get_table for an existing table or
1447
+ # KirbyBase.create_table for a new table.
1448
+ #
1449
+ def initialize(db, name, filename)
1450
+ @db = db
1451
+ @name = name
1452
+ @filename = filename
1453
+ @encrypted = false
1454
+ @lookup_key = :recno
1455
+
1456
+ # Alias delete_all to clear method.
1457
+ alias delete_all clear
1458
+
1459
+ update_header_vars
1460
+ create_indexes
1461
+ create_table_class unless @db.server?
1462
+ end
1463
+
1464
+ #-----------------------------------------------------------------------
1465
+ # encrypted?
1466
+ #-----------------------------------------------------------------------
1467
+ #++
1468
+ # Returns true if table is encrypted.
1469
+ #
1470
+ def encrypted?
1471
+ if @encrypted
1472
+ return true
1473
+ else
1474
+ return false
1475
+ end
1476
+ end
1477
+
1478
+ #-----------------------------------------------------------------------
1479
+ # field_names
1480
+ #-----------------------------------------------------------------------
1481
+ #++
1482
+ # Return array containing table field names.
1483
+ #
1484
+ def field_names
1485
+ return @field_names
1486
+ end
1487
+
1488
+ #-----------------------------------------------------------------------
1489
+ # field_types
1490
+ #-----------------------------------------------------------------------
1491
+ #++
1492
+ # Return array containing table field types.
1493
+ #
1494
+ def field_types
1495
+ return @field_types
1496
+ end
1497
+
1498
+ #-----------------------------------------------------------------------
1499
+ # field_extras
1500
+ #-----------------------------------------------------------------------
1501
+ #++
1502
+ # Return array containing table field extras.
1503
+ #
1504
+ def field_extras
1505
+ return @field_extras
1506
+ end
1507
+
1508
+ #-----------------------------------------------------------------------
1509
+ # field_indexes
1510
+ #-----------------------------------------------------------------------
1511
+ #++
1512
+ # Return array containing table field indexes.
1513
+ #
1514
+ def field_indexes
1515
+ return @field_indexes
1516
+ end
1517
+
1518
+ #-----------------------------------------------------------------------
1519
+ # insert
1520
+ #-----------------------------------------------------------------------
1521
+ #++
1522
+ # Insert a new record into a table, return unique record number.
1523
+ #
1524
+ # *data*:: Array, Hash, Struct instance containing field values of
1525
+ # new record.
1526
+ # *insert_proc*:: Proc instance containing insert code. This and the
1527
+ # data parameter are mutually exclusive.
1528
+ #
1529
+ def insert(*data, &insert_proc)
1530
+ raise 'Cannot specify both a hash/array/struct and a ' + \
1531
+ 'proc for method #insert!' unless data.empty? or insert_proc.nil?
1532
+
1533
+ raise 'Must specify either hash/array/struct or insert ' + \
1534
+ 'proc for method #insert!' if data.empty? and insert_proc.nil?
1535
+
1536
+ # Update the header variables.
1537
+ update_header_vars
1538
+
1539
+ # Convert input, which could be an array, a hash, or a Struct
1540
+ # into a common format (i.e. hash).
1541
+ if data.empty?
1542
+ input_rec = convert_input_data(insert_proc)
1543
+ else
1544
+ input_rec = convert_input_data(data)
1545
+ end
1546
+
1547
+ # Check the field values to make sure they are proper types.
1548
+ validate_input(input_rec)
1549
+
1550
+ return @db.engine.insert_record(self, @field_names.zip(@field_types
1551
+ ).collect do |fn, ft|
1552
+ convert_to_string(ft, input_rec.fetch(fn, ''))
1553
+ end)
1554
+ end
1555
+
1556
+ #-----------------------------------------------------------------------
1557
+ # update_all
1558
+ #-----------------------------------------------------------------------
1559
+ #++
1560
+ # Return array of records (Structs) to be updated, in this case all
1561
+ # records.
1562
+ #
1563
+ # *updates*:: Hash or Struct containing updates.
1564
+ #
1565
+ def update_all(*updates)
1566
+ update(*updates) { true }
1567
+ end
1568
+
1569
+ #-----------------------------------------------------------------------
1570
+ # update
1571
+ #-----------------------------------------------------------------------
1572
+ #++
1573
+ # Return array of records (Structs) to be updated based on select cond.
1574
+ #
1575
+ # *updates*:: Hash or Struct containing updates.
1576
+ # *select_cond*:: Proc containing code to select records to update.
1577
+ #
1578
+ def update(*updates, &select_cond)
1579
+ raise ArgumentError, "Must specify select condition code " + \
1580
+ "block. To update all records, use #update_all instead." if \
1581
+ select_cond.nil?
1582
+
1583
+ # Update the header variables.
1584
+ update_header_vars
1585
+
1586
+ # Get all records that match the selection criteria and
1587
+ # return them in an array.
1588
+ result_set = get_matches(:update, @field_names, select_cond)
1589
+
1590
+ return result_set if updates.empty?
1591
+
1592
+ set(result_set, updates)
1593
+ end
1594
+
1595
+ #-----------------------------------------------------------------------
1596
+ # []=
1597
+ #-----------------------------------------------------------------------
1598
+ #++
1599
+ # Update record whose recno field equals index.
1600
+ #
1601
+ # *index*:: Integer specifying recno you wish to select.
1602
+ # *updates*:: Hash, Struct, or Array containing updates.
1603
+ #
1604
+ def []=(index, updates)
1605
+ return update(updates) { |r| r.recno == index }
1606
+ end
1607
+
1608
+ #-----------------------------------------------------------------------
1609
+ # set
1610
+ #-----------------------------------------------------------------------
1611
+ #++
1612
+ # Set fields of records to updated values. Returns number of records
1613
+ # updated.
1614
+ #
1615
+ # *recs*:: Array of records (Structs) that will be updated.
1616
+ # *data*:: Hash, Struct, Proc containing updates.
1617
+ #
1618
+ def set(recs, data)
1619
+ # Convert updates, which could be an array, a hash, or a Struct
1620
+ # into a common format (i.e. hash).
1621
+ update_rec = convert_input_data(data)
1622
+
1623
+ # Make sure all of the fields of the update rec are of the proper
1624
+ # type.
1625
+ validate_input(update_rec)
1626
+
1627
+ updated_recs = []
1628
+
1629
+ # For each one of the recs that matched the update query, apply the
1630
+ # updates to it and write it back to the database table.
1631
+ recs.each do |rec|
1632
+ updated_rec = {}
1633
+ updated_rec[:rec] = \
1634
+ @field_names.zip(@field_types).collect do |fn, ft|
1635
+ convert_to_string(ft, update_rec.fetch(fn, rec.send(fn)))
1636
+ end
1637
+ updated_rec[:fpos] = rec.fpos
1638
+ updated_rec[:line_length] = rec.line_length
1639
+ updated_recs << updated_rec
1640
+ end
1641
+ @db.engine.update_records(self, updated_recs)
1642
+
1643
+ # Return the number of records updated.
1644
+ return recs.size
1645
+ end
1646
+
1647
+ #-----------------------------------------------------------------------
1648
+ # delete
1649
+ #-----------------------------------------------------------------------
1650
+ #++
1651
+ # Delete records from table and return # deleted.
1652
+ #
1653
+ # *select_cond*:: Proc containing code to select records.
1654
+ #
1655
+ def delete(&select_cond)
1656
+ raise ArgumentError, 'Must specify select condition code ' + \
1657
+ 'block. To delete all records, use #clear instead.' if \
1658
+ select_cond.nil?
1659
+
1660
+ # Get all records that match the selection criteria and
1661
+ # return them in an array.
1662
+ result_set = get_matches(:delete, [:recno], select_cond)
1663
+
1664
+ @db.engine.delete_records(self, result_set)
1665
+
1666
+ # Return the number of records deleted.
1667
+ return result_set.size
1668
+ end
1669
+
1670
+ #-----------------------------------------------------------------------
1671
+ # clear
1672
+ #-----------------------------------------------------------------------
1673
+ #++
1674
+ # Delete all records from table. You can also use #delete_all.
1675
+ #
1676
+ # *reset_recno_ctr*:: true/false specifying whether recno counter should
1677
+ # be reset to 0.
1678
+ #
1679
+ def clear(reset_recno_ctr=true)
1680
+ delete { true }
1681
+ pack
1682
+
1683
+ @db.engine.reset_recno_ctr if reset_recno_ctr
1684
+ end
1685
+
1686
+ #-----------------------------------------------------------------------
1687
+ # []
1688
+ #-----------------------------------------------------------------------
1689
+ #++
1690
+ # Return the record(s) whose recno field is included in index.
1691
+ #
1692
+ # *index*:: Array of Integer(s) specifying recno(s) you wish to select.
1693
+ #
1694
+ def [](*index)
1695
+ return nil if index[0].nil?
1696
+
1697
+ return get_match_by_recno(:select, @field_names, index[0]) if \
1698
+ index.size == 1
1699
+
1700
+ recs = select_by_recno_index(*@field_names) { |r|
1701
+ index.includes?(r.recno)
1702
+ }
1703
+
1704
+ return recs
1705
+ end
1706
+
1707
+ #-----------------------------------------------------------------------
1708
+ # select
1709
+ #-----------------------------------------------------------------------
1710
+ #++
1711
+ # Return array of records (Structs) matching select conditions.
1712
+ #
1713
+ # *filter*:: List of field names (Symbols) to include in result set.
1714
+ # *select_cond*:: Proc containing select code.
1715
+ #
1716
+ def select(*filter, &select_cond)
1717
+ # Declare these variables before the code block so they don't go
1718
+ # after the code block is done.
1719
+ result_set = []
1720
+
1721
+ # Validate that all names in filter are valid field names.
1722
+ validate_filter(filter)
1723
+
1724
+ filter = @field_names if filter.empty?
1725
+
1726
+ # Get all records that match the selection criteria and
1727
+ # return them in an array of Struct instances.
1728
+ return get_matches(:select, filter, select_cond)
1729
+ end
1730
+
1731
+ #-----------------------------------------------------------------------
1732
+ # select_by_recno_index
1733
+ #-----------------------------------------------------------------------
1734
+ #++
1735
+ # Return array of records (Structs) matching select conditions. Select
1736
+ # condition block should not contain references to any table column
1737
+ # except :recno. If you need to select by other table columns than just
1738
+ # :recno, use #select instead.
1739
+ #
1740
+ # *filter*:: List of field names (Symbols) to include in result set.
1741
+ # *select_cond*:: Proc containing select code.
1742
+ #
1743
+ def select_by_recno_index(*filter, &select_cond)
1744
+ # Declare these variables before the code block so they don't go
1745
+ # after the code block is done.
1746
+ result_set = []
1747
+
1748
+ # Validate that all names in filter are valid field names.
1749
+ validate_filter(filter)
1750
+
1751
+ filter = @field_names if filter.empty?
1752
+
1753
+ # Get all records that match the selection criteria and
1754
+ # return them in an array of Struct instances.
1755
+ return get_matches_by_recno_index(:select, filter, select_cond)
1756
+ end
1757
+
1758
+ #-----------------------------------------------------------------------
1759
+ # pack
1760
+ #-----------------------------------------------------------------------
1761
+ #++
1762
+ # Remove blank records from table, return total removed.
1763
+ #
1764
+ def pack
1765
+ lines_deleted = @db.engine.pack_table(self)
1766
+
1767
+ update_header_vars
1768
+
1769
+ @db.engine.remove_recno_index(@name)
1770
+ @db.engine.remove_indexes(@name)
1771
+
1772
+ create_indexes
1773
+ create_table_class unless @db.server?
1774
+
1775
+ return lines_deleted
1776
+ end
1777
+
1778
+ #-----------------------------------------------------------------------
1779
+ # total_recs
1780
+ #-----------------------------------------------------------------------
1781
+ #++
1782
+ # Return total number of undeleted (blank) records in table.
1783
+ #
1784
+ def total_recs
1785
+ return @db.engine.get_total_recs(self)
1786
+ end
1787
+
1788
+ #-----------------------------------------------------------------------
1789
+ # import_csv
1790
+ #-----------------------------------------------------------------------
1791
+ #++
1792
+ # Import csv file into table.
1793
+ #
1794
+ # *csv_filename*:: filename of csv file to import.
1795
+ #
1796
+ def import_csv(csv_filename)
1797
+ tbl_rec = @table_class.new(self)
1798
+
1799
+ CSV.open(csv_filename, 'r') do |row|
1800
+ tbl_rec.populate([nil] + row)
1801
+ insert(tbl_rec)
1802
+ end
1803
+ end
1804
+
1805
+ #-----------------------------------------------------------------------
1806
+ # PRIVATE METHODS
1807
+ #-----------------------------------------------------------------------
1808
+ private
1809
+
1810
+ #-----------------------------------------------------------------------
1811
+ # create_indexes
1812
+ #-----------------------------------------------------------------------
1813
+ def create_indexes
1814
+ # Create the recno index. A recno index always gets created even if
1815
+ # there are no user-defined indexes for the table.
1816
+ @db.engine.init_recno_index(self)
1817
+
1818
+ # There can be up to 5 different indexes on a table. Any of these
1819
+ # indexes can be single or compound.
1820
+ ['Index->1', 'Index->2', 'Index->3', 'Index->4',
1821
+ 'Index->5'].each do |idx|
1822
+ index_col_names = []
1823
+ @field_indexes.each_with_index do |fi,i|
1824
+ next if fi.nil?
1825
+ index_col_names << @field_names[i] if fi.include?(idx)
1826
+ end
1827
+
1828
+ # If no fields were indexed on this number (1..5), go to the
1829
+ # next index number.
1830
+ next if index_col_names.empty?
1831
+
1832
+ # Create this index on the engine.
1833
+ @db.engine.init_index(self, index_col_names)
1834
+
1835
+ # For each index found, add an instance method for it so that
1836
+ # it can be used for #selects.
1837
+ select_meth_str = <<-END_OF_STRING
1838
+ def select_by_#{index_col_names.join('_')}_index(*filter,
1839
+ &select_cond)
1840
+ result_set = []
1841
+ validate_filter(filter)
1842
+ filter = @field_names if filter.empty?
1843
+ return get_matches_by_index(:select,
1844
+ [:#{index_col_names.join(',:')}], filter, select_cond)
1845
+ end
1846
+ END_OF_STRING
1847
+ self.class.class_eval(select_meth_str)
1848
+ end
1849
+ end
1850
+
1851
+ #-----------------------------------------------------------------------
1852
+ # create_table_class
1853
+ #-----------------------------------------------------------------------
1854
+ def create_table_class
1855
+ #This is the class that will be used in #select condition blocks.
1856
+ @table_class = Class.new(KBTableRec)
1857
+
1858
+ get_meth_str = ''
1859
+ get_meth_upd_res_str = ''
1860
+ set_meth_str = ''
1861
+
1862
+ @field_names.zip(@field_types, @field_extras) do |x|
1863
+ field_name, field_type, field_extra = x
1864
+
1865
+ @lookup_key = field_name if field_extra.has_key?('Key')
1866
+
1867
+ # These are the default get/set methods for the table column.
1868
+ get_meth_str = <<-END_OF_STRING
1869
+ def #{field_name}
1870
+ return @#{field_name}
1871
+ end
1872
+ END_OF_STRING
1873
+ get_meth_upd_res_str = <<-END_OF_STRING
1874
+ def #{field_name}_upd_res
1875
+ return @#{field_name}
1876
+ end
1877
+ END_OF_STRING
1878
+ set_meth_str = <<-END_OF_STRING
1879
+ def #{field_name}=(s)
1880
+ @#{field_name} = convert_to(:#{field_type}, s)
1881
+ end
1882
+ END_OF_STRING
1883
+
1884
+ # If this is a Lookup field, modify the get_method.
1885
+ if field_extra.has_key?('Lookup')
1886
+ lookup_table, key_field = field_extra['Lookup'].split('.')
1887
+ if key_field == 'recno'
1888
+ get_meth_str = <<-END_OF_STRING
1889
+ def #{field_name}
1890
+ table = @tbl.db.get_table(:#{lookup_table})
1891
+ return table[@#{field_name}]
1892
+ end
1893
+ END_OF_STRING
1894
+ else
1895
+ begin
1896
+ @db.get_table(lookup_table)
1897
+ rescue RuntimeError
1898
+ raise "Must create child table first when using " +
1899
+ "'Lookup'"
1900
+ end
1901
+
1902
+ if @db.get_table(lookup_table).respond_to?(
1903
+ 'select_by_%s_index' % key_field)
1904
+ get_meth_str = <<-END_OF_STRING
1905
+ def #{field_name}
1906
+ table = @tbl.db.get_table(:#{lookup_table})
1907
+ return table.select_by_#{key_field}_index { |r|
1908
+ r.#{key_field} == @#{field_name} }.first
1909
+ end
1910
+ END_OF_STRING
1911
+ else
1912
+ get_meth_str = <<-END_OF_STRING
1913
+ def #{field_name}
1914
+ table = @tbl.db.get_table(:#{lookup_table})
1915
+ return table.select { |r|
1916
+ r.#{key_field} == @#{field_name} }.first
1917
+ end
1918
+ END_OF_STRING
1919
+ end
1920
+ end
1921
+ end
1922
+
1923
+ # If this is a Link_many field, modify the get/set methods.
1924
+ if field_extra.has_key?('Link_many')
1925
+ lookup_field, rest = field_extra['Link_many'].split('=')
1926
+ link_table, link_field = rest.split('.')
1927
+
1928
+ begin
1929
+ @db.get_table(link_table)
1930
+ rescue RuntimeError
1931
+ raise "Must create child table first when using " +
1932
+ "'Link_many'"
1933
+ end
1934
+
1935
+ if @db.get_table(link_table).respond_to?(
1936
+ 'select_by_%s_index' % link_field)
1937
+ get_meth_str = <<-END_OF_STRING
1938
+ def #{field_name}
1939
+ table = @tbl.db.get_table(:#{link_table})
1940
+ return table.select_by_#{link_field}_index { |r|
1941
+ r.send(:#{link_field}) == @#{lookup_field} }
1942
+ end
1943
+ END_OF_STRING
1944
+ else
1945
+ get_meth_str = <<-END_OF_STRING
1946
+ def #{field_name}
1947
+ table = @tbl.db.get_table(:#{link_table})
1948
+ return table.select { |r|
1949
+ r.send(:#{link_field}) == @#{lookup_field} }
1950
+ end
1951
+ END_OF_STRING
1952
+ end
1953
+
1954
+ get_meth_upd_res_str = <<-END_OF_STRING
1955
+ def #{field_name}_upd_res
1956
+ return nil
1957
+ end
1958
+ END_OF_STRING
1959
+ set_meth_str = <<-END_OF_STRING
1960
+ def #{field_name}=(s)
1961
+ @#{field_name} = nil
1962
+ end
1963
+ END_OF_STRING
1964
+ end
1965
+
1966
+ # If this is a Calculated field, modify the get/set methods.
1967
+ if field_extra.has_key?('Calculated')
1968
+ calculation = field_extra['Calculated']
1969
+
1970
+ get_meth_str = <<-END_OF_STRING
1971
+ def #{field_name}()
1972
+ return #{calculation}
1973
+ end
1974
+ END_OF_STRING
1975
+ get_meth_upd_res_str = <<-END_OF_STRING
1976
+ def #{field_name}_upd_res()
1977
+ return nil
1978
+ end
1979
+ END_OF_STRING
1980
+ set_meth_str = <<-END_OF_STRING
1981
+ def #{field_name}=(s)
1982
+ @#{field_name} = nil
1983
+ end
1984
+ END_OF_STRING
1985
+ end
1986
+
1987
+ @table_class.class_eval(get_meth_str)
1988
+ @table_class.class_eval(get_meth_upd_res_str)
1989
+ @table_class.class_eval(set_meth_str)
1990
+ end
1991
+ end
1992
+
1993
+ #-----------------------------------------------------------------------
1994
+ # convert_to_string
1995
+ #-----------------------------------------------------------------------
1996
+ def convert_to_string(data_type, x)
1997
+ case data_type
1998
+ when :YAML
1999
+ y = x.to_yaml
2000
+ if y =~ ENCODE_RE
2001
+ return y.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub(
2002
+ "\r", '&carriage_return;').gsub("\032", '&substitute;'
2003
+ ).gsub("|", '&pipe;')
2004
+ else
2005
+ return y
2006
+ end
2007
+ when :String
2008
+ if x =~ ENCODE_RE
2009
+ return x.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub(
2010
+ "\r", '&carriage_return;').gsub("\032", '&substitute;'
2011
+ ).gsub("|", '&pipe;')
2012
+ else
2013
+ return x
2014
+ end
2015
+ else
2016
+ return x.to_s
2017
+ end
2018
+ end
2019
+
2020
+ #-----------------------------------------------------------------------
2021
+ # validate_filter
2022
+ #-----------------------------------------------------------------------
2023
+ #++
2024
+ # Check that filter contains valid field names.
2025
+ #
2026
+ def validate_filter(filter)
2027
+ # Each field in the filter array must be a valid fieldname in the
2028
+ # table.
2029
+ filter.each { |f|
2030
+ raise 'Invalid field name: %s in filter!' % f unless \
2031
+ @field_names.include?(f)
2032
+ }
2033
+ end
2034
+
2035
+ #-----------------------------------------------------------------------
2036
+ # convert_input_data
2037
+ #-----------------------------------------------------------------------
2038
+ #++
2039
+ # Convert data passed to #input, #update, or #set to a common format.
2040
+ #
2041
+ def convert_input_data(values)
2042
+ if values.class == Proc
2043
+ tbl_struct = Struct.new(*@field_names[1..-1])
2044
+ tbl_rec = tbl_struct.new
2045
+ begin
2046
+ values.call(tbl_rec)
2047
+ rescue NoMethodError
2048
+ raise 'Invalid field name in code block: %s' % $!
2049
+ end
2050
+ temp_hash = {}
2051
+ @field_names[1..-1].collect { |f|
2052
+ temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
2053
+ }
2054
+ return temp_hash
2055
+ elsif values[0].class.to_s == @record_class or \
2056
+ values[0].class == @table_class
2057
+ temp_hash = {}
2058
+ @field_names[1..-1].collect { |f|
2059
+ temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
2060
+ }
2061
+ return temp_hash
2062
+ elsif values[0].class == Hash
2063
+ return values[0].dup
2064
+ elsif values[0].kind_of?(Struct)
2065
+ temp_hash = {}
2066
+ @field_names[1..-1].collect { |f|
2067
+ temp_hash[f] = values[0][f] if values[0].members.include?(
2068
+ f.to_s)
2069
+ }
2070
+ return temp_hash
2071
+ elsif values[0].class == Array
2072
+ raise ArgumentError, 'Must specify all fields in input array!' \
2073
+ unless values[0].size == @field_names[1..-1].size
2074
+ temp_hash = {}
2075
+ @field_names[1..-1].collect { |f|
2076
+ temp_hash[f] = values[0][@field_names.index(f)-1]
2077
+ }
2078
+ return temp_hash
2079
+ elsif values.class == Array
2080
+ raise ArgumentError, 'Must specify all fields in input array!' \
2081
+ unless values.size == @field_names[1..-1].size
2082
+ temp_hash = {}
2083
+ @field_names[1..-1].collect { |f|
2084
+ temp_hash[f] = values[@field_names.index(f)-1]
2085
+ }
2086
+ return temp_hash
2087
+ else
2088
+ raise(ArgumentError, 'Invalid type for values container!')
2089
+ end
2090
+ end
2091
+
2092
+ #-----------------------------------------------------------------------
2093
+ # validate_input
2094
+ #-----------------------------------------------------------------------
2095
+ #++
2096
+ # Check input data to ensure proper data types.
2097
+ #
2098
+ def validate_input(data)
2099
+ raise 'Cannot insert/update recno field!' if data.has_key?(:recno)
2100
+
2101
+ @field_names[1..-1].each do |f|
2102
+ next unless data.has_key?(f)
2103
+
2104
+ next if data[f].nil?
2105
+ case @field_types[@field_names.index(f)]
2106
+ when /:String|:Memo|:Blob/
2107
+ raise 'Invalid String value for: %s' % f unless \
2108
+ data[f].respond_to?(:to_str)
2109
+ when :Boolean
2110
+ raise 'Invalid Boolean value for: %s' % f unless \
2111
+ data[f].is_a?(TrueClass) or data[f].kind_of?(FalseClass)
2112
+ when :Integer
2113
+ raise 'Invalid Integer value for: %s' % f unless \
2114
+ data[f].respond_to?(:to_int)
2115
+ when :Float
2116
+ raise 'Invalid Float value for: %s' % f unless \
2117
+ data[f].respond_to?(:to_f)
2118
+ when :Date
2119
+ raise 'Invalid Date value for: %s' % f unless \
2120
+ data[f].is_a?(Date)
2121
+ when :Time
2122
+ raise 'Invalid Time value for: %s' % f unless \
2123
+ data[f].is_a?(Time)
2124
+ when :DateTime
2125
+ raise 'Invalid DateTime value for: %s' % f unless \
2126
+ data[f].is_a?(DateTime)
2127
+ when :YAML
2128
+ raise 'Invalid YAML value for: %s' % f unless \
2129
+ data[f].respond_to?(:to_yaml)
2130
+ end
2131
+ end
2132
+ end
2133
+
2134
+ #-----------------------------------------------------------------------
2135
+ # update_header_vars
2136
+ #-----------------------------------------------------------------------
2137
+ #++
2138
+ # Read header record and update instance variables.
2139
+ #
2140
+ def update_header_vars
2141
+ @encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \
2142
+ @field_types, @field_indexes, @field_extras = \
2143
+ @db.engine.get_header_vars(self)
2144
+ end
2145
+
2146
+ #-----------------------------------------------------------------------
2147
+ # get_result_struct
2148
+ #-----------------------------------------------------------------------
2149
+ def get_result_struct(query_type, filter)
2150
+ case query_type
2151
+ when :select
2152
+ return Struct.new(*filter) if @record_class == 'Struct'
2153
+ when :update
2154
+ return Struct.new(*(filter + [:fpos, :line_length]))
2155
+ when :delete
2156
+ return Struct.new(:recno, :fpos, :line_length)
2157
+ end
2158
+ return nil
2159
+ end
2160
+
2161
+ #-----------------------------------------------------------------------
2162
+ # create_result_rec
2163
+ #-----------------------------------------------------------------------
2164
+ def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
2165
+ # If this isn't a select query or if it is a select query, but
2166
+ # the table record class is simply a Struct, then we will use
2167
+ # a Struct for the result record type.
2168
+ if query_type != :select
2169
+ result_rec = result_struct.new(*filter.collect { |f|
2170
+ tbl_rec.send("#{f}_upd_res".to_sym) })
2171
+ elsif @record_class == 'Struct'
2172
+ result_rec = result_struct.new(*filter.collect { |f|
2173
+ tbl_rec.send(f) })
2174
+ else
2175
+ if Object.full_const_get(@record_class).respond_to?(:kb_create)
2176
+ result_rec = Object.full_const_get(@record_class
2177
+ ).kb_create(*@field_names.collect { |f|
2178
+ # Just a warning here: If you specify a filter on
2179
+ # a select, you are only going to get those fields
2180
+ # you specified in the result set, EVEN IF
2181
+ # record_class is a custom class instead of Struct.
2182
+ if filter.include?(f)
2183
+ tbl_rec.send(f)
2184
+ else
2185
+ nil
2186
+ end
2187
+ })
2188
+ elsif Object.full_const_get(@record_class).respond_to?(
2189
+ :kb_defaults)
2190
+ result_rec = Object.full_const_get(@record_class).new(
2191
+ *@field_names.collect { |f|
2192
+ tbl_rec.send(f) || Object.full_const_get(
2193
+ @record_class).kb_defaults[@field_names.index(f)]
2194
+ }
2195
+ )
2196
+ end
2197
+ end
2198
+
2199
+ unless query_type == :select
2200
+ result_rec.fpos = rec[-2]
2201
+ result_rec.line_length = rec[-1]
2202
+ end
2203
+ return result_rec
2204
+ end
2205
+
2206
+ #-----------------------------------------------------------------------
2207
+ # get_matches
2208
+ #-----------------------------------------------------------------------
2209
+ #++
2210
+ # Return records from table that match select condition.
2211
+ #
2212
+ def get_matches(query_type, filter, select_cond)
2213
+ result_struct = get_result_struct(query_type, filter)
2214
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
2215
+ @field_types[@field_names.index(f)] })
2216
+
2217
+ tbl_rec = @table_class.new(self)
2218
+
2219
+ # Loop through table.
2220
+ @db.engine.get_recs(self).each do |rec|
2221
+ tbl_rec.populate(rec)
2222
+ next unless select_cond.call(tbl_rec) unless select_cond.nil?
2223
+
2224
+ match_array << create_result_rec(query_type, filter,
2225
+ result_struct, tbl_rec, rec)
2226
+
2227
+ end
2228
+ return match_array
2229
+ end
2230
+
2231
+ #-----------------------------------------------------------------------
2232
+ # get_matches_by_index
2233
+ #-----------------------------------------------------------------------
2234
+ #++
2235
+ # Return records from table that match select condition using one of
2236
+ # the table's indexes instead of searching the whole file.
2237
+ #
2238
+ def get_matches_by_index(query_type, index_fields, filter, select_cond)
2239
+ good_matches = []
2240
+
2241
+ idx_struct = Struct.new(*(index_fields + [:recno]))
2242
+
2243
+ begin
2244
+ @db.engine.get_index(self, index_fields.join('_')).each do |rec|
2245
+ good_matches << rec[-1] if select_cond.call(
2246
+ idx_struct.new(*rec))
2247
+ end
2248
+ rescue NoMethodError
2249
+ raise 'Field name in select block not part of index!'
2250
+ end
2251
+
2252
+ return get_matches_by_recno(query_type, filter, good_matches)
2253
+ end
2254
+
2255
+ #-----------------------------------------------------------------------
2256
+ # get_matches_by_recno_index
2257
+ #-----------------------------------------------------------------------
2258
+ #++
2259
+ # Return records from table that match select condition using the
2260
+ # table's recno index instead of searching the whole file.
2261
+ #
2262
+ def get_matches_by_recno_index(query_type, filter, select_cond)
2263
+ good_matches = []
2264
+
2265
+ idx_struct = Struct.new(:recno)
2266
+
2267
+ begin
2268
+ @db.engine.get_recno_index(self).each_key do |key|
2269
+ good_matches << key if select_cond.call(
2270
+ idx_struct.new(key))
2271
+ end
2272
+ rescue NoMethodError
2273
+ raise "Field name in select block not part of index!"
2274
+ end
2275
+
2276
+ return nil if good_matches.empty?
2277
+ return get_matches_by_recno(query_type, filter, good_matches)
2278
+ end
2279
+
2280
+ #-----------------------------------------------------------------------
2281
+ # get_match_by_recno
2282
+ #-----------------------------------------------------------------------
2283
+ #++
2284
+ # Return record from table that matches supplied recno.
2285
+ #
2286
+ def get_match_by_recno(query_type, filter, recno)
2287
+ result_struct = get_result_struct(query_type, filter)
2288
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
2289
+ @field_types[@field_names.index(f)] })
2290
+
2291
+ tbl_rec = @table_class.new(self)
2292
+
2293
+ rec = @db.engine.get_rec_by_recno(self, recno)
2294
+ return nil if rec.nil?
2295
+ tbl_rec.populate(rec)
2296
+
2297
+ return create_result_rec(query_type, filter, result_struct,
2298
+ tbl_rec, rec)
2299
+ end
2300
+
2301
+ #-----------------------------------------------------------------------
2302
+ # get_matches_by_recno
2303
+ #-----------------------------------------------------------------------
2304
+ #++
2305
+ # Return records from table that match select condition.
2306
+ #
2307
+ def get_matches_by_recno(query_type, filter, recnos)
2308
+ result_struct = get_result_struct(query_type, filter)
2309
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
2310
+ @field_types[@field_names.index(f)] })
2311
+
2312
+ tbl_rec = @table_class.new(self)
2313
+
2314
+ @db.engine.get_recs_by_recno(self, recnos).each do |rec|
2315
+ next if rec.nil?
2316
+ tbl_rec.populate(rec)
2317
+
2318
+ match_array << create_result_rec(query_type, filter,
2319
+ result_struct, tbl_rec, rec)
2320
+ end
2321
+ return match_array
2322
+ end
2323
+ end
2324
+
2325
+
2326
+ #---------------------------------------------------------------------------
2327
+ # KBMemo
2328
+ #---------------------------------------------------------------------------
2329
+ class KBMemo
2330
+ attr_reader :filepath, :memo
2331
+
2332
+ #-----------------------------------------------------------------------
2333
+ # initialize
2334
+ #-----------------------------------------------------------------------
2335
+ def initialize(db, filepath)
2336
+ @filepath = filepath
2337
+ @memo = db.engine.get_memo(@filepath)
2338
+ end
2339
+ end
2340
+
2341
+ #---------------------------------------------------------------------------
2342
+ # KBBlob
2343
+ #---------------------------------------------------------------------------
2344
+ class KBBlob
2345
+ attr_reader :filepath, :blob
2346
+
2347
+ #-----------------------------------------------------------------------
2348
+ # initialize
2349
+ #-----------------------------------------------------------------------
2350
+ def initialize(db, filepath)
2351
+ @filepath = filepath
2352
+ @blob = db.engine.get_blob(@filepath)
2353
+ end
2354
+ end
2355
+
2356
+
2357
+ #---------------------------------------------------------------------------
2358
+ # KBIndex
2359
+ #---------------------------------------------------------------------------
2360
+ class KBIndex
2361
+ include KBTypeConversionsMixin
2362
+
2363
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
2364
+
2365
+ #-----------------------------------------------------------------------
2366
+ # initialize
2367
+ #-----------------------------------------------------------------------
2368
+ def initialize(table, index_fields)
2369
+ @idx_arr = []
2370
+ @table = table
2371
+ @index_fields = index_fields
2372
+ @col_poss = index_fields.collect {|i| table.field_names.index(i) }
2373
+ @col_names = index_fields
2374
+ @col_types = index_fields.collect {|i|
2375
+ table.field_types[table.field_names.index(i)]}
2376
+ end
2377
+
2378
+ #-----------------------------------------------------------------------
2379
+ # get_idx
2380
+ #-----------------------------------------------------------------------
2381
+ def get_idx
2382
+ return @idx_arr
2383
+ end
2384
+
2385
+ #-----------------------------------------------------------------------
2386
+ # rebuild
2387
+ #-----------------------------------------------------------------------
2388
+ def rebuild(fptr)
2389
+ @idx_arr.clear
2390
+
2391
+ encrypted = @table.encrypted?
2392
+
2393
+ # Skip header rec.
2394
+ fptr.readline
2395
+
2396
+ begin
2397
+ # Loop through table.
2398
+ while true
2399
+ line = fptr.readline
2400
+
2401
+ line = unencrypt_str(line) if encrypted
2402
+ line.strip!
2403
+
2404
+ # If blank line (i.e. 'deleted'), skip it.
2405
+ next if line == ''
2406
+
2407
+ # Split the line up into fields.
2408
+ rec = line.split('|', @col_poss.max+2)
2409
+
2410
+ # Create the index record by pulling out the record fields
2411
+ # that make up this index and converting them to their
2412
+ # native types.
2413
+ idx_rec = []
2414
+ @col_poss.zip(@col_types).each do |col_pos, col_type|
2415
+ idx_rec << convert_to(col_type, rec[col_pos])
2416
+ end
2417
+
2418
+ # Were all the index fields for this record equal to NULL?
2419
+ # Then don't add this index record to index array; skip to
2420
+ # next record.
2421
+ next if idx_rec.compact.empty?
2422
+
2423
+ # Add recno to the end of this index record.
2424
+ idx_rec << rec.first.to_i
2425
+
2426
+ # Add index record to index array.
2427
+ @idx_arr << idx_rec
2428
+ end
2429
+ # Here's how we break out of the loop...
2430
+ rescue EOFError
2431
+ end
2432
+ end
2433
+
2434
+ #-----------------------------------------------------------------------
2435
+ # add_index_rec
2436
+ #-----------------------------------------------------------------------
2437
+ def add_index_rec(rec)
2438
+ @idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type|
2439
+ convert_to(col_type, rec[col_pos])
2440
+ end + [rec.first.to_i]
2441
+ end
2442
+
2443
+ #-----------------------------------------------------------------------
2444
+ # delete_index_rec
2445
+ #-----------------------------------------------------------------------
2446
+ def delete_index_rec(recno)
2447
+ i = @idx_arr.rassoc(recno.to_i)
2448
+ @idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
2449
+ end
2450
+
2451
+ #-----------------------------------------------------------------------
2452
+ # update_index_rec
2453
+ #-----------------------------------------------------------------------
2454
+ def update_index_rec(rec)
2455
+ delete_index_rec(rec.first.to_i)
2456
+ add_index_rec(rec)
2457
+ end
2458
+ end
2459
+
2460
+
2461
+ #---------------------------------------------------------------------------
2462
+ # KBRecnoIndex
2463
+ #---------------------------------------------------------------------------
2464
+ class KBRecnoIndex
2465
+ #-----------------------------------------------------------------------
2466
+ # initialize
2467
+ #-----------------------------------------------------------------------
2468
+ def initialize(table)
2469
+ @idx_hash = {}
2470
+ @table = table
2471
+ end
2472
+
2473
+ #-----------------------------------------------------------------------
2474
+ # get_idx
2475
+ #-----------------------------------------------------------------------
2476
+ def get_idx
2477
+ return @idx_hash
2478
+ end
2479
+
2480
+ #-----------------------------------------------------------------------
2481
+ # rebuild
2482
+ #-----------------------------------------------------------------------
2483
+ def rebuild(fptr)
2484
+ @idx_hash.clear
2485
+
2486
+ encrypted = @table.encrypted?
2487
+
2488
+ begin
2489
+ # Skip header rec.
2490
+ fptr.readline
2491
+
2492
+ # Loop through table.
2493
+ while true
2494
+ # Record current position in table. Then read first
2495
+ # detail record.
2496
+ fpos = fptr.tell
2497
+ line = fptr.readline
2498
+
2499
+ line = unencrypt_str(line) if encrypted
2500
+ line.strip!
2501
+
2502
+ # If blank line (i.e. 'deleted'), skip it.
2503
+ next if line == ''
2504
+
2505
+ # Split the line up into fields.
2506
+ rec = line.split('|', 2)
2507
+
2508
+ @idx_hash[rec.first.to_i] = fpos
2509
+ end
2510
+ # Here's how we break out of the loop...
2511
+ rescue EOFError
2512
+ end
2513
+ end
2514
+
2515
+ #-----------------------------------------------------------------------
2516
+ # add_index_rec
2517
+ #-----------------------------------------------------------------------
2518
+ def add_index_rec(recno, fpos)
2519
+ raise 'Table already has index record for recno: %s' % recno if \
2520
+ @idx_hash.has_key?(recno.to_i)
2521
+ @idx_hash[recno.to_i] = fpos
2522
+ end
2523
+
2524
+ #-----------------------------------------------------------------------
2525
+ # update_index_rec
2526
+ #-----------------------------------------------------------------------
2527
+ def update_index_rec(recno, fpos)
2528
+ raise 'Table has no index record for recno: %s' % recno unless \
2529
+ @idx_hash.has_key?(recno.to_i)
2530
+ @idx_hash[recno.to_i] = fpos
2531
+ end
2532
+
2533
+ #-----------------------------------------------------------------------
2534
+ # delete_index_rec
2535
+ #-----------------------------------------------------------------------
2536
+ def delete_index_rec(recno)
2537
+ raise 'Table has no index record for recno: %s' % recno unless \
2538
+ @idx_hash.has_key?(recno.to_i)
2539
+ @idx_hash.delete(recno.to_i)
2540
+ end
2541
+ end
2542
+
2543
+
2544
+ #---------------------------------------------------------------------------
2545
+ # KBTableRec
2546
+ #---------------------------------------------------------------------------
2547
+ class KBTableRec
2548
+ include KBTypeConversionsMixin
2549
+
2550
+ def initialize(tbl)
2551
+ @tbl = tbl
2552
+ end
2553
+
2554
+ def populate(rec)
2555
+ @tbl.field_names.zip(rec).each do |fn, val|
2556
+ send("#{fn}=", val)
2557
+ end
2558
+ end
2559
+
2560
+ def clear
2561
+ @tbl.field_names.each do |fn|
2562
+ send("#{fn}=", nil)
2563
+ end
2564
+ end
2565
+ end
2566
+
2567
+
2568
+ #---------------------------------------------------------
2569
+ # KBResultSet
2570
+ #---------------------------------------------------------------------------
2571
+ class KBResultSet < Array
2572
+ #-----------------------------------------------------------------------
2573
+ # KBResultSet.reverse
2574
+ #-----------------------------------------------------------------------
2575
+ def KBResultSet.reverse(sort_field)
2576
+ return [sort_field, :desc]
2577
+ end
2578
+
2579
+ #-----------------------------------------------------------------------
2580
+ # initialize
2581
+ #-----------------------------------------------------------------------
2582
+ def initialize(table, filter, filter_types, *args)
2583
+ @table = table
2584
+ @filter = filter
2585
+ @filter_types = filter_types
2586
+ super(*args)
2587
+
2588
+ @filter.each do |f|
2589
+ get_meth_str = <<-END_OF_STRING
2590
+ def #{f}()
2591
+ if defined?(@#{f}) then
2592
+ return @#{f}
2593
+ else
2594
+ @#{f} = self.collect { |x| x.#{f} }
2595
+ return @#{f}
2596
+ end
2597
+ end
2598
+ END_OF_STRING
2599
+ self.class.class_eval(get_meth_str)
2600
+ end
2601
+ end
2602
+
2603
+ #-----------------------------------------------------------------------
2604
+ # to_ary
2605
+ #-----------------------------------------------------------------------
2606
+ def to_ary
2607
+ to_a
2608
+ end
2609
+
2610
+ #-----------------------------------------------------------------------
2611
+ # set
2612
+ #-----------------------------------------------------------------------
2613
+ #++
2614
+ # Update record(s) in table, return number of records updated.
2615
+ #
2616
+ def set(*updates, &update_cond)
2617
+ raise 'Cannot specify both a hash and a proc for method #set!' \
2618
+ unless updates.empty? or update_cond.nil?
2619
+
2620
+ raise 'Must specify update proc or hash for method #set!' if \
2621
+ updates.empty? and update_cond.nil?
2622
+
2623
+ if updates.empty?
2624
+ @table.set(self, update_cond)
2625
+ else
2626
+ @table.set(self, updates)
2627
+ end
2628
+ end
2629
+
2630
+ #-----------------------------------------------------------------------
2631
+ # sort
2632
+ #-----------------------------------------------------------------------
2633
+ def sort(*sort_fields)
2634
+ sort_fields_arrs = []
2635
+ sort_fields.each do |f|
2636
+ if f.to_s[0..0] == '-'
2637
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
2638
+ elsif f.to_s[0..0] == '+'
2639
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
2640
+ else
2641
+ sort_fields_arrs << [f, :asc]
2642
+ end
2643
+ end
2644
+
2645
+ sort_fields_arrs.each do |f|
2646
+ raise "Invalid sort field" unless @filter.include?(f[0])
2647
+ end
2648
+
2649
+ super() { |a,b|
2650
+ x = []
2651
+ y = []
2652
+ sort_fields_arrs.each do |s|
2653
+ if [:Integer, :Float].include?(
2654
+ @filter_types[@filter.index(s[0])])
2655
+ a_value = a.send(s[0]) || 0
2656
+ b_value = b.send(s[0]) || 0
2657
+ else
2658
+ a_value = a.send(s[0])
2659
+ b_value = b.send(s[0])
2660
+ end
2661
+ if s[1] == :desc
2662
+ x << b_value
2663
+ y << a_value
2664
+ else
2665
+ x << a_value
2666
+ y << b_value
2667
+ end
2668
+ end
2669
+ x <=> y
2670
+ }
2671
+ end
2672
+
2673
+ #-----------------------------------------------------------------------
2674
+ # to_report
2675
+ #-----------------------------------------------------------------------
2676
+ def to_report(recs_per_page=0, print_rec_sep=false)
2677
+ result = collect { |r| @filter.collect {|f| r.send(f)} }
2678
+
2679
+ # How many records before a formfeed.
2680
+ delim = ' | '
2681
+
2682
+ # columns of physical rows
2683
+ columns = [@filter].concat(result).transpose
2684
+
2685
+ max_widths = columns.collect { |c|
2686
+ c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
2687
+ }
2688
+
2689
+ row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
2690
+ delim.length * (max_widths.size - 1))
2691
+
2692
+ justify_hash = { :String => :ljust, :Integer => :rjust,
2693
+ :Float => :rjust, :Boolean => :ljust, :Date => :ljust,
2694
+ :Time => :ljust, :DateTime => :ljust }
2695
+
2696
+ header_line = @filter.zip(max_widths, @filter.collect { |f|
2697
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
2698
+ x.to_s.send(justify_hash[z], y) }.join(delim)
2699
+
2700
+ output = ''
2701
+ recs_on_page_cnt = 0
2702
+
2703
+ result.each do |row|
2704
+ if recs_on_page_cnt == 0
2705
+ output << header_line + "\n" << row_dashes + "\n"
2706
+ end
2707
+
2708
+ output << row.zip(max_widths, @filter.collect { |f|
2709
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
2710
+ x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
2711
+
2712
+ output << row_dashes + '\n' if print_rec_sep
2713
+ recs_on_page_cnt += 1
2714
+
2715
+ if recs_per_page > 0 and (recs_on_page_cnt ==
2716
+ num_recs_per_page)
2717
+ output << '\f'
2718
+ recs_on_page_count = 0
2719
+ end
2720
+ end
2721
+ return output
2722
+ end
2723
+ end
2724
+
2725
+
2726
+ #---------------------------------------------------------------------------
2727
+ # Object
2728
+ #---------------------------------------------------------------------------
2729
+ class Object
2730
+ def full_const_get(name)
2731
+ list = name.split("::")
2732
+ obj = Object
2733
+ list.each {|x| obj = obj.const_get(x) }
2734
+ obj
2735
+ end
2736
+ end
2737
+
2738
+
2739
+ #---------------------------------------------------------------------------
2740
+ # NilClass
2741
+ #---------------------------------------------------------------------------
2742
+ class NilClass
2743
+ #-----------------------------------------------------------------------
2744
+ # method_missing
2745
+ #-----------------------------------------------------------------------
2746
+ #
2747
+ # This code is necessary because if, inside a select condition code
2748
+ # block, there is a case where you are trying to do an expression
2749
+ # against a table field that is equal to nil, I don't want a method
2750
+ # missing exception to occur. I just want the expression to be nil. I
2751
+ # initially had this method returning false, but then I had an issue
2752
+ # where I had a YAML field that was supposed to hold an Array. If the
2753
+ # field was empty (i.e. nil) it was actually returning false when it
2754
+ # should be returning nil. Since nil evaluates to false, it works if I
2755
+ # return nil.
2756
+ # Here's an example:
2757
+ # #select { |r| r.speed > 300 }
2758
+ # What happens if speed is nil (basically NULL in DBMS terms)? Without
2759
+ # this code, an exception is going to be raised, which is not what we
2760
+ # really want. We really want this expression to return nil.
2761
+ def method_missing(method_id, *stuff)
2762
+ return nil
2763
+ end
2764
+ end
2765
+
2766
+
2767
+ #---------------------------------------------------------------------------
2768
+ # Symbol
2769
+ #---------------------------------------------------------------------------
2770
+ class Symbol
2771
+ #-----------------------------------------------------------------------
2772
+ # -@
2773
+ #-----------------------------------------------------------------------
2774
+ #
2775
+ # This allows you to put a minus sign in front of a field name in order
2776
+ # to specify descending sort order.
2777
+ def -@
2778
+ ("-"+self.to_s).to_sym
2779
+ end
2780
+
2781
+ #-----------------------------------------------------------------------
2782
+ # +@
2783
+ #-----------------------------------------------------------------------
2784
+ #
2785
+ # This allows you to put a plus sign in front of a field name in order
2786
+ # to specify ascending sort order.
2787
+ def +@
2788
+ ("+"+self.to_s).to_sym
2789
+ end
2790
+ end