def build_header_field_string(field_name_def, field_type_def)
temp_field_def = field_name_def.to_s + ':'
if field_type_def.is_a?(Hash)
raise 'Missing :DataType key in field_type hash!' unless \
field_type_def.has_key?(:DataType)
temp_type = field_type_def[:DataType]
raise 'Invalid field type: %s' % temp_type unless \
KBTable.valid_field_type?(temp_type)
temp_field_def += field_type_def[:DataType].to_s
if field_type_def.has_key?(:Key)
temp_field_def += ':Key->true'
end
if field_type_def.has_key?(:Index)
raise 'Invalid field type for index: %s' % temp_type \
unless KBTable.valid_index_type?(temp_type)
temp_field_def += ':Index->' + field_type_def[:Index].to_s
end
if field_type_def.has_key?(:Default)
raise 'Cannot set default value for this type: ' + \
'%s' % temp_type unless KBTable.valid_default_type?(
temp_type)
unless field_type_def[:Default].nil?
raise 'Invalid default value ' + \
'%s for column %s' % [field_type_def[:Default],
field_name_def] unless KBTable.valid_data_type?(
temp_type, field_type_def[:Default])
temp_field_def += ':Default->' + \
convert_to_encoded_string(temp_type,
field_type_def[:Default])
end
end
if field_type_def.has_key?(:Required)
raise 'Required must be true or false!' unless \
[true, false].include?(field_type_def[:Required])
temp_field_def += \
':Required->%s' % field_type_def[:Required]
end
if field_type_def.has_key?(:Lookup)
if field_type_def[:Lookup].is_a?(Array)
temp_field_def += \
':Lookup->%s.%s' % field_type_def[:Lookup]
else
tbl = get_table(field_type_def[:Lookup])
temp_field_def += \
':Lookup->%s.%s' % [field_type_def[:Lookup],
tbl.lookup_key]
end
elsif field_type_def.has_key?(:Link_many)
raise 'Field type for Link_many field must be :ResultSet' \
unless temp_type == :ResultSet
temp_field_def += \
':Link_many->%s=%s.%s' % field_type_def[:Link_many]
elsif field_type_def.has_key?(:Calculated)
temp_field_def += \
':Calculated->%s' % field_type_def[:Calculated]
end
else
if KBTable.valid_field_type?(field_type_def)
temp_field_def += field_type_def.to_s
elsif table_exists?(field_type_def)
tbl = get_table(field_type_def)
temp_field_def += \
'%s:Lookup->%s.%s' % [tbl.field_types[
tbl.field_names.index(tbl.lookup_key)], field_type_def,
tbl.lookup_key]
else
raise 'Invalid field type: %s' % field_type_def
end
end
return temp_field_def
end
def rename_table(old_tablename, new_tablename)
raise "Cannot rename table running in client mode!" if client?
raise "Table does not exist!" unless table_exists?(old_tablename)
raise(ArgumentError, 'Existing table name must be a symbol!') \
unless old_tablename.is_a?(Symbol)
raise(ArgumentError, 'New table name must be a symbol!') unless \
new_tablename.is_a?(Symbol)
raise "Table already exists!" if table_exists?(new_tablename)
@table_hash.delete(old_tablename)
@engine.rename_table(old_tablename, new_tablename)
get_table(new_tablename)
end
def drop_table(tablename)
raise(ArgumentError, 'Table name must be a symbol!') unless \
tablename.is_a?(Symbol)
raise "Table does not exist!" unless table_exists?(tablename)
@table_hash.delete(tablename)
return @engine.delete_table(tablename)
end
def table_exists?(tablename)
raise(ArgumentError, 'Table name must be a symbol!') unless \
tablename.is_a?(Symbol)
return @engine.table_exists?(tablename)
end
end
class KBEngine
include DRb::DRbUndumped
include KBTypeConversionsMixin
include KBEncryptionMixin
private_class_method :new
def KBEngine.create_called_from_database_instance(db)
return new(db)
end
def initialize(db)
@db = db
@recno_indexes = {}
@indexes = {}
@mutex_hash = {} if @db.server?
end
def init_recno_index(table)
return if recno_index_exists?(table)
with_write_locked_table(table) do |fptr|
@recno_indexes[table.name] = KBRecnoIndex.new(table)
@recno_indexes[table.name].rebuild(fptr)
end
end
def rebuild_recno_index(table)
with_write_locked_table(table) do |fptr|
@recno_indexes[table.name].rebuild(fptr)
end
end
def remove_recno_index(tablename)
@recno_indexes.delete(tablename)
end
def update_recno_index(table, recno, fpos)
@recno_indexes[table.name].update_index_rec(recno, fpos)
end
def recno_index_exists?(table)
@recno_indexes.include?(table.name)
end
def get_recno_index(table)
return @recno_indexes[table.name].get_idx
end
def init_index(table, index_fields)
return if index_exists?(table, index_fields)
with_write_locked_table(table) do |fptr|
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym] = \
KBIndex.new(table, index_fields)
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym
].rebuild(fptr)
end
end
def rebuild_index(table, index_fields)
with_write_locked_table(table) do |fptr|
@indexes["#{table.name}_#{index_fields.join('_')}".to_sym
].rebuild(fptr)
end
end
def remove_indexes(tablename)
re_table_name = Regexp.new(tablename.to_s)
@indexes.delete_if { |k,v| k.to_s =~ re_table_name }
end
def add_to_indexes(table, rec, fpos)
@recno_indexes[table.name].add_index_rec(rec.first, fpos)
re_table_name = Regexp.new(table.name.to_s)
@indexes.each_pair do |key, index|
index.add_index_rec(rec) if key.to_s =~ re_table_name
end
end
def delete_from_indexes(table, rec, fpos)
@recno_indexes[table.name].delete_index_rec(rec.recno)
re_table_name = Regexp.new(table.name.to_s)
@indexes.each_pair do |key, index|
index.delete_index_rec(rec.recno) if key.to_s =~ re_table_name
end
end
def update_to_indexes(table, rec)
re_table_name = Regexp.new(table.name.to_s)
@indexes.each_pair do |key, index|
index.update_index_rec(rec) if key.to_s =~ re_table_name
end
end
def index_exists?(table, index_fields)
@indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
end
def get_index(table, index_name)
return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
end
def get_index_timestamp(table, index_name)
return @indexes["#{table.name}_#{index_name}".to_sym].get_timestamp
end
def table_exists?(tablename)
return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
end
def tables
list = []
Dir.foreach(@db.path) { |filename|
list << File.basename(filename, '.*').to_sym if \
File.extname(filename) == @db.ext
}
return list
end
def new_table(name, field_defs, encrypt, record_class)
header_rec = ['000000', '000000', record_class, 'recno:Integer',
field_defs].join('|')
header_rec = 'Z' + encrypt_str(header_rec) if encrypt
begin
fptr = open(File.join(@db.path, name.to_s + @db.ext), 'w')
fptr.write(header_rec + "\n")
ensure
fptr.close
end
end
def delete_table(tablename)
with_write_lock(tablename) do
File.delete(File.join(@db.path, tablename.to_s + @db.ext))
remove_indexes(tablename)
remove_recno_index(tablename)
return true
end
end
def get_total_recs(table)
return get_recs(table).size
end
def reset_recno_ctr(table)
with_write_locked_table(table) do |fptr|
encrypted, header_line = get_header_record(table, fptr)
last_rec_no, rest_of_line = header_line.split('|', 2)
write_header_record(table, fptr,
['%06d' % 0, rest_of_line].join('|'))
return true
end
end
def get_header_vars(table)
with_table(table) do |fptr|
encrypted, line = get_header_record(table, fptr)
last_rec_no, del_ctr, record_class, *flds = line.split('|')
field_names = flds.collect { |x| x.split(':')[0].to_sym }
field_types = flds.collect { |x| x.split(':')[1].to_sym }
field_indexes = [nil] * field_names.size
field_defaults = [nil] * field_names.size
field_requireds = [false] * field_names.size
field_extras = [nil] * field_names.size
flds.each_with_index do |x,i|
field_extras[i] = {}
if x.split(':').size > 2
x.split(':')[2..-1].each do |y|
if y =~ /Index/
field_indexes[i] = y
elsif y =~ /Default/
field_defaults[i] = \
convert_to_native_type(field_types[i],
y.split('->')[1])
elsif y =~ /Required/
field_requireds[i] = \
convert_to_native_type(:Boolean,
y.split('->')[1])
else
field_extras[i][y.split('->')[0]] = \
y.split('->')[1]
end
end
end
end
return [encrypted, last_rec_no.to_i, del_ctr.to_i,
record_class, field_names, field_types, field_indexes,
field_defaults, field_requireds, field_extras]
end
end
def get_recs(table)
encrypted = table.encrypted?
recs = []
with_table(table) do |fptr|
begin
fptr.readline
while true
fpos = fptr.tell
rec, line_length = line_to_rec(fptr.readline, encrypted)
next if rec.empty?
rec << fpos << line_length
recs << rec
end
rescue EOFError
end
return recs
end
end
def get_recs_by_recno(table, recnos)
encrypted = table.encrypted?
recs = []
recno_idx = get_recno_index(table)
with_table(table) do |fptr|
fptr.readline
recnos.collect { |r| [recno_idx[r], r] }.sort.each do |r|
fptr.seek(r[0])
rec, line_length = line_to_rec(fptr.readline, encrypted)
next if rec.empty?
raise "Index Corrupt!" unless rec[0].to_i == r[1]
rec << r[0] << line_length
recs << rec
end
return recs
end
end
def get_rec_by_recno(table, recno)
encrypted = table.encrypted?
recno_idx = get_recno_index(table)
return nil unless recno_idx.has_key?(recno)
with_table(table) do |fptr|
fptr.seek(recno_idx[recno])
rec, line_length = line_to_rec(fptr.readline, encrypted)
raise "Recno Index Corrupt for table %s!" % table.name if \
rec.empty?
raise "Recno Index Corrupt for table %s!" % table.name unless \
rec[0].to_i == recno
rec << recno_idx[recno] << line_length
return rec
end
end
def line_to_rec(line, encrypted)
line.chomp!
line_length = line.length
line = unencrypt_str(line) if encrypted
line.strip!
return line.split('|', -1), line_length
end
def insert_record(table, rec)
with_write_locked_table(table) do |fptr|
rec_no = incr_rec_no_ctr(table, fptr)
rec[0] = rec_no
fptr.seek(0, IO::SEEK_END)
fpos = fptr.tell
write_record(table, fptr, 'end', rec.join('|'))
add_to_indexes(table, rec, fpos)
return rec_no
end
end
def update_records(table, recs)
with_write_locked_table(table) do |fptr|
recs.each do |rec|
line = rec[:rec].join('|')
write_record(table, fptr, rec[:fpos],
' ' * rec[:line_length])
if line.length > rec[:line_length]
fptr.seek(0, IO::SEEK_END)
new_fpos = fptr.tell
write_record(table, fptr, 'end', line)
incr_del_ctr(table, fptr)
update_recno_index(table, rec[:rec].first, new_fpos)
else
write_record(table, fptr, rec[:fpos], line)
end
update_to_indexes(table, rec[:rec])
end
return recs.size
end
end
def delete_records(table, recs)
with_write_locked_table(table) do |fptr|
recs.each do |rec|
write_record(table, fptr, rec.fpos, ' ' * rec.line_length)
incr_del_ctr(table, fptr)
delete_from_indexes(table, rec, rec.fpos)
end
return recs.size
end
end
def change_column_type(table, col_name, col_type)
col_index = table.field_names.index(col_name)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
temp_fields = header_rec[col_index+3].split(':')
temp_fields[1] = col_type.to_s
header_rec[col_index+3] = temp_fields.join(':')
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def rename_column(table, old_col_name, new_col_name)
col_index = table.field_names.index(old_col_name)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
temp_fields = header_rec[col_index+3].split(':')
temp_fields[0] = new_col_name.to_s
header_rec[col_index+3] = temp_fields.join(':')
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def add_column(table, field_def, after)
if after.nil? or table.field_names.last == after
insert_after = -1
else
insert_after = table.field_names.index(after)+1
end
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
if insert_after == -1
header_rec.insert(insert_after, field_def)
else
header_rec.insert(insert_after+3, field_def)
end
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
line = fptr.readline.chomp
if table.encrypted?
temp_line = unencrypt_str(line)
else
temp_line = line
end
rec = temp_line.split('|', -1)
rec.insert(insert_after, '')
if table.encrypted?
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
else
new_fptr.write(rec.join('|') + "\n")
end
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def drop_column(table, col_name)
col_index = table.field_names.index(col_name)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
header_rec.delete_at(col_index+3)
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
header_rec = line.split('|')
header_rec.delete_at(col_index+3)
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
line = fptr.readline.chomp
if table.encrypted?
temp_line = unencrypt_str(line)
else
temp_line = line
end
rec = temp_line.split('|', -1)
rec.delete_at(col_index)
if table.encrypted?
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
else
new_fptr.write(rec.join('|') + "\n")
end
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def rename_table(old_tablename, new_tablename)
old_full_path = File.join(@db.path, old_tablename.to_s + @db.ext)
new_full_path = File.join(@db.path, new_tablename.to_s + @db.ext)
File.rename(old_full_path, new_full_path)
end
def add_index(table, col_names, index_no)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
col_names.each do |c|
header_rec[table.field_names.index(c)+3] += \
':Index->%d' % index_no
end
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def drop_index(table, col_names)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
col_names.each do |c|
temp_field_def = \
header_rec[table.field_names.index(c)+3].split(':')
temp_field_def = temp_field_def.delete_if {|x|
x =~ /Index->/
}
header_rec[table.field_names.index(c)+3] = \
temp_field_def.join(':')
end
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def change_column_default_value(table, col_name, value)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
if header_rec[table.field_names.index(col_name)+3] =~ \
/Default->/
hr_chunks = \
header_rec[table.field_names.index(col_name)+3].split(':')
if value.nil?
hr_chunks = hr_chunks.delete_if { |x| x =~ /Default->/ }
header_rec[table.field_names.index(col_name)+3] = \
hr_chunks.join(':')
else
hr_chunks.collect! do |x|
if x =~ /Default->/
'Default->%s' % value
else
x
end
end
header_rec[table.field_names.index(col_name)+3] = \
hr_chunks.join(':')
end
else
if value.nil?
else
header_rec[table.field_names.index(col_name)+3] += \
':Default->%s' % value
end
end
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def change_column_required(table, col_name, required)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
else
header_rec = line.split('|')
end
if header_rec[table.field_names.index(col_name)+3
] =~ /Required->/
hr_chunks = \
header_rec[table.field_names.index(col_name)+3].split(':')
if not required
hr_chunks = hr_chunks.delete_if {|x| x =~ /Required->/}
header_rec[table.field_names.index(col_name)+3] = \
hr_chunks.join(':')
else
hr_chunks.collect! do |x|
if x =~ /Required->/
'Default->%s' % required
else
x
end
end
header_rec[table.field_names.index(col_name)+3] = \
hr_chunks.join(':')
end
else
if not required
else
header_rec[table.field_names.index(col_name)+3] += \
':Required->%s' % required
end
end
if line[0..0] == 'Z'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
new_fptr.write(fptr.readline)
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
end
end
def pack_table(table)
with_write_lock(table.name) do
fptr = open(table.filename, 'r')
new_fptr = open(table.filename+'temp', 'w')
line = fptr.readline.chomp
if line[0..0] == 'Z'
header_rec = unencrypt_str(line[1..-1]).split('|')
header_rec[1] = '000000'
new_fptr.write('Z' + encrypt_str(header_rec.join('|')) +
"\n")
else
header_rec = line.split('|')
header_rec[1] = '000000'
new_fptr.write(header_rec.join('|') + "\n")
end
lines_deleted = 0
begin
while true
line = fptr.readline
if table.encrypted?
temp_line = unencrypt_str(line)
else
temp_line = line
end
if temp_line.strip == ''
lines_deleted += 1
else
new_fptr.write(line)
end
end
rescue EOFError
end
fptr.close
new_fptr.close
File.delete(table.filename)
FileUtils.mv(table.filename+'temp', table.filename)
return lines_deleted
end
end
def read_memo_file(filepath)
begin
f = File.new(File.join(@db.memo_blob_path, filepath))
return f.read
ensure
f.close
end
end
def write_memo_file(filepath, contents)
begin
f = File.new(File.join(@db.memo_blob_path, filepath), 'w')
f.write(contents)
ensure
f.close
end
end
def read_blob_file(filepath)
begin
f = File.new(File.join(@db.memo_blob_path, filepath), 'rb')
return f.read
ensure
f.close
end
end
def write_blob_file(filepath, contents)
begin
f = File.new(File.join(@db.memo_blob_path, filepath), 'wb')
f.write(contents)
ensure
f.close
end
end
private
def with_table(table, access='r')
begin
yield fptr = open(table.filename, access)
ensure
fptr.close
end
end
def with_write_lock(tablename)
begin
write_lock(tablename) if @db.server?
yield
ensure
write_unlock(tablename) if @db.server?
end
end
def with_write_locked_table(table, access='r+')
begin
write_lock(table.name) if @db.server?
yield fptr = open(table.filename, access)
ensure
fptr.close
write_unlock(table.name) if @db.server?
end
end
def write_lock(tablename)
@mutex_hash[tablename] = Mutex.new unless (
@mutex_hash.has_key?(tablename))
@mutex_hash[tablename].lock
return true
end
def write_unlock(tablename)
@mutex_hash[tablename].unlock
return true
end
def write_record(table, fptr, pos, record)
if table.encrypted?
temp_rec = encrypt_str(record)
else
temp_rec = record
end
if pos == 'end'
fptr.seek(0, IO::SEEK_END)
fptr.write(temp_rec + "\n")
else
fptr.seek(pos)
fptr.write(temp_rec)
end
end
def write_header_record(table, fptr, record)
fptr.seek(0)
if table.encrypted?
fptr.write('Z' + encrypt_str(record) + "\n")
else
fptr.write(record + "\n")
end
end
def get_header_record(table, fptr)
fptr.seek(0)
line = fptr.readline.chomp
if line[0..0] == 'Z'
return [true, unencrypt_str(line[1..-1])]
else
return [false, line]
end
end
def incr_rec_no_ctr(table, fptr)
encrypted, header_line = get_header_record(table, fptr)
last_rec_no, rest_of_line = header_line.split('|', 2)
last_rec_no = last_rec_no.to_i + 1
write_header_record(table, fptr, ['%06d' % last_rec_no,
rest_of_line].join('|'))
return last_rec_no
end
def incr_del_ctr(table, fptr)
encrypted, header_line = get_header_record(table, fptr)
last_rec_no, del_ctr, rest_of_line = header_line.split('|', 3)
del_ctr = del_ctr.to_i + 1
write_header_record(table, fptr, [last_rec_no, '%06d' % del_ctr,
rest_of_line].join('|'))
return true
end
end
class KBTable
include DRb::DRbUndumped
include KBTypeConversionsMixin
private_class_method :new
VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
:DateTime, :Memo, :Blob, :ResultSet, :YAML]
VALID_DEFAULT_TYPES = [:String, :Integer, :Float, :Boolean, :Date,
:Time, :DateTime, :YAML]
VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
:DateTime]
attr_reader :filename, :name, :table_class, :db, :lookup_key, \
:last_rec_no, :del_ctr
def KBTable.valid_field_type?(field_type)
VALID_FIELD_TYPES.include?(field_type)
end
def KBTable.valid_data_type?(data_type, value)
case data_type
when /:String|:Blob/
return false unless value.respond_to?(:to_str)
when :Memo
return false unless value.is_a?(KBMemo)
when :Blob
return false unless value.is_a?(KBBlob)
when :Boolean
return false unless value.is_a?(TrueClass) or value.is_a?(
FalseClass)
when :Integer
return false unless value.respond_to?(:to_int)
when :Float
return false unless value.respond_to?(:to_f)
when :Time
return false unless value.is_a?(Time)
when :Date
return false unless value.is_a?(Date)
when :DateTime
return false unless value.is_a?(DateTime)
when :YAML
return false unless value.respond_to?(:to_yaml)
end
return true
end
def KBTable.valid_default_type?(field_type)
VALID_DEFAULT_TYPES.include?(field_type)
end
def KBTable.valid_index_type?(field_type)
VALID_INDEX_TYPES.include?(field_type)
end
def KBTable.create_called_from_database_instance(db, name, filename)
return new(db, name, filename)
end
def initialize(db, name, filename)
@db = db
@name = name
@filename = filename
@encrypted = false
@lookup_key = :recno
@idx_timestamps = {}
@idx_arrs = {}
alias delete_all clear
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def encrypted?
if @encrypted
return true
else
return false
end
end
def field_names
return @field_names
end
def field_types
return @field_types
end
def field_extras
return @field_extras
end
def field_indexes
return @field_indexes
end
def field_defaults
return @field_defaults
end
def field_requireds
return @field_requireds
end
def insert(*data, &insert_proc)
raise 'Cannot specify both a hash/array/struct and a ' + \
'proc for method #insert!' unless data.empty? or insert_proc.nil?
raise 'Must specify either hash/array/struct or insert ' + \
'proc for method #insert!' if data.empty? and insert_proc.nil?
update_header_vars
if data.empty?
input_rec = convert_input_data(insert_proc)
else
input_rec = convert_input_data(data)
end
validate_input(input_rec)
input_rec = Struct.new(*field_names).new(*field_names.zip(
@field_defaults).collect do |fn, fd|
if input_rec.has_key?(fn)
input_rec[fn]
else
fd
end
end)
check_required_fields(input_rec)
check_against_input_for_specials(input_rec)
new_recno = @db.engine.insert_record(self, @field_names.zip(
@field_types).collect do |fn, ft|
convert_to_encoded_string(ft, input_rec[fn])
end)
input_rec.each { |r| r.write_to_file if r.is_a?(KBMemo) } if \
@field_types.include?(:Memo)
input_rec.each { |r| r.write_to_file if r.is_a?(KBBlob) } if \
@field_types.include?(:Blob)
return new_recno
end
def update_all(*updates, &update_proc)
raise 'Cannot specify both a hash/array/struct and a ' + \
'proc for method #update_all!' unless updates.empty? or \
update_proc.nil?
raise 'Must specify either hash/array/struct or update ' + \
'proc for method #update_all!' if updates.empty? and \
update_proc.nil?
if updates.empty?
update { true }.set &update_proc
else
update(*updates) { true }
end
end
def update(*updates, &select_cond)
raise ArgumentError, "Must specify select condition code " + \
"block. To update all records, use #update_all instead." if \
select_cond.nil?
update_header_vars
result_set = get_matches(:update, @field_names, select_cond)
return result_set if updates.empty?
set(result_set, updates)
end
def []=(index, updates)
return update(updates) { |r| r.recno == index }
end
def set(recs, data)
update_rec = convert_input_data(data) unless data.is_a?(Proc)
updated_recs = []
recs.each do |rec|
temp_rec = rec.dup
if data.is_a?(Proc)
begin
data.call(temp_rec)
rescue NoMethodError
raise 'Invalid field name in code block: %s' % $!
end
else
@field_names.each { |fn| temp_rec[fn] = update_rec.fetch(fn,
temp_rec.send(fn)) }
end
raise 'Cannot update recno field!' unless \
rec.recno == temp_rec.recno
raise 'Cannot update internal fpos field!' unless \
rec.fpos == temp_rec.fpos
raise 'Cannot update internal line_length field!' unless \
rec.line_length == temp_rec.line_length
validate_input(temp_rec)
check_required_fields(temp_rec)
check_against_input_for_specials(temp_rec)
updated_recs << { :rec => @field_names.zip(@field_types
).collect { |fn, ft| convert_to_encoded_string(ft,
temp_rec.send(fn)) }, :fpos => rec.fpos,
:line_length => rec.line_length }
temp_rec.each { |r| r.write_to_file if r.is_a?(KBMemo) } if \
@field_types.include?(:Memo)
temp_rec.each { |r| r.write_to_file if r.is_a?(KBBlob) } if \
@field_types.include?(:Blob)
end
@db.engine.update_records(self, updated_recs)
return recs.size
end
def delete(&select_cond)
raise ArgumentError, 'Must specify select condition code ' + \
'block. To delete all records, use #clear instead.' if \
select_cond.nil?
result_set = get_matches(:delete, [:recno], select_cond)
@db.engine.delete_records(self, result_set)
return result_set.size
end
def clear(reset_recno_ctr=true)
recs_deleted = delete { true }
pack
@db.engine.reset_recno_ctr(self) if reset_recno_ctr
update_header_vars
return recs_deleted
end
def [](*index)
return nil if index[0].nil?
return get_match_by_recno(:select, @field_names, index[0]) if \
index.size == 1
recs = select_by_recno_index(*@field_names) { |r|
index.include?(r.recno)
}
return recs
end
def select(*filter, &select_cond)
result_set = []
validate_filter(filter)
filter = @field_names if filter.empty?
return get_matches(:select, filter, select_cond)
end
def select_by_recno_index(*filter, &select_cond)
result_set = []
validate_filter(filter)
filter = @field_names if filter.empty?
return get_matches_by_recno_index(:select, filter, select_cond)
end
def pack
raise "Do not execute this method in client/server mode!" if \
@db.client?
lines_deleted = @db.engine.pack_table(self)
update_header_vars
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
create_indexes
create_table_class unless @db.server?
return lines_deleted
end
def rename_column(old_col_name, new_col_name)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise "Cannot rename recno column!" if old_col_name == :recno
raise "Cannot give column name of recno!" if new_col_name == :recno
raise 'Invalid column name to rename: ' % old_col_name unless \
@field_names.include?(old_col_name)
raise 'New column name already exists: ' % new_col_name if \
@field_names.include?(new_col_name)
@db.engine.rename_column(self, old_col_name, new_col_name)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def change_column_type(col_name, col_type)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise "Cannot change type for recno column!" if col_name == :recno
raise 'Invalid column name: ' % col_name unless \
@field_names.include?(col_name)
raise 'Invalid field type: %s' % col_type unless \
KBTable.valid_field_type?(col_type)
@db.engine.change_column_type(self, col_name, col_type)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def add_column(col_name, col_type, after=nil)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise "Invalid column name in 'after': #{after}" unless after.nil? \
or @field_names.include?(after)
raise "Invalid column name in 'after': #{after}" if after == :recno
raise "Column name cannot be recno!" if col_name == :recno
raise "Column name already exists!" if @field_names.include?(
col_name)
if col_type.is_a?(Hash)
temp_type = col_type[:DataType]
else
temp_type = col_type
end
raise 'Invalid field type: %s' % temp_type unless \
KBTable.valid_field_type?(temp_type)
field_def = @db.build_header_field_string(col_name, col_type)
@db.engine.add_column(self, field_def, after)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def drop_column(col_name)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise 'Invalid column name: ' % col_name unless \
@field_names.include?(col_name)
raise "Cannot drop :recno column!" if col_name == :recno
@db.engine.drop_column(self, col_name)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def add_index(*col_names)
raise "Do not execute this method in client/server mode!" if \
@db.client?
col_names.each do |c|
raise "Invalid column name: #{c}" unless \
@field_names.include?(c)
raise "recno column cannot be indexed!" if c == :recno
raise "Column already indexed: #{c}" unless \
@field_indexes[@field_names.index(c)].nil?
end
last_index_no_used = 0
@field_indexes.each do |i|
next if i.nil?
index_no = i[-1..-1].to_i
last_index_no_used = index_no if index_no > last_index_no_used
end
@db.engine.add_index(self, col_names, last_index_no_used+1)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def drop_index(*col_names)
raise "Do not execute this method in client/server mode!" if \
@db.client?
col_names.each do |c|
raise "Invalid column name: #{c}" unless \
@field_names.include?(c)
raise "recno column index cannot be dropped!" if c == :recno
raise "Column not indexed: #{c}" if \
@field_indexes[@field_names.index(c)].nil?
end
@db.engine.drop_index(self, col_names)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def change_column_default_value(col_name, value)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise ":recno cannot have a default value!" if col_name == :recno
raise 'Invalid column name: ' % col_name unless \
@field_names.include?(col_name)
raise 'Cannot set default value for this type: ' + \
'%s' % @field_types.index(col_name) unless \
KBTable.valid_default_type?(
@field_types[@field_names.index(col_name)])
if value.nil?
@db.engine.change_column_default_value(self, col_name, nil)
else
@db.engine.change_column_default_value(self, col_name,
convert_to_encoded_string(
@field_types[@field_names.index(col_name)], value))
end
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def change_column_required(col_name, required)
raise "Do not execute this method in client/server mode!" if \
@db.client?
raise ":recno is always required!" if col_name == :recno
raise 'Invalid column name: ' % col_name unless \
@field_names.include?(col_name)
raise 'Required must be either true or false!' unless \
[true, false].include?(required)
@db.engine.change_column_required(self, col_name, required)
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
create_indexes
create_table_class unless @db.server?
end
def total_recs
return @db.engine.get_total_recs(self)
end
def import_csv(csv_filename)
records_inserted = 0
tbl_rec = @table_class.new(self)
(defined?(FasterCSV) ? FasterCSV : CSV).foreach(csv_filename
) do |row|
tbl_rec.populate([nil] + row)
insert(tbl_rec)
records_inserted += 1
end
return records_inserted
end
private
def create_indexes
methods.each do |m|
next if m == 'select_by_recno_index'
if m =~ /select_by_.*_index/
class << self; self end.send(:remove_method, m.to_sym)
end
end
@db.engine.init_recno_index(self)
['Index->1', 'Index->2', 'Index->3', 'Index->4',
'Index->5'].each do |idx|
index_col_names = []
@field_indexes.each_with_index do |fi,i|
next if fi.nil?
index_col_names << @field_names[i] if fi.include?(idx)
end
next if index_col_names.empty?
@db.engine.init_index(self, index_col_names)
select_meth_str = "def select_by_\#{index_col_names.join('_')}_index(*filter,\n&select_cond)\nresult_set = []\nvalidate_filter(filter)\nfilter = @field_names if filter.empty?\nreturn get_matches_by_index(:select,\n[:\#{index_col_names.join(',:')}], filter, select_cond)\nend\n"
instance_eval(select_meth_str) unless @db.server?
@idx_timestamps[index_col_names.join('_')] = nil
@idx_arrs[index_col_names.join('_')] = nil
end
end
def create_table_class
@table_class = Class.new(KBTableRec)
get_meth_str = ''
get_meth_upd_res_str = ''
set_meth_str = ''
@field_names.zip(@field_types, @field_extras) do |x|
field_name, field_type, field_extra = x
@lookup_key = field_name if field_extra.has_key?('Key')
get_meth_str = "def \#{field_name}\nreturn @\#{field_name}\nend\n"
get_meth_upd_res_str = "def \#{field_name}_upd_res\nreturn @\#{field_name}\nend\n"
set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = convert_to_native_type(:\#{field_type}, s)\nend\n"
if field_extra.has_key?('Lookup')
lookup_table, key_field = field_extra['Lookup'].split('.')
if key_field == 'recno'
get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table[@\#{field_name}]\nend\n"
else
begin
unless @db.get_table(lookup_table.to_sym
).respond_to?('select_by_%s_index' % key_field)
raise RuntimeError
end
get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table.select_by_\#{key_field}_index { |r|\nr.\#{key_field} == @\#{field_name} }[0]\nend\n"
rescue RuntimeError
get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{lookup_table})\nreturn table.select { |r|\nr.\#{key_field} == @\#{field_name} }[0]\nend\n"
end
end
end
if field_extra.has_key?('Link_many')
lookup_field, rest = field_extra['Link_many'].split('=')
link_table, link_field = rest.split('.')
begin
unless @db.get_table(link_table.to_sym).respond_to?(
'select_by_%s_index' % link_field)
raise RuntimeError
end
get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{link_table})\nreturn table.select_by_\#{link_field}_index { |r|\nr.send(:\#{link_field}) == @\#{lookup_field} }\nend\n"
rescue RuntimeError
get_meth_str = "def \#{field_name}\ntable = @tbl.db.get_table(:\#{link_table})\nreturn table.select { |r|\nr.send(:\#{link_field}) == @\#{lookup_field} }\nend\n"
end
get_meth_upd_res_str = "def \#{field_name}_upd_res\nreturn kb_nil\nend\n"
set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = kb_nil\nend\n"
end
if field_extra.has_key?('Calculated')
calculation = field_extra['Calculated']
get_meth_str = "def \#{field_name}()\nreturn \#{calculation}\nend\n"
get_meth_upd_res_str = "def \#{field_name}_upd_res()\nreturn kb_nil\nend\n"
set_meth_str = "def \#{field_name}=(s)\n@\#{field_name} = kb_nil\nend\n"
end
@table_class.class_eval(get_meth_str)
@table_class.class_eval(get_meth_upd_res_str)
@table_class.class_eval(set_meth_str)
end
end
def validate_filter(filter)
filter.each { |f|
raise 'Invalid field name: %s in filter!' % f unless \
@field_names.include?(f)
}
end
def convert_input_data(values)
temp_hash = {}
if values.is_a?(Proc)
tbl_rec = Struct.new(*@field_names[1..-1]).new
begin
values.call(tbl_rec)
rescue NoMethodError
raise 'Invalid field name in code block: %s' % $!
end
@field_names[1..-1].each do |f|
temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
end
elsif values.first.is_a?(Object.full_const_get(@record_class)) or \
values.first.is_a?(Struct) or values.first.class == @table_class
@field_names[1..-1].each do |f|
temp_hash[f] = values.first.send(f) if \
values.first.respond_to?(f)
end
elsif values.first.is_a?(Hash)
temp_hash = values.first.dup
elsif values.is_a?(Array)
raise ArgumentError, 'Must specify all fields in input array!' \
unless values.size == @field_names[1..-1].size
@field_names[1..-1].each do |f|
temp_hash[f] = values[@field_names.index(f)-1]
end
else
raise(ArgumentError, 'Invalid type for values container!')
end
return temp_hash
end
def check_required_fields(data)
@field_names[1..-1].each do |f|
raise(ArgumentError,
'A value for this field is required: %s' % f) if \
@field_requireds[@field_names.index(f)] and data[f].nil?
end
end
def check_against_input_for_specials(data)
@field_names[1..-1].each do |f|
raise(ArgumentError,
'You cannot input a value for this field: %s' % f) if \
@field_extras[@field_names.index(f)].has_key?('Calculated') \
or @field_extras[@field_names.index(f)].has_key?('Link_many') \
and not data[f].nil?
end
end
def validate_input(data)
@field_names[1..-1].each do |f|
next if data[f].nil?
raise 'Invalid data %s for column %s' % [data[f], f] unless \
KBTable.valid_data_type?(@field_types[@field_names.index(f)],
data[f])
end
end
def update_header_vars
@encrypted, @last_rec_no, @del_ctr, @record_class, @col_names, \
@col_types, @col_indexes, @col_defaults, @col_requireds, \
@col_extras = @db.engine.get_header_vars(self)
@field_names = @col_names
@field_types = @col_types
@field_indexes = @col_indexes
@field_defaults = @col_defaults
@field_requireds = @col_requireds
@field_extras = @col_extras
end
def get_result_struct(query_type, filter)
case query_type
when :select
return Struct.new(*filter) if @record_class == 'Struct'
when :update
return Struct.new(*(filter + [:fpos, :line_length]))
when :delete
return Struct.new(:recno, :fpos, :line_length)
end
return nil
end
def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
if query_type != :select
result_rec = result_struct.new(*filter.collect { |f|
tbl_rec.send("#{f}_upd_res".to_sym) })
elsif @record_class == 'Struct'
result_rec = result_struct.new(*filter.collect do |f|
if tbl_rec.send(f).kb_nil?
nil
else
tbl_rec.send(f)
end
end)
else
if Object.full_const_get(@record_class).respond_to?(:kb_create)
result_rec = Object.full_const_get(@record_class
).kb_create(*@field_names.collect do |f|
if filter.include?(f)
if tbl_rec.send(f).kb_nil?
nil
else
tbl_rec.send(f)
end
else
nil
end
end)
elsif Object.full_const_get(@record_class).respond_to?(
:kb_defaults)
result_rec = Object.full_const_get(@record_class).new(
*@field_names.collect do |f|
if tbl_rec.send(f).kb_nil?
nil
else
tbl_rec.send(f) || Object.full_const_get(
@record_class).kb_defaults[@field_names.index(f)]
end
end)
else
result_rec = Object.full_const_get(@record_class).allocate
@field_names.each do |fn|
if tbl_rec.send(fn).kb_nil?
result_rec.send("#{fn}=", nil)
else
result_rec.send("#{fn}=", tbl_rec.send(fn))
end
end
end
end
unless query_type == :select
result_rec.fpos = rec[-2]
result_rec.line_length = rec[-1]
end
return result_rec
end
def get_matches(query_type, filter, select_cond)
result_struct = get_result_struct(query_type, filter)
match_array = KBResultSet.new(self, filter, filter.collect { |f|
@field_types[@field_names.index(f)] })
tbl_rec = @table_class.new(self)
@db.engine.get_recs(self).each do |rec|
tbl_rec.populate(rec)
next if select_cond and not select_cond.call(tbl_rec)
match_array << create_result_rec(query_type, filter,
result_struct, tbl_rec, rec)
end
return match_array
end
def get_matches_by_index(query_type, index_fields, filter, select_cond)
good_matches = []
idx_struct = Struct.new(*(index_fields + [:recno]))
begin
if @db.client?
unless @idx_timestamps[index_fields.join('_')] == \
@db.engine.get_index_timestamp(self, index_fields.join(
'_'))
@idx_timestamps[index_fields.join('_')] = \
@db.engine.get_index_timestamp(self, index_fields.join(
'_'))
@idx_arrs[index_fields.join('_')] = \
@db.engine.get_index(self, index_fields.join('_'))
end
else
@idx_arrs[index_fields.join('_')] = \
@db.engine.get_index(self, index_fields.join('_'))
end
@idx_arrs[index_fields.join('_')].each do |rec|
good_matches << rec[-1] if select_cond.call(
idx_struct.new(*rec))
end
rescue NoMethodError
raise 'Field name in select block not part of index!'
end
return get_matches_by_recno(query_type, filter, good_matches)
end
def get_matches_by_recno_index(query_type, filter, select_cond)
good_matches = []
idx_struct = Struct.new(:recno)
begin
@db.engine.get_recno_index(self).each_key do |key|
good_matches << key if select_cond.call(idx_struct.new(key))
end
rescue NoMethodError
raise "You can only use recno field in select block!"
end
return nil if good_matches.empty?
return get_matches_by_recno(query_type, filter, good_matches)
end
def get_match_by_recno(query_type, filter, recno)
result_struct = get_result_struct(query_type, filter)
match_array = KBResultSet.new(self, filter, filter.collect { |f|
@field_types[@field_names.index(f)] })
tbl_rec = @table_class.new(self)
rec = @db.engine.get_rec_by_recno(self, recno)
return nil if rec.nil?
tbl_rec.populate(rec)
return create_result_rec(query_type, filter, result_struct,
tbl_rec, rec)
end
def get_matches_by_recno(query_type, filter, recnos)
result_struct = get_result_struct(query_type, filter)
match_array = KBResultSet.new(self, filter, filter.collect { |f|
@field_types[@field_names.index(f)] })
tbl_rec = @table_class.new(self)
@db.engine.get_recs_by_recno(self, recnos).each do |rec|
next if rec.nil?
tbl_rec.populate(rec)
match_array << create_result_rec(query_type, filter,
result_struct, tbl_rec, rec)
end
return match_array
end
end
class KBMemo
attr_accessor :filepath, :contents
def initialize(db, filepath, contents='')
@db = db
@filepath = filepath
@contents = contents
end
def read_from_file
@contents = @db.engine.read_memo_file(@filepath)
end
def write_to_file
@db.engine.write_memo_file(@filepath, @contents)
end
end
class KBBlob
attr_accessor :filepath, :contents
def initialize(db, filepath, contents='')
@db = db
@filepath = filepath
@contents = contents
end
def read_from_file
@contents = @db.engine.read_blob_file(@filepath)
end
def write_to_file
@db.engine.write_blob_file(@filepath, @contents)
end
end
class KBIndex
include KBTypeConversionsMixin
include KBEncryptionMixin
def initialize(table, index_fields)
@last_update = Time.new
@idx_arr = []
@table = table
@index_fields = index_fields
@col_poss = index_fields.collect {|i| table.field_names.index(i) }
@col_names = index_fields
@col_types = index_fields.collect {|i|
table.field_types[table.field_names.index(i)]}
end
def get_idx
return @idx_arr
end
def get_timestamp
return @last_update
end
def rebuild(fptr)
@idx_arr.clear
encrypted = @table.encrypted?
fptr.readline
begin
while true
line = fptr.readline
line = unencrypt_str(line) if encrypted
line.strip!
next if line == ''
rec = line.split('|', @col_poss.max+2)
append_new_rec_to_index_array(rec)
end
rescue EOFError
end
@last_update = Time.new
end
def add_index_rec(rec)
@last_upddate = Time.new if append_new_rec_to_index_array(rec)
end
def delete_index_rec(recno)
i = @idx_arr.rassoc(recno.to_i)
@idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
@last_update = Time.new
end
def update_index_rec(rec)
delete_index_rec(rec.first.to_i)
add_index_rec(rec)
end
def append_new_rec_to_index_array(rec)
idx_rec = []
@col_poss.zip(@col_types).each do |col_pos, col_type|
idx_rec << convert_to_native_type(col_type, rec[col_pos])
end
return false if idx_rec.uniq == [kb_nil]
idx_rec << rec.first.to_i
@idx_arr << idx_rec
return true
end
end
class KBRecnoIndex
include KBEncryptionMixin
def initialize(table)
@idx_hash = {}
@table = table
end
def get_idx
return @idx_hash
end
def rebuild(fptr)
@idx_hash.clear
encrypted = @table.encrypted?
begin
fptr.readline
while true
fpos = fptr.tell
line = fptr.readline
line = unencrypt_str(line) if encrypted
line.strip!
next if line == ''
rec = line.split('|', 2)
@idx_hash[rec.first.to_i] = fpos
end
rescue EOFError
end
end
def add_index_rec(recno, fpos)
raise 'Table already has index record for recno: %s' % recno if \
@idx_hash.has_key?(recno.to_i)
@idx_hash[recno.to_i] = fpos
end
def update_index_rec(recno, fpos)
raise 'Table has no index record for recno: %s' % recno unless \
@idx_hash.has_key?(recno.to_i)
@idx_hash[recno.to_i] = fpos
end
def delete_index_rec(recno)
raise 'Table has no index record for recno: %s' % recno unless \
@idx_hash.has_key?(recno.to_i)
@idx_hash.delete(recno.to_i)
end
end
class KBTableRec
include KBTypeConversionsMixin
def initialize(tbl)
@tbl = tbl
end
def populate(rec)
@tbl.field_names.zip(rec).each do |fn, val|
send("#{fn}=", val)
end
end
def clear
@tbl.field_names.each do |fn|
send("#{fn}=", kb_nil)
end
end
end
class KBResultSet < Array
def KBResultSet.reverse(sort_field)
return [sort_field, :desc]
end
def initialize(table, filter, filter_types, *args)
@table = table
@filter = filter
@filter_types = filter_types
super(*args)
@filter.each do |f|
get_meth_str = "def \#{f}()\nif defined?(@\#{f}) then\nreturn @\#{f}\nelse\n@\#{f} = self.collect { |x| x.\#{f} }\nreturn @\#{f}\nend\nend\n"
self.class.class_eval(get_meth_str)
end
end
def to_ary
to_a
end
def set(*updates, &update_cond)
raise 'Cannot specify both a hash and a proc for method #set!' \
unless updates.empty? or update_cond.nil?
raise 'Must specify update proc or hash for method #set!' if \
updates.empty? and update_cond.nil?
if updates.empty?
@table.set(self, update_cond)
else
@table.set(self, updates)
end
end
def sort(*sort_fields)
sort_fields_arrs = []
sort_fields.each do |f|
if f.to_s[0..0] == '-'
sort_fields_arrs << [f.to_s[1..-1].to_sym, :desc]
elsif f.to_s[0..0] == '+'
sort_fields_arrs << [f.to_s[1..-1].to_sym, :asc]
else
sort_fields_arrs << [f, :asc]
end
end
sort_fields_arrs.each do |f|
raise "Invalid sort field" unless @filter.include?(f[0])
end
super() { |a,b|
x = []
y = []
sort_fields_arrs.each do |s|
if [:Integer, :Float].include?(
@filter_types[@filter.index(s[0])])
a_value = a.send(s[0]) || 0
b_value = b.send(s[0]) || 0
else
a_value = a.send(s[0])
b_value = b.send(s[0])
end
if s[1] == :desc
x << b_value
y << a_value
else
x << a_value
y << b_value
end
end
x <=> y
}
end
def to_report(recs_per_page=0, print_rec_sep=false)
result = collect { |r| @filter.collect {|f| r.send(f)} }
delim = ' | '
columns = [@filter].concat(result).transpose
max_widths = columns.collect { |c|
c.max { |a,b| a.to_s.length <=> b.to_s.length }.to_s.length
}
row_dashes = '-' * (max_widths.inject {|sum, n| sum + n} +
delim.length * (max_widths.size - 1))
justify_hash = { :String => :ljust, :Integer => :rjust,
:Float => :rjust, :Boolean => :ljust, :Date => :ljust,
:Time => :ljust, :DateTime => :ljust }
header_line = @filter.zip(max_widths, @filter.collect { |f|
@filter_types[@filter.index(f)] }).collect { |x,y,z|
x.to_s.send(justify_hash[z], y) }.join(delim)
output = ''
recs_on_page_cnt = 0
result.each do |row|
if recs_on_page_cnt == 0
output << header_line + "\n" << row_dashes + "\n"
end
output << row.zip(max_widths, @filter.collect { |f|
@filter_types[@filter.index(f)] }).collect { |x,y,z|
x.to_s.send(justify_hash[z], y) }.join(delim) + "\n"
output << row_dashes + '\n' if print_rec_sep
recs_on_page_cnt += 1
if recs_per_page > 0 and (recs_on_page_cnt ==
num_recs_per_page)
output << '\f'
recs_on_page_count = 0
end
end
return output
end
end
class KBNilClass
include Comparable
class << self
def new
@kb_nil ||= KBNilClass.allocate
end
end
def inspect
'kb_nil'
end
def kb_nil?
true
end
def to_s
""
end
def to_i
0
end
def to_f
0.0
end
def to_a
[]
end
def <=>(other)
return 0 if other.kb_nil?
return -1
end
def coerce(other)
return [other, to_i] if other.kind_of? Fixnum
return [other, to_f] if other.kind_of? Float
raise "Didn't know how to coerce kb_nil to a #{other.class}"
end
def method_missing(sym, *args)
kb_nil
end
end
module Kernel
def kb_nil
KBNilClass.new
end
end
class Object
def full_const_get(name)
list = name.split("::")
obj = Object
list.each {|x| obj = obj.const_get(x) }
obj
end
def kb_nil?
false
end
end
class Symbol
def -@
("-"+self.to_s).to_sym
end
def +@
("+"+self.to_s).to_sym
end
end