KirbyBase 2.5

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