og 0.17.0 → 0.18.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.
@@ -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
+