KirbyBase 2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/README +73 -0
  2. data/bin/kbserver.rb +20 -0
  3. data/changes.txt +105 -0
  4. data/examples/aaa_try_this_first/kbtest.rb +207 -0
  5. data/examples/add_column_test/add_column_test.rb +27 -0
  6. data/examples/calculated_field_test/calculated_field_test.rb +51 -0
  7. data/examples/change_column_type_test/change_column_type_test.rb +25 -0
  8. data/examples/column_required_test/column_required_test.rb +33 -0
  9. data/examples/crosstab_test/crosstab_test.rb +100 -0
  10. data/examples/csv_import_test/csv_import_test.rb +31 -0
  11. data/examples/csv_import_test/plane.csv +11 -0
  12. data/examples/default_value_test/default_value_test.rb +43 -0
  13. data/examples/drop_column_test/drop_column_test.rb +24 -0
  14. data/examples/indexes_test/add_index_test.rb +46 -0
  15. data/examples/indexes_test/drop_index_test.rb +66 -0
  16. data/examples/indexes_test/index_test.rb +71 -0
  17. data/examples/kbserver_as_win32_service/kbserver_daemon.rb +47 -0
  18. data/examples/kbserver_as_win32_service/kbserverctl.rb +75 -0
  19. data/examples/link_many_test/link_many_test.rb +70 -0
  20. data/examples/lookup_field_test/lookup_field_test.rb +55 -0
  21. data/examples/lookup_field_test/lookup_field_test_2.rb +62 -0
  22. data/examples/lookup_field_test/the_hal_fulton_feature_test.rb +69 -0
  23. data/examples/many_to_many_test/many_to_many_test.rb +65 -0
  24. data/examples/memo_test/memo_test.rb +63 -0
  25. data/examples/memo_test/memos/blank.txt +0 -0
  26. data/examples/record_class_test/record_class_test.rb +77 -0
  27. data/examples/rename_column_test/rename_column_test.rb +46 -0
  28. data/examples/rename_table_test/rename_table_test.rb +38 -0
  29. data/examples/yaml_field_test/yaml_field_test.rb +47 -0
  30. data/images/blank.png +0 -0
  31. data/images/callouts/1.png +0 -0
  32. data/images/callouts/10.png +0 -0
  33. data/images/callouts/11.png +0 -0
  34. data/images/callouts/12.png +0 -0
  35. data/images/callouts/13.png +0 -0
  36. data/images/callouts/14.png +0 -0
  37. data/images/callouts/15.png +0 -0
  38. data/images/callouts/2.png +0 -0
  39. data/images/callouts/3.png +0 -0
  40. data/images/callouts/4.png +0 -0
  41. data/images/callouts/5.png +0 -0
  42. data/images/callouts/6.png +0 -0
  43. data/images/callouts/7.png +0 -0
  44. data/images/callouts/8.png +0 -0
  45. data/images/callouts/9.png +0 -0
  46. data/images/caution.png +0 -0
  47. data/images/client_server.png +0 -0
  48. data/images/example.png +0 -0
  49. data/images/home.png +0 -0
  50. data/images/important.png +0 -0
  51. data/images/kirby1.jpg +0 -0
  52. data/images/next.png +0 -0
  53. data/images/note.png +0 -0
  54. data/images/prev.png +0 -0
  55. data/images/single_user.png +0 -0
  56. data/images/smallnew.png +0 -0
  57. data/images/tip.png +0 -0
  58. data/images/toc-blank.png +0 -0
  59. data/images/toc-minus.png +0 -0
  60. data/images/toc-plus.png +0 -0
  61. data/images/up.png +0 -0
  62. data/images/warning.png +0 -0
  63. data/kirbybaserubymanual.html +2243 -0
  64. data/lib/kirbybase.rb +3662 -0
  65. metadata +126 -0
