og 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,3 +16,5 @@ module Timestamped
16
16
  end
17
17
 
18
18
  end
19
+
20
+ # * George Moschovitis <gm@navel.gr>
data/lib/og/relation.rb CHANGED
@@ -11,6 +11,9 @@ class Relation
11
11
 
12
12
  attr_accessor :options
13
13
 
14
+ # A generalized initialize method for all relations.
15
+ # Contains common setup code.
16
+
14
17
  def initialize(args, options = {})
15
18
  @options = options
16
19
  @options.update(args.pop) if args.last.is_a?(Hash)
@@ -27,6 +30,8 @@ class Relation
27
30
  :target_singular_name
28
31
  end
29
32
 
33
+ # Inflect the relation name.
34
+
30
35
  unless args.empty?
31
36
  @options[target_name] = args.first
32
37
  else
@@ -36,7 +41,7 @@ class Relation
36
41
  target_class.to_s.demodulize.underscore.downcase.intern
37
42
  end
38
43
  end
39
-
44
+
40
45
  @options[:name] = options[target_name]
41
46
  end
42
47
 
@@ -33,6 +33,7 @@ class HasMany < Relation
33
33
  @#{target_plural_name} = HasManyCollection.new(
34
34
  self,
35
35
  :add_#{target_singular_name},
36
+ :remove_#{target_singular_name},
36
37
  :find_#{target_plural_name},
37
38
  options
38
39
  )
@@ -46,12 +47,17 @@ class HasMany < Relation
46
47
  obj.#{foreign_key} = @#{owner_pk}
47
48
  obj.save
48
49
  end
50
+
51
+ def remove_#{target_singular_name}(obj)
52
+ obj.#{foreign_key} = nil
53
+ end
49
54
 
50
55
  def find_#{target_plural_name}(options = {})
51
56
  find_options = {
52
57
  :condition => "#{foreign_key} = \#\{@#{owner_pk}\}"
53
58
  }
54
59
  find_options.update(options) if options
60
+ #{"find_options.update(:order => #{options[:order].inspect})" if options[:order]}
55
61
  #{target_class}.find(find_options)
56
62
  end
57
63
  }
@@ -39,6 +39,7 @@ class JoinsMany < Relation
39
39
  @#{target_plural_name} = JoinsManyCollection.new(
40
40
  self,
41
41
  :add_#{target_singular_name},
42
+ :remove_#{target_singular_name},
42
43
  :find_#{target_plural_name}
43
44
  )
44
45
  end
@@ -52,6 +53,10 @@ class JoinsMany < Relation
52
53
  obj.class.ogmanager.store.join(self, obj, "#{join_table}")
53
54
  end
54
55
 
56
+ def remove_#{target_singular_name}(obj)
57
+ obj.class.ogmanager.store.unjoin(self, obj, "#{join_table}")
58
+ end
59
+
55
60
  def find_#{target_plural_name}(options = {})
