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.
- data/CHANGELOG +81 -0
- data/README +1 -0
- data/doc/AUTHORS +9 -6
- data/doc/RELEASES +24 -1
- data/lib/og.rb +8 -2
- data/lib/og/collection.rb +38 -22
- data/lib/og/entity.rb +15 -0
- data/lib/og/manager.rb +52 -10
- data/lib/og/mixin/hierarchical.rb +3 -4
- data/lib/og/mixin/orderable.rb +3 -2
- data/lib/og/mixin/timestamped.rb +2 -0
- data/lib/og/relation.rb +6 -1
- data/lib/og/relation/has_many.rb +6 -0
- data/lib/og/relation/joins_many.rb +5 -0
- data/lib/og/store/kirby.rb +280 -0
- data/lib/og/store/kirby/README +6 -0
- data/lib/og/store/kirby/kirbybase.rb +1601 -0
- data/lib/og/store/kirby/readme.txt +63 -0
- data/lib/og/store/madeleine.rb +0 -2
- data/lib/og/store/mysql.rb +5 -5
- data/lib/og/store/psql.rb +15 -7
- data/lib/og/store/sql.rb +50 -23
- data/lib/og/store/sqlite.rb +16 -12
- data/lib/og/store/sqlserver.rb +7 -3
- data/test/og/tc_store.rb +19 -6
- metadata +8 -3
data/lib/og/mixin/timestamped.rb
CHANGED
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
|
|
data/lib/og/relation/has_many.rb
CHANGED
@@ -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,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("&", '&').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('&', "&")
|
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
|
+
|