@@ -0,0 +1,3662 @@
1
+ require 'date'
2
+ require 'time'
3
+ require 'drb'
4
+ require 'csv'
5
+ require 'fileutils'
6
+ require 'yaml'
7
+
8
+ #
9
+ # :main:KirbyBase
10
+ # :title:KirbyBase Class Documentation
11
+ # KirbyBase is a class that allows you to create and manipulate simple,
12
+ # plain-text databases. You can use it in either a single-user or
13
+ # client-server mode. You can select records for retrieval/updating using
14
+ # code blocks.
15
+ #
16
+ # Author:: Jamey Cribbs (mailto:jcribbs@twmi.rr.com)
17
+ # Homepage:: http://www.netpromi.com/kirbybase.html
18
+ # Copyright:: Copyright (c) 2005 NetPro Technologies, LLC
19
+ # License:: Distributes under the same terms as Ruby
20
+ # History:
21
+ # 2005-03-28:: Version 2.0
22
+ # * This is a completely new version. The interface has changed
23
+ # dramatically.
24
+ # 2005-04-11:: Version 2.1
25
+ # * Changed the interface to KirbyBase#new and KirbyBase#create_table. You
26
+ # now specify arguments via a code block or as part of the argument list.
27
+ # * Added the ability to specify a class at table creation time.
28
+ # Thereafter, whenever you do a #select, the result set will be an array
29
+ # of instances of that class, instead of instances of Struct. You can
30
+ # also use instances of this class as the argument to KBTable#insert,
31
+ # KBTable#update, and KBTable#set.
32
+ # * Added the ability to encrypt a table so that it is no longer stored as
33
+ # a plain-text file.
34
+ # * Added the ability to explicity specify that you want a result set to be
35
+ # sorted in ascending order.
36
+ # * Added the ability to import a csv file into an existing table.
37
+ # * Added the ability to select a record as if the table were a Hash with
38
+ # it's key being the recno field.
39
+ # * Added the ability to update a record as if the table were a Hash with
40
+ # it's key being the recno field.
41
+ # 2005-05-02:: Version 2.2
42
+ # * By far the biggest change in this version is that I have completely
43
+ # redesigned the internal structure of the database code. Because the
44
+ # KirbyBase and KBTable classes were too tightly coupled, I have created
45
+ # a KBEngine class and moved all low-level I/O logic and locking logic
46
+ # to this class. This allowed me to restructure the KirbyBase class to
47
+ # remove all of the methods that should have been private, but couldn't be
48
+ # because of the coupling to KBTable. In addition, it has allowed me to
49
+ # take all of the low-level code that should not have been in the KBTable
50
+ # class and put it where it belongs, as part of the underlying engine. I
51
+ # feel that the design of KirbyBase is much cleaner now. No changes were
52
+ # made to the class interfaces, so you should not have to change any of
53
+ # your code.
54
+ # * Changed str_to_date and str_to_datetime to use Date#parse method.
55
+ # * Changed #pack method so that it no longer reads the whole file into
56
+ # memory while packing it.
57
+ # * Changed code so that special character sequences like &linefeed; can be
58
+ # part of input data and KirbyBase will not interpret it as special
59
+ # characters.
60
+ # 2005-08-09:: Version 2.2.1
61
+ # * Fixed a bug in with_write_lock.
62
+ # * Fixed a bug that occurred if @record_class was a nested class.
63
+ # 2005-09-08:: Version 2.3 Beta 1
64
+ # * Added ability to specify one-to-one links between tables.
65
+ # * Added ability to specify one-to-many links between tables.
66
+ # * Added ability to specify calculated fields in tables.
67
+ # * Added Memo and Blob field types.
68
+ # * Added indexing to speed up queries.
69
+ # 2005-10-03:: Version 2.3 Beta 2
70
+ # * New column type: :YAML. Many thanks to Logan Capaldo for this idea!
71
+ # * Two new methods: #add_table_column and #drop_table_column.
72
+ # * I have refined the select code so that, when you are doing a one-to-one
73
+ # or one-to-many select, if an appropriate index exists for the child
74
+ # table, KirbyBase automatically uses it.
75
+ # * I have changed the designation for a one-to-one link from Link-> to
76
+ # Lookup-> after googling helped me see that this is a more correct term
77
+ # for what I am trying to convey with this link type.
78
+ # 2005-10-10:: Version 2.3 Production
79
+ # * Added the ability to designate a table field as the "key" field, for
80
+ # Lookup purposes. This simply makes it easier to define Lookup fields.
81
+ # * This led me to finally give in and add "the Hal Fulton Feature" as I am
82
+ # forever going to designate it. You can now specify a Lookup field
83
+ # simply by specifying it's field type as a table, for example:
84
+ # :manager, :person (where :manager is the field name, and :person is the
85
+ # name of a table). See the docs for the specifics or ask Hal. :)
86
+ #
87
+ # 2005-11-13:: Version 2.4
88
+ # * Added a new column type: :Time. Thanks to George Moschovitis for coding
89
+ # this enhancement.
90
+ # * Added more functionality to Memo and Blob fields. They are no longer
91
+ # just read-only. You can now also write to them from KirbyBase. The
92
+ # interface for Memo and Blob fields has changed because of this.
93
+ # * Added the ability to specify, when you initialize a database connection,
94
+ # a base directory where memo/blob fields will be stored.
95
+ # * Changed the way indexes are handled by KBTable in client/server mode.
96
+ # Now, when KBTable grabs an index from KBEngine, it will hold onto it and
97
+ # re-use it unless it has been modified since the last time it grabbed it.
98
+ # This speeds up subsequent queries on the same index.
99
+ # * Removed the restriction that the child table had to exist before you
100
+ # could define a Link_many field in #create_table. I did this so that
101
+ # it would possible to now define many-to-many links. See the example in
102
+ # the distribution. This also goes for Lookup fields.
103
+ # * Added two sample scripts: kbserverctl.rb and kbserver_daemon.rb, that
104
+ # show how to set up a KirbyBase server process as a Windows Service.
105
+ # Thanks to Daniel Berger for his excellent package, win32-service.
106
+ # * Thouroughly revised the manual. I used the excellent text document
107
+ # formatter, AsciiDoc. Many thanks to Stuart Rackham for developing this
108
+ # great tool.
109
+ # * Fixed a bug in KBTable#clear that was causing the recno counter not to
110
+ # be reset. Thanks to basi for this.
111
+ #
112
+ # 2005-12-01:: Version 2.5
113
+ # * Fixed a subtle bug in KBTable#create_indexes.
114
+ # * Added the following new methods to KBTable: add_index, drop_index,
115
+ # rename_column, change_column_type, change_column_default_value, and
116
+ # change_column_required.
117
+ # * Added the ability to specify a default column value at table creation
118
+ # time.
119
+ # * Added the ability to specify, at table creation time, that a column
120
+ # value is required when inserting or updating records.
121
+ # * Removed #add_table_column and #drop_table_column from KirbyBase class
122
+ # and added #add_column and #drop_column to KBTable class. I felt like
123
+ # it made more sense to have these methods in the table's class rather
124
+ # than the database's class.
125
+ # * Added KirbyBase#rename_table method.
126
+ # * Added the ability to, upon database initialization, specify that index
127
+ # creation should not happen until a table is actually opened. This
128
+ # speeds up database initialization at the cost of slower table
129
+ # initialization later.
130
+ #
131
+ #---------------------------------------------------------------------------
132
+ # KirbyBase
133
+ #---------------------------------------------------------------------------
134
+ class KirbyBase
135
+ include DRb::DRbUndumped
136
+
137
+ attr_reader :engine
138
+
139
+ attr_accessor(:connect_type, :host, :port, :path, :ext, :memo_blob_path,
140
+ :delay_index_creation)
141
+
142
+ #-----------------------------------------------------------------------
143
+ # initialize
144
+ #-----------------------------------------------------------------------
145
+ #++
146
+ # Create a new database instance.
147
+ #
148
+ # *connect_type*:: Symbol (:local, :client, :server) specifying role to
149
+ # play.
150
+ # *host*:: String containing IP address or DNS name of server hosting
151
+ # database. (Only valid if connect_type is :client.)
152
+ # *port*:: Integer specifying port database server is listening on.
153
+ # (Only valid if connect_type is :client.)
154
+ # *path*:: String specifying path to location of database tables.
155
+ # *ext*:: String specifying extension of table files.
156
+ # *memo_blob_path*:: String specifying path to location of memo/blob
157
+ # files.
158
+ #
159
+ def initialize(connect_type=:local, host=nil, port=nil, path='./',
160
+ ext='.tbl', memo_blob_path='./', delay_index_creation=false)
161
+ @connect_type = connect_type
162
+ @host = host
163
+ @port = port
164
+ @path = path
165
+ @ext = ext
166
+ @memo_blob_path = memo_blob_path
167
+ @delay_index_creation = delay_index_creation
168
+
169
+ # See if user specified any method arguments via a code block.
170
+ yield self if block_given?
171
+
172
+ # After the yield, make sure the user doesn't change any of these
173
+ # instance variables.
174
+ class << self
175
+ private(:connect_type=, :host=, :path=, :ext=, :memo_blob_path=,
176
+ :delay_index_creation=)
177
+ end
178
+
179
+ # Did user supply full and correct arguments to method?
180
+ raise ArgumentError, 'Invalid connection type specified' unless (
181
+ [:local, :client, :server].include?(@connect_type))
182
+ raise "Must specify hostname or IP address!" if \
183
+ @connect_type == :client and @host.nil?
184
+ raise "Must specify port number!" if @connect_type == :client and \
185
+ @port.nil?
186
+ raise "Invalid path!" if @path.nil?
187
+ raise "Invalid extension!" if @ext.nil?
188
+ raise "Invalid memo/blob path!" if @memo_blob_path.nil?
189
+
190
+ @table_hash = {}
191
+
192
+ # If running as a client, start druby and connect to server.
193
+ if client?
194
+ DRb.start_service()
195
+ @server = DRbObject.new(nil, 'druby://%s:%d' % [@host, @port])
196
+ @engine = @server.engine
197
+ @path = @server.path
198
+ @ext = @server.ext
199
+ @memo_blob_path = @server.memo_blob_path
200
+ else
201
+ @engine = KBEngine.create_called_from_database_instance(self)
202
+ end
203
+
204
+ # The reason why I create all the table instances here is two
205
+ # reasons: (1) I want all of the tables ready to go when a user
206
+ # does a #get_table, so they don't have to wait for the instance
207
+ # to be created, and (2) I want all of the table indexes to get
208
+ # created at the beginning during database initialization so that
209
+ # they are ready for the user to use. Since index creation
210
+ # happens when the table instance is first created, I go ahead and
211
+ # create table instances right off the bat.
212
+ #
213
+ # Also, I use to only execute the code below if this was either a
214
+ # single-user instance of KirbyBase or if client-server, I would
215
+ # only let the client-side KirbyBase instance create the table
216
+ # instances, since there was no need for the server-side KirbyBase
217
+ # instance to create table instances. But, since I want indexes
218
+ # created at db initialization and the server's db instance might
219
+ # be initialized long before any client's db is initialized, I now
220
+ # let the server create table instances also. This is strictly to
221
+ # get the indexes created, there is no other use for the table
222
+ # instances on the server side as they will never be used.
223
+ # Everything should and does go through the table instances created
224
+ # on the client-side.
225
+ #
226
+ # Ok, I added back in a conditional flag that allows me to turn off
227
+ # index initialization if this is a server. The reason I added this
228
+ # back in was that I was running into a problem when running a
229
+ # KirbyBase server as a win32 service. When I tried to start the
230
+ # service, it kept bombing out saying that the application had not
231
+ # responded in a timely manner. It appears to be because it was
232
+ # taking a few seconds to build the indexes when it initialized.
233
+ # When I deleted the index from the table, the service would start
234
+ # just fine. I need to find out if I can set a timeout parameter
235
+ # when starting the win32 service.
236
+ if server? and @delay_index_creation
237
+ else
238
+ @engine.tables.each do |tbl|
239
+ @table_hash[tbl] = \
240
+ KBTable.create_called_from_database_instance(self, tbl,
241
+ File.join(@path, tbl.to_s + @ext))
242
+ end
243
+ end
244
+ end
245
+
246
+ #-----------------------------------------------------------------------
247
+ # server?
248
+ #-----------------------------------------------------------------------
249
+ #++
250
+ # Is this running as a server?
251
+ #
252
+ def server?
253
+ @connect_type == :server
254
+ end
255
+
256
+ #-----------------------------------------------------------------------
257
+ # client?
258
+ #-----------------------------------------------------------------------
259
+ #++
260
+ # Is this running as a client?
261
+ #
262
+ def client?
263
+ @connect_type == :client
264
+ end
265
+
266
+ #-----------------------------------------------------------------------
267
+ # local?
268
+ #-----------------------------------------------------------------------
269
+ #++
270
+ # Is this running in single-user, embedded mode?
271
+ #
272
+ def local?
273
+ @connect_type == :local
274
+ end
275
+
276
+ #-----------------------------------------------------------------------
277
+ # tables
278
+ #-----------------------------------------------------------------------
279
+ #++
280
+ # Return an array containing the names of all tables in this database.
281
+ #
282
+ def tables
283
+ return @engine.tables
284
+ end
285
+
286
+ #-----------------------------------------------------------------------
287
+ # get_table
288
+ #-----------------------------------------------------------------------
289
+ #++
290
+ # Return a reference to the requested table.
291
+ # *name*:: Symbol of table name.
292
+ #
293
+ def get_table(name)
294
+ raise('Do not call this method from a server instance!') if server?
295
+ raise(ArgumentError, 'Table name must be a symbol!') unless \
296
+ name.is_a?(Symbol)
297
+ raise('Table not found!') unless table_exists?(name)
298
+
299
+ if @table_hash.has_key?(name)
300
+ return @table_hash[name]
301
+ else
302
+ @table_hash[name] = \
303
+ KBTable.create_called_from_database_instance(self, name,
304
+ File.join(@path, name.to_s + @ext))
305
+ return @table_hash[name]
306
+ end
307
+ end
308
+
309
+ #-----------------------------------------------------------------------
310
+ # create_table
311
+ #-----------------------------------------------------------------------
312
+ #++
313
+ # Create new table and return a reference to the new table.
314
+ # *name*:: Symbol of table name.
315
+ # *field_defs*:: List of field names (Symbols), field types (Symbols),
316
+ # field indexes, and field extras (Indexes, Lookups,
317
+ # Link_manys, Calculateds, etc.)
318
+ # *Block*:: Optional code block allowing you to set the following:
319
+ # *encrypt*:: true/false specifying whether table should be encrypted.
320
+ # *record_class*:: Class or String specifying the user create class that
321
+ # will be associated with table records.
322
+ #
323
+ def create_table(name=nil, *field_defs)
324
+ raise "Can't call #create_table from server!" if server?
325
+
326
+ t_struct = Struct.new(:name, :field_defs, :encrypt, :record_class)
327
+ t = t_struct.new
328
+ t.name = name
329
+ t.field_defs = field_defs
330
+ t.encrypt = false
331
+ t.record_class = 'Struct'
332
+
333
+ yield t if block_given?
334
+
335
+ raise "Name must be a symbol!" unless t.name.is_a?(Symbol)
336
+ raise "No table name specified!" if t.name.nil?
337
+ raise "No table field definitions specified!" if t.field_defs.nil?
338
+
339
+ @engine.new_table(t.name, t.field_defs, t.encrypt,
340
+ t.record_class.to_s)
341
+
342
+ return get_table(t.name)
343
+ end
344
+
345
+ #-----------------------------------------------------------------------
346
+ # rename_table
347
+ #-----------------------------------------------------------------------
348
+ #++
349
+ # Rename a table.
350
+ #
351
+ # *old_tablename*:: Symbol of old table name.
352
+ # *new_tablename*:: Symbol of new table name.
353
+ #
354
+ def rename_table(old_tablename, new_tablename)
355
+ raise "Table does not exist!" unless table_exists?(old_tablename)
356
+ raise(ArgumentError, 'Existing table name must be a symbol!') \
357
+ unless old_tablename.is_a?(Symbol)
358
+ raise(ArgumentError, 'New table name must be a symbol!') unless \
359
+ new_tablename.is_a?(Symbol)
360
+ raise "Table already exists!" if table_exists?(new_tablename)
361
+ @table_hash.delete(old_tablename)
362
+ @engine.rename_table(old_tablename, new_tablename)
363
+ get_table(new_tablename)
364
+ end
365
+
366
+ #-----------------------------------------------------------------------
367
+ # drop_table
368
+ #-----------------------------------------------------------------------
369
+ #++
370
+ # Delete a table.
371
+ #
372
+ # *tablename*:: Symbol of table name.
373
+ #
374
+ def drop_table(tablename)
375
+ raise(ArgumentError, 'Table name must be a symbol!') unless \
376
+ tablename.is_a?(Symbol)
377
+ raise "Table does not exist!" unless table_exists?(tablename)
378
+ @table_hash.delete(tablename)
379
+ return @engine.delete_table(tablename)
380
+ end
381
+
382
+ #-----------------------------------------------------------------------
383
+ # table_exists?
384
+ #-----------------------------------------------------------------------
385
+ #++
386
+ # Return true if table exists.
387
+ #
388
+ # *tablename*:: Symbol of table name.
389
+ #
390
+ def table_exists?(tablename)
391
+ raise(ArgumentError, 'Table name must be a symbol!') unless \
392
+ tablename.is_a?(Symbol)
393
+
394
+ return @engine.table_exists?(tablename)
395
+ end
396
+
397
+ end
398
+
399
+
400
+ #---------------------------------------------------------------------------
401
+ # KBTypeConversionsMixin
402
+ #---------------------------------------------------------------------------
403
+ module KBTypeConversionsMixin
404
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
405
+
406
+ #-----------------------------------------------------------------------
407
+ # convert_to
408
+ #-----------------------------------------------------------------------
409
+ def convert_to(data_type, s)
410
+ return nil if s.empty? or s.nil?
411
+
412
+ case data_type
413
+ when :String
414
+ if s =~ UNENCODE_RE
415
+ return s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
416
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
417
+ ).gsub('&amp;', "&")
418
+ else
419
+ return s
420
+ end
421
+ when :Integer
422
+ return s.to_i
423
+ when :Float
424
+ return s.to_f
425
+ when :Boolean
426
+ if ['false', 'False', nil, false].include?(s)
427
+ return false
428
+ else
429
+ return true
430
+ end
431
+ when :Time
432
+ return Time.parse(s)
433
+ when :Date
434
+ return Date.parse(s)
435
+ when :DateTime
436
+ return DateTime.parse(s)
437
+ when :YAML
438
+ # This code is here in case the YAML field is the last
439
+ # field in the record. Because YAML normall defines a
440
+ # nil value as "--- ", but KirbyBase strips trailing
441
+ # spaces off the end of the record, so if this is the
442
+ # last field in the record, KirbyBase will strip the
443
+ # trailing space off and make it "---". When KirbyBase
444
+ # attempts to convert this value back using to_yaml,
445
+ # you get an exception.
446
+ if s == "---"
447
+ return nil
448
+ elsif s =~ UNENCODE_RE
449
+ y = s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
450
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
451
+ ).gsub('&amp;', "&")
452
+ return YAML.load(y)
453
+ else
454
+ return YAML.load(s)
455
+ end
456
+ when :Memo
457
+ memo = KBMemo.new(@tbl.db, s)
458
+ memo.read_from_file
459
+ return memo
460
+ when :Blob
461
+ blob = KBBlob.new(@tbl.db, s)
462
+ blob.read_from_file
463
+ return blob
464
+ else
465
+ raise "Invalid field type: %s" % data_type
466
+ end
467
+ end
468
+ end
469
+
470
+
471
+ #---------------------------------------------------------------------------
472
+ # KBEngine
473
+ #---------------------------------------------------------------------------
474
+ class KBEngine
475
+ include DRb::DRbUndumped
476
+ include KBTypeConversionsMixin
477
+
478
+ EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
479
+ '0123456789.+-,$:|&;_ '
480
+ EN_STR_LEN = EN_STR.length
481
+ EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
482
+ EN_KEY = EN_KEY1.unpack("u")[0]
483
+ EN_KEY_LEN = EN_KEY.length
484
+
485
+ # Make constructor private.
486
+ private_class_method :new
487
+
488
+ #-----------------------------------------------------------------------
489
+ # KBEngine.create_called_from_database_instance
490
+ #-----------------------------------------------------------------------
491
+ def KBEngine.create_called_from_database_instance(db)
492
+ return new(db)
493
+ end
494
+
495
+ #-----------------------------------------------------------------------
496
+ # initialize
497
+ #-----------------------------------------------------------------------
498
+ def initialize(db)
499
+ @db = db
500
+ @recno_indexes = {}
501
+ @indexes = {}
502
+
503
+ # This hash will hold the table locks if in client/server mode.
504
+ @mutex_hash = {} if @db.server?
505
+ end
506
+
507
+ #-----------------------------------------------------------------------
508
+ # init_recno_index
509
+ #-----------------------------------------------------------------------
510
+ def init_recno_index(table)
511
+ return if recno_index_exists?(table)
512
+
513
+ with_write_locked_table(table) do |fptr|
514
+ @recno_indexes[table.name] = KBRecnoIndex.new(table)
515
+ @recno_indexes[table.name].rebuild(fptr)
516
+ end
517
+ end
518
+
519
+ #-----------------------------------------------------------------------
520
+ # rebuild_recno_index
521
+ #-----------------------------------------------------------------------
522
+ def rebuild_recno_index(table)
523
+ with_write_locked_table(table) do |fptr|
524
+ @recno_indexes[table.name].rebuild(fptr)
525
+ end
526
+ end
527
+
528
+ #-----------------------------------------------------------------------
529
+ # remove_recno_index
530
+ #-----------------------------------------------------------------------
531
+ def remove_recno_index(tablename)
532
+ @recno_indexes.delete(tablename)
533
+ end
534
+
535
+ #-----------------------------------------------------------------------
536
+ # update_recno_index
537
+ #-----------------------------------------------------------------------
538
+ def update_recno_index(table, recno, fpos)
539
+ @recno_indexes[table.name].update_index_rec(recno, fpos)
540
+ end
541
+
542
+ #-----------------------------------------------------------------------
543
+ # recno_index_exists?
544
+ #-----------------------------------------------------------------------
545
+ def recno_index_exists?(table)
546
+ @recno_indexes.include?(table.name)
547
+ end
548
+
549
+ #-----------------------------------------------------------------------
550
+ # init_index
551
+ #-----------------------------------------------------------------------
552
+ def init_index(table, index_fields)
553
+ return if index_exists?(table, index_fields)
554
+
555
+ with_write_locked_table(table) do |fptr|
556
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym] = \
557
+ KBIndex.new(table, index_fields)
558
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
559
+ ].rebuild(fptr)
560
+ end
561
+ end
562
+
563
+ #-----------------------------------------------------------------------
564
+ # rebuild_index
565
+ #-----------------------------------------------------------------------
566
+ def rebuild_index(table, index_fields)
567
+ with_write_locked_table(table) do |fptr|
568
+ @indexes["#{table.name}_#{index_fields.join('_')}".to_sym
569
+ ].rebuild(fptr)
570
+ end
571
+ end
572
+
573
+ #-----------------------------------------------------------------------
574
+ # remove_indexes
575
+ #-----------------------------------------------------------------------
576
+ def remove_indexes(tablename)
577
+ re_table_name = Regexp.new(tablename.to_s)
578
+ @indexes.delete_if { |k,v| k.to_s =~ re_table_name }
579
+ end
580
+
581
+ #-----------------------------------------------------------------------
582
+ # add_to_indexes
583
+ #-----------------------------------------------------------------------
584
+ def add_to_indexes(table, rec, fpos)
585
+ @recno_indexes[table.name].add_index_rec(rec.first, fpos)
586
+
587
+ re_table_name = Regexp.new(table.name.to_s)
588
+ @indexes.each_pair do |key, index|
589
+ index.add_index_rec(rec) if key.to_s =~ re_table_name
590
+ end
591
+ end
592
+
593
+ #-----------------------------------------------------------------------
594
+ # delete_from_indexes
595
+ #-----------------------------------------------------------------------
596
+ def delete_from_indexes(table, rec, fpos)
597
+ @recno_indexes[table.name].delete_index_rec(rec.recno)
598
+
599
+ re_table_name = Regexp.new(table.name.to_s)
600
+ @indexes.each_pair do |key, index|
601
+ index.delete_index_rec(rec.recno) if key.to_s =~ re_table_name
602
+ end
603
+ end
604
+
605
+ #-----------------------------------------------------------------------
606
+ # update_to_indexes
607
+ #-----------------------------------------------------------------------
608
+ def update_to_indexes(table, rec)
609
+ re_table_name = Regexp.new(table.name.to_s)
610
+ @indexes.each_pair do |key, index|
611
+ index.update_index_rec(rec) if key.to_s =~ re_table_name
612
+ end
613
+ end
614
+
615
+ #-----------------------------------------------------------------------
616
+ # index_exists?
617
+ #-----------------------------------------------------------------------
618
+ def index_exists?(table, index_fields)
619
+ @indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
620
+ end
621
+
622
+ #-----------------------------------------------------------------------
623
+ # get_index
624
+ #-----------------------------------------------------------------------
625
+ def get_index(table, index_name)
626
+ return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
627
+ end
628
+
629
+ #-----------------------------------------------------------------------
630
+ # get_index_timestamp
631
+ #-----------------------------------------------------------------------
632
+ def get_index_timestamp(table, index_name)
633
+ return @indexes["#{table.name}_#{index_name}".to_sym].get_timestamp
634
+ end
635
+
636
+ #-----------------------------------------------------------------------
637
+ # get_recno_index
638
+ #-----------------------------------------------------------------------
639
+ def get_recno_index(table)
640
+ return @recno_indexes[table.name].get_idx
641
+ end
642
+
643
+ #-----------------------------------------------------------------------
644
+ # table_exists?
645
+ #-----------------------------------------------------------------------
646
+ def table_exists?(tablename)
647
+ return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
648
+ end
649
+
650
+ #-----------------------------------------------------------------------
651
+ # tables
652
+ #-----------------------------------------------------------------------
653
+ def tables
654
+ list = []
655
+ Dir.foreach(@db.path) { |filename|
656
+ list << File.basename(filename, '.*').to_sym if \
657
+ File.extname(filename) == @db.ext
658
+ }
659
+ return list
660
+ end
661
+
662
+ #-----------------------------------------------------------------------
663
+ # build_header_field_string
664
+ #-----------------------------------------------------------------------
665
+ def build_header_field_string(field_name_def, field_type_def)
666
+ # Put field name at start of string definition.
667
+ temp_field_def = field_name_def.to_s + ':'
668
+
669
+ # if field type is a hash, that means that it is not just a
670
+ # simple field. Either is is being used in an index, it is a
671
+ # Lookup field, it is a Link_many field, or it is a Calculated
672
+ # field. This next bit of code is to piece together a proper
673
+ # string so that it can be written out to the header rec.
674
+ if field_type_def.is_a?(Hash)
675
+ raise 'Missing :DataType key in field type hash!' unless \
676
+ field_type_def.has_key?(:DataType)
677
+
678
+ temp_type = field_type_def[:DataType]
679
+
680
+ raise 'Invalid field type: %s' % temp_type unless \
681
+ KBTable.valid_field_type?(temp_type)
682
+
683
+ temp_field_def += field_type_def[:DataType].to_s
684
+
685
+ if field_type_def.has_key?(:Key)
686
+ temp_field_def += ':Key->true'
687
+ end
688
+
689
+ if field_type_def.has_key?(:Index)
690
+ raise 'Invalid field type for index: %s' % temp_type \
691
+ unless KBTable.valid_index_type?(temp_type)
692
+
693
+ temp_field_def += ':Index->' + field_type_def[:Index].to_s
694
+ end
695
+
696
+ if field_type_def.has_key?(:Default)
697
+ raise 'Cannot set default value for this type: ' + \
698
+ '%s' % temp_type unless KBTable.valid_default_type?(
699
+ temp_type)
700
+
701
+ unless field_type_def[:Default].nil?
702
+ temp_field_def += ':Default->' + \
703
+ KBTable.convert_to_string(temp_type,
704
+ field_type_def[:Default])
705
+ end
706
+ end
707
+
708
+ if field_type_def.has_key?(:Required)
709
+ raise 'Required must be true or false!' unless \
710
+ [true, false].include?(field_type_def[:Required])
711
+
712
+ temp_field_def += \
713
+ ':Required->%s' % field_type_def[:Required]
714
+ end
715
+
716
+ if field_type_def.has_key?(:Lookup)
717
+ if field_type_def[:Lookup].is_a?(Array)
718
+ temp_field_def += \
719
+ ':Lookup->%s.%s' % field_type_def[:Lookup]
720
+ else
721
+ tbl = @db.get_table(field_type_def[:Lookup])
722
+ temp_field_def += \
723
+ ':Lookup->%s.%s' % [field_type_def[:Lookup],
724
+ tbl.lookup_key]
725
+ end
726
+ elsif field_type_def.has_key?(:Link_many)
727
+ raise 'Field type for Link_many field must be :ResultSet' \
728
+ unless temp_type == :ResultSet
729
+ temp_field_def += \
730
+ ':Link_many->%s=%s.%s' % field_type_def[:Link_many]
731
+ elsif field_type_def.has_key?(:Calculated)
732
+ temp_field_def += \
733
+ ':Calculated->%s' % field_type_def[:Calculated]
734
+ end
735
+ else
736
+ if KBTable.valid_field_type?(field_type_def)
737
+ temp_field_def += field_type_def.to_s
738
+ elsif @db.table_exists?(field_type_def)
739
+ tbl = @db.get_table(field_type_def)
740
+ temp_field_def += \
741
+ '%s:Lookup->%s.%s' % [tbl.field_types[
742
+ tbl.field_names.index(tbl.lookup_key)], field_type_def,
743
+ tbl.lookup_key]
744
+ else
745
+ raise 'Invalid field type: %s' % field_type_def
746
+ end
747
+ end
748
+ return temp_field_def
749
+ end
750
+
751
+ #-----------------------------------------------------------------------
752
+ # new_table
753
+ #-----------------------------------------------------------------------
754
+ #++
755
+ # Create physical file holding table. This table should not be directly
756
+ # called in your application, but only called by #create_table.
757
+ #
758
+ def new_table(name, field_defs, encrypt, record_class)
759
+ # Can't create a table that already exists!
760
+ raise "Table already exists!" if table_exists?(name)
761
+
762
+ raise 'Must have a field type for each field name' \
763
+ unless field_defs.size.remainder(2) == 0
764
+ temp_field_defs = []
765
+ (0...field_defs.size).step(2) do |x|
766
+ temp_field_defs << build_header_field_string(field_defs[x],
767
+ field_defs[x+1])
768
+ end
769
+
770
+ # Header rec consists of last record no. used, delete count, and
771
+ # all field names/types. Here, I am inserting the 'recno' field
772
+ # at the beginning of the fields.
773
+ header_rec = ['000000', '000000', record_class, 'recno:Integer',
774
+ temp_field_defs].join('|')
775
+
776
+ header_rec = 'Z' + encrypt_str(header_rec) if encrypt
777
+
778
+ begin
779
+ fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
780
+ fptr.write(header_rec + "\n")
781
+ ensure
782
+ fptr.close
783
+ end
784
+ end
785
+
786
+ #-----------------------------------------------------------------------
787
+ # delete_table
788
+ #-----------------------------------------------------------------------
789
+ def delete_table(tablename)
790
+ with_write_lock(tablename) do
791
+ remove_indexes(tablename)
792
+ remove_recno_index(tablename)
793
+ File.delete(File.join(@db.path, tablename.to_s + @db.ext))
794
+ return true
795
+ end
796
+ end
797
+
798
+ #----------------------------------------------------------------------
799
+ # get_total_recs
800
+ #----------------------------------------------------------------------
801
+ def get_total_recs(table)
802
+ return get_recs(table).size
803
+ end
804
+
805
+ #-----------------------------------------------------------------------
806
+ # reset_recno_ctr
807
+ #-----------------------------------------------------------------------
808
+ def reset_recno_ctr(table)
809
+ with_write_locked_table(table) do |fptr|
810
+ last_rec_no, rest_of_line = get_header_record(table, fptr
811
+ ).split('|', 2)
812
+ write_header_record(table, fptr,
813
+ ['%06d' % 0, rest_of_line].join('|'))
814
+ return true
815
+ end
816
+ end
817
+
818
+ #-----------------------------------------------------------------------
819
+ # get_header_vars
820
+ #-----------------------------------------------------------------------
821
+ def get_header_vars(table)
822
+ with_table(table) do |fptr|
823
+ line = get_header_record(table, fptr)
824
+
825
+ last_rec_no, del_ctr, record_class, *flds = line.split('|')
826
+ field_names = flds.collect { |x| x.split(':')[0].to_sym }
827
+ field_types = flds.collect { |x| x.split(':')[1].to_sym }
828
+ field_indexes = [nil] * field_names.size
829
+ field_defaults = [nil] * field_names.size
830
+ field_requireds = [false] * field_names.size
831
+ field_extras = [nil] * field_names.size
832
+
833
+ flds.each_with_index do |x,i|
834
+ field_extras[i] = {}
835
+ if x.split(':').size > 2
836
+ x.split(':')[2..-1].each do |y|
837
+ if y =~ /Index/
838
+ field_indexes[i] = y
839
+ elsif y =~ /Default/
840
+ field_defaults[i] = \
841
+ convert_to(field_types[i], y.split('->')[1])
842
+ elsif y =~ /Required/
843
+ field_requireds[i] = \
844
+ convert_to(:Boolean, y.split('->')[1])
845
+ else
846
+ field_extras[i][y.split('->')[0]] = \
847
+ y.split('->')[1]
848
+ end
849
+ end
850
+ end
851
+ end
852
+ return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
853
+ record_class, field_names, field_types, field_indexes,
854
+ field_defaults, field_requireds, field_extras]
855
+ end
856
+ end
857
+
858
+ #-----------------------------------------------------------------------
859
+ # get_recs
860
+ #-----------------------------------------------------------------------
861
+ def get_recs(table)
862
+ encrypted = table.encrypted?
863
+ recs = []
864
+
865
+ with_table(table) do |fptr|
866
+ begin
867
+ # Skip header rec.
868
+ fptr.readline
869
+
870
+ # Loop through table.
871
+ while true
872
+ # Record current position in table. Then read first
873
+ # detail record.
874
+ fpos = fptr.tell
875
+ line = fptr.readline
876
+ line.chomp!
877
+ line_length = line.length
878
+
879
+ line = unencrypt_str(line) if encrypted
880
+ line.strip!
881
+
882
+ # If blank line (i.e. 'deleted'), skip it.
883
+ next if line == ''
884
+
885
+ # Split the line up into fields.
886
+ rec = line.split('|', -1)
887
+ rec << fpos << line_length
888
+ recs << rec
889
+ end
890
+ # Here's how we break out of the loop...
891
+ rescue EOFError
892
+ end
893
+ return recs
894
+ end
895
+ end
896
+
897
+ #-----------------------------------------------------------------------
898
+ # get_recs_by_recno
899
+ #-----------------------------------------------------------------------
900
+ def get_recs_by_recno(table, recnos)
901
+ encrypted = table.encrypted?
902
+ recs = []
903
+ recno_idx = get_recno_index(table)
904
+
905
+ with_table(table) do |fptr|
906
+ # Skip header rec.
907
+ fptr.readline
908
+
909
+ # Take all the recnos you want to get, add the file positions
910
+ # to them, and sort by file position, so that when we seek
911
+ # through the physical file we are going in ascending file
912
+ # position order, which should be fastest.
913
+ recnos.collect { |r| [recno_idx[r], r] }.sort.each do |r|
914
+ fptr.seek(r[0])
915
+ line = fptr.readline
916
+ line.chomp!
917
+ line_length = line.length
918
+
919
+ line = unencrypt_str(line) if encrypted
920
+ line.strip!
921
+
922
+ # If blank line (i.e. 'deleted'), skip it.
923
+ next if line == ''
924
+
925
+ # Split the line up into fields.
926
+ rec = line.split('|', -1)
927
+ raise "Index Corrupt!" unless rec[0].to_i == r[1]
928
+ rec << r[0] << line_length
929
+ recs << rec
930
+ end
931
+ return recs
932
+ end
933
+ end
934
+
935
+ #-----------------------------------------------------------------------
936
+ # get_rec_by_recno
937
+ #-----------------------------------------------------------------------
938
+ def get_rec_by_recno(table, recno)
939
+ encrypted = table.encrypted?
940
+ recno_idx = get_recno_index(table)
941
+
942
+ return nil unless recno_idx.has_key?(recno)
943
+
944
+ with_table(table) do |fptr|
945
+ fptr.seek(recno_idx[recno])
946
+ line = fptr.readline
947
+ line.chomp!
948
+ line_length = line.length
949
+
950
+ line = unencrypt_str(line) if encrypted
951
+ line.strip!
952
+
953
+ return nil if line == ''
954
+
955
+ # Split the line up into fields.
956
+ rec = line.split('|', -1)
957
+
958
+ raise "Index Corrupt!" unless rec[0].to_i == recno
959
+ rec << recno_idx[recno] << line_length
960
+ return rec
961
+ end
962
+ end
963
+
964
+ #-----------------------------------------------------------------------
965
+ # insert_record
966
+ #-----------------------------------------------------------------------
967
+ def insert_record(table, rec)
968
+ with_write_locked_table(table) do |fptr|
969
+ # Auto-increment the record number field.
970
+ rec_no = incr_rec_no_ctr(table, fptr)
971
+
972
+ # Insert the newly created record number value at the beginning
973
+ # of the field values.
974
+ rec[0] = rec_no
975
+
976
+ fptr.seek(0, IO::SEEK_END)
977
+ fpos = fptr.tell
978
+
979
+ write_record(table, fptr, 'end', rec.join('|'))
980
+
981
+ add_to_indexes(table, rec, fpos)
982
+
983
+ # Return the record number of the newly created record.
984
+ return rec_no
985
+ end
986
+ end
987
+
988
+ #-----------------------------------------------------------------------
989
+ # update_records
990
+ #-----------------------------------------------------------------------
991
+ def update_records(table, recs)
992
+ with_write_locked_table(table) do |fptr|
993
+ recs.each do |rec|
994
+ line = rec[:rec].join('|')
995
+
996
+ # This doesn't actually 'delete' the line, it just
997
+ # makes it all spaces. That way, if the updated
998
+ # record is the same or less length than the old
999
+ # record, we can write the record back into the
1000
+ # same spot. If the updated record is greater than
1001
+ # the old record, we will leave the now spaced-out
1002
+ # line and write the updated record at the end of
1003
+ # the file.
1004
+ write_record(table, fptr, rec[:fpos],
1005
+ ' ' * rec[:line_length])
1006
+ if line.length > rec[:line_length]
1007
+ fptr.seek(0, IO::SEEK_END)
1008
+ new_fpos = fptr.tell
1009
+ write_record(table, fptr, 'end', line)
1010
+ incr_del_ctr(table, fptr)
1011
+
1012
+ update_recno_index(table, rec[:rec].first, new_fpos)
1013
+ else
1014
+ write_record(table, fptr, rec[:fpos], line)
1015
+ end
1016
+ update_to_indexes(table, rec[:rec])
1017
+ end
1018
+ # Return the number of records updated.
1019
+ return recs.size
1020
+ end
1021
+ end
1022
+
1023
+ #-----------------------------------------------------------------------
1024
+ # delete_records
1025
+ #-----------------------------------------------------------------------
1026
+ def delete_records(table, recs)
1027
+ with_write_locked_table(table) do |fptr|
1028
+ recs.each do |rec|
1029
+ # Go to offset within the file where the record is and
1030
+ # replace it with all spaces.
1031
+ write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
1032
+ incr_del_ctr(table, fptr)
1033
+
1034
+ delete_from_indexes(table, rec, rec.fpos)
1035
+ end
1036
+
1037
+ # Return the number of records deleted.
1038
+ return recs.size
1039
+ end
1040
+ end
1041
+
1042
+ #-----------------------------------------------------------------------
1043
+ # change_column_type
1044
+ #-----------------------------------------------------------------------
1045
+ def change_column_type(table, col_name, col_type)
1046
+ col_index = table.field_names.index(col_name)
1047
+ with_write_lock(table.name) do
1048
+ fptr = open(table.filename, 'r')
1049
+ new_fptr = open(table.filename+'temp', 'w')
1050
+
1051
+ line = fptr.readline.chomp
1052
+
1053
+ if line[0..0] == 'Z'
1054
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1055
+ else
1056
+ header_rec = line.split('|')
1057
+ end
1058
+
1059
+ temp_fields = header_rec[col_index+3].split(':')
1060
+ temp_fields[1] = col_type.to_s
1061
+ header_rec[col_index+3] = temp_fields.join(':')
1062
+
1063
+ if line[0..0] == 'Z'
1064
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1065
+ "\n")
1066
+ else
1067
+ new_fptr.write(header_rec.join('|') + "\n")
1068
+ end
1069
+
1070
+ begin
1071
+ while true
1072
+ new_fptr.write(fptr.readline)
1073
+ end
1074
+ # Here's how we break out of the loop...
1075
+ rescue EOFError
1076
+ end
1077
+
1078
+ # Close the table and release the write lock.
1079
+ fptr.close
1080
+ new_fptr.close
1081
+ File.delete(table.filename)
1082
+ FileUtils.mv(table.filename+'temp', table.filename)
1083
+ end
1084
+ end
1085
+
1086
+ #-----------------------------------------------------------------------
1087
+ # rename_column
1088
+ #-----------------------------------------------------------------------
1089
+ def rename_column(table, old_col_name, new_col_name)
1090
+ col_index = table.field_names.index(old_col_name)
1091
+ with_write_lock(table.name) do
1092
+ fptr = open(table.filename, 'r')
1093
+ new_fptr = open(table.filename+'temp', 'w')
1094
+
1095
+ line = fptr.readline.chomp
1096
+
1097
+ if line[0..0] == 'Z'
1098
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1099
+ else
1100
+ header_rec = line.split('|')
1101
+ end
1102
+
1103
+ temp_fields = header_rec[col_index+3].split(':')
1104
+ temp_fields[0] = new_col_name.to_s
1105
+ header_rec[col_index+3] = temp_fields.join(':')
1106
+
1107
+ if line[0..0] == 'Z'
1108
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1109
+ "\n")
1110
+ else
1111
+ new_fptr.write(header_rec.join('|') + "\n")
1112
+ end
1113
+
1114
+ begin
1115
+ while true
1116
+ new_fptr.write(fptr.readline)
1117
+ end
1118
+ # Here's how we break out of the loop...
1119
+ rescue EOFError
1120
+ end
1121
+
1122
+ # Close the table and release the write lock.
1123
+ fptr.close
1124
+ new_fptr.close
1125
+ File.delete(table.filename)
1126
+ FileUtils.mv(table.filename+'temp', table.filename)
1127
+ end
1128
+ end
1129
+
1130
+ #-----------------------------------------------------------------------
1131
+ # add_column
1132
+ #-----------------------------------------------------------------------
1133
+ def add_column(table, col_name, col_type, after)
1134
+ temp_field_def = build_header_field_string(col_name, col_type)
1135
+
1136
+ if after.nil?
1137
+ insert_after = -1
1138
+ else
1139
+ if table.field_names.last == after
1140
+ insert_after = -1
1141
+ else
1142
+ insert_after = table.field_names.index(after)+1
1143
+ end
1144
+ end
1145
+
1146
+ with_write_lock(table.name) do
1147
+ fptr = open(table.filename, 'r')
1148
+ new_fptr = open(table.filename+'temp', 'w')
1149
+
1150
+ line = fptr.readline.chomp
1151
+
1152
+ if line[0..0] == 'Z'
1153
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1154
+ if insert_after == -1
1155
+ header_rec.insert(insert_after, temp_field_def)
1156
+ else
1157
+ header_rec.insert(insert_after+3, temp_field_def)
1158
+ end
1159
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1160
+ "\n")
1161
+ else
1162
+ header_rec = line.split('|')
1163
+ if insert_after == -1
1164
+ header_rec.insert(insert_after, temp_field_def)
1165
+ else
1166
+ header_rec.insert(insert_after+3, temp_field_def)
1167
+ end
1168
+ new_fptr.write(header_rec.join('|') + "\n")
1169
+ end
1170
+
1171
+ begin
1172
+ while true
1173
+ line = fptr.readline.chomp
1174
+
1175
+ if table.encrypted?
1176
+ temp_line = unencrypt_str(line)
1177
+ else
1178
+ temp_line = line
1179
+ end
1180
+
1181
+ rec = temp_line.split('|')
1182
+ rec.insert(insert_after, '')
1183
+
1184
+ if table.encrypted?
1185
+ new_fptr.write(encrypt_str(rec.join('|')) + "\n")
1186
+ else
1187
+ new_fptr.write(rec.join('|') + "\n")
1188
+ end
1189
+ end
1190
+ # Here's how we break out of the loop...
1191
+ rescue EOFError
1192
+ end
1193
+
1194
+ # Close the table and release the write lock.
1195
+ fptr.close
1196
+ new_fptr.close
1197
+ File.delete(table.filename)
1198
+ FileUtils.mv(table.filename+'temp', table.filename)
1199
+ end
1200
+ end
1201
+
1202
+ #-----------------------------------------------------------------------
1203
+ # drop_column
1204
+ #-----------------------------------------------------------------------
1205
+ def drop_column(table, col_name)
1206
+ col_index = table.field_names.index(col_name)
1207
+ with_write_lock(table.name) do
1208
+ fptr = open(table.filename, 'r')
1209
+ new_fptr = open(table.filename+'temp', 'w')
1210
+
1211
+ line = fptr.readline.chomp
1212
+
1213
+ if line[0..0] == 'Z'
1214
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1215
+ header_rec.delete_at(col_index+3)
1216
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1217
+ "\n")
1218
+ else
1219
+ header_rec = line.split('|')
1220
+ header_rec.delete_at(col_index+3)
1221
+ new_fptr.write(header_rec.join('|') + "\n")
1222
+ end
1223
+
1224
+ begin
1225
+ while true
1226
+ line = fptr.readline.chomp
1227
+
1228
+ if table.encrypted?
1229
+ temp_line = unencrypt_str(line)
1230
+ else
1231
+ temp_line = line
1232
+ end
1233
+
1234
+ rec = temp_line.split('|')
1235
+ rec.delete_at(col_index)
1236
+
1237
+ if table.encrypted?
1238
+ new_fptr.write(encrypt_str(rec.join('|')) + "\n")
1239
+ else
1240
+ new_fptr.write(rec.join('|') + "\n")
1241
+ end
1242
+ end
1243
+ # Here's how we break out of the loop...
1244
+ rescue EOFError
1245
+ end
1246
+
1247
+ # Close the table and release the write lock.
1248
+ fptr.close
1249
+ new_fptr.close
1250
+ File.delete(table.filename)
1251
+ FileUtils.mv(table.filename+'temp', table.filename)
1252
+ end
1253
+ end
1254
+
1255
+ #-----------------------------------------------------------------------
1256
+ # rename_table
1257
+ #-----------------------------------------------------------------------
1258
+ def rename_table(old_tablename, new_tablename)
1259
+ old_full_path = File.join(@db.path, old_tablename.to_s + @db.ext)
1260
+ new_full_path = File.join(@db.path, new_tablename.to_s + @db.ext)
1261
+ File.rename(old_full_path, new_full_path)
1262
+ end
1263
+
1264
+ #-----------------------------------------------------------------------
1265
+ # add_index
1266
+ #-----------------------------------------------------------------------
1267
+ def add_index(table, col_names, index_no)
1268
+ with_write_lock(table.name) do
1269
+ fptr = open(table.filename, 'r')
1270
+ new_fptr = open(table.filename+'temp', 'w')
1271
+
1272
+ line = fptr.readline.chomp
1273
+
1274
+ if line[0..0] == 'Z'
1275
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1276
+ else
1277
+ header_rec = line.split('|')
1278
+ end
1279
+
1280
+ col_names.each do |c|
1281
+ header_rec[table.field_names.index(c)+3] += \
1282
+ ':Index->%d' % index_no
1283
+ end
1284
+
1285
+ if line[0..0] == 'Z'
1286
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1287
+ "\n")
1288
+ else
1289
+ new_fptr.write(header_rec.join('|') + "\n")
1290
+ end
1291
+
1292
+ begin
1293
+ while true
1294
+ new_fptr.write(fptr.readline)
1295
+ end
1296
+ # Here's how we break out of the loop...
1297
+ rescue EOFError
1298
+ end
1299
+
1300
+ # Close the table and release the write lock.
1301
+ fptr.close
1302
+ new_fptr.close
1303
+ File.delete(table.filename)
1304
+ FileUtils.mv(table.filename+'temp', table.filename)
1305
+ end
1306
+ end
1307
+
1308
+ #-----------------------------------------------------------------------
1309
+ # drop_index
1310
+ #-----------------------------------------------------------------------
1311
+ def drop_index(table, col_names)
1312
+ with_write_lock(table.name) do
1313
+ fptr = open(table.filename, 'r')
1314
+ new_fptr = open(table.filename+'temp', 'w')
1315
+
1316
+ line = fptr.readline.chomp
1317
+
1318
+ if line[0..0] == 'Z'
1319
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1320
+ else
1321
+ header_rec = line.split('|')
1322
+ end
1323
+
1324
+ col_names.each do |c|
1325
+ temp_field_def = \
1326
+ header_rec[table.field_names.index(c)+3].split(':')
1327
+ temp_field_def = temp_field_def.delete_if {|x|
1328
+ x =~ /Index->/
1329
+ }
1330
+ header_rec[table.field_names.index(c)+3] = \
1331
+ temp_field_def.join(':')
1332
+ end
1333
+
1334
+ if line[0..0] == 'Z'
1335
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1336
+ "\n")
1337
+ else
1338
+ new_fptr.write(header_rec.join('|') + "\n")
1339
+ end
1340
+
1341
+ begin
1342
+ while true
1343
+ new_fptr.write(fptr.readline)
1344
+ end
1345
+ # Here's how we break out of the loop...
1346
+ rescue EOFError
1347
+ end
1348
+
1349
+ # Close the table and release the write lock.
1350
+ fptr.close
1351
+ new_fptr.close
1352
+ File.delete(table.filename)
1353
+ FileUtils.mv(table.filename+'temp', table.filename)
1354
+ end
1355
+ end
1356
+
1357
+ #-----------------------------------------------------------------------
1358
+ # change_column_default_value
1359
+ #-----------------------------------------------------------------------
1360
+ def change_column_default_value(table, col_name, value)
1361
+ with_write_lock(table.name) do
1362
+ fptr = open(table.filename, 'r')
1363
+ new_fptr = open(table.filename+'temp', 'w')
1364
+
1365
+ line = fptr.readline.chomp
1366
+
1367
+ if line[0..0] == 'Z'
1368
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1369
+ else
1370
+ header_rec = line.split('|')
1371
+ end
1372
+
1373
+ if header_rec[table.field_names.index(col_name)+3] =~ \
1374
+ /Default->/
1375
+ hr_chunks = \
1376
+ header_rec[table.field_names.index(col_name)+3].split(':')
1377
+
1378
+ if value.nil?
1379
+ hr_chunks = hr_chunks.delete_if { |x| x =~ /Default->/ }
1380
+ header_rec[table.field_names.index(col_name)+3] = \
1381
+ hr_chunks.join(':')
1382
+ else
1383
+ hr_chunks.collect! do |x|
1384
+ if x =~ /Default->/
1385
+ 'Default->%s' % value
1386
+ else
1387
+ x
1388
+ end
1389
+ end
1390
+ header_rec[table.field_names.index(col_name)+3] = \
1391
+ hr_chunks.join(':')
1392
+ end
1393
+ else
1394
+ if value.nil?
1395
+ else
1396
+ header_rec[table.field_names.index(col_name)+3] += \
1397
+ ':Default->%s' % value
1398
+ end
1399
+ end
1400
+
1401
+ if line[0..0] == 'Z'
1402
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1403
+ "\n")
1404
+ else
1405
+ new_fptr.write(header_rec.join('|') + "\n")
1406
+ end
1407
+
1408
+ begin
1409
+ while true
1410
+ new_fptr.write(fptr.readline)
1411
+ end
1412
+ # Here's how we break out of the loop...
1413
+ rescue EOFError
1414
+ end
1415
+
1416
+ # Close the table and release the write lock.
1417
+ fptr.close
1418
+ new_fptr.close
1419
+ File.delete(table.filename)
1420
+ FileUtils.mv(table.filename+'temp', table.filename)
1421
+ end
1422
+ end
1423
+
1424
+ #-----------------------------------------------------------------------
1425
+ # change_column_required
1426
+ #-----------------------------------------------------------------------
1427
+ def change_column_required(table, col_name, required)
1428
+ with_write_lock(table.name) do
1429
+ fptr = open(table.filename, 'r')
1430
+ new_fptr = open(table.filename+'temp', 'w')
1431
+
1432
+ line = fptr.readline.chomp
1433
+
1434
+ if line[0..0] == 'Z'
1435
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1436
+ else
1437
+ header_rec = line.split('|')
1438
+ end
1439
+
1440
+ if header_rec[table.field_names.index(col_name)+3
1441
+ ] =~ /Required->/
1442
+ hr_chunks = \
1443
+ header_rec[table.field_names.index(col_name)+3].split(':')
1444
+ if not required
1445
+ hr_chunks = hr_chunks.delete_if {|x| x =~ /Required->/}
1446
+ header_rec[table.field_names.index(col_name)+3] = \
1447
+ hr_chunks.join(':')
1448
+ else
1449
+ hr_chunks.collect! do |x|
1450
+ if x =~ /Required->/
1451
+ 'Default->%s' % required
1452
+ else
1453
+ x
1454
+ end
1455
+ end
1456
+ header_rec[table.field_names.index(col_name)+3] = \
1457
+ hr_chunks.join(':')
1458
+ end
1459
+ else
1460
+ if not required
1461
+ else
1462
+ header_rec[table.field_names.index(col_name)+3] += \
1463
+ ':Required->%s' % required
1464
+ end
1465
+ end
1466
+
1467
+ if line[0..0] == 'Z'
1468
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1469
+ "\n")
1470
+ else
1471
+ new_fptr.write(header_rec.join('|') + "\n")
1472
+ end
1473
+
1474
+ begin
1475
+ while true
1476
+ new_fptr.write(fptr.readline)
1477
+ end
1478
+ # Here's how we break out of the loop...
1479
+ rescue EOFError
1480
+ end
1481
+
1482
+ # Close the table and release the write lock.
1483
+ fptr.close
1484
+ new_fptr.close
1485
+ File.delete(table.filename)
1486
+ FileUtils.mv(table.filename+'temp', table.filename)
1487
+ end
1488
+ end
1489
+
1490
+ #-----------------------------------------------------------------------
1491
+ # pack_table
1492
+ #-----------------------------------------------------------------------
1493
+ def pack_table(table)
1494
+ with_write_lock(table.name) do
1495
+ fptr = open(table.filename, 'r')
1496
+ new_fptr = open(table.filename+'temp', 'w')
1497
+
1498
+ line = fptr.readline.chomp
1499
+ # Reset the delete counter in the header rec to 0.
1500
+ if line[0..0] == 'Z'
1501
+ header_rec = unencrypt_str(line[1..-1]).split('|')
1502
+ header_rec[1] = '000000'
1503
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
1504
+ "\n")
1505
+ else
1506
+ header_rec = line.split('|')
1507
+ header_rec[1] = '000000'
1508
+ new_fptr.write(header_rec.join('|') + "\n")
1509
+ end
1510
+
1511
+ lines_deleted = 0
1512
+
1513
+ begin
1514
+ while true
1515
+ line = fptr.readline
1516
+
1517
+ if table.encrypted?
1518
+ temp_line = unencrypt_str(line)
1519
+ else
1520
+ temp_line = line
1521
+ end
1522
+
1523
+ if temp_line.strip == ''
1524
+ lines_deleted += 1
1525
+ else
1526
+ new_fptr.write(line)
1527
+ end
1528
+ end
1529
+ # Here's how we break out of the loop...
1530
+ rescue EOFError
1531
+ end
1532
+
1533
+ # Close the table and release the write lock.
1534
+ fptr.close
1535
+ new_fptr.close
1536
+ File.delete(table.filename)
1537
+ FileUtils.mv(table.filename+'temp', table.filename)
1538
+
1539
+ # Return the number of deleted records that were removed.
1540
+ return lines_deleted
1541
+ end
1542
+ end
1543
+
1544
+ #-----------------------------------------------------------------------
1545
+ # read_memo_file
1546
+ #-----------------------------------------------------------------------
1547
+ def read_memo_file(filepath)
1548
+ begin
1549
+ f = File.new(File.join(@db.memo_blob_path, filepath))
1550
+ return f.read
1551
+ ensure
1552
+ f.close
1553
+ end
1554
+ end
1555
+
1556
+ #-----------------------------------------------------------------------
1557
+ # write_memo_file
1558
+ #-----------------------------------------------------------------------
1559
+ def write_memo_file(filepath, contents)
1560
+ begin
1561
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'w')
1562
+ f.write(contents)
1563
+ ensure
1564
+ f.close
1565
+ end
1566
+ end
1567
+
1568
+ #-----------------------------------------------------------------------
1569
+ # read_blob_file
1570
+ #-----------------------------------------------------------------------
1571
+ def read_blob_file(filepath)
1572
+ begin
1573
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'rb')
1574
+ return f.read
1575
+ ensure
1576
+ f.close
1577
+ end
1578
+ end
1579
+
1580
+ #-----------------------------------------------------------------------
1581
+ # write_blob_file
1582
+ #-----------------------------------------------------------------------
1583
+ def write_blob_file(filepath, contents)
1584
+ begin
1585
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'wb')
1586
+ f.write(contents)
1587
+ ensure
1588
+ f.close
1589
+ end
1590
+ end
1591
+
1592
+
1593
+ #-----------------------------------------------------------------------
1594
+ # PRIVATE METHODS
1595
+ #-----------------------------------------------------------------------
1596
+ private
1597
+
1598
+ #-----------------------------------------------------------------------
1599
+ # with_table
1600
+ #-----------------------------------------------------------------------
1601
+ def with_table(table, access='r')
1602
+ begin
1603
+ yield fptr = open(table.filename, access)
1604
+ ensure
1605
+ fptr.close
1606
+ end
1607
+ end
1608
+
1609
+ #-----------------------------------------------------------------------
1610
+ # with_write_lock
1611
+ #-----------------------------------------------------------------------
1612
+ def with_write_lock(tablename)
1613
+ begin
1614
+ write_lock(tablename) if @db.server?
1615
+ yield
1616
+ ensure
1617
+ write_unlock(tablename) if @db.server?
1618
+ end
1619
+ end
1620
+
1621
+ #-----------------------------------------------------------------------
1622
+ # with_write_locked_table
1623
+ #-----------------------------------------------------------------------
1624
+ def with_write_locked_table(table, access='r+')
1625
+ begin
1626
+ write_lock(table.name) if @db.server?
1627
+ yield fptr = open(table.filename, access)
1628
+ ensure
1629
+ fptr.close
1630
+ write_unlock(table.name) if @db.server?
1631
+ end
1632
+ end
1633
+
1634
+ #-----------------------------------------------------------------------
1635
+ # write_lock
1636
+ #-----------------------------------------------------------------------
1637
+ def write_lock(tablename)
1638
+ # Unless an key already exists in the hash holding mutex records
1639
+ # for this table, create a write key for this table in the mutex
1640
+ # hash. Then, place a lock on that mutex.
1641
+ @mutex_hash[tablename] = Mutex.new unless (
1642
+ @mutex_hash.has_key?(tablename))
1643
+ @mutex_hash[tablename].lock
1644
+
1645
+ return true
1646
+ end
1647
+
1648
+ #----------------------------------------------------------------------
1649
+ # write_unlock
1650
+ #----------------------------------------------------------------------
1651
+ def write_unlock(tablename)
1652
+ # Unlock the write mutex for this table.
1653
+ @mutex_hash[tablename].unlock
1654
+
1655
+ return true
1656
+ end
1657
+
1658
+ #----------------------------------------------------------------------
1659
+ # write_record
1660
+ #----------------------------------------------------------------------
1661
+ def write_record(table, fptr, pos, record)
1662
+ if table.encrypted?
1663
+ temp_rec = encrypt_str(record)
1664
+ else
1665
+ temp_rec = record
1666
+ end
1667
+
1668
+ # If record is to be appended, go to end of table and write
1669
+ # record, adding newline character.
1670
+ if pos == 'end'
1671
+ fptr.seek(0, IO::SEEK_END)
1672
+ fptr.write(temp_rec + "\n")
1673
+ else
1674
+ # Otherwise, overwrite another record (that's why we don't
1675
+ # add the newline character).
1676
+ fptr.seek(pos)
1677
+ fptr.write(temp_rec)
1678
+ end
1679
+ end
1680
+
1681
+ #----------------------------------------------------------------------
1682
+ # write_header_record
1683
+ #----------------------------------------------------------------------
1684
+ def write_header_record(table, fptr, record)
1685
+ fptr.seek(0)
1686
+
1687
+ if table.encrypted?
1688
+ fptr.write('Z' + encrypt_str(record) + "\n")
1689
+ else
1690
+ fptr.write(record + "\n")
1691
+ end
1692
+ end
1693
+
1694
+ #----------------------------------------------------------------------
1695
+ # get_header_record
1696
+ #----------------------------------------------------------------------
1697
+ def get_header_record(table, fptr)
1698
+ fptr.seek(0)
1699
+
1700
+ if table.encrypted?
1701
+ return unencrypt_str(fptr.readline[1..-1].chomp)
1702
+ else
1703
+ return fptr.readline.chomp
1704
+ end
1705
+ end
1706
+
1707
+ #-----------------------------------------------------------------------
1708
+ # incr_rec_no_ctr
1709
+ #-----------------------------------------------------------------------
1710
+ def incr_rec_no_ctr(table, fptr)
1711
+ last_rec_no, rest_of_line = get_header_record(table, fptr).split(
1712
+ '|', 2)
1713
+ last_rec_no = last_rec_no.to_i + 1
1714
+
1715
+ write_header_record(table, fptr, ['%06d' % last_rec_no,
1716
+ rest_of_line].join('|'))
1717
+
1718
+ # Return the new recno.
1719
+ return last_rec_no
1720
+ end
1721
+
1722
+ #-----------------------------------------------------------------------
1723
+ # incr_del_ctr
1724
+ #-----------------------------------------------------------------------
1725
+ def incr_del_ctr(table, fptr)
1726
+ last_rec_no, del_ctr, rest_of_line = get_header_record(table,
1727
+ fptr).split('|', 3)
1728
+ del_ctr = del_ctr.to_i + 1
1729
+
1730
+ write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
1731
+ rest_of_line].join('|'))
1732
+
1733
+ return true
1734
+ end
1735
+
1736
+ #-----------------------------------------------------------------------
1737
+ # encrypt_str
1738
+ #-----------------------------------------------------------------------
1739
+ def encrypt_str(s)
1740
+ # Returns an encrypted string, using the Vignere Cipher.
1741
+
1742
+ new_str = ''
1743
+ i_key = -1
1744
+ s.each_byte do |c|
1745
+ if i_key < EN_KEY_LEN - 1
1746
+ i_key += 1
1747
+ else
1748
+ i_key = 0
1749
+ end
1750
+
1751
+ if EN_STR.index(c.chr).nil?
1752
+ new_str << c.chr
1753
+ next
1754
+ end
1755
+
1756
+ i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
1757
+ i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
1758
+ new_str << EN_STR[i_from_str]
1759
+ end
1760
+ return new_str
1761
+ end
1762
+
1763
+ #-----------------------------------------------------------------------
1764
+ # unencrypt_str
1765
+ #-----------------------------------------------------------------------
1766
+ def unencrypt_str(s)
1767
+ # Returns an unencrypted string, using the Vignere Cipher.
1768
+
1769
+ new_str = ''
1770
+ i_key = -1
1771
+ s.each_byte do |c|
1772
+ if i_key < EN_KEY_LEN - 1
1773
+ i_key += 1
1774
+ else
1775
+ i_key = 0
1776
+ end
1777
+
1778
+ if EN_STR.index(c.chr).nil?
1779
+ new_str << c.chr
1780
+ next
1781
+ end
1782
+
1783
+ i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
1784
+ i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
1785
+ new_str << EN_STR[i_from_str]
1786
+ end
1787
+ return new_str
1788
+ end
1789
+ end
1790
+
1791
+
1792
+ #---------------------------------------------------------------------------
1793
+ # KBTable
1794
+ #---------------------------------------------------------------------------
1795
+ class KBTable
1796
+ include DRb::DRbUndumped
1797
+
1798
+ # Make constructor private. KBTable instances should only be created
1799
+ # from KirbyBase#get_table.
1800
+ private_class_method :new
1801
+
1802
+ VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1803
+ :DateTime, :Memo, :Blob, :ResultSet, :YAML]
1804
+ VALID_DEFAULT_TYPES = [:String, :Integer, :Float, :Boolean, :Date,
1805
+ :Time, :DateTime, :YAML]
1806
+ VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1807
+ :DateTime]
1808
+
1809
+ # Regular expression used to determine if field needs to be
1810
+ # encoded.
1811
+ ENCODE_RE = /&|\n|\r|\032|\|/
1812
+
1813
+ attr_reader :filename, :name, :table_class, :db, :lookup_key
1814
+
1815
+ #-----------------------------------------------------------------------
1816
+ # KBTable.valid_field_type
1817
+ #-----------------------------------------------------------------------
1818
+ #++
1819
+ # Return true if valid field type.
1820
+ #
1821
+ # *field_type*:: Symbol specifying field type.
1822
+ #
1823
+ def KBTable.valid_field_type?(field_type)
1824
+ VALID_FIELD_TYPES.include?(field_type)
1825
+ end
1826
+
1827
+ #-----------------------------------------------------------------------
1828
+ # KBTable.valid_default_type
1829
+ #-----------------------------------------------------------------------
1830
+ #++
1831
+ # Return true if valid default type.
1832
+ #
1833
+ # *field_type*:: Symbol specifying field type.
1834
+ #
1835
+ def KBTable.valid_default_type?(field_type)
1836
+ VALID_DEFAULT_TYPES.include?(field_type)
1837
+ end
1838
+
1839
+ #-----------------------------------------------------------------------
1840
+ # KBTable.valid_index_type
1841
+ #-----------------------------------------------------------------------
1842
+ #++
1843
+ # Return true if valid index type.
1844
+ #
1845
+ # *field_type*:: Symbol specifying field type.
1846
+ #
1847
+ def KBTable.valid_index_type?(field_type)
1848
+ VALID_INDEX_TYPES.include?(field_type)
1849
+ end
1850
+
1851
+ #-----------------------------------------------------------------------
1852
+ # KBTable.convert_to_string
1853
+ #-----------------------------------------------------------------------
1854
+ #++
1855
+ # Return value converted to String object.
1856
+ #
1857
+ # *data_type*:: Symbol specifying data type.
1858
+ # *value*:: Value to convert to String.
1859
+ #
1860
+ def KBTable.convert_to_string(data_type, value)
1861
+ case data_type
1862
+ when :YAML
1863
+ y = value.to_yaml
1864
+ if y =~ ENCODE_RE
1865
+ return y.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub(
1866
+ "\r", '&carriage_return;').gsub("\032", '&substitute;'
1867
+ ).gsub("|", '&pipe;')
1868
+ else
1869
+ return y
1870
+ end
1871
+ when :String
1872
+ if value =~ ENCODE_RE
1873
+ return value.gsub("&", '&amp;').gsub("\n", '&linefeed;'
1874
+ ).gsub("\r", '&carriage_return;').gsub("\032",
1875
+ '&substitute;').gsub("|", '&pipe;')
1876
+ else
1877
+ return value
1878
+ end
1879
+ when :Memo
1880
+ return value.filepath
1881
+ when :Blob
1882
+ return value.filepath
1883
+ else
1884
+ return value.to_s
1885
+ end
1886
+ end
1887
+
1888
+ #-----------------------------------------------------------------------
1889
+ # create_called_from_database_instance
1890
+ #-----------------------------------------------------------------------
1891
+ #++
1892
+ # Return a new instance of KBTable. Should never be called directly by
1893
+ # your application. Should only be called from KirbyBase#get_table.
1894
+ #
1895
+ def KBTable.create_called_from_database_instance(db, name, filename)
1896
+ return new(db, name, filename)
1897
+ end
1898
+
1899
+ #-----------------------------------------------------------------------
1900
+ # initialize
1901
+ #-----------------------------------------------------------------------
1902
+ #++
1903
+ # This has been declared private so user's cannot create new instances
1904
+ # of KBTable from their application. A user gets a handle to a KBTable
1905
+ # instance by calling KirbyBase#get_table for an existing table or
1906
+ # KirbyBase.create_table for a new table.
1907
+ #
1908
+ def initialize(db, name, filename)
1909
+ @db = db
1910
+ @name = name
1911
+ @filename = filename
1912
+ @encrypted = false
1913
+ @lookup_key = :recno
1914
+ @idx_timestamps = {}
1915
+ @idx_arrs = {}
1916
+
1917
+ # Alias delete_all to clear method.
1918
+ alias delete_all clear
1919
+
1920
+ update_header_vars
1921
+ create_indexes
1922
+ create_table_class unless @db.server?
1923
+ end
1924
+
1925
+ #-----------------------------------------------------------------------
1926
+ # encrypted?
1927
+ #-----------------------------------------------------------------------
1928
+ #++
1929
+ # Returns true if table is encrypted.
1930
+ #
1931
+ def encrypted?
1932
+ if @encrypted
1933
+ return true
1934
+ else
1935
+ return false
1936
+ end
1937
+ end
1938
+
1939
+ #-----------------------------------------------------------------------
1940
+ # field_names
1941
+ #-----------------------------------------------------------------------
1942
+ #++
1943
+ # Return array containing table field names.
1944
+ #
1945
+ def field_names
1946
+ return @field_names
1947
+ end
1948
+
1949
+ #-----------------------------------------------------------------------
1950
+ # field_types
1951
+ #-----------------------------------------------------------------------
1952
+ #++
1953
+ # Return array containing table field types.
1954
+ #
1955
+ def field_types
1956
+ return @field_types
1957
+ end
1958
+
1959
+ #-----------------------------------------------------------------------
1960
+ # field_extras
1961
+ #-----------------------------------------------------------------------
1962
+ #++
1963
+ # Return array containing table field extras.
1964
+ #
1965
+ def field_extras
1966
+ return @field_extras
1967
+ end
1968
+
1969
+ #-----------------------------------------------------------------------
1970
+ # field_indexes
1971
+ #-----------------------------------------------------------------------
1972
+ #++
1973
+ # Return array containing table field indexes.
1974
+ #
1975
+ def field_indexes
1976
+ return @field_indexes
1977
+ end
1978
+
1979
+ #-----------------------------------------------------------------------
1980
+ # field_defaults
1981
+ #-----------------------------------------------------------------------
1982
+ #++
1983
+ # Return array containing table field defaults.
1984
+ #
1985
+ def field_defaults
1986
+ return @field_defaults
1987
+ end
1988
+
1989
+ #-----------------------------------------------------------------------
1990
+ # field_requireds
1991
+ #-----------------------------------------------------------------------
1992
+ #++
1993
+ # Return array containing table field requireds.
1994
+ #
1995
+ def field_requireds
1996
+ return @field_requireds
1997
+ end
1998
+
1999
+ #-----------------------------------------------------------------------
2000
+ # insert
2001
+ #-----------------------------------------------------------------------
2002
+ #++
2003
+ # Insert a new record into a table, return unique record number.
2004
+ #
2005
+ # *data*:: Array, Hash, Struct instance containing field values of
2006
+ # new record.
2007
+ # *insert_proc*:: Proc instance containing insert code. This and the
2008
+ # data parameter are mutually exclusive.
2009
+ #
2010
+ def insert(*data, &insert_proc)
2011
+ raise 'Cannot specify both a hash/array/struct and a ' + \
2012
+ 'proc for method #insert!' unless data.empty? or insert_proc.nil?
2013
+
2014
+ raise 'Must specify either hash/array/struct or insert ' + \
2015
+ 'proc for method #insert!' if data.empty? and insert_proc.nil?
2016
+
2017
+ # Update the header variables.
2018
+ update_header_vars
2019
+
2020
+ # Convert input, which could be an array, a hash, or a Struct
2021
+ # into a common format (i.e. hash).
2022
+ if data.empty?
2023
+ input_rec = convert_input_data(insert_proc)
2024
+ else
2025
+ input_rec = convert_input_data(data)
2026
+ end
2027
+
2028
+ # Check the field values to make sure they are proper types.
2029
+ validate_input(input_rec)
2030
+
2031
+ if @field_types.include?(:Memo)
2032
+ input_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
2033
+ end
2034
+
2035
+ if @field_types.include?(:Blob)
2036
+ input_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
2037
+ end
2038
+
2039
+
2040
+
2041
+ return @db.engine.insert_record(self, @field_names.zip(@field_types,
2042
+ @field_defaults).collect do |fn, ft, fd|
2043
+ if input_rec.has_key?(fn)
2044
+ if input_rec[fn].nil?
2045
+ if fd.nil?
2046
+ ''
2047
+ else
2048
+ KBTable.convert_to_string(ft, fd)
2049
+ end
2050
+ else
2051
+ KBTable.convert_to_string(ft, input_rec[fn])
2052
+ end
2053
+ else
2054
+ if fd.nil?
2055
+ ''
2056
+ else
2057
+ KBTable.convert_to_string(ft, fd)
2058
+ end
2059
+ end
2060
+ end)
2061
+ end
2062
+
2063
+ #-----------------------------------------------------------------------
2064
+ # update_all
2065
+ #-----------------------------------------------------------------------
2066
+ #++
2067
+ # Return array of records (Structs) to be updated, in this case all
2068
+ # records.
2069
+ #
2070
+ # *updates*:: Hash or Struct containing updates.
2071
+ #
2072
+ def update_all(*updates)
2073
+ update(*updates) { true }
2074
+ end
2075
+
2076
+ #-----------------------------------------------------------------------
2077
+ # update
2078
+ #-----------------------------------------------------------------------
2079
+ #++
2080
+ # Return array of records (Structs) to be updated based on select cond.
2081
+ #
2082
+ # *updates*:: Hash or Struct containing updates.
2083
+ # *select_cond*:: Proc containing code to select records to update.
2084
+ #
2085
+ def update(*updates, &select_cond)
2086
+ raise ArgumentError, "Must specify select condition code " + \
2087
+ "block. To update all records, use #update_all instead." if \
2088
+ select_cond.nil?
2089
+
2090
+ # Update the header variables.
2091
+ update_header_vars
2092
+
2093
+ # Get all records that match the selection criteria and
2094
+ # return them in an array.
2095
+ result_set = get_matches(:update, @field_names, select_cond)
2096
+
2097
+ return result_set if updates.empty?
2098
+
2099
+ set(result_set, updates)
2100
+ end
2101
+
2102
+ #-----------------------------------------------------------------------
2103
+ # []=
2104
+ #-----------------------------------------------------------------------
2105
+ #++
2106
+ # Update record whose recno field equals index.
2107
+ #
2108
+ # *index*:: Integer specifying recno you wish to select.
2109
+ # *updates*:: Hash, Struct, or Array containing updates.
2110
+ #
2111
+ def []=(index, updates)
2112
+ return update(updates) { |r| r.recno == index }
2113
+ end
2114
+
2115
+ #-----------------------------------------------------------------------
2116
+ # set
2117
+ #-----------------------------------------------------------------------
2118
+ #++
2119
+ # Set fields of records to updated values. Returns number of records
2120
+ # updated.
2121
+ #
2122
+ # *recs*:: Array of records (Structs) that will be updated.
2123
+ # *data*:: Hash, Struct, Proc containing updates.
2124
+ #
2125
+ def set(recs, data)
2126
+ # Convert updates, which could be an array, a hash, or a Struct
2127
+ # into a common format (i.e. hash).
2128
+ update_rec = convert_input_data(data)
2129
+
2130
+ # Make sure all of the fields of the update rec are of the proper
2131
+ # type.
2132
+ validate_input(update_rec)
2133
+
2134
+ if @field_types.include?(:Memo)
2135
+ update_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
2136
+ end
2137
+
2138
+ if @field_types.include?(:Blob)
2139
+ update_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
2140
+ end
2141
+
2142
+ updated_recs = []
2143
+
2144
+ # For each one of the recs that matched the update query, apply the
2145
+ # updates to it and write it back to the database table.
2146
+ recs.each do |rec|
2147
+ updated_rec = {}
2148
+ updated_rec[:rec] = \
2149
+ @field_names.zip(@field_types).collect do |fn, ft|
2150
+ KBTable.convert_to_string(ft,
2151
+ update_rec.fetch(fn, rec.send(fn)))
2152
+ end
2153
+ updated_rec[:fpos] = rec.fpos
2154
+ updated_rec[:line_length] = rec.line_length
2155
+ updated_recs << updated_rec
2156
+ end
2157
+ @db.engine.update_records(self, updated_recs)
2158
+
2159
+ # Return the number of records updated.
2160
+ return recs.size
2161
+ end
2162
+
2163
+ #-----------------------------------------------------------------------
2164
+ # delete
2165
+ #-----------------------------------------------------------------------
2166
+ #++
2167
+ # Delete records from table and return # deleted.
2168
+ #
2169
+ # *select_cond*:: Proc containing code to select records.
2170
+ #
2171
+ def delete(&select_cond)
2172
+ raise ArgumentError, 'Must specify select condition code ' + \
2173
+ 'block. To delete all records, use #clear instead.' if \
2174
+ select_cond.nil?
2175
+
2176
+ # Get all records that match the selection criteria and
2177
+ # return them in an array.
2178
+ result_set = get_matches(:delete, [:recno], select_cond)
2179
+
2180
+ @db.engine.delete_records(self, result_set)
2181
+
2182
+ # Return the number of records deleted.
2183
+ return result_set.size
2184
+ end
2185
+
2186
+ #-----------------------------------------------------------------------
2187
+ # clear
2188
+ #-----------------------------------------------------------------------
2189
+ #++
2190
+ # Delete all records from table. You can also use #delete_all.
2191
+ #
2192
+ # *reset_recno_ctr*:: true/false specifying whether recno counter should
2193
+ # be reset to 0.
2194
+ #
2195
+ def clear(reset_recno_ctr=true)
2196
+ delete { true }
2197
+ pack
2198
+
2199
+ @db.engine.reset_recno_ctr(self) if reset_recno_ctr
2200
+ end
2201
+
2202
+ #-----------------------------------------------------------------------
2203
+ # []
2204
+ #-----------------------------------------------------------------------
2205
+ #++
2206
+ # Return the record(s) whose recno field is included in index.
2207
+ #
2208
+ # *index*:: Array of Integer(s) specifying recno(s) you wish to select.
2209
+ #
2210
+ def [](*index)
2211
+ return nil if index[0].nil?
2212
+
2213
+ return get_match_by_recno(:select, @field_names, index[0]) if \
2214
+ index.size == 1
2215
+
2216
+ recs = select_by_recno_index(*@field_names) { |r|
2217
+ index.include?(r.recno)
2218
+ }
2219
+
2220
+ return recs
2221
+ end
2222
+
2223
+ #-----------------------------------------------------------------------
2224
+ # select
2225
+ #-----------------------------------------------------------------------
2226
+ #++
2227
+ # Return array of records (Structs) matching select conditions.
2228
+ #
2229
+ # *filter*:: List of field names (Symbols) to include in result set.
2230
+ # *select_cond*:: Proc containing select code.
2231
+ #
2232
+ def select(*filter, &select_cond)
2233
+ # Declare these variables before the code block so they don't go
2234
+ # after the code block is done.
2235
+ result_set = []
2236
+
2237
+ # Validate that all names in filter are valid field names.
2238
+ validate_filter(filter)
2239
+
2240
+ filter = @field_names if filter.empty?
2241
+
2242
+ # Get all records that match the selection criteria and
2243
+ # return them in an array of Struct instances.
2244
+ return get_matches(:select, filter, select_cond)
2245
+ end
2246
+
2247
+ #-----------------------------------------------------------------------
2248
+ # select_by_recno_index
2249
+ #-----------------------------------------------------------------------
2250
+ #++
2251
+ # Return array of records (Structs) matching select conditions. Select
2252
+ # condition block should not contain references to any table column
2253
+ # except :recno. If you need to select by other table columns than just
2254
+ # :recno, use #select instead.
2255
+ #
2256
+ # *filter*:: List of field names (Symbols) to include in result set.
2257
+ # *select_cond*:: Proc containing select code.
2258
+ #
2259
+ def select_by_recno_index(*filter, &select_cond)
2260
+ # Declare these variables before the code block so they don't go
2261
+ # after the code block is done.
2262
+ result_set = []
2263
+
2264
+ # Validate that all names in filter are valid field names.
2265
+ validate_filter(filter)
2266
+
2267
+ filter = @field_names if filter.empty?
2268
+
2269
+ # Get all records that match the selection criteria and
2270
+ # return them in an array of Struct instances.
2271
+ return get_matches_by_recno_index(:select, filter, select_cond)
2272
+ end
2273
+
2274
+ #-----------------------------------------------------------------------
2275
+ # pack
2276
+ #-----------------------------------------------------------------------
2277
+ #++
2278
+ # Remove blank records from table, return total removed.
2279
+ #
2280
+ def pack
2281
+ lines_deleted = @db.engine.pack_table(self)
2282
+
2283
+ update_header_vars
2284
+
2285
+ @db.engine.remove_recno_index(@name)
2286
+ @db.engine.remove_indexes(@name)
2287
+ create_indexes
2288
+ create_table_class unless @db.server?
2289
+
2290
+ return lines_deleted
2291
+ end
2292
+
2293
+ #-----------------------------------------------------------------------
2294
+ # rename_column
2295
+ #-----------------------------------------------------------------------
2296
+ #++
2297
+ # Rename a column.
2298
+ #
2299
+ # Make sure you are executing this method while in single-user mode
2300
+ # (i.e. not running in client/server mode).
2301
+ #
2302
+ # *old_col_name*:: Symbol of old column name.
2303
+ # *new_col_name*:: Symbol of new column name.
2304
+ #
2305
+ def rename_column(old_col_name, new_col_name)
2306
+ raise "Do not execute this method in client/server mode!" if \
2307
+ @db.client?
2308
+
2309
+ raise "Cannot rename recno column!" if old_col_name == :recno
2310
+ raise "Cannot give column name of recno!" if new_col_name == :recno
2311
+
2312
+ raise 'Invalid column name to rename: ' % old_col_name unless \
2313
+ @field_names.include?(old_col_name)
2314
+
2315
+ raise 'New column name already exists: ' % new_col_name if \
2316
+ @field_names.include?(new_col_name)
2317
+
2318
+ @db.engine.rename_column(self, old_col_name, new_col_name)
2319
+
2320
+ # Need to reinitialize the table instance and associated indexes.
2321
+ @db.engine.remove_recno_index(@name)
2322
+ @db.engine.remove_indexes(@name)
2323
+
2324
+ update_header_vars
2325
+ create_indexes
2326
+ create_table_class unless @db.server?
2327
+ end
2328
+
2329
+ #-----------------------------------------------------------------------
2330
+ # change_column_type
2331
+ #-----------------------------------------------------------------------
2332
+ #++
2333
+ # Change a column's type.
2334
+ #
2335
+ # Make sure you are executing this method while in single-user mode
2336
+ # (i.e. not running in client/server mode).
2337
+ #
2338
+ # *col_name*:: Symbol of column name.
2339
+ # *col_type*:: Symbol of new column type.
2340
+ #
2341
+ def change_column_type(col_name, col_type)
2342
+ raise "Do not execute this method in client/server mode!" if \
2343
+ @db.client?
2344
+
2345
+ raise "Cannot change type for recno column!" if col_name == :recno
2346
+ raise 'Invalid column name: ' % col_name unless \
2347
+ @field_names.include?(col_name)
2348
+
2349
+ raise 'Invalid field type: %s' % col_type unless \
2350
+ KBTable.valid_field_type?(col_type)
2351
+
2352
+ @db.engine.change_column_type(self, col_name, col_type)
2353
+
2354
+ # Need to reinitialize the table instance and associated indexes.
2355
+ @db.engine.remove_recno_index(@name)
2356
+ @db.engine.remove_indexes(@name)
2357
+
2358
+ update_header_vars
2359
+ create_indexes
2360
+ create_table_class unless @db.server?
2361
+ end
2362
+
2363
+ #-----------------------------------------------------------------------
2364
+ # add_column
2365
+ #-----------------------------------------------------------------------
2366
+ #++
2367
+ # Add a column to table.
2368
+ #
2369
+ # Make sure you are executing this method while in single-user mode
2370
+ # (i.e. not running in client/server mode).
2371
+ #
2372
+ # *col_name*:: Symbol of column name to add.
2373
+ # *col_type*:: Symbol (or Hash if includes field extras) of column type
2374
+ # to add.
2375
+ # *after*:: Symbol of column name that you want to add this column
2376
+ # after.
2377
+ #
2378
+ def add_column(col_name, col_type, after=nil)
2379
+ raise "Do not execute this method in client/server mode!" if \
2380
+ @db.client?
2381
+
2382
+ raise "Invalid column name in 'after': #{after}" unless after.nil? \
2383
+ or @field_names.include?(after)
2384
+
2385
+ raise "Invalid column name in 'after': #{after}" if after == :recno
2386
+
2387
+ raise "Column name cannot be recno!" if col_name == :recno
2388
+
2389
+ # Does this new column have field extras (i.e. Index, Lookup, etc.)
2390
+ if col_type.is_a?(Hash)
2391
+ temp_type = col_type[:DataType]
2392
+ else
2393
+ temp_type = col_type
2394
+ end
2395
+
2396
+ raise 'Invalid field type: %s' % temp_type unless \
2397
+ KBTable.valid_field_type?(temp_type)
2398
+
2399
+ @db.engine.add_column(self, col_name, col_type, after)
2400
+
2401
+ # Need to reinitialize the table instance and associated indexes.
2402
+ @db.engine.remove_recno_index(@name)
2403
+ @db.engine.remove_indexes(@name)
2404
+
2405
+ update_header_vars
2406
+ create_indexes
2407
+ create_table_class unless @db.server?
2408
+ end
2409
+
2410
+ #-----------------------------------------------------------------------
2411
+ # drop_column
2412
+ #-----------------------------------------------------------------------
2413
+ #++
2414
+ # Drop a column from table.
2415
+ #
2416
+ # Make sure you are executing this method while in single-user mode
2417
+ # (i.e. not running in client/server mode).
2418
+ #
2419
+ # *col_name*:: Symbol of column name to add.
2420
+ #
2421
+ def drop_column(col_name)
2422
+ raise "Do not execute this method in client/server mode!" if \
2423
+ @db.client?
2424
+
2425
+ raise 'Invalid column name: ' % col_name unless \
2426
+ @field_names.include?(col_name)
2427
+
2428
+ raise "Cannot drop :recno column!" if col_name == :recno
2429
+
2430
+ @db.engine.drop_column(self, col_name)
2431
+
2432
+ # Need to reinitialize the table instance and associated indexes.
2433
+ @db.engine.remove_recno_index(@name)
2434
+ @db.engine.remove_indexes(@name)
2435
+
2436
+ update_header_vars
2437
+ create_indexes
2438
+ create_table_class unless @db.server?
2439
+ end
2440
+
2441
+ #-----------------------------------------------------------------------
2442
+ # add_index
2443
+ #-----------------------------------------------------------------------
2444
+ #++
2445
+ # Add an index to a column.
2446
+ #
2447
+ # Make sure you are executing this method while in single-user mode
2448
+ # (i.e. not running in client/server mode).
2449
+ #
2450
+ # *col_names*:: Array containing column name(s) of new index.
2451
+ #
2452
+ def add_index(*col_names)
2453
+ raise "Do not execute this method in client/server mode!" if \
2454
+ @db.client?
2455
+
2456
+ col_names.each do |c|
2457
+ raise "Invalid column name: #{c}" unless \
2458
+ @field_names.include?(c)
2459
+
2460
+ raise "recno column cannot be indexed!" if c == :recno
2461
+
2462
+ raise "Column already indexed: #{c}" unless \
2463
+ @field_indexes[@field_names.index(c)].nil?
2464
+ end
2465
+
2466
+ last_index_no_used = 0
2467
+ @field_indexes.each do |i|
2468
+ next if i.nil?
2469
+ index_no = i[-1..-1].to_i
2470
+ last_index_no_used = index_no if index_no > last_index_no_used
2471
+ end
2472
+
2473
+ @db.engine.add_index(self, col_names, last_index_no_used+1)
2474
+
2475
+ # Need to reinitialize the table instance and associated indexes.
2476
+ @db.engine.remove_recno_index(@name)
2477
+ @db.engine.remove_indexes(@name)
2478
+
2479
+ update_header_vars
2480
+ create_indexes
2481
+ create_table_class unless @db.server?
2482
+ end
2483
+
2484
+ #-----------------------------------------------------------------------
2485
+ # drop_index
2486
+ #-----------------------------------------------------------------------
2487
+ #++
2488
+ # Drop an index on a column(s).
2489
+ #
2490
+ # Make sure you are executing this method while in single-user mode
2491
+ # (i.e. not running in client/server mode).
2492
+ #
2493
+ # *col_names*:: Array containing column name(s) of new index.
2494
+ #
2495
+ def drop_index(*col_names)
2496
+ raise "Do not execute this method in client/server mode!" if \
2497
+ @db.client?
2498
+
2499
+ col_names.each do |c|
2500
+ raise "Invalid column name: #{c}" unless \
2501
+ @field_names.include?(c)
2502
+
2503
+ raise "recno column index cannot be dropped!" if c == :recno
2504
+
2505
+ raise "Column not indexed: #{c}" if \
2506
+ @field_indexes[@field_names.index(c)].nil?
2507
+ end
2508
+
2509
+ @db.engine.drop_index(self, col_names)
2510
+
2511
+ # Need to reinitialize the table instance and associated indexes.
2512
+ @db.engine.remove_recno_index(@name)
2513
+ @db.engine.remove_indexes(@name)
2514
+
2515
+ update_header_vars
2516
+ create_indexes
2517
+ create_table_class unless @db.server?
2518
+ end
2519
+
2520
+ #-----------------------------------------------------------------------
2521
+ # change_column_default_value
2522
+ #-----------------------------------------------------------------------
2523
+ #++
2524
+ # Change a column's default value.
2525
+ #
2526
+ # Make sure you are executing this method while in single-user mode
2527
+ # (i.e. not running in client/server mode).
2528
+ #
2529
+ # *col_name*:: Symbol of column name.
2530
+ # *value*:: New default value for column.
2531
+ #
2532
+ def change_column_default_value(col_name, value)
2533
+ raise "Do not execute this method in client/server mode!" if \
2534
+ @db.client?
2535
+
2536
+ raise ":recno cannot have a default value!" if col_name == :recno
2537
+
2538
+ raise 'Invalid column name: ' % col_name unless \
2539
+ @field_names.include?(col_name)
2540
+
2541
+ raise 'Cannot set default value for this type: ' + \
2542
+ '%s' % @field_types.index(col_name) unless \
2543
+ KBTable.valid_default_type?(
2544
+ @field_types[@field_names.index(col_name)])
2545
+
2546
+ if value.nil?
2547
+ @db.engine.change_column_default_value(self, col_name, nil)
2548
+ else
2549
+ @db.engine.change_column_default_value(self, col_name,
2550
+ KBTable.convert_to_string(
2551
+ @field_types[@field_names.index(col_name)], value))
2552
+ end
2553
+
2554
+ # Need to reinitialize the table instance and associated indexes.
2555
+ @db.engine.remove_recno_index(@name)
2556
+ @db.engine.remove_indexes(@name)
2557
+
2558
+ update_header_vars
2559
+ create_indexes
2560
+ create_table_class unless @db.server?
2561
+ end
2562
+
2563
+ #-----------------------------------------------------------------------
2564
+ # change_column_required
2565
+ #-----------------------------------------------------------------------
2566
+ #++
2567
+ # Change whether a column is required.
2568
+ #
2569
+ # Make sure you are executing this method while in single-user mode
2570
+ # (i.e. not running in client/server mode).
2571
+ #
2572
+ # *col_name*:: Symbol of column name.
2573
+ # *required*:: true or false.
2574
+ #
2575
+ def change_column_required(col_name, required)
2576
+ raise "Do not execute this method in client/server mode!" if \
2577
+ @db.client?
2578
+
2579
+ raise ":recno is always required!" if col_name == :recno
2580
+
2581
+ raise 'Invalid column name: ' % col_name unless \
2582
+ @field_names.include?(col_name)
2583
+
2584
+ raise 'Required must be either true or false!' unless \
2585
+ [true, false].include?(required)
2586
+
2587
+ @db.engine.change_column_required(self, col_name, required)
2588
+
2589
+ # Need to reinitialize the table instance and associated indexes.
2590
+ @db.engine.remove_recno_index(@name)
2591
+ @db.engine.remove_indexes(@name)
2592
+
2593
+ update_header_vars
2594
+ create_indexes
2595
+ create_table_class unless @db.server?
2596
+ end
2597
+
2598
+ #-----------------------------------------------------------------------
2599
+ # total_recs
2600
+ #-----------------------------------------------------------------------
2601
+ #++
2602
+ # Return total number of undeleted (blank) records in table.
2603
+ #
2604
+ def total_recs
2605
+ return @db.engine.get_total_recs(self)
2606
+ end
2607
+
2608
+ #-----------------------------------------------------------------------
2609
+ # import_csv
2610
+ #-----------------------------------------------------------------------
2611
+ #++
2612
+ # Import csv file into table.
2613
+ #
2614
+ # *csv_filename*:: filename of csv file to import.
2615
+ #
2616
+ def import_csv(csv_filename)
2617
+ records_inserted = 0
2618
+ tbl_rec = @table_class.new(self)
2619
+
2620
+ CSV.open(csv_filename, 'r') do |row|
2621
+ tbl_rec.populate([nil] + row)
2622
+ insert(tbl_rec)
2623
+ records_inserted += 1
2624
+ end
2625
+ return records_inserted
2626
+ end
2627
+
2628
+ #-----------------------------------------------------------------------
2629
+ # PRIVATE METHODS
2630
+ #-----------------------------------------------------------------------
2631
+ private
2632
+
2633
+ #-----------------------------------------------------------------------
2634
+ # create_indexes
2635
+ #-----------------------------------------------------------------------
2636
+ def create_indexes
2637
+ # First remove any existing select_by_index methods. This is in
2638
+ # case we are dropping an index or a column. We want to make sure
2639
+ # an select_by_index method doesn't hang around if it's index or
2640
+ # column has been dropped.
2641
+ methods.each do |m|
2642
+ next if m == 'select_by_recno_index'
2643
+
2644
+ if m =~ /select_by_.*_index/
2645
+ class << self; self end.send(:remove_method, m.to_sym)
2646
+ end
2647
+ end
2648
+
2649
+ # Create the recno index. A recno index always gets created even if
2650
+ # there are no user-defined indexes for the table.
2651
+ @db.engine.init_recno_index(self)
2652
+
2653
+ # There can be up to 5 different indexes on a table. Any of these
2654
+ # indexes can be single or compound.
2655
+ ['Index->1', 'Index->2', 'Index->3', 'Index->4',
2656
+ 'Index->5'].each do |idx|
2657
+ index_col_names = []
2658
+ @field_indexes.each_with_index do |fi,i|
2659
+ next if fi.nil?
2660
+ index_col_names << @field_names[i] if fi.include?(idx)
2661
+ end
2662
+
2663
+ # If no fields were indexed on this number (1..5), go to the
2664
+ # next index number.
2665
+ next if index_col_names.empty?
2666
+
2667
+ # Create this index on the engine.
2668
+ @db.engine.init_index(self, index_col_names)
2669
+
2670
+ # For each index found, add an instance method for it so that
2671
+ # it can be used for #selects.
2672
+ select_meth_str = <<-END_OF_STRING
2673
+ def select_by_#{index_col_names.join('_')}_index(*filter,
2674
+ &select_cond)
2675
+ result_set = []
2676
+ validate_filter(filter)
2677
+ filter = @field_names if filter.empty?
2678
+ return get_matches_by_index(:select,
2679
+ [:#{index_col_names.join(',:')}], filter, select_cond)
2680
+ end
2681
+ END_OF_STRING
2682
+
2683
+ instance_eval(select_meth_str) unless @db.server?
2684
+
2685
+ @idx_timestamps[index_col_names.join('_')] = nil
2686
+ @idx_arrs[index_col_names.join('_')] = nil
2687
+ end
2688
+ end
2689
+
2690
+ #-----------------------------------------------------------------------
2691
+ # create_table_class
2692
+ #-----------------------------------------------------------------------
2693
+ def create_table_class
2694
+ #This is the class that will be used in #select condition blocks.
2695
+ @table_class = Class.new(KBTableRec)
2696
+
2697
+ get_meth_str = ''
2698
+ get_meth_upd_res_str = ''
2699
+ set_meth_str = ''
2700
+
2701
+ @field_names.zip(@field_types, @field_extras) do |x|
2702
+ field_name, field_type, field_extra = x
2703
+
2704
+ @lookup_key = field_name if field_extra.has_key?('Key')
2705
+
2706
+ # These are the default get/set methods for the table column.
2707
+ get_meth_str = <<-END_OF_STRING
2708
+ def #{field_name}
2709
+ return @#{field_name}
2710
+ end
2711
+ END_OF_STRING
2712
+ get_meth_upd_res_str = <<-END_OF_STRING
2713
+ def #{field_name}_upd_res
2714
+ return @#{field_name}
2715
+ end
2716
+ END_OF_STRING
2717
+ set_meth_str = <<-END_OF_STRING
2718
+ def #{field_name}=(s)
2719
+ @#{field_name} = convert_to(:#{field_type}, s)
2720
+ end
2721
+ END_OF_STRING
2722
+
2723
+ # If this is a Lookup field, modify the get_method.
2724
+ if field_extra.has_key?('Lookup')
2725
+ lookup_table, key_field = field_extra['Lookup'].split('.')
2726
+ if key_field == 'recno'
2727
+ get_meth_str = <<-END_OF_STRING
2728
+ def #{field_name}
2729
+ table = @tbl.db.get_table(:#{lookup_table})
2730
+ return table[@#{field_name}]
2731
+ end
2732
+ END_OF_STRING
2733
+ else
2734
+ begin
2735
+ unless @db.get_table(lookup_table.to_sym
2736
+ ).respond_to?('select_by_%s_index' % key_field)
2737
+ raise RuntimeError
2738
+ end
2739
+
2740
+ get_meth_str = <<-END_OF_STRING
2741
+ def #{field_name}
2742
+ table = @tbl.db.get_table(:#{lookup_table})
2743
+ return table.select_by_#{key_field}_index { |r|
2744
+ r.#{key_field} == @#{field_name} }.first
2745
+ end
2746
+ END_OF_STRING
2747
+ rescue RuntimeError
2748
+ get_meth_str = <<-END_OF_STRING
2749
+ def #{field_name}
2750
+ table = @tbl.db.get_table(:#{lookup_table})
2751
+ return table.select { |r|
2752
+ r.#{key_field} == @#{field_name} }.first
2753
+ end
2754
+ END_OF_STRING
2755
+ end
2756
+ end
2757
+ end
2758
+
2759
+ # If this is a Link_many field, modify the get/set methods.
2760
+ if field_extra.has_key?('Link_many')
2761
+ lookup_field, rest = field_extra['Link_many'].split('=')
2762
+ link_table, link_field = rest.split('.')
2763
+
2764
+ begin
2765
+ unless @db.get_table(link_table.to_sym).respond_to?(
2766
+ 'select_by_%s_index' % link_field)
2767
+ raise RuntimeError
2768
+ end
2769
+
2770
+ get_meth_str = <<-END_OF_STRING
2771
+ def #{field_name}
2772
+ table = @tbl.db.get_table(:#{link_table})
2773
+ return table.select_by_#{link_field}_index { |r|
2774
+ r.send(:#{link_field}) == @#{lookup_field} }
2775
+ end
2776
+ END_OF_STRING
2777
+ rescue RuntimeError
2778
+ get_meth_str = <<-END_OF_STRING
2779
+ def #{field_name}
2780
+ table = @tbl.db.get_table(:#{link_table})
2781
+ return table.select { |r|
2782
+ r.send(:#{link_field}) == @#{lookup_field} }
2783
+ end
2784
+ END_OF_STRING
2785
+ end
2786
+
2787
+ get_meth_upd_res_str = <<-END_OF_STRING
2788
+ def #{field_name}_upd_res
2789
+ return nil
2790
+ end
2791
+ END_OF_STRING
2792
+ set_meth_str = <<-END_OF_STRING
2793
+ def #{field_name}=(s)
2794
+ @#{field_name} = nil
2795
+ end
2796
+ END_OF_STRING
2797
+ end
2798
+
2799
+ # If this is a Calculated field, modify the get/set methods.
2800
+ if field_extra.has_key?('Calculated')
2801
+ calculation = field_extra['Calculated']
2802
+
2803
+ get_meth_str = <<-END_OF_STRING
2804
+ def #{field_name}()
2805
+ return #{calculation}
2806
+ end
2807
+ END_OF_STRING
2808
+ get_meth_upd_res_str = <<-END_OF_STRING
2809
+ def #{field_name}_upd_res()
2810
+ return nil
2811
+ end
2812
+ END_OF_STRING
2813
+ set_meth_str = <<-END_OF_STRING
2814
+ def #{field_name}=(s)
2815
+ @#{field_name} = nil
2816
+ end
2817
+ END_OF_STRING
2818
+ end
2819
+
2820
+ @table_class.class_eval(get_meth_str)
2821
+ @table_class.class_eval(get_meth_upd_res_str)
2822
+ @table_class.class_eval(set_meth_str)
2823
+ end
2824
+ end
2825
+
2826
+ #-----------------------------------------------------------------------
2827
+ # validate_filter
2828
+ #-----------------------------------------------------------------------
2829
+ #++
2830
+ # Check that filter contains valid field names.
2831
+ #
2832
+ def validate_filter(filter)
2833
+ # Each field in the filter array must be a valid fieldname in the
2834
+ # table.
2835
+ filter.each { |f|
2836
+ raise 'Invalid field name: %s in filter!' % f unless \
2837
+ @field_names.include?(f)
2838
+ }
2839
+ end
2840
+
2841
+ #-----------------------------------------------------------------------
2842
+ # convert_input_data
2843
+ #-----------------------------------------------------------------------
2844
+ #++
2845
+ # Convert data passed to #input, #update, or #set to a common format.
2846
+ #
2847
+ def convert_input_data(values)
2848
+ if values.class == Proc
2849
+ tbl_struct = Struct.new(*@field_names[1..-1])
2850
+ tbl_rec = tbl_struct.new
2851
+ begin
2852
+ values.call(tbl_rec)
2853
+ rescue NoMethodError
2854
+ raise 'Invalid field name in code block: %s' % $!
2855
+ end
2856
+ temp_hash = {}
2857
+ @field_names[1..-1].collect { |f|
2858
+ temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
2859
+ }
2860
+ return temp_hash
2861
+ elsif values[0].class.to_s == @record_class or \
2862
+ values[0].class == @table_class
2863
+ temp_hash = {}
2864
+ @field_names[1..-1].collect { |f|
2865
+ temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
2866
+ }
2867
+ return temp_hash
2868
+ elsif values[0].class == Hash
2869
+ return values[0].dup
2870
+ elsif values[0].kind_of?(Struct)
2871
+ temp_hash = {}
2872
+ @field_names[1..-1].collect { |f|
2873
+ temp_hash[f] = values[0][f] if values[0].members.include?(
2874
+ f.to_s)
2875
+ }
2876
+ return temp_hash
2877
+ elsif values[0].class == Array
2878
+ raise ArgumentError, 'Must specify all fields in input array!' \
2879
+ unless values[0].size == @field_names[1..-1].size
2880
+ temp_hash = {}
2881
+ @field_names[1..-1].collect { |f|
2882
+ temp_hash[f] = values[0][@field_names.index(f)-1]
2883
+ }
2884
+ return temp_hash
2885
+ elsif values.class == Array
2886
+ raise ArgumentError, 'Must specify all fields in input array!' \
2887
+ unless values.size == @field_names[1..-1].size
2888
+ temp_hash = {}
2889
+ @field_names[1..-1].collect { |f|
2890
+ temp_hash[f] = values[@field_names.index(f)-1]
2891
+ }
2892
+ return temp_hash
2893
+ else
2894
+ raise(ArgumentError, 'Invalid type for values container!')
2895
+ end
2896
+ end
2897
+
2898
+ #-----------------------------------------------------------------------
2899
+ # validate_input
2900
+ #-----------------------------------------------------------------------
2901
+ #++
2902
+ # Check input data to ensure proper data types.
2903
+ #
2904
+ def validate_input(data)
2905
+ raise 'Cannot insert/update recno field!' if data.has_key?(:recno)
2906
+
2907
+ @field_names[1..-1].each do |f|
2908
+ next unless data.has_key?(f)
2909
+
2910
+ if data[f].nil?
2911
+ raise 'A value for this field is required: %s' % f if \
2912
+ @field_requireds[@field_names.index(f)]
2913
+ next
2914
+ end
2915
+
2916
+ case @field_types[@field_names.index(f)]
2917
+ when /:String|:Blob/
2918
+ raise 'Invalid String value for: %s' % f unless \
2919
+ data[f].respond_to?(:to_str)
2920
+ when :Memo
2921
+ raise 'Invalid Memo value for: %s' % f unless \
2922
+ data[f].is_a?(KBMemo)
2923
+ when :Blob
2924
+ raise 'Invalid Blob value for: %s' % f unless \
2925
+ data[f].is_a?(KBBlob)
2926
+ when :Boolean
2927
+ raise 'Invalid Boolean value for: %s' % f unless \
2928
+ data[f].is_a?(TrueClass) or data[f].kind_of?(FalseClass)
2929
+ when :Integer
2930
+ raise 'Invalid Integer value for: %s' % f unless \
2931
+ data[f].respond_to?(:to_int)
2932
+ when :Float
2933
+ raise 'Invalid Float value for: %s' % f unless \
2934
+ data[f].respond_to?(:to_f)
2935
+ when :Time
2936
+ raise 'Invalid Time value for: %s' % f unless \
2937
+ data[f].is_a?(Time)
2938
+ when :Date
2939
+ raise 'Invalid Date value for: %s' % f unless \
2940
+ data[f].is_a?(Date)
2941
+ when :DateTime
2942
+ raise 'Invalid DateTime value for: %s' % f unless \
2943
+ data[f].is_a?(DateTime)
2944
+ when :YAML
2945
+ raise 'Invalid YAML value for: %s' % f unless \
2946
+ data[f].respond_to?(:to_yaml)
2947
+ end
2948
+ end
2949
+ end
2950
+
2951
+ #-----------------------------------------------------------------------
2952
+ # update_header_vars
2953
+ #-----------------------------------------------------------------------
2954
+ #++
2955
+ # Read header record and update instance variables.
2956
+ #
2957
+ def update_header_vars
2958
+ @encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \
2959
+ @field_types, @field_indexes, @field_defaults, @field_requireds, \
2960
+ @field_extras = @db.engine.get_header_vars(self)
2961
+ end
2962
+
2963
+ #-----------------------------------------------------------------------
2964
+ # get_result_struct
2965
+ #-----------------------------------------------------------------------
2966
+ def get_result_struct(query_type, filter)
2967
+ case query_type
2968
+ when :select
2969
+ return Struct.new(*filter) if @record_class == 'Struct'
2970
+ when :update
2971
+ return Struct.new(*(filter + [:fpos, :line_length]))
2972
+ when :delete
2973
+ return Struct.new(:recno, :fpos, :line_length)
2974
+ end
2975
+ return nil
2976
+ end
2977
+
2978
+ #-----------------------------------------------------------------------
2979
+ # create_result_rec
2980
+ #-----------------------------------------------------------------------
2981
+ def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
2982
+ # If this isn't a select query or if it is a select query, but
2983
+ # the table record class is simply a Struct, then we will use
2984
+ # a Struct for the result record type.
2985
+ if query_type != :select
2986
+ result_rec = result_struct.new(*filter.collect { |f|
2987
+ tbl_rec.send("#{f}_upd_res".to_sym) })
2988
+ elsif @record_class == 'Struct'
2989
+ result_rec = result_struct.new(*filter.collect { |f|
2990
+ tbl_rec.send(f) })
2991
+ else
2992
+ if Object.full_const_get(@record_class).respond_to?(:kb_create)
2993
+ result_rec = Object.full_const_get(@record_class
2994
+ ).kb_create(*@field_names.collect { |f|
2995
+ # Just a warning here: If you specify a filter on
2996
+ # a select, you are only going to get those fields
2997
+ # you specified in the result set, EVEN IF
2998
+ # record_class is a custom class instead of Struct.
2999
+ if filter.include?(f)
3000
+ tbl_rec.send(f)
3001
+ else
3002
+ nil
3003
+ end
3004
+ })
3005
+ elsif Object.full_const_get(@record_class).respond_to?(
3006
+ :kb_defaults)
3007
+ result_rec = Object.full_const_get(@record_class).new(
3008
+ *@field_names.collect { |f|
3009
+ tbl_rec.send(f) || Object.full_const_get(
3010
+ @record_class).kb_defaults[@field_names.index(f)]
3011
+ }
3012
+ )
3013
+ end
3014
+ end
3015
+
3016
+ unless query_type == :select
3017
+ result_rec.fpos = rec[-2]
3018
+ result_rec.line_length = rec[-1]
3019
+ end
3020
+ return result_rec
3021
+ end
3022
+
3023
+ #-----------------------------------------------------------------------
3024
+ # get_matches
3025
+ #-----------------------------------------------------------------------
3026
+ #++
3027
+ # Return records from table that match select condition.
3028
+ #
3029
+ def get_matches(query_type, filter, select_cond)
3030
+ result_struct = get_result_struct(query_type, filter)
3031
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
3032
+ @field_types[@field_names.index(f)] })
3033
+
3034
+ tbl_rec = @table_class.new(self)
3035
+
3036
+ # Loop through table.
3037
+ @db.engine.get_recs(self).each do |rec|
3038
+ tbl_rec.populate(rec)
3039
+ next unless select_cond.call(tbl_rec) unless select_cond.nil?
3040
+
3041
+ match_array << create_result_rec(query_type, filter,
3042
+ result_struct, tbl_rec, rec)
3043
+
3044
+ end
3045
+ return match_array
3046
+ end
3047
+
3048
+ #-----------------------------------------------------------------------
3049
+ # get_matches_by_index
3050
+ #-----------------------------------------------------------------------
3051
+ #++
3052
+ # Return records from table that match select condition using one of
3053
+ # the table's indexes instead of searching the whole file.
3054
+ #
3055
+ def get_matches_by_index(query_type, index_fields, filter, select_cond)
3056
+ good_matches = []
3057
+
3058
+ idx_struct = Struct.new(*(index_fields + [:recno]))
3059
+
3060
+ begin
3061
+ if @db.client?
3062
+ # If client, check to see if the copy of the index we have
3063
+ # is up-to-date. If it is not up-to-date, grab a new copy
3064
+ # of the index array from the engine.
3065
+ unless @idx_timestamps[index_fields.join('_')] == \
3066
+ @db.engine.get_index_timestamp(self, index_fields.join(
3067
+ '_'))
3068
+ @idx_timestamps[index_fields.join('_')] = \
3069
+ @db.engine.get_index_timestamp(self, index_fields.join(
3070
+ '_'))
3071
+
3072
+ @idx_arrs[index_fields.join('_')] = \
3073
+ @db.engine.get_index(self, index_fields.join('_'))
3074
+ end
3075
+ else
3076
+ # If running single-user, grab the index array from the
3077
+ # engine.
3078
+ @idx_arrs[index_fields.join('_')] = \
3079
+ @db.engine.get_index(self, index_fields.join('_'))
3080
+ end
3081
+
3082
+ @idx_arrs[index_fields.join('_')].each do |rec|
3083
+ good_matches << rec[-1] if select_cond.call(
3084
+ idx_struct.new(*rec))
3085
+ end
3086
+ rescue NoMethodError
3087
+ raise 'Field name in select block not part of index!'
3088
+ end
3089
+
3090
+ return get_matches_by_recno(query_type, filter, good_matches)
3091
+ end
3092
+
3093
+ #-----------------------------------------------------------------------
3094
+ # get_matches_by_recno_index
3095
+ #-----------------------------------------------------------------------
3096
+ #++
3097
+ # Return records from table that match select condition using the
3098
+ # table's recno index instead of searching the whole file.
3099
+ #
3100
+ def get_matches_by_recno_index(query_type, filter, select_cond)
3101
+ good_matches = []
3102
+
3103
+ idx_struct = Struct.new(:recno)
3104
+
3105
+ begin
3106
+ @db.engine.get_recno_index(self).each_key do |key|
3107
+ good_matches << key if select_cond.call(
3108
+ idx_struct.new(key))
3109
+ end
3110
+ rescue NoMethodError
3111
+ raise "Field name in select block not part of index!"
3112
+ end
3113
+
3114
+ return nil if good_matches.empty?
3115
+ return get_matches_by_recno(query_type, filter, good_matches)
3116
+ end
3117
+
3118
+ #-----------------------------------------------------------------------
3119
+ # get_match_by_recno
3120
+ #-----------------------------------------------------------------------
3121
+ #++
3122
+ # Return record from table that matches supplied recno.
3123
+ #
3124
+ def get_match_by_recno(query_type, filter, recno)
3125
+ result_struct = get_result_struct(query_type, filter)
3126
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
3127
+ @field_types[@field_names.index(f)] })
3128
+
3129
+ tbl_rec = @table_class.new(self)
3130
+
3131
+ rec = @db.engine.get_rec_by_recno(self, recno)
3132
+ return nil if rec.nil?
3133
+ tbl_rec.populate(rec)
3134
+
3135
+ return create_result_rec(query_type, filter, result_struct,
3136
+ tbl_rec, rec)
3137
+ end
3138
+
3139
+ #-----------------------------------------------------------------------
3140
+ # get_matches_by_recno
3141
+ #-----------------------------------------------------------------------
3142
+ #++
3143
+ # Return records from table that match select condition.
3144
+ #
3145
+ def get_matches_by_recno(query_type, filter, recnos)
3146
+ result_struct = get_result_struct(query_type, filter)
3147
+ match_array = KBResultSet.new(self, filter, filter.collect { |f|
3148
+ @field_types[@field_names.index(f)] })
3149
+
3150
+ tbl_rec = @table_class.new(self)
3151
+
3152
+ @db.engine.get_recs_by_recno(self, recnos).each do |rec|
3153
+
3154
+ next if rec.nil?
3155
+ tbl_rec.populate(rec)
3156
+
3157
+ match_array << create_result_rec(query_type, filter,
3158
+ result_struct, tbl_rec, rec)
3159
+ end
3160
+ return match_array
3161
+ end
3162
+ end
3163
+
3164
+
3165
+ #---------------------------------------------------------------------------
3166
+ # KBMemo
3167
+ #---------------------------------------------------------------------------
3168
+ class KBMemo
3169
+ attr_accessor :filepath, :contents
3170
+
3171
+ #-----------------------------------------------------------------------
3172
+ # initialize
3173
+ #-----------------------------------------------------------------------
3174
+ def initialize(db, filepath, contents='')
3175
+ @db = db
3176
+ @filepath = filepath
3177
+ @contents = contents
3178
+ end
3179
+
3180
+ def read_from_file
3181
+ @contents = @db.engine.read_memo_file(@filepath)
3182
+ end
3183
+
3184
+ def write_to_file
3185
+ @db.engine.write_memo_file(@filepath, @contents)
3186
+ end
3187
+ end
3188
+
3189
+ #---------------------------------------------------------------------------
3190
+ # KBBlob
3191
+ #---------------------------------------------------------------------------
3192
+ class KBBlob
3193
+ attr_accessor :filepath, :contents
3194
+
3195
+ #-----------------------------------------------------------------------
3196
+ # initialize
3197
+ #-----------------------------------------------------------------------
3198
+ def initialize(db, filepath, contents='')
3199
+ @db = db
3200
+ @filepath = filepath
3201
+ @contents = contents
3202
+ end
3203
+
3204
+ def read_from_file
3205
+ @contents = @db.engine.read_blob_file(@filepath)
3206
+ end
3207
+
3208
+ def write_to_file
3209
+ @db.engine.write_blob_file(@filepath, @contents)
3210
+ end
3211
+ end
3212
+
3213
+
3214
+ #---------------------------------------------------------------------------
3215
+ # KBIndex
3216
+ #---------------------------------------------------------------------------
3217
+ class KBIndex
3218
+ include KBTypeConversionsMixin
3219
+
3220
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
3221
+
3222
+ #-----------------------------------------------------------------------
3223
+ # initialize
3224
+ #-----------------------------------------------------------------------
3225
+ def initialize(table, index_fields)
3226
+ @last_update = Time.new
3227
+ @idx_arr = []
3228
+ @table = table
3229
+ @index_fields = index_fields
3230
+ @col_poss = index_fields.collect {|i| table.field_names.index(i) }
3231
+ @col_names = index_fields
3232
+ @col_types = index_fields.collect {|i|
3233
+ table.field_types[table.field_names.index(i)]}
3234
+ end
3235
+
3236
+ #-----------------------------------------------------------------------
3237
+ # get_idx
3238
+ #-----------------------------------------------------------------------
3239
+ def get_idx
3240
+ return @idx_arr
3241
+ end
3242
+
3243
+ #-----------------------------------------------------------------------
3244
+ # get_timestamp
3245
+ #-----------------------------------------------------------------------
3246
+ def get_timestamp
3247
+ return @last_update
3248
+ end
3249
+
3250
+ #-----------------------------------------------------------------------
3251
+ # rebuild
3252
+ #-----------------------------------------------------------------------
3253
+ def rebuild(fptr)
3254
+ @idx_arr.clear
3255
+
3256
+ encrypted = @table.encrypted?
3257
+
3258
+ # Skip header rec.
3259
+ fptr.readline
3260
+
3261
+ begin
3262
+ # Loop through table.
3263
+ while true
3264
+ line = fptr.readline
3265
+
3266
+ line = unencrypt_str(line) if encrypted
3267
+ line.strip!
3268
+
3269
+ # If blank line (i.e. 'deleted'), skip it.
3270
+ next if line == ''
3271
+
3272
+ # Split the line up into fields.
3273
+ rec = line.split('|', @col_poss.max+2)
3274
+
3275
+ # Create the index record by pulling out the record fields
3276
+ # that make up this index and converting them to their
3277
+ # native types.
3278
+ idx_rec = []
3279
+ @col_poss.zip(@col_types).each do |col_pos, col_type|
3280
+ idx_rec << convert_to(col_type, rec[col_pos])
3281
+ end
3282
+
3283
+ # Were all the index fields for this record equal to NULL?
3284
+ # Then don't add this index record to index array; skip to
3285
+ # next record.
3286
+ next if idx_rec.compact.empty?
3287
+
3288
+ # Add recno to the end of this index record.
3289
+ idx_rec << rec.first.to_i
3290
+
3291
+ # Add index record to index array.
3292
+ @idx_arr << idx_rec
3293
+ end
3294
+ # Here's how we break out of the loop...
3295
+ rescue EOFError
3296
+ end
3297
+
3298
+ @last_update = Time.new
3299
+ end
3300
+
3301
+ #-----------------------------------------------------------------------
3302
+ # add_index_rec
3303
+ #-----------------------------------------------------------------------
3304
+ def add_index_rec(rec)
3305
+ @idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type|
3306
+ convert_to(col_type, rec[col_pos])
3307
+ end + [rec.first.to_i]
3308
+
3309
+ @last_update = Time.new
3310
+ end
3311
+
3312
+ #-----------------------------------------------------------------------
3313
+ # delete_index_rec
3314
+ #-----------------------------------------------------------------------
3315
+ def delete_index_rec(recno)
3316
+ i = @idx_arr.rassoc(recno.to_i)
3317
+ @idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
3318
+ @last_update = Time.new
3319
+ end
3320
+
3321
+ #-----------------------------------------------------------------------
3322
+ # update_index_rec
3323
+ #-----------------------------------------------------------------------
3324
+ def update_index_rec(rec)
3325
+ delete_index_rec(rec.first.to_i)
3326
+ add_index_rec(rec)
3327
+ end
3328
+ end
3329
+
3330
+
3331
+ #---------------------------------------------------------------------------
3332
+ # KBRecnoIndex
3333
+ #---------------------------------------------------------------------------
3334
+ class KBRecnoIndex
3335
+ # include DRb::DRbUndumped
3336
+
3337
+ #-----------------------------------------------------------------------
3338
+ # initialize
3339
+ #-----------------------------------------------------------------------
3340
+ def initialize(table)
3341
+ @idx_hash = {}
3342
+ @table = table
3343
+ end
3344
+
3345
+ #-----------------------------------------------------------------------
3346
+ # get_idx
3347
+ #-----------------------------------------------------------------------
3348
+ def get_idx
3349
+ return @idx_hash
3350
+ end
3351
+
3352
+ #-----------------------------------------------------------------------
3353
+ # rebuild
3354
+ #-----------------------------------------------------------------------
3355
+ def rebuild(fptr)
3356
+ @idx_hash.clear
3357
+
3358
+ encrypted = @table.encrypted?
3359
+
3360
+ begin
3361
+ # Skip header rec.
3362
+ fptr.readline
3363
+
3364
+ # Loop through table.
3365
+ while true
3366
+ # Record current position in table. Then read first
3367
+ # detail record.
3368
+ fpos = fptr.tell
3369
+ line = fptr.readline
3370
+
3371
+ line = unencrypt_str(line) if encrypted
3372
+ line.strip!
3373
+
3374
+ # If blank line (i.e. 'deleted'), skip it.
3375
+ next if line == ''
3376
+
3377
+ # Split the line up into fields.
3378
+ rec = line.split('|', 2)
3379
+
3380
+ @idx_hash[rec.first.to_i] = fpos
3381
+ end
3382
+ # Here's how we break out of the loop...
3383
+ rescue EOFError
3384
+ end
3385
+ end
3386
+
3387
+ #-----------------------------------------------------------------------
3388
+ # add_index_rec
3389
+ #-----------------------------------------------------------------------
3390
+ def add_index_rec(recno, fpos)
3391
+ raise 'Table already has index record for recno: %s' % recno if \
3392
+ @idx_hash.has_key?(recno.to_i)
3393
+ @idx_hash[recno.to_i] = fpos
3394
+ end
3395
+
3396
+ #-----------------------------------------------------------------------
3397
+ # update_index_rec
3398
+ #-----------------------------------------------------------------------
3399
+ def update_index_rec(recno, fpos)
3400
+ raise 'Table has no index record for recno: %s' % recno unless \
3401
+ @idx_hash.has_key?(recno.to_i)
3402
+ @idx_hash[recno.to_i] = fpos
3403
+ end
3404
+
3405
+ #-----------------------------------------------------------------------
3406
+ # delete_index_rec
3407
+ #-----------------------------------------------------------------------
3408
+ def delete_index_rec(recno)
3409
+ raise 'Table has no index record for recno: %s' % recno unless \
3410
+ @idx_hash.has_key?(recno.to_i)
3411
+ @idx_hash.delete(recno.to_i)
3412
+ end
3413
+ end
3414
+
3415
+
3416
+ #---------------------------------------------------------------------------
3417
+ # KBTableRec
3418
+ #---------------------------------------------------------------------------
3419
+ class KBTableRec
3420
+ include KBTypeConversionsMixin
3421
+
3422
+ def initialize(tbl)
3423
+ @tbl = tbl
3424
+ end
3425
+
3426
+ def populate(rec)
3427
+ @tbl.field_names.zip(rec).each do |fn, val|
3428
+ send("#{fn}=", val)
3429
+ end
3430
+ end
3431
+
3432
+ def clear
3433
+ @tbl.field_names.each do |fn|
3434
+ send("#{fn}=", nil)
3435
+ end
3436
+ end
3437
+ end
3438
+
3439
+
3440
+ #---------------------------------------------------------
3441
+ # KBResultSet
3442
+ #---------------------------------------------------------------------------
3443
+ class KBResultSet < Array
3444
+ #-----------------------------------------------------------------------
3445
+ # KBResultSet.reverse
3446
+ #-----------------------------------------------------------------------
3447
+ def KBResultSet.reverse(sort_field)
3448
+ return [sort_field, :desc]
3449
+ end
3450
+
3451
+ #-----------------------------------------------------------------------
3452
+ # initialize
3453
+ #-----------------------------------------------------------------------
3454
+ def initialize(table, filter, filter_types, *args)
3455
+ @table = table
3456
+ @filter = filter
3457
+ @filter_types = filter_types
3458
+ super(*args)
3459
+
3460
+ @filter.each do |f|
3461
+ get_meth_str = <<-END_OF_STRING
3462
+ def #{f}()
3463
+ if defined?(@#{f}) then
3464
+ return @#{f}
3465
+ else
3466
+ @#{f} = self.collect { |x| x.#{f} }
3467
+ return @#{f}
3468
+ end
3469
+ end
3470
+ END_OF_STRING
3471
+ self.class.class_eval(get_meth_str)
3472
+ end
3473
+ end
3474
+
3475
+ #-----------------------------------------------------------------------
3476
+ # to_ary
3477
+ #-----------------------------------------------------------------------
3478
+ def to_ary
3479
+ to_a
3480
+ end
3481
+
3482
+ #-----------------------------------------------------------------------
3483
+ # set
3484
+ #-----------------------------------------------------------------------
3485
+ #++
3486
+ # Update record(s) in table, return number of records updated.
3487
+ #
3488
+ def set(*updates, &update_cond)
3489
+ raise 'Cannot specify both a hash and a proc for method #set!' \
3490
+ unless updates.empty? or update_cond.nil?
3491
+
3492
+ raise 'Must specify update proc or hash for method #set!' if \
3493
+ updates.empty? and update_cond.nil?
3494
+
3495
+ if updates.empty?
3496
+ @table.set(self, update_cond)
3497
+ else
3498
+ @table.set(self, updates)
3499
+ end
3500
+ end
3501
+
3502
+ #-----------------------------------------------------------------------
3503
+ # sort
3504
+ #-----------------------------------------------------------------------
3505
+ def sort(*sort_fields)
3506
+ sort_fields_arrs = []
3507
+ sort_fields.each do |f|
3508
+ if f.to_s[0..0] == '-'
3509
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
3510
+ elsif f.to_s[0..0] == '+'
3511
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
3512
+ else
3513
+ sort_fields_arrs << [f, :asc]
3514
+ end
3515
+ end
3516
+
3517
+ sort_fields_arrs.each do |f|
3518
+ raise "Invalid sort field" unless @filter.include?(f[0])
3519
+ end
3520
+
3521
+ super() { |a,b|
3522
+ x = []
3523
+ y = []
3524
+ sort_fields_arrs.each do |s|
3525
+ if [:Integer, :Float].include?(
3526
+ @filter_types[@filter.index(s[0])])
3527
+ a_value = a.send(s[0]) || 0
3528
+ b_value = b.send(s[0]) || 0
3529
+ else
3530
+ a_value = a.send(s[0])
3531
+ b_value = b.send(s[0])
3532
+ end
3533
+ if s[1] == :desc
3534
+ x << b_value
3535
+ y << a_value
3536
+ else
3537
+ x << a_value
3538
+ y << b_value
3539
+ end
3540
+ end
3541
+ x <=> y
3542
+ }
3543
+ end
3544
+
3545
+ #-----------------------------------------------------------------------
3546
+ # to_report
3547
+ #-----------------------------------------------------------------------
3548
+ def to_report(recs_per_page=0, print_rec_sep=false)
3549
+ result = collect { |r| @filter.collect {|f| r.send(f)} }
3550
+
3551
+ # How many records before a formfeed.
3552
+ delim = ' | '
3553
+
3554
+ # columns of physical rows
3555
+ columns = [@filter].concat(result).transpose
3556
+
3557
+ max_widths = columns.collect { |c|
3558
+ c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
3559
+ }
3560
+
3561
+ row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
3562
+ delim.length * (max_widths.size - 1))
3563
+
3564
+ justify_hash = { :String => :ljust, :Integer => :rjust,
3565
+ :Float => :rjust, :Boolean => :ljust, :Date => :ljust,
3566
+ :Time => :ljust, :DateTime => :ljust }
3567
+
3568
+ header_line = @filter.zip(max_widths, @filter.collect { |f|
3569
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
3570
+ x.to_s.send(justify_hash[z], y) }.join(delim)
3571
+
3572
+ output = ''
3573
+ recs_on_page_cnt = 0
3574
+
3575
+ result.each do |row|
3576
+ if recs_on_page_cnt == 0
3577
+ output << header_line + "\n" << row_dashes + "\n"
3578
+ end
3579
+
3580
+ output << row.zip(max_widths, @filter.collect { |f|
3581
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
3582
+ x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
3583
+
3584
+ output << row_dashes + '\n' if print_rec_sep
3585
+ recs_on_page_cnt += 1
3586
+
3587
+ if recs_per_page > 0 and (recs_on_page_cnt ==
3588
+ num_recs_per_page)
3589
+ output << '\f'
3590
+ recs_on_page_count = 0
3591
+ end
3592
+ end
3593
+ return output
3594
+ end
3595
+ end
3596
+
3597
+
3598
+ #---------------------------------------------------------------------------
3599
+ # Object
3600
+ #---------------------------------------------------------------------------
3601
+ class Object
3602
+ def full_const_get(name)
3603
+ list = name.split("::")
3604
+ obj = Object
3605
+ list.each {|x| obj = obj.const_get(x) }
3606
+ obj
3607
+ end
3608
+ end
3609
+
3610
+
3611
+ #---------------------------------------------------------------------------
3612
+ # NilClass
3613
+ #---------------------------------------------------------------------------
3614
+ class NilClass
3615
+ #-----------------------------------------------------------------------
3616
+ # method_missing
3617
+ #-----------------------------------------------------------------------
3618
+ #
3619
+ # This code is necessary because if, inside a select condition code
3620
+ # block, there is a case where you are trying to do an expression
3621
+ # against a table field that is equal to nil, I don't want a method
3622
+ # missing exception to occur. I just want the expression to be nil. I
3623
+ # initially had this method returning false, but then I had an issue
3624
+ # where I had a YAML field that was supposed to hold an Array. If the
3625
+ # field was empty (i.e. nil) it was actually returning false when it
3626
+ # should be returning nil. Since nil evaluates to false, it works if I
3627
+ # return nil.
3628
+ # Here's an example:
3629
+ # #select { |r| r.speed > 300 }
3630
+ # What happens if speed is nil (basically NULL in DBMS terms)? Without
3631
+ # this code, an exception is going to be raised, which is not what we
3632
+ # really want. We really want this expression to return nil.
3633
+ def method_missing(method_id, *stuff)
3634
+ return nil
3635
+ end
3636
+ end
3637
+
3638
+
3639
+ #---------------------------------------------------------------------------
3640
+ # Symbol
3641
+ #---------------------------------------------------------------------------
3642
+ class Symbol
3643
+ #-----------------------------------------------------------------------
3644
+ # -@
3645
+ #-----------------------------------------------------------------------
3646
+ #
3647
+ # This allows you to put a minus sign in front of a field name in order
3648
+ # to specify descending sort order.
3649
+ def -@
3650
+ ("-"+self.to_s).to_sym
3651
+ end
3652
+
3653
+ #-----------------------------------------------------------------------
3654
+ # +@
3655
+ #-----------------------------------------------------------------------
3656
+ #
3657
+ # This allows you to put a plus sign in front of a field name in order
3658
+ # to specify ascending sort order.
3659
+ def +@
3660
+ ("+"+self.to_s).to_sym
3661
+ end
3662
+ end