56
61
  find_options = {
57
62
  :join_table => "#{join_table}",
@@ -0,0 +1,280 @@
1
+ begin
2
+ require 'lib/og/store/kirby/kirbybase'
3
+ rescue Object => ex
4
+ Logger.error 'KirbyBase is not installed!'
5
+ Logger.error ex
6
+ end
7
+
8
+ require 'og/store/sql'
9
+
10
+ module Og
11
+
12
+ # A Store that persists objects into an KirbyBase database.
13
+ # KirbyBase is a pure-ruby database implementation.
14
+ # To read documentation about the methods, consult the
15
+ # documentation for SqlStore and Store.
16
+
17
+ class KirbyStore < SqlStore
18
+
19
+ def self.db_dir(options)
20
+ "#{options[:name]}_db"
21
+ end
22
+
23
+ def self.destroy(options)
24
+ begin
25
+ FileUtils.rm_rf(db_dir(options))
26
+ super
27
+ rescue Object
28
+ Logger.info "Cannot drop '#{options[:name]}'!"
29
+ end
30
+ end
31
+
32
+ def initialize(options)
33
+ super
34
+
35
+ if options[:embedded]
36
+ name = self.class.db_dir(options)
37
+ FileUtils.mkdir_p(name)
38
+ @conn = KirbyBase.new(:local, nil, nil, name)
39
+ else
40
+ # TODO
41
+ end
42
+ end
43
+
44
+ def close
45
+ super
46
+ end
47
+
48
+ def enchant(klass, manager)
49
+ klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
50
+ super
51
+ end
52
+
53
+ def query(sql)
54
+ Logger.debug sql if $DBG
55
+ return @conn.query(sql)
56
+ rescue => ex
57
+ handle_sql_exception(ex, sql)
58
+ end
59
+
60
+ def exec(sql)
61
+ Logger.debug sql if $DBG
62
+ @conn.query(sql).close
63
+ rescue => ex
64
+ handle_sql_exception(ex, sql)
65
+ end
66
+
67
+ def start
68
+ # nop
69
+ end
70
+
71
+ def commit
72
+ # nop
73
+ end
74
+
75
+ def rollback
76
+ # nop
77
+ end
78
+
79
+ private
80
+
81
+ def create_table(klass)
82
+ fields = fields_for_class(klass)
83
+
84
+ table = @conn.create_table(klass::OGTABLE, *fields) { |obj| obj.encrypt = false }
85
+ =begin
86
+ # Create join tables if needed. Join tables are used in
87
+ # 'many_to_many' relations.
88
+
89
+ if klass.__meta and join_tables = klass.__meta[:join_tables]
90
+ for join_table in join_tables
91
+ begin
92
+ @conn.query("CREATE TABLE #{join_table} (key1 integer NOT NULL, key2 integer NOT NULL)").close
93
+ @conn.query("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)").close
94
+ @conn.query("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)").close
95
+ rescue Object => ex
96
+ # gmosx: any idea how to better test this?
97
+ if ex.to_s =~ /table .* already exists/i
98
+ Logger.debug 'Join table already exists' if $DBG
99
+ else
100
+ raise
101
+ end
102
+ end
103
+ end
104
+ end
105
+ =end
106
+ end
107
+
108
+ def drop_table(klass)
109
+ @conn.drop_table(klass.table) if @conn.table_exists?(klass.table)
110
+ end
111
+
112
+ def fields_for_class(klass)
113
+ fields = []
114
+
115
+ klass.properties.each do |p|
116
+ klass.index(p.symbol) if p.meta[:index]
117
+
118
+ fields << p.symbol
119
+
120
+ type = p.klass.name.intern
121
+ type = :Integer if type == :Fixnum
122
+
123
+ fields << type
124
+ end
125
+
126
+ return fields
127
+ end
128
+
129
+ def create_field_map(klass)
130
+ map = {}
131
+ fields = @conn.get_table(klass.table).field_names
132
+
133
+ fields.size.times do |i|
134
+ map[fields[i]] = i
135
+ end
136
+
137
+ return map
138
+ end
139
+
140
+ # Return an sql string evaluator for the property.
141
+ # No need to optimize this, used only to precalculate code.
142
+ # YAML is used to store general Ruby objects to be more
143
+ # portable.
144
+ #--
145
+ # FIXME: add extra handling for float.
146
+ #++
147
+
148
+ def write_prop(p)
149
+ if p.klass.ancestors.include?(Integer)
150
+ return "@#{p.symbol} || nil"
151
+ elsif p.klass.ancestors.include?(Float)
152
+ return "@#{p.symbol} || nil"
153
+ elsif p.klass.ancestors.include?(String)
154
+ return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : nil|
155
+ elsif p.klass.ancestors.include?(Time)
156
+ return %|@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : nil|
157
+ elsif p.klass.ancestors.include?(Date)
158
+ return %|@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : nil|
159
+ elsif p.klass.ancestors.include?(TrueClass)
160
+ return "@#{p.symbol} ? \"'t'\" : nil"
161
+ else
162
+ # gmosx: keep the '' for nil symbols.
163
+ return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"|
164
+ end
165
+ end
166
+
167
+ # Return an evaluator for reading the property.
168
+ # No need to optimize this, used only to precalculate code.
169
+
170
+ def read_prop(p, col)
171
+ if p.klass.ancestors.include?(Integer)
172
+ return "#{self.class}.parse_int(res[#{col} + offset])"
173
+ elsif p.klass.ancestors.include?(Float)
174
+ return "#{self.class}.parse_float(res[#{col} + offset])"
175
+ elsif p.klass.ancestors.include?(String)
176
+ return "res[#{col} + offset]"
177
+ elsif p.klass.ancestors.include?(Time)
178
+ return "#{self.class}.parse_timestamp(res[#{col} + offset])"
179
+ elsif p.klass.ancestors.include?(Date)
180
+ return "#{self.class}.parse_date(res[#{col} + offset])"
181
+ elsif p.klass.ancestors.include?(TrueClass)
182
+ return "('0' != res[#{col} + offset])"
183
+ else
184
+ return "YAML::load(res[#{col} + offset])"
185
+ end
186
+ end
187
+
188
+ # :section: Lifecycle method compilers.
189
+
190
+ # Compile the og_update method for the class.
191
+
192
+ def eval_og_insert(klass)
193
+ pk = klass.pk_symbol
194
+ props = klass.properties
195
+
196
+ data = props.collect {|p| ":#{p.symbol} => #{write_prop(p)}"}.join(', ')
197
+ # data.gsub!(/#|\{|\}/, '')
198
+
199
+ klass.module_eval %{
200
+ def og_insert(store)
201
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
202
+ store.conn.get_table('#{klass.table}').insert(#{data})
203
+ #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
204
+ end
205
+ }
206
+ end
207
+
208
+ # Compile the og_update method for the class.
209
+
210
+ def eval_og_update(klass)
211
+ pk = klass.pk_symbol
212
+ props = klass.properties.reject { |p| pk == p.symbol }
213
+
214
+ updates = props.collect { |p|
215
+ "#{p.symbol}=#{write_prop(p)}"
216
+ }
217
+
218
+ sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}"
219
+
220
+ klass.module_eval %{
221
+ def og_update(store)
222
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)}
223
+ store.exec "#{sql}"
224
+ #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)}
225
+ end
226
+ }
227
+ end
228
+
229
+ # Compile the og_read method for the class. This method is
230
+ # used to read (deserialize) the given class from the store.
231
+ # In order to allow for changing field/attribute orders a
232
+ # field mapping hash is used.
233
+
234
+ def eval_og_read(klass)
235
+ code = []
236
+ props = klass.properties
237
+ field_map = create_field_map(klass)
238
+
239
+ props.each do |p|
240
+ if col = field_map[p.symbol]
241
+ code << "@#{p.symbol} = #{read_prop(p, col)}"
242
+ end
243
+ end
244
+
245
+ code = code.join('; ')
246
+
247
+ klass.module_eval %{
248
+ def og_read(res, row = 0, offset = 0)
249
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)}
250
+ #{code}
251
+ #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)}
252
+ end
253
+ }
254
+ end
255
+
256
+ #--
257
+ # FIXME: is pk needed as parameter?
258
+ #++
259
+
260
+ def eval_og_delete(klass)
261
+ klass.module_eval %{
262
+ def og_delete(store, pk, cascade = true)
263
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)}
264
+ pk ||= @#{klass.pk_symbol}
265
+ transaction do |tx|
266
+ tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}"
267
+ if cascade and #{klass}.__meta[:descendants]
268
+ #{klass}.__meta[:descendants].each do |dclass, foreign_key|
269
+ tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{foreign_key}=\#{pk}"
270
+ end
271
+ end
272
+ end
273
+ #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)}
274
+ end
275
+ }
276
+ end
277
+
278
+ end
279
+
280
+ end
@@ -0,0 +1,6 @@
1
+ = KirbyBase
2
+
3
+ The contents of this folder where copied from the KirbyBase
4
+ 2.2 distribution.
5
+
6
+ Copyright (c) 2005 Jamey Cribbs
@@ -0,0 +1,1601 @@
1
+ require 'date'
2
+ require 'drb'
3
+ require 'csv'
4
+ require 'fileutils'
5
+
6
+ # KirbyBase is a class that allows you to create and manipulate simple,
7
+ # plain-text databases. You can use it in either a single-user or
8
+ # client-server mode. You can select records for retrieval/updating using
9
+ # code blocks.
10
+ #
11
+ # Author:: Jamey Cribbs (mailto:jcribbs@twmi.rr.com)
12
+ # Homepage:: http://www.netpromi.com/kirbybase.html
13
+ # Copyright:: Copyright (c) 2005 NetPro Technologies, LLC
14
+ # License:: Distributes under the same terms as Ruby
15
+ # History:
16
+ # 2005-03-28:: Version 2.0
17
+ # * This is a completely new version. The interface has changed
18
+ # dramatically.
19
+ # 2005-04-11:: Version 2.1
20
+ # * Changed the interface to KirbyBase#new and KirbyBase#create_table. You
21
+ # now specify arguments via a code block or as part of the argument list.
22
+ # * Added the ability to specify a class at table creation time.
23
+ # Thereafter, whenever you do a #select, the result set will be an array
24
+ # of instances of that class, instead of instances of Struct. You can
25
+ # also use instances of this class as the argument to KBTable#insert,
26
+ # KBTable#update, and KBTable#set.
27
+ # * Added the ability to encrypt a table so that it is no longer stored as
28
+ # a plain-text file.
29
+ # * Added the ability to explicity specify that you want a result set to be
30
+ # sorted in ascending order.
31
+ # * Added the ability to import a csv file into an existing table.
32
+ # * Added the ability to select a record as if the table were a Hash with
33
+ # it's key being the recno field.
34
+ # * Added the ability to update a record as if the table were a Hash with
35
+ # it's key being the recno field.
36
+ # 2005-05-02:: Version 2.2
37
+ # * By far the biggest change in this version is that I have completely
38
+ # redesigned the internal structure of the database code. Because the
39
+ # KirbyBase and KBTable classes were too tightly coupled, I have created
40
+ # a KBEngine class and moved all low-level I/O logic and locking logic
41
+ # to this class. This allowed me to restructure the KirbyBase class to
42
+ # remove all of the methods that should have been private, but couldn't be
43
+ # because of the coupling to KBTable. In addition, it has allowed me to
44
+ # take all of the low-level code that should not have been in the KBTable
45
+ # class and put it where it belongs, as part of the underlying engine. I
46
+ # feel that the design of KirbyBase is much cleaner now. No changes were
47
+ # made to the class interfaces, so you should not have to change any of
48
+ # your code.
49
+ # * Changed str_to_date and str_to_datetime to use Date#parse method.
50
+ # * Changed #pack method so that it no longer reads the whole file into
51
+ # memory while packing it.
52
+ # * Changed code so that special character sequences like &linefeed; can be
53
+ # part of input data and KirbyBase will not interpret it as special
54
+ # characters.
55
+ #
56
+ #---------------------------------------------------------------------------
57
+ # KirbyBase
58
+ #---------------------------------------------------------------------------
59
+ class KirbyBase
60
+ include DRb::DRbUndumped
61
+
62
+ attr_reader :engine
63
+
64
+ attr_accessor :connect_type, :host, :port, :path, :ext
65
+
66
+ #-----------------------------------------------------------------------
67
+ # initialize
68
+ #-----------------------------------------------------------------------
69
+ #++
70
+ # Create a new database instance.
71
+ #
72
+ # *connect_type*:: Symbol (:local, :client, :server) specifying role to
73
+ # play.
74
+ # *host*:: String containing IP address or DNS name of server hosting
75
+ # database. (Only valid if connect_type is :client.)
76
+ # *port*:: Integer specifying port database server is listening on.
77
+ # (Only valid if connect_type is :client.)
78
+ # *path*:: String specifying path to location of database tables.
79
+ # *ext*:: String specifying extension of table files.
80
+ #
81
+ def initialize(connect_type=:local, host=nil, port=nil, path='./',
82
+ ext='.tbl')
83
+ @connect_type = connect_type
84
+ @host = host
85
+ @port = port
86
+ @path = path
87
+ @ext = ext
88
+
89
+ # See if user specified any method arguments via a code block.
90
+ yield self if block_given?
91
+
92
+ # After the yield, make sure the user doesn't change any of these
93
+ # instance variables.
94
+ class << self
95
+ private :connect_type=, :host=, :path=, :ext=
96
+ end
97
+
98
+ # Did user supply full and correct arguments to method?
99
+ raise ArgumentError, 'Invalid connection type specified' unless (
100
+ [:local, :client, :server].include?(@connect_type))
101
+ raise "Must specify hostname or IP address!" if \
102
+ @connect_type == :client and @host.nil?
103
+ raise "Must specify port number!" if @connect_type == :client and \
104
+ @port.nil?
105
+ raise "Invalid path!" if @path.nil?
106
+ raise "Invalid extension!" if @ext.nil?
107
+
108
+ # If running as a client, start druby and connect to server.
109
+ if client?
110
+ DRb.start_service()
111
+ @server = DRbObject.new(nil, 'druby://%s:%d' % [@host, @port])
112
+ @engine = @server.engine
113
+ @path = @server.path
114
+ @ext = @server.ext
115
+ else
116
+ @engine = KBEngine.create_called_from_database_instance(self)
117
+ end
118
+ end
119
+
120
+ #-----------------------------------------------------------------------
121
+ # server?
122
+ #-----------------------------------------------------------------------
123
+ #++
124
+ # Is this running as a server?
125
+ #
126
+ def server?
127
+ @connect_type == :server
128
+ end
129
+
130
+ #-----------------------------------------------------------------------
131
+ # client?
132
+ #-----------------------------------------------------------------------
133
+ #++
134
+ # Is this running as a client?
135
+ #
136
+ def client?
137
+ @connect_type == :client
138
+ end
139
+
140
+ #-----------------------------------------------------------------------
141
+ # local?
142
+ #-----------------------------------------------------------------------
143
+ #++
144
+ # Is this running in single-user, embedded mode?
145
+ #
146
+ def local?
147
+ @connect_type == :local
148
+ end
149
+
150
+ #-----------------------------------------------------------------------
151
+ # tables
152
+ #-----------------------------------------------------------------------
153
+ #++
154
+ # Return an array containing the names of all tables in this database.
155
+ #
156
+ def tables
157
+ return @engine.tables
158
+ end
159
+
160
+ #-----------------------------------------------------------------------
161
+ # get_table
162
+ #-----------------------------------------------------------------------
163
+ #++
164
+ # Return a reference to the requested table.
165
+ # *name*:: Symbol or string of table name.
166
+ #
167
+ def get_table(name)
168
+ raise('Do not call this method from a server instance!') if server?
169
+ raise('Table not found!') unless table_exists?(name)
170
+
171
+ return KBTable.create_called_from_database_instance(self,
172
+ name.to_sym, File.join(@path, name.to_s + @ext))
173
+ end
174
+
175
+ #-----------------------------------------------------------------------
176
+ # create_table
177
+ #-----------------------------------------------------------------------
178
+ #++
179
+ # Create new table and return a reference to the new table.
180
+ # *name*:: Symbol or string of table name.
181
+ # *field_defs*:: List of field names (Symbols) and field types (Symbols)
182
+ # *Block*:: Optional code block allowing you to set the following:
183
+ # *encrypt*:: true/false specifying whether table should be encrypted.
184
+ # *record_class*:: Class or String specifying the user create class that
185
+ # will be associated with table records.
186
+ #
187
+ def create_table(name=nil, *field_defs)
188
+ raise "Can't call #create_table from server!" if server?
189
+
190
+ t_struct = Struct.new(:name, :field_defs, :encrypt, :record_class)
191
+ t = t_struct.new
192
+ t.name = name
193
+ t.field_defs = field_defs
194
+ t.encrypt = false
195
+ t.record_class = 'Struct'
196
+
197
+ yield t if block_given?
198
+
199
+ raise "No table name specified!" if t.name.nil?
200
+ raise "No table field definitions specified!" if t.field_defs.nil?
201
+
202
+ @engine.new_table(t.name, t.field_defs, t.encrypt,
203
+ t.record_class.to_s)
204
+ return get_table(t.name)
205
+ end
206
+
207
+ #-----------------------------------------------------------------------
208
+ # drop_table
209
+ #-----------------------------------------------------------------------
210
+ #++
211
+ # Delete a table.
212
+ #
213
+ # *tablename*:: Symbol or string of table name.
214
+ #
215
+ def drop_table(tablename)
216
+ return @engine.delete_table(tablename)
217
+ end
218
+
219
+ #-----------------------------------------------------------------------
220
+ # table_exists?
221
+ #-----------------------------------------------------------------------
222
+ #++
223
+ # Return true if table exists.
224
+ #
225
+ # *tablename*:: Symbol or string of table name.
226
+ #
227
+ def table_exists?(tablename)
228
+ return @engine.table_exists?(tablename)
229
+ end
230
+ end
231
+
232
+ #---------------------------------------------------------------------------
233
+ # KBEngine
234
+ #---------------------------------------------------------------------------
235
+ class KBEngine
236
+ include DRb::DRbUndumped
237
+
238
+ EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
239
+ '0123456789.+-,$:|&;_ '
240
+ EN_STR_LEN = EN_STR.length
241
+ EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
242
+ EN_KEY = EN_KEY1.unpack("u")[0]
243
+ EN_KEY_LEN = EN_KEY.length
244
+
245
+ # Regular expression used to determine if field needs to be
246
+ # encoded.
247
+ ENCODE_RE = /&|\n|\r|\032|\|/
248
+
249
+ # Make constructor private.
250
+ private_class_method :new
251
+
252
+ def KBEngine.create_called_from_database_instance(db)
253
+ return new(db)
254
+ end
255
+
256
+ def initialize(db)
257
+ @db = db
258
+ # This hash will hold the table locks if in client/server mode.
259
+ @mutex_hash = {} if @db.server?
260
+ end
261
+
262
+ #-----------------------------------------------------------------------
263
+ # table_exists?
264
+ #-----------------------------------------------------------------------
265
+ #++
266
+ # Return true if table exists.
267
+ #
268
+ # *tablename*:: Symbol or string of table name.
269
+ #
270
+ def table_exists?(tablename)
271
+ return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
272
+ end
273
+
274
+ #-----------------------------------------------------------------------
275
+ # tables
276
+ #-----------------------------------------------------------------------
277
+ #++
278
+ # Return an array containing the names of all tables in this database.
279
+ #
280
+ def tables
281
+ list = []
282
+ Dir.foreach(@db.path) { |filename|
283
+ list << File.basename(filename, '.*').to_sym if \
284
+ filename =~ Regexp.new(@db.ext)
285
+ }
286
+ return list
287
+ end
288
+
289
+ #-----------------------------------------------------------------------
290
+ # new_table
291
+ #-----------------------------------------------------------------------
292
+ #++
293
+ # Create physical file holding table. This table should not be directly
294
+ # called in your application, but only called by #create_table.
295
+ #
296
+ # *name*:: Symbol or string of table name.
297
+ # *field_defs*:: List of field names (Symbols) and field types (Symbols)
298
+ # *encrypt*:: true/false specifying whether table should be encrypted.
299
+ # *record_class*:: Class or String specifying the user create class that
300
+ #
301
+ def new_table(name, field_defs, encrypt, record_class)
302
+ # Can't create a table that already exists!
303
+ raise "Table #{name.to_s} already exists!" if table_exists?(name)
304
+
305
+ raise "Must have a field type for each field name" \
306
+ unless field_defs.size.remainder(2) == 0
307
+
308
+ temp_field_defs = []
309
+ (0...field_defs.size).step(2) { |x|
310
+ raise "Invalid field type: #{field_defs[x+1]}" unless \
311
+ KBTable.valid_field_type?(field_defs[x+1])
312
+ temp_field_defs << \
313
+ "#{field_defs[x].to_s}:#{field_defs[x+1]}"
314
+ }
315
+
316
+ # Header rec consists of last record no. used, delete count, and
317
+ # all field names/types. Here, I am inserting the 'recno' field
318
+ # at the beginning of the fields.
319
+ header_rec = ['000000', '000000', record_class, 'recno:Integer',
320
+ temp_field_defs].join('|')
321
+
322
+ header_rec = 'Z' + encrypt_str(header_rec) if encrypt
323
+
324
+ begin
325
+ fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
326
+ fptr.write(header_rec + "\n")
327
+ ensure
328
+ fptr.close
329
+ end
330
+ end
331
+
332
+ #-----------------------------------------------------------------------
333
+ # delete_table
334
+ #-----------------------------------------------------------------------
335
+ #++
336
+ # Delete a table.
337
+ #
338
+ # *tablename*:: Symbol or string of table name.
339
+ #
340
+ def delete_table(tablename)
341
+ with_write_lock(tablename) do
342
+ File.delete(File.join(@db.path, tablename.to_s + @db.ext))
343
+ return true
344
+ end
345
+ end
346
+
347
+ #----------------------------------------------------------------------
348
+ # get_total_recs
349
+ #----------------------------------------------------------------------
350
+ #++
351
+ # Return total number of non-deleted records in table.
352
+ #
353
+ # *table*:: Table instance.
354
+ #
355
+ def get_total_recs(table)
356
+ return get_recs(table).size
357
+ end
358
+
359
+ #-----------------------------------------------------------------------
360
+ # get_header_vars
361
+ #-----------------------------------------------------------------------
362
+ #++
363
+ # Returns array containing first line of file.
364
+ #
365
+ # *table*:: Table instance.
366
+ #
367
+ def get_header_vars(table)
368
+ with_table(table) do |fptr|
369
+ line = get_header_record(table, fptr)
370
+
371
+ last_rec_no, del_ctr, record_class, *flds = line.split('|')
372
+ field_names = flds.collect { |x| x.split(':')[0].to_sym }
373
+ field_types = flds.collect { |x| x.split(':')[1].to_sym }
374
+
375
+ return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
376
+ record_class, field_names, field_types]
377
+ end
378
+ end
379
+
380
+ #-----------------------------------------------------------------------
381
+ # get_recs
382
+ #-----------------------------------------------------------------------
383
+ #++
384
+ # Return array of all table records (arrays).
385
+ #
386
+ # *table*:: Table instance.
387
+ #
388
+ def get_recs(table)
389
+ encrypted = table.encrypted?
390
+ recs = []
391
+
392
+ with_table(table) do |fptr|
393
+ begin
394
+ # Skip header rec.
395
+ fptr.readline
396
+
397
+ # Loop through table.
398
+ while true
399
+ # Record current position in table. Then read first
400
+ # detail record.
401
+ fpos = fptr.tell
402
+ line = fptr.readline
403
+
404
+ line = unencrypt_str(line) if encrypted
405
+ line.strip!
406
+
407
+ # If blank line (i.e. 'deleted'), skip it.
408
+ next if line == ''
409
+
410
+ # Split the line up into fields.
411
+ rec = line.split('|', -1)
412
+ rec << fpos << line.length
413
+ recs << rec
414
+ end
415
+ # Here's how we break out of the loop...
416
+ rescue EOFError
417
+ end
418
+ return recs
419
+ end
420
+ end
421
+
422
+ #-----------------------------------------------------------------------
423
+ # insert_record
424
+ #-----------------------------------------------------------------------
425
+ #
426
+ def insert_record(table, rec)
427
+ with_write_locked_table(table) do |fptr|
428
+ # Auto-increment the record number field.
429
+ rec_no = incr_rec_no_ctr(table, fptr)
430
+
431
+ # Insert the newly created record number value at the beginning
432
+ # of the field values.
433
+ rec[0] = rec_no
434
+
435
+ # Encode any special characters (like newlines) before writing
436
+ # out the record.
437
+ write_record(table, fptr, 'end', rec.collect { |r| encode_str(r)
438
+ }.join('|'))
439
+
440
+ # Return the record number of the newly created record.
441
+ return rec_no
442
+ end
443
+ end
444
+
445
+ #-----------------------------------------------------------------------
446
+ # update_records
447
+ #-----------------------------------------------------------------------
448
+ #
449
+ def update_records(table, recs)
450
+ with_write_locked_table(table) do |fptr|
451
+ recs.each do |rec|
452
+ line = rec[:rec].collect { |r| encode_str(r) }.join('|')
453
+
454
+ # This doesn't actually 'delete' the line, it just
455
+ # makes it all spaces. That way, if the updated
456
+ # record is the same or less length than the old
457
+ # record, we can write the record back into the
458
+ # same spot. If the updated record is greater than
459
+ # the old record, we will leave the now spaced-out
460
+ # line and write the updated record at the end of
461
+ # the file.
462
+ write_record(table, fptr, rec[:fpos],
463
+ ' ' * rec[:line_length])
464
+ if line.length > rec[:line_length]
465
+ write_record(table, fptr, 'end', line)
466
+ incr_del_ctr(table, fptr)
467
+ else
468
+ write_record(table, fptr, rec[:fpos], line)
469
+ end
470
+ end
471
+ # Return the number of records updated.
472
+ return recs.size
473
+ end
474
+ end
475
+
476
+ #-----------------------------------------------------------------------
477
+ # delete_records
478
+ #-----------------------------------------------------------------------
479
+ #
480
+ def delete_records(table, recs)
481
+ with_write_locked_table(table) do |fptr|
482
+ recs.each do |rec|
483
+ # Go to offset within the file where the record is and
484
+ # replace it with all spaces.
485
+ write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
486
+ incr_del_ctr(table, fptr)
487
+ end
488
+
489
+ # Return the number of records deleted.
490
+ return recs.size
491
+ end
492
+ end
493
+
494
+ #-----------------------------------------------------------------------
495
+ # pack_table
496
+ #-----------------------------------------------------------------------
497
+ #
498
+ def pack_table(table)
499
+ with_write_lock(table) do
500
+ fptr = open(table.filename, 'r')
501
+ new_fptr = open(table.filename+'temp', 'w')
502
+
503
+ line = fptr.readline.chomp
504
+ # Reset the delete counter in the header rec to 0.
505
+ if line[0..0] == 'Z'
506
+ header_rec = unencrypt_str(line[1..-1]).split('|')
507
+ header_rec[1] = '000000'
508
+ new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
509
+ "\n")
510
+ else
511
+ header_rec = line.split('|')
512
+ header_rec[1] = '000000'
513
+ new_fptr.write(header_rec.join('|') + "\n")
514
+ end
515
+
516
+ lines_deleted = 0
517
+
518
+ begin
519
+ while true
520
+ line = fptr.readline
521
+
522
+ if table.encrypted?
523
+ temp_line = unencrypt_str(line)
524
+ else
525
+ temp_line = line
526
+ end
527
+
528
+ if temp_line.strip == ''
529
+ lines_deleted += 1
530
+ else
531
+ new_fptr.write(line)
532
+ end
533
+ end
534
+ # Here's how we break out of the loop...
535
+ rescue EOFError
536
+ end
537
+
538
+ # Close the table and release the write lock.
539
+ fptr.close
540
+ new_fptr.close
541
+ File.delete(table.filename)
542
+ FileUtils.mv(table.filename+'temp', table.filename)
543
+
544
+ # Return the number of deleted records that were removed.
545
+ return lines_deleted
546
+ end
547
+ end
548
+
549
+ #-----------------------------------------------------------------------
550
+ # PRIVATE METHODS
551
+ #-----------------------------------------------------------------------
552
+ private
553
+
554
+ #-----------------------------------------------------------------------
555
+ # with_table
556
+ #-----------------------------------------------------------------------
557
+ #
558
+ def with_table(table, access='r')
559
+ begin
560
+ yield fptr = open(table.filename, access)
561
+ ensure
562
+ fptr.close
563
+ end
564
+ end
565
+
566
+ #-----------------------------------------------------------------------
567
+ # with_write_lock
568
+ #-----------------------------------------------------------------------
569
+ #
570
+ def with_write_lock(table)
571
+ begin
572
+ write_lock(table.name) if @db.server?
573
+ yield
574
+ ensure
575
+ write_unlock(table.name) if @db.server?
576
+ end
577
+ end
578
+
579
+ #-----------------------------------------------------------------------
580
+ # with_write_locked_table
581
+ #-----------------------------------------------------------------------
582
+ #
583
+ def with_write_locked_table(table, access='r+')
584
+ begin
585
+ write_lock(table.name) if @db.server?
586
+ yield fptr = open(table.filename, access)
587
+ ensure
588
+ fptr.close
589
+ write_unlock(table.name) if @db.server?
590
+ end
591
+ end
592
+
593
+ #-----------------------------------------------------------------------
594
+ # write_lock
595
+ #-----------------------------------------------------------------------
596
+ #
597
+ def write_lock(tablename)
598
+ # Unless an key already exists in the hash holding mutex records
599
+ # for this table, create a write key for this table in the mutex
600
+ # hash. Then, place a lock on that mutex.
601
+ @mutex_hash[tablename] = Mutex.new unless (
602
+ @mutex_hash.has_key?(tablename))
603
+ @mutex_hash[tablename].lock
604
+
605
+ return true
606
+ end
607
+
608
+ #----------------------------------------------------------------------
609
+ # write_unlock
610
+ #----------------------------------------------------------------------
611
+ #
612
+ def write_unlock(tablename)
613
+ # Unlock the write mutex for this table.
614
+ @mutex_hash[tablename].unlock
615
+
616
+ return true
617
+ end
618
+
619
+ #----------------------------------------------------------------------
620
+ # write_record
621
+ #----------------------------------------------------------------------
622
+ #
623
+ def write_record(table, fptr, pos, record)
624
+ if table.encrypted?
625
+ temp_rec = encrypt_str(record)
626
+ else
627
+ temp_rec = record
628
+ end
629
+
630
+ # If record is to be appended, go to end of table and write
631
+ # record, adding newline character.
632
+ if pos == 'end'
633
+ fptr.seek(0, IO::SEEK_END)
634
+ fptr.write(temp_rec + "\n")
635
+ else
636
+ # Otherwise, overwrite another record (that's why we don't
637
+ # add the newline character).
638
+ fptr.seek(pos)
639
+ fptr.write(temp_rec)
640
+ end
641
+ end
642
+
643
+ #----------------------------------------------------------------------
644
+ # write_header_record
645
+ #----------------------------------------------------------------------
646
+ #
647
+ def write_header_record(table, fptr, record)
648
+ fptr.seek(0)
649
+
650
+ if table.encrypted?
651
+ fptr.write('Z' + encrypt_str(record) + "\n")
652
+ else
653
+ fptr.write(record + "\n")
654
+ end
655
+ end
656
+
657
+ #----------------------------------------------------------------------
658
+ # get_header_record
659
+ #----------------------------------------------------------------------
660
+ #
661
+ def get_header_record(table, fptr)
662
+ fptr.seek(0)
663
+
664
+ if table.encrypted?
665
+ return unencrypt_str(fptr.readline[1..-1].chomp)
666
+ else
667
+ return fptr.readline.chomp
668
+ end
669
+ end
670
+
671
+ #-----------------------------------------------------------------------
672
+ # reset_rec_no_ctr
673
+ #-----------------------------------------------------------------------
674
+ #
675
+ def reset_rec_no_ctr(table, fptr)
676
+ last_rec_no, rest_of_line = get_header_record(table, fptr).split(
677
+ '|', 2)
678
+ write_header_record(table, fptr, ['%06d' % 0, rest_of_line].join(
679
+ '|'))
680
+ return true
681
+ end
682
+
683
+ #-----------------------------------------------------------------------
684
+ # incr_rec_no_ctr
685
+ #-----------------------------------------------------------------------
686
+ #
687
+ def incr_rec_no_ctr(table, fptr)
688
+ last_rec_no, rest_of_line = get_header_record(table, fptr).split(
689
+ '|', 2)
690
+ last_rec_no = last_rec_no.to_i + 1
691
+
692
+ write_header_record(table, fptr, ['%06d' % last_rec_no,
693
+ rest_of_line].join('|'))
694
+
695
+ # Return the new recno.
696
+ return last_rec_no
697
+ end
698
+
699
+ #-----------------------------------------------------------------------
700
+ # incr_del_ctr
701
+ #-----------------------------------------------------------------------
702
+ #
703
+ def incr_del_ctr(table, fptr)
704
+ last_rec_no, del_ctr, rest_of_line = get_header_record(table,
705
+ fptr).split('|', 3)
706
+ del_ctr = del_ctr.to_i + 1
707
+
708
+ write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
709
+ rest_of_line].join('|'))
710
+
711
+ return true
712
+ end
713
+
714
+ #-----------------------------------------------------------------------
715
+ # encrypt_str
716
+ #-----------------------------------------------------------------------
717
+ #++
718
+ # Returns an encrypted string, using the Vignere Cipher.
719
+ #
720
+ # *s*:: String to encrypt.
721
+ #
722
+ def encrypt_str(s)
723
+ new_str = ''
724
+ i_key = -1
725
+ s.each_byte do |c|
726
+ if i_key < EN_KEY_LEN - 1
727
+ i_key += 1
728
+ else
729
+ i_key = 0
730
+ end
731
+
732
+ if EN_STR.index(c.chr).nil?
733
+ new_str << c.chr
734
+ next
735
+ end
736
+
737
+ i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
738
+ i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
739
+ new_str << EN_STR[i_from_str]
740
+ end
741
+ return new_str
742
+ end
743
+
744
+ #-----------------------------------------------------------------------
745
+ # unencrypt_str
746
+ #-----------------------------------------------------------------------
747
+ #++
748
+ # Returns an unencrypted string, using the Vignere Cipher.
749
+ #
750
+ # *s*:: String to unencrypt.
751
+ #
752
+ def unencrypt_str(s)
753
+ new_str = ''
754
+ i_key = -1
755
+ s.each_byte do |c|
756
+ if i_key < EN_KEY_LEN - 1
757
+ i_key += 1
758
+ else
759
+ i_key = 0
760
+ end
761
+
762
+ if EN_STR.index(c.chr).nil?
763
+ new_str << c.chr
764
+ next
765
+ end
766
+
767
+ i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
768
+ i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
769
+ new_str << EN_STR[i_from_str]
770
+ end
771
+ return new_str
772
+ end
773
+
774
+ #-----------------------------------------------------------------------
775
+ # encode_str
776
+ #-----------------------------------------------------------------------
777
+ #++
778
+ # Replace characters in string that can cause problems when storing.
779
+ #
780
+ # *s*:: String to be encoded.
781
+ #
782
+ def encode_str(s)
783
+ if s =~ ENCODE_RE
784
+ return s.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub(
785
+ "\r", '&carriage_return;').gsub("\032", '&substitute;').gsub(
786
+ "|", '&pipe;')
787
+ else
788
+ return s
789
+ end
790
+ end
791
+ end
792
+
793
+ #---------------------------------------------------------------------------
794
+ # KBTable
795
+ #---------------------------------------------------------------------------
796
+ class KBTable
797
+ include DRb::DRbUndumped
798
+
799
+ # Make constructor private. KBTable instances should only be created
800
+ # from KirbyBase#get_table.
801
+ private_class_method :new
802
+
803
+ # Regular expression used to determine if field needs to be
804
+ # un-encoded.
805
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
806
+
807
+ TYPE_CONV = { :Integer => :Integer, :Float => :Float,
808
+ :String => :unencode_str, :Boolean => :str_to_bool,
809
+ :Date => :str_to_date, :DateTime => :str_to_datetime }
810
+
811
+ attr_reader :filename, :name
812
+
813
+ #-----------------------------------------------------------------------
814
+ # KBTable.valid_field_type
815
+ #-----------------------------------------------------------------------
816
+ #++
817
+ # Return true if valid field type.
818
+ #
819
+ # *field_type*:: Symbol specifying field type.
820
+ #
821
+ def KBTable.valid_field_type?(field_type)
822
+ TYPE_CONV.key?(field_type)
823
+ end
824
+
825
+ #-----------------------------------------------------------------------
826
+ # create_called_from_database_instance
827
+ #-----------------------------------------------------------------------
828
+ #++
829
+ # Return a new instance of KBTable. Should never be called directly by
830
+ # your application. Should only be called from KirbyBase#get_table.
831
+ #
832
+ # *db*:: KirbyBase instance.
833
+ # *name*:: Symbol specifying table name.
834
+ # *filename*:: String specifying filename of physical file that holds
835
+ # table.
836
+ #
837
+ def KBTable.create_called_from_database_instance(db, name, filename)
838
+ return new(db, name, filename)
839
+ end
840
+
841
+ # This has been declared private so user's cannot create new instances
842
+ # of KBTable from their application. A user gets a handle to a KBTable
843
+ # instance by calling KirbyBase#get_table for an existing table or
844
+ # KirbyBase#create_table for a new table.
845
+ def initialize(db, name, filename)
846
+ @db = db
847
+ @name = name
848
+ @filename = filename
849
+ @encrypted = false
850
+
851
+ # Alias delete_all to clear method.
852
+ alias delete_all clear
853
+
854
+ update_header_vars
855
+ end
856
+
857
+ #-----------------------------------------------------------------------
858
+ # encrypted?
859
+ #-----------------------------------------------------------------------
860
+ #++
861
+ # Returns true if table is encrypted.
862
+ #
863
+ def encrypted?
864
+ if @encrypted
865
+ return true
866
+ else
867
+ return false
868
+ end
869
+ end
870
+
871
+ #-----------------------------------------------------------------------
872
+ # field_names
873
+ #-----------------------------------------------------------------------
874
+ #++
875
+ # Return array containing table field names.
876
+ #
877
+ def field_names
878
+ update_header_vars
879
+ return @field_names
880
+ end
881
+
882
+ #-----------------------------------------------------------------------
883
+ # field_types
884
+ #-----------------------------------------------------------------------
885
+ #++
886
+ # Return array containing table field types.
887
+ #
888
+ def field_types
889
+ update_header_vars
890
+ return @field_types
891
+ end
892
+
893
+ #-----------------------------------------------------------------------
894
+ # insert
895
+ #-----------------------------------------------------------------------
896
+ #++
897
+ # Insert a new record into a table, return unique record number.
898
+ #
899
+ # *data*:: Array, Hash, Struct instance containing field values of
900
+ # new record.
901
+ # *insert_proc*:: Proc instance containing insert code. This and the
902
+ # data parameter are mutually exclusive.
903
+ #
904
+ def insert(*data, &insert_proc)
905
+ raise 'Cannot specify both a hash/array/struct and a ' + \
906
+ 'proc for method #insert!' unless data.empty? or insert_proc.nil?
907
+
908
+ raise 'Must specify either hash/array/struct or insert ' + \
909
+ 'proc for method #insert!' if data.empty? and insert_proc.nil?
910
+
911
+ # Update the header variables.
912
+ update_header_vars
913
+
914
+ # Convert input, which could be an array, a hash, or a Struct
915
+ # into a common format (i.e. hash).
916
+ if data.empty?
917
+ input_rec = convert_input_data(insert_proc)
918
+ else
919
+ input_rec = convert_input_data(data)
920
+ end
921
+
922
+ # Check the field values to make sure they are proper types.
923
+ validate_input(input_rec)
924
+
925
+ return @db.engine.insert_record(self, @field_names.collect { |f|
926
+ input_rec.fetch(f, '')
927
+ })
928
+ end
929
+
930
+ #-----------------------------------------------------------------------
931
+ # update_all
932
+ #-----------------------------------------------------------------------
933
+ #++
934
+ # Return array of records (Structs) to be updated, in this case all
935
+ # records.
936
+ #
937
+ # *updates*:: Hash or Struct containing updates.
938
+ #
939
+ def update_all(*updates)
940
+ update(*updates) { true }
941
+ end
942
+
943
+ #-----------------------------------------------------------------------
944
+ # update
945
+ #-----------------------------------------------------------------------
946
+ #++
947
+ # Return array of records (Structs) to be updated based on select cond.
948
+ #
949
+ # *updates*:: Hash or Struct containing updates.
950
+ # *select_cond*:: Proc containing code to select records to update.
951
+ #
952
+ def update(*updates, &select_cond)
953
+ raise ArgumentError, "Must specify select condition code " + \
954
+ "block. To update all records, use #update_all instead." if \
955
+ select_cond.nil?
956
+
957
+ # Update the header variables.
958
+ update_header_vars
959
+
960
+ # Get all records that match the selection criteria and
961
+ # return them in an array.
962
+ result_set = get_matches(:update, @field_names, select_cond)
963
+
964
+ return result_set if updates.empty?
965
+
966
+ set(result_set, updates)
967
+ end
968
+
969
+ #-----------------------------------------------------------------------
970
+ # []=
971
+ #-----------------------------------------------------------------------
972
+ #++
973
+ # Update record whose recno field equals index.
974
+ #
975
+ # *index*:: Integer specifying recno you wish to select.
976
+ # *updates*:: Hash, Struct, or Array containing updates.
977
+ #
978
+ def []=(index, updates)
979
+ return update(updates) { |r| r.recno == index }
980
+ end
981
+
982
+ #-----------------------------------------------------------------------
983
+ # set
984
+ #-----------------------------------------------------------------------
985
+ #++
986
+ # Set fields of records to updated values. Returns number of records
987
+ # updated.
988
+ #
989
+ # *recs*:: Array of records (Structs) that will be updated.
990
+ # *data*:: Hash, Struct, Proc containing updates.
991
+ #
992
+ def set(recs, data)
993
+ # Update the header variables.
994
+ update_header_vars
995
+
996
+ # Convert updates, which could be an array, a hash, or a Struct
997
+ # into a common format (i.e. hash).
998
+ update_rec = convert_input_data(data)
999
+
1000
+ validate_input(update_rec)
1001
+
1002
+ updated_recs = []
1003
+ recs.each do |rec|
1004
+ updated_rec = {}
1005
+ updated_rec[:rec] = @field_names.collect { |f|
1006
+ if update_rec.has_key?(f)
1007
+ update_rec[f]
1008
+ else
1009
+ rec.send(f)
1010
+ end
1011
+ }
1012
+ updated_rec[:fpos] = rec.fpos
1013
+ updated_rec[:line_length] = rec.line_length
1014
+ updated_recs << updated_rec
1015
+ end
1016
+ @db.engine.update_records(self, updated_recs)
1017
+
1018
+ # Return the number of records updated.
1019
+ return recs.size
1020
+ end
1021
+
1022
+ #-----------------------------------------------------------------------
1023
+ # delete
1024
+ #-----------------------------------------------------------------------
1025
+ #++
1026
+ # Delete records from table and return # deleted.
1027
+ #
1028
+ # *select_cond*:: Proc containing code to select records.
1029
+ #
1030
+ def delete(&select_cond)
1031
+ raise ArgumentError, "Must specify select condition code " + \
1032
+ "block. To delete all records, use #clear instead." if \
1033
+ select_cond.nil?
1034
+
1035
+ # Update the header variables.
1036
+ update_header_vars
1037
+
1038
+ # Get all records that match the selection criteria and
1039
+ # return them in an array.
1040
+ result_set = get_matches(:delete, [], select_cond)
1041
+
1042
+ @db.engine.delete_records(self, result_set)
1043
+
1044
+ # Return the number of records deleted.
1045
+ return result_set.size
1046
+ end
1047
+
1048
+ #-----------------------------------------------------------------------
1049
+ # clear
1050
+ #-----------------------------------------------------------------------
1051
+ #++
1052
+ # Delete all records from table. You can also use #delete_all.
1053
+ #
1054
+ # *reset_recno_ctr*:: true/false specifying whether recno counter should
1055
+ # be reset to 0.
1056
+ #
1057
+ def clear(reset_recno_ctr=true)
1058
+ delete { true }
1059
+ pack
1060
+
1061
+ @db.engine.reset_recno_ctr if reset_recno_ctr
1062
+ end
1063
+
1064
+ #-----------------------------------------------------------------------
1065
+ # []
1066
+ #-----------------------------------------------------------------------
1067
+ #++
1068
+ # Return the record(s) whose recno field is included in index.
1069
+ #
1070
+ # *index*:: Array of Integer(s) specifying recno(s) you wish to select.
1071
+ #
1072
+ def [](*index)
1073
+ recs = select { |r| index.include?(r.recno) }
1074
+ if recs.size == 1
1075
+ return recs[0]
1076
+ else
1077
+ return recs
1078
+ end
1079
+ end
1080
+
1081
+ #-----------------------------------------------------------------------
1082
+ # select
1083
+ #-----------------------------------------------------------------------
1084
+ #++
1085
+ # Return array of records (Structs) matching select conditions.
1086
+ #
1087
+ # *filter*:: List of field names (Symbols) to include in result set.
1088
+ # *select_cond*:: Proc containing select code.
1089
+ #
1090
+ def select(*filter, &select_cond)
1091
+ # Declare these variables before the code block so they don't go
1092
+ # after the code block is done.
1093
+ result_set = []
1094
+
1095
+ # Update the header variables.
1096
+ update_header_vars
1097
+
1098
+ # Validate that all names in filter are valid field names.
1099
+ validate_filter(filter)
1100
+
1101
+ filter = @field_names if filter.empty?
1102
+
1103
+ # Get all records that match the selection criteria and
1104
+ # return them in an array of Struct instances.
1105
+ return get_matches(:select, filter, select_cond)
1106
+ end
1107
+
1108
+ #-----------------------------------------------------------------------
1109
+ # pack
1110
+ #-----------------------------------------------------------------------
1111
+ #++
1112
+ # Remove blank records from table, return total removed.
1113
+ #
1114
+ def pack
1115
+ return @db.engine.pack_table(self)
1116
+ end
1117
+
1118
+ #-----------------------------------------------------------------------
1119
+ # total_recs
1120
+ #-----------------------------------------------------------------------
1121
+ #++
1122
+ # Return total number of undeleted (blank) records in table.
1123
+ #
1124
+ def total_recs
1125
+ return @db.engine.get_total_recs(self)
1126
+ end
1127
+
1128
+ #-----------------------------------------------------------------------
1129
+ # import_csv
1130
+ #-----------------------------------------------------------------------
1131
+ #++
1132
+ # Import csv file into table.
1133
+ #
1134
+ # *csv_filename*:: filename of csv file to import.
1135
+ #
1136
+ def import_csv(csv_filename)
1137
+ type_convs = @field_types.collect { |x|
1138
+ TYPE_CONV[x]
1139
+ }
1140
+
1141
+ CSV.open(csv_filename, 'r') do |row|
1142
+ temp_row = []
1143
+ (0...@field_names.size-1).each { |x|
1144
+ if row[x].to_s == ''
1145
+ temp_row << nil
1146
+ else
1147
+ temp_row << send(type_convs[x+1], row[x].to_s)
1148
+ end
1149
+ }
1150
+ insert(*temp_row)
1151
+ end
1152
+ end
1153
+
1154
+ #-----------------------------------------------------------------------
1155
+ # PRIVATE METHODS
1156
+ #-----------------------------------------------------------------------
1157
+ private
1158
+
1159
+ #-----------------------------------------------------------------------
1160
+ # str_to_date
1161
+ #-----------------------------------------------------------------------
1162
+ #++
1163
+ # Convert a String to a Date.
1164
+ #
1165
+ # *s*:: String to be converted.
1166
+ #
1167
+ def str_to_date(s)
1168
+ # Convert a string to a date object. NOTE: THIS IS SLOW!!!!
1169
+ # If you can, just define any date fields in the database as
1170
+ # string fields. Queries will be much faster if you do.
1171
+ return Date.parse(s)
1172
+ end
1173
+
1174
+ #-----------------------------------------------------------------------
1175
+ # str_to_datetime
1176
+ #-----------------------------------------------------------------------
1177
+ #++
1178
+ # Convert a String to a DateTime.
1179
+ #
1180
+ # *s*:: String to be converted.
1181
+ #
1182
+ def str_to_datetime(s)
1183
+ # Convert a string to a datetime object. NOTE: THIS IS SLOW!!!!
1184
+ # If you can, just define any datetime fields in the database as
1185
+ # string fields. Queries will be much faster if you do.
1186
+ return DateTime.parse(s)
1187
+ end
1188
+
1189
+ #-----------------------------------------------------------------------
1190
+ # str_to_bool
1191
+ #-----------------------------------------------------------------------
1192
+ #++
1193
+ # Convert a String to a TrueClass or FalseClass.
1194
+ #
1195
+ # *s*:: String to be converted.
1196
+ #
1197
+ def str_to_bool(s)
1198
+ if s == 'false' or s.nil?
1199
+ return false
1200
+ else
1201
+ return true
1202
+ end
1203
+ end
1204
+
1205
+ #-----------------------------------------------------------------------
1206
+ # validate_filter
1207
+ #-----------------------------------------------------------------------
1208
+ #++
1209
+ # Check that filter contains valid field names.
1210
+ #
1211
+ # *filter*:: Array holding field names to include in result set.
1212
+ #
1213
+ def validate_filter(filter)
1214
+ # Each field in the filter array must be a valid fieldname in the
1215
+ # table.
1216
+ filter.each { |x|
1217
+ raise "Invalid field name: #{x}" unless (
1218
+ @field_names.include?(x))
1219
+ }
1220
+ end
1221
+
1222
+ #-----------------------------------------------------------------------
1223
+ # convert_input_data
1224
+ #-----------------------------------------------------------------------
1225
+ #++
1226
+ # Convert data passed to #input, #update, or #set to a common format.
1227
+ #
1228
+ # *values*:: Proc, user class, hash, or array holding input values.
1229
+ #
1230
+ def convert_input_data(values)
1231
+ if values.class == Proc
1232
+ tbl_struct = Struct.new(*@field_names[1..-1])
1233
+ tbl_rec = tbl_struct.new
1234
+ begin
1235
+ values.call(tbl_rec)
1236
+ rescue NoMethodError
1237
+ raise "Invalid field name in code block: %s" % $!
1238
+ end
1239
+ temp_hash = {}
1240
+ @field_names[1..-1].collect { |f|
1241
+ temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
1242
+ }
1243
+ return temp_hash
1244
+ elsif values[0].class.to_s == @record_class
1245
+ temp_hash = {}
1246
+ @field_names[1..-1].collect { |f|
1247
+ temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
1248
+ }
1249
+ return temp_hash
1250
+ elsif values[0].class == Hash
1251
+ return values[0].dup
1252
+ elsif values[0].kind_of?(Struct)
1253
+ temp_hash = {}
1254
+ @field_names[1..-1].collect { |f|
1255
+ temp_hash[f] = values[0][f] if values[0].members.include?(
1256
+ f.to_s)
1257
+ }
1258
+ return temp_hash
1259
+ elsif values[0].class == Array
1260
+ raise ArgumentError, "Must specify all fields in input " + \
1261
+ "array." unless values[0].size == @field_names[1..-1].size
1262
+ temp_hash = {}
1263
+ @field_names[1..-1].collect { |f|
1264
+ temp_hash[f] = values[0][@field_names.index(f)-1]
1265
+ }
1266
+ return temp_hash
1267
+ elsif values.class == Array
1268
+ raise ArgumentError, "Must specify all fields in input " + \
1269
+ "array." unless values.size == @field_names[1..-1].size
1270
+ temp_hash = {}
1271
+ @field_names[1..-1].collect { |f|
1272
+ temp_hash[f] = values[@field_names.index(f)-1]
1273
+ }
1274
+ return temp_hash
1275
+ else
1276
+ raise(ArgumentError, 'Invalid type for values container.')
1277
+ end
1278
+ end
1279
+
1280
+ #-----------------------------------------------------------------------
1281
+ # validate_input
1282
+ #-----------------------------------------------------------------------
1283
+ #++
1284
+ # Check input data to ensure proper data types.
1285
+ #
1286
+ # *data*:: Hash of data values.
1287
+ #
1288
+ def validate_input(data)
1289
+ raise "Cannot insert/update recno field!" if data.has_key?(:recno)
1290
+
1291
+ @field_names[1..-1].each do |f|
1292
+ next unless data.has_key?(f)
1293
+
1294
+ next if data[f].nil?
1295
+ case @field_types[@field_names.index(f)]
1296
+ when :String
1297
+ raise "Invalid String value for: %s" % f unless \
1298
+ data[f].respond_to?(:to_str)
1299
+ when :Boolean
1300
+ raise "Invalid Boolean value for: %s" % f unless \
1301
+ data[f].kind_of?(TrueClass) or data[f].kind_of?(FalseClass)
1302
+ when :Integer
1303
+ raise "Invalid Integer value for: %s" % f unless \
1304
+ data[f].respond_to?(:to_int)
1305
+ when :Float
1306
+ raise "Invalid Float value for: %s" % f unless \
1307
+ data[f].class == Float
1308
+ when :Date
1309
+ raise "Invalid Date value for: %s" % f unless \
1310
+ data[f].class == Date
1311
+ when :DateTime
1312
+ raise "Invalid DateTime value for: %s" % f unless \
1313
+ data[f].class == DateTime
1314
+ end
1315
+ end
1316
+ end
1317
+
1318
+ #-----------------------------------------------------------------------
1319
+ # update_header_vars
1320
+ #-----------------------------------------------------------------------
1321
+ #++
1322
+ # Read header record and update instance variables.
1323
+ #
1324
+ def update_header_vars
1325
+ @encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \
1326
+ @field_types = @db.engine.get_header_vars(self)
1327
+ end
1328
+
1329
+ #-----------------------------------------------------------------------
1330
+ # get_matches
1331
+ #-----------------------------------------------------------------------
1332
+ #++
1333
+ # Return records from table that match select condition.
1334
+ #
1335
+ # *query_type*:: Symbol specifying type of query (:select, :update,
1336
+ # or :delete).
1337
+ # *filter*:: Array of field names to include in each record of result
1338
+ # set.
1339
+ # *select_cond*:: Proc containing select condition.
1340
+ #
1341
+ def get_matches(query_type, filter, select_cond)
1342
+ tbl_struct = Struct.new(*@field_names)
1343
+ case query_type
1344
+ when :select
1345
+ if @record_class == 'Struct'
1346
+ result_struct = Struct.new(*filter)
1347
+ end
1348
+ when :update
1349
+ result_struct = Struct.new(*(filter + [:fpos, :line_length]))
1350
+ when :delete
1351
+ result_struct = Struct.new(:fpos, :line_length)
1352
+ end
1353
+
1354
+ # Create an empty array to hold any matches found.
1355
+ matchList = KBResultSet.new(self, filter,
1356
+ filter.collect { |f| @field_types[@field_names.index(f)] })
1357
+
1358
+ type_convs = @field_types.collect { |x| TYPE_CONV[x] }
1359
+
1360
+ # Loop through table.
1361
+ @db.engine.get_recs(self).each do |rec|
1362
+ tbl_rec = tbl_struct.new(*(0...@field_names.size).collect do |i|
1363
+ if rec[i] == ''
1364
+ nil
1365
+ else
1366
+ send(type_convs[i], rec[i])
1367
+ end
1368
+ end)
1369
+
1370
+ next unless select_cond.call(tbl_rec) unless select_cond.nil?
1371
+
1372
+ if query_type != :select or @record_class == 'Struct'
1373
+ result_rec = result_struct.new(*filter.collect { |f|
1374
+ tbl_rec.send(f) })
1375
+ else
1376
+ if Object.const_get(@record_class).respond_to?(:kb_defaults)
1377
+ result_rec = Object.const_get(@record_class).new(
1378
+ *@field_names.collect { |f|
1379
+ tbl_rec.send(f) || Object.const_get(@record_class
1380
+ ).kb_defaults[@field_names.index(f)]
1381
+ }
1382
+ )
1383
+ else
1384
+ result_rec = Object.const_get(@record_class).kb_create(
1385
+ *@field_names.collect { |f|
1386
+ # Just a warning here: If you specify a filter on
1387
+ # a select, you are only going to get those fields
1388
+ # you specified in the result set, EVEN IF
1389
+ # record_class is a custom class instead of Struct.
1390
+ if filter.include?(f)
1391
+ tbl_rec.send(f)
1392
+ else
1393
+ nil
1394
+ end
1395
+ }
1396
+ )
1397
+ end
1398
+ end
1399
+
1400
+ unless query_type == :select
1401
+ result_rec.fpos = rec[-2]
1402
+ result_rec.line_length = rec[-1]
1403
+ end
1404
+ matchList << result_rec
1405
+ end
1406
+ return matchList
1407
+ end
1408
+
1409
+ #-----------------------------------------------------------------------
1410
+ # unencode_str
1411
+ #-----------------------------------------------------------------------
1412
+ #++
1413
+ # Return string to unencoded format.
1414
+ #
1415
+ # *s*:: String to be unencoded.
1416
+ #
1417
+ def unencode_str(s)
1418
+ if s =~ UNENCODE_RE
1419
+ return s.gsub('&linefeed;', "\n").gsub(
1420
+ '&carriage_return;', "\r").gsub('&substitute;', "\032").gsub(
1421
+ '&pipe;', "|").gsub('&amp;', "&")
1422
+ else
1423
+ return s
1424
+ end
1425
+ end
1426
+ end
1427
+
1428
+
1429
+ #---------------------------------------------------------------------------
1430
+ # KBResult
1431
+ #---------------------------------------------------------------------------
1432
+ class KBResultSet < Array
1433
+ #-----------------------------------------------------------------------
1434
+ # KBResultSet.reverse
1435
+ #-----------------------------------------------------------------------
1436
+ #
1437
+ def KBResultSet.reverse(sort_field)
1438
+ return [sort_field, :desc]
1439
+ end
1440
+
1441
+ #-----------------------------------------------------------------------
1442
+ # initialize
1443
+ #-----------------------------------------------------------------------
1444
+ #
1445
+ def initialize(table, filter, filter_types, *args)
1446
+ @table = table
1447
+ @filter = filter
1448
+ @filter_types = filter_types
1449
+ super(*args)
1450
+ end
1451
+
1452
+ #-----------------------------------------------------------------------
1453
+ # to_ary
1454
+ #-----------------------------------------------------------------------
1455
+ #
1456
+ def to_ary
1457
+ to_a
1458
+ end
1459
+
1460
+ #-----------------------------------------------------------------------
1461
+ # set
1462
+ #-----------------------------------------------------------------------
1463
+ #++
1464
+ # Update record(s) in table, return number of records updated.
1465
+ #
1466
+ def set(*updates, &update_cond)
1467
+ raise 'Cannot specify both a hash and a proc for method #set!' \
1468
+ unless updates.empty? or update_cond.nil?
1469
+
1470
+ raise 'Must specify update proc or hash for method #set!' if \
1471
+ updates.empty? and update_cond.nil?
1472
+
1473
+ if updates.empty?
1474
+ @table.set(self, update_cond)
1475
+ else
1476
+ @table.set(self, updates)
1477
+ end
1478
+ end
1479
+
1480
+ #-----------------------------------------------------------------------
1481
+ # sort
1482
+ #-----------------------------------------------------------------------
1483
+ #
1484
+ def sort(*sort_fields)
1485
+ sort_fields_arrs = []
1486
+ sort_fields.each do |f|
1487
+ if f.to_s[0..0] == '-'
1488
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
1489
+ elsif f.to_s[0..0] == '+'
1490
+ sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
1491
+ else
1492
+ sort_fields_arrs << [f, :asc]
1493
+ end
1494
+ end
1495
+
1496
+ sort_fields_arrs.each do |f|
1497
+ raise "Invalid sort field" unless @filter.include?(f[0])
1498
+ end
1499
+
1500
+ super() { |a,b|
1501
+ x = []
1502
+ y = []
1503
+ sort_fields_arrs.each do |s|
1504
+ if [:Integer, :Float].include?(
1505
+ @filter_types[@filter.index(s[0])])
1506
+ a_value = a.send(s[0]) || 0
1507
+ b_value = b.send(s[0]) || 0
1508
+ else
1509
+ a_value = a.send(s[0])
1510
+ b_value = b.send(s[0])
1511
+ end
1512
+ if s[1] == :desc
1513
+ x << b_value
1514
+ y << a_value
1515
+ else
1516
+ x << a_value
1517
+ y << b_value
1518
+ end
1519
+ end
1520
+ x <=> y
1521
+ }
1522
+ end
1523
+
1524
+ #-----------------------------------------------------------------------
1525
+ # to_report
1526
+ #-----------------------------------------------------------------------
1527
+ #
1528
+ def to_report(recs_per_page=0, print_rec_sep=false)
1529
+ result = collect { |r| @filter.collect {|f| r.send(f)} }
1530
+
1531
+ # How many records before a formfeed.
1532
+ delim = ' | '
1533
+
1534
+ # columns of physical rows
1535
+ columns = [@filter].concat(result).transpose
1536
+
1537
+ max_widths = columns.collect { |c|
1538
+ c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
1539
+ }
1540
+
1541
+ row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
1542
+ delim.length * (max_widths.size - 1))
1543
+
1544
+ justify_hash = { :String => :ljust, :Integer => :rjust,
1545
+ :Float => :rjust, :Boolean => :ljust, :Date => :ljust,
1546
+ :DateTime => :ljust }
1547
+
1548
+ header_line = @filter.zip(max_widths, @filter.collect { |f|
1549
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
1550
+ x.to_s.send(justify_hash[z], y) }.join(delim)
1551
+
1552
+ output = ''
1553
+ recs_on_page_cnt = 0
1554
+
1555
+ result.each do |row|
1556
+ if recs_on_page_cnt == 0
1557
+ output << header_line + "\n" << row_dashes + "\n"
1558
+ end
1559
+
1560
+ output << row.zip(max_widths, @filter.collect { |f|
1561
+ @filter_types[@filter.index(f)] }).collect { |x,y,z|
1562
+ x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
1563
+
1564
+ output << row_dashes + '\n' if print_rec_sep
1565
+ recs_on_page_cnt += 1
1566
+
1567
+ if recs_per_page > 0 and (recs_on_page_cnt ==
1568
+ num_recs_per_page)
1569
+ output << '\f'
1570
+ recs_on_page_count = 0
1571
+ end
1572
+ end
1573
+ return output
1574
+ end
1575
+ end
1576
+
1577
+
1578
+ #---------------------------------------------------------------------------
1579
+ # NilClass
1580
+ #---------------------------------------------------------------------------
1581
+ #
1582
+ class NilClass
1583
+ def method_missing(method_id, stuff)
1584
+ return false
1585
+ end
1586
+ end
1587
+
1588
+ #---------------------------------------------------------------------------
1589
+ # Symbol
1590
+ #---------------------------------------------------------------------------
1591
+ #
1592
+ class Symbol
1593
+ def -@
1594
+ ("-"+self.to_s).to_sym
1595
+ end
1596
+
1597
+ def +@
1598
+ ("+"+self.to_s).to_sym
1599
+ end
1600
+ end
1601
+