og 0.25.0 → 0.26.0

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