og 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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