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.
- data/README +73 -0
- data/bin/kbserver.rb +20 -0
- data/changes.txt +105 -0
- data/examples/aaa_try_this_first/kbtest.rb +207 -0
- data/examples/add_column_test/add_column_test.rb +27 -0
- data/examples/calculated_field_test/calculated_field_test.rb +51 -0
- data/examples/change_column_type_test/change_column_type_test.rb +25 -0
- data/examples/column_required_test/column_required_test.rb +33 -0
- data/examples/crosstab_test/crosstab_test.rb +100 -0
- data/examples/csv_import_test/csv_import_test.rb +31 -0
- data/examples/csv_import_test/plane.csv +11 -0
- data/examples/default_value_test/default_value_test.rb +43 -0
- data/examples/drop_column_test/drop_column_test.rb +24 -0
- data/examples/indexes_test/add_index_test.rb +46 -0
- data/examples/indexes_test/drop_index_test.rb +66 -0
- data/examples/indexes_test/index_test.rb +71 -0
- data/examples/kbserver_as_win32_service/kbserver_daemon.rb +47 -0
- data/examples/kbserver_as_win32_service/kbserverctl.rb +75 -0
- data/examples/link_many_test/link_many_test.rb +70 -0
- data/examples/lookup_field_test/lookup_field_test.rb +55 -0
- data/examples/lookup_field_test/lookup_field_test_2.rb +62 -0
- data/examples/lookup_field_test/the_hal_fulton_feature_test.rb +69 -0
- data/examples/many_to_many_test/many_to_many_test.rb +65 -0
- data/examples/memo_test/memo_test.rb +63 -0
- data/examples/memo_test/memos/blank.txt +0 -0
- data/examples/record_class_test/record_class_test.rb +77 -0
- data/examples/rename_column_test/rename_column_test.rb +46 -0
- data/examples/rename_table_test/rename_table_test.rb +38 -0
- data/examples/yaml_field_test/yaml_field_test.rb +47 -0
- data/images/blank.png +0 -0
- data/images/callouts/1.png +0 -0
- data/images/callouts/10.png +0 -0
- data/images/callouts/11.png +0 -0
- data/images/callouts/12.png +0 -0
- data/images/callouts/13.png +0 -0
- data/images/callouts/14.png +0 -0
- data/images/callouts/15.png +0 -0
- data/images/callouts/2.png +0 -0
- data/images/callouts/3.png +0 -0
- data/images/callouts/4.png +0 -0
- data/images/callouts/5.png +0 -0
- data/images/callouts/6.png +0 -0
- data/images/callouts/7.png +0 -0
- data/images/callouts/8.png +0 -0
- data/images/callouts/9.png +0 -0
- data/images/caution.png +0 -0
- data/images/client_server.png +0 -0
- data/images/example.png +0 -0
- data/images/home.png +0 -0
- data/images/important.png +0 -0
- data/images/kirby1.jpg +0 -0
- data/images/next.png +0 -0
- data/images/note.png +0 -0
- data/images/prev.png +0 -0
- data/images/single_user.png +0 -0
- data/images/smallnew.png +0 -0
- data/images/tip.png +0 -0
- data/images/toc-blank.png +0 -0
- data/images/toc-minus.png +0 -0
- data/images/toc-plus.png +0 -0
- data/images/up.png +0 -0
- data/images/warning.png +0 -0
- data/kirbybaserubymanual.html +2243 -0
- data/lib/kirbybase.rb +3662 -0
- metadata +126 -0
data/lib/kirbybase.rb
ADDED
@@ -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('&', "&")
|
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('&', "&")
|
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("&", '&').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("&", '&').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
|