lib/kirbybase.rb in KirbyBase-2.5 vs lib/kirbybase.rb in KirbyBase-2.5.1
- old
+ new
@@ -126,16 +126,220 @@
# * Added the ability to, upon database initialization, specify that index
# creation should not happen until a table is actually opened. This
# speeds up database initialization at the cost of slower table
# initialization later.
#
+# 2005-12-28:: Version 2.5.1
+# * Fixed a bug that had broken encrypted tables.
+# * Changed KBTable#pack method so that it raises an error if trying to
+# execute when :connect_type==:client.
+# * Fixed a bug where it was possible to insert records missing a required
+# field if using a hash. Thanks to Adam Shelly for this.
+# * Fixed a bug that occurred when you tried to update records using a
+# block and you tried to reference a field in the current record inside
+# the block. Much thanks to Assaph Mehr for reporting this.
+# * Fixed a bug that allowed you to have duplicate column names. Thanks to
+# Assaph Mehr for spotting this.
+# * Changed the way KBTable#set works with memo/blob fields.
+# * Started creating unit tests.
+# * Changed the KBTable#clear method to return number of records deleted.
+# Thanks to Assaph Mehr for this enhancement.
+# * Moved #build_header_string from KBEngine class to KirbyBase class.
+# * Added KirbyBase::VERSION constant.
+#
+#
#---------------------------------------------------------------------------
+# KBTypeConversionsMixin
+#---------------------------------------------------------------------------
+module KBTypeConversionsMixin
+ # Regular expression used to determine if field needs to be un-encoded.
+ UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
+
+ # Regular expression used to determine if field needs to be encoded.
+ ENCODE_RE = /&|\n|\r|\032|\|/
+
+ #-----------------------------------------------------------------------
+ # convert_to_native_type
+ #-----------------------------------------------------------------------
+ #++
+ # Return value converted from storage string to native field type.
+ #
+ def convert_to_native_type(data_type, s)
+ return nil if s.empty? or s.nil?
+
+ case data_type
+ when :String
+ if s =~ UNENCODE_RE
+ return s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
+ ).gsub('&', "&")
+ else
+ return s
+ end
+ when :Integer
+ return s.to_i
+ when :Float
+ return s.to_f
+ when :Boolean
+ if ['false', 'False', nil, false].include?(s)
+ return false
+ else
+ return true
+ end
+ when :Time
+ return Time.parse(s)
+ when :Date
+ return Date.parse(s)
+ when :DateTime
+ return DateTime.parse(s)
+ when :YAML
+ # This code is here in case the YAML field is the last
+ # field in the record. Because YAML normally defines a
+ # nil value as "--- ", but KirbyBase strips trailing
+ # spaces off the end of the record, so if this is the
+ # last field in the record, KirbyBase will strip the
+ # trailing space off and make it "---". When KirbyBase
+ # attempts to convert this value back using to_yaml,
+ # you get an exception.
+ if s == "---"
+ return nil
+ elsif s =~ UNENCODE_RE
+ y = s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
+ "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
+ ).gsub('&', "&")
+ return YAML.load(y)
+ else
+ return YAML.load(s)
+ end
+ when :Memo
+ memo = KBMemo.new(@tbl.db, s)
+ memo.read_from_file
+ return memo
+ when :Blob
+ blob = KBBlob.new(@tbl.db, s)
+ blob.read_from_file
+ return blob
+ else
+ raise "Invalid field type: %s" % data_type
+ end
+ end
+
+ #-----------------------------------------------------------------------
+ # convert_to_encoded_string
+ #-----------------------------------------------------------------------
+ #++
+ # Return value converted to encoded String object suitable for storage.
+ #
+ def convert_to_encoded_string(data_type, value)
+ case data_type
+ when :YAML
+ y = value.to_yaml
+ if y =~ ENCODE_RE
+ return y.gsub("&", '&').gsub("\n", '&linefeed;').gsub(
+ "\r", '&carriage_return;').gsub("\032", '&substitute;'
+ ).gsub("|", '&pipe;')
+ else
+ return y
+ end
+ when :String
+ if value =~ ENCODE_RE
+ return value.gsub("&", '&').gsub("\n", '&linefeed;'
+ ).gsub("\r", '&carriage_return;').gsub("\032",
+ '&substitute;').gsub("|", '&pipe;')
+ else
+ return value
+ end
+ when :Memo
+ return value.filepath
+ when :Blob
+ return value.filepath
+ else
+ return value.to_s
+ end
+ end
+end
+
+
+#---------------------------------------------------------------------------
+# KBEncryptionMixin
+#---------------------------------------------------------------------------
+module KBEncryptionMixin
+ EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
+ '0123456789.+-,$:|&;_ '
+ EN_STR_LEN = EN_STR.length
+ EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
+ EN_KEY = EN_KEY1.unpack("u")[0]
+ EN_KEY_LEN = EN_KEY.length
+
+
+ #-----------------------------------------------------------------------
+ # encrypt_str
+ #-----------------------------------------------------------------------
+ #++
+ # Returns an encrypted string, using the Vignere Cipher.
+ #
+ def encrypt_str(s)
+ new_str = ''
+ i_key = -1
+ s.each_byte do |c|
+ if i_key < EN_KEY_LEN - 1
+ i_key += 1
+ else
+ i_key = 0
+ end
+
+ if EN_STR.index(c.chr).nil?
+ new_str << c.chr
+ next
+ end
+
+ i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
+ i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
+ new_str << EN_STR[i_from_str]
+ end
+ return new_str
+ end
+
+ #-----------------------------------------------------------------------
+ # unencrypt_str
+ #-----------------------------------------------------------------------
+ #++
+ # Returns an unencrypted string, using the Vignere Cipher.
+ #
+ def unencrypt_str(s)
+ new_str = ''
+ i_key = -1
+ s.each_byte do |c|
+ if i_key < EN_KEY_LEN - 1
+ i_key += 1
+ else
+ i_key = 0
+ end
+
+ if EN_STR.index(c.chr).nil?
+ new_str << c.chr
+ next
+ end
+
+ i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
+ i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
+ new_str << EN_STR[i_from_str]
+ end
+ return new_str
+ end
+end
+
+
+#---------------------------------------------------------------------------
# KirbyBase
#---------------------------------------------------------------------------
class KirbyBase
include DRb::DRbUndumped
+ include KBTypeConversionsMixin
+ VERSION = "2.5.1"
+
attr_reader :engine
attr_accessor(:connect_type, :host, :port, :path, :ext, :memo_blob_path,
:delay_index_creation)
@@ -153,11 +357,13 @@
# (Only valid if connect_type is :client.)
# *path*:: String specifying path to location of database tables.
# *ext*:: String specifying extension of table files.
# *memo_blob_path*:: String specifying path to location of memo/blob
# files.
- #
+ # *delay_index_creation*:: Boolean specifying whether to delay index
+ # creation for each table until that table is
+ # requested by user.
def initialize(connect_type=:local, host=nil, port=nil, path='./',
ext='.tbl', memo_blob_path='./', delay_index_creation=false)
@connect_type = connect_type
@host = host
@port = port
@@ -208,33 +414,16 @@
# created at the beginning during database initialization so that
# they are ready for the user to use. Since index creation
# happens when the table instance is first created, I go ahead and
# create table instances right off the bat.
#
- # Also, I use to only execute the code below if this was either a
- # single-user instance of KirbyBase or if client-server, I would
- # only let the client-side KirbyBase instance create the table
- # instances, since there was no need for the server-side KirbyBase
- # instance to create table instances. But, since I want indexes
- # created at db initialization and the server's db instance might
- # be initialized long before any client's db is initialized, I now
- # let the server create table instances also. This is strictly to
- # get the indexes created, there is no other use for the table
- # instances on the server side as they will never be used.
+ # Also, I let the server create table instances also. This is
+ # strictly to get the indexes created, there is no other use for
+ # the table instances on the server side as they will never be used.
# Everything should and does go through the table instances created
- # on the client-side.
- #
- # Ok, I added back in a conditional flag that allows me to turn off
- # index initialization if this is a server. The reason I added this
- # back in was that I was running into a problem when running a
- # KirbyBase server as a win32 service. When I tried to start the
- # service, it kept bombing out saying that the application had not
- # responded in a timely manner. It appears to be because it was
- # taking a few seconds to build the indexes when it initialized.
- # When I deleted the index from the table, the service would start
- # just fine. I need to find out if I can set a timeout parameter
- # when starting the win32 service.
+ # on the client-side. You can turn this off by specifying true for
+ # the delay_index_creation argument.
if server? and @delay_index_creation
else
@engine.tables.each do |tbl|
@table_hash[tbl] = \
KBTable.create_called_from_database_instance(self, tbl,
@@ -334,32 +523,155 @@
raise "Name must be a symbol!" unless t.name.is_a?(Symbol)
raise "No table name specified!" if t.name.nil?
raise "No table field definitions specified!" if t.field_defs.nil?
- @engine.new_table(t.name, t.field_defs, t.encrypt,
+ # Can't create a table that already exists!
+ raise "Table already exists!" if table_exists?(t.name)
+
+ raise 'Must have a field type for each field name' \
+ unless t.field_defs.size.remainder(2) == 0
+
+ # Check to make sure there are no duplicate field names.
+ temp_field_names = []
+ (0...t.field_defs.size).step(2) do |x|
+ temp_field_names << t.field_defs[x]
+ end
+ raise 'Duplicate field names are not allowed!' unless \
+ temp_field_names == temp_field_names.uniq
+
+ temp_field_defs = []
+ (0...t.field_defs.size).step(2) do |x|
+ temp_field_defs << build_header_field_string(t.field_defs[x],
+ t.field_defs[x+1])
+ end
+
+ @engine.new_table(t.name, temp_field_defs, t.encrypt,
t.record_class.to_s)
return get_table(t.name)
end
#-----------------------------------------------------------------------
+ # build_header_field_string
+ #-----------------------------------------------------------------------
+ def build_header_field_string(field_name_def, field_type_def)
+ # Put field name at start of string definition.
+ temp_field_def = field_name_def.to_s + ':'
+
+ # If field type is a hash, that means that it is not just a
+ # simple field. Either is is a key field, it is being used in an
+ # index, it is a default value, it is a required field, it is a
+ # Lookup field, it is a Link_many field, or it is a Calculated
+ # field. This next bit of code is to piece together a proper
+ # string so that it can be written out to the header rec.
+ 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
+
+ # Check if this field is a key for the table.
+ if field_type_def.has_key?(:Key)
+ temp_field_def += ':Key->true'
+ end
+
+ # Check for Index definition.
+ 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
+
+ # Check for Default value definition.
+ 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
+
+ # Check for Required definition.
+ 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
+
+ # Check for Lookup field, Link_many field, Calculated field
+ # definition.
+ 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
+
+ #-----------------------------------------------------------------------
# rename_table
#-----------------------------------------------------------------------
#++
# Rename a table.
#
# *old_tablename*:: Symbol of old table name.
# *new_tablename*:: Symbol of new table name.
#
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
@@ -374,10 +686,11 @@
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
#-----------------------------------------------------------------------
# table_exists?
@@ -396,94 +709,17 @@
end
#---------------------------------------------------------------------------
-# KBTypeConversionsMixin
-#---------------------------------------------------------------------------
-module KBTypeConversionsMixin
- UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
-
- #-----------------------------------------------------------------------
- # convert_to
- #-----------------------------------------------------------------------
- def convert_to(data_type, s)
- return nil if s.empty? or s.nil?
-
- case data_type
- when :String
- if s =~ UNENCODE_RE
- return s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
- "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
- ).gsub('&', "&")
- else
- return s
- end
- when :Integer
- return s.to_i
- when :Float
- return s.to_f
- when :Boolean
- if ['false', 'False', nil, false].include?(s)
- return false
- else
- return true
- end
- when :Time
- return Time.parse(s)
- when :Date
- return Date.parse(s)
- when :DateTime
- return DateTime.parse(s)
- when :YAML
- # This code is here in case the YAML field is the last
- # field in the record. Because YAML normall defines a
- # nil value as "--- ", but KirbyBase strips trailing
- # spaces off the end of the record, so if this is the
- # last field in the record, KirbyBase will strip the
- # trailing space off and make it "---". When KirbyBase
- # attempts to convert this value back using to_yaml,
- # you get an exception.
- if s == "---"
- return nil
- elsif s =~ UNENCODE_RE
- y = s.gsub('&linefeed;', "\n").gsub('&carriage_return;',
- "\r").gsub('&substitute;', "\032").gsub('&pipe;', "|"
- ).gsub('&', "&")
- return YAML.load(y)
- else
- return YAML.load(s)
- end
- when :Memo
- memo = KBMemo.new(@tbl.db, s)
- memo.read_from_file
- return memo
- when :Blob
- blob = KBBlob.new(@tbl.db, s)
- blob.read_from_file
- return blob
- else
- raise "Invalid field type: %s" % data_type
- end
- end
-end
-
-
-#---------------------------------------------------------------------------
# KBEngine
#---------------------------------------------------------------------------
class KBEngine
include DRb::DRbUndumped
include KBTypeConversionsMixin
+ include KBEncryptionMixin
- EN_STR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + \
- '0123456789.+-,$:|&;_ '
- EN_STR_LEN = EN_STR.length
- EN_KEY1 = ")2VER8GE\"87-E\n" #*** DO NOT CHANGE ***
- EN_KEY = EN_KEY1.unpack("u")[0]
- EN_KEY_LEN = EN_KEY.length
-
# Make constructor private.
private_class_method :new
#-----------------------------------------------------------------------
# KBEngine.create_called_from_database_instance
@@ -545,10 +781,17 @@
def recno_index_exists?(table)
@recno_indexes.include?(table.name)
end
#-----------------------------------------------------------------------
+ # get_recno_index
+ #-----------------------------------------------------------------------
+ def get_recno_index(table)
+ return @recno_indexes[table.name].get_idx
+ end
+
+ #-----------------------------------------------------------------------
# init_index
#-----------------------------------------------------------------------
def init_index(table, index_fields)
return if index_exists?(table, index_fields)
@@ -632,17 +875,10 @@
def get_index_timestamp(table, index_name)
return @indexes["#{table.name}_#{index_name}".to_sym].get_timestamp
end
#-----------------------------------------------------------------------
- # get_recno_index
- #-----------------------------------------------------------------------
- def get_recno_index(table)
- return @recno_indexes[table.name].get_idx
- end
-
- #-----------------------------------------------------------------------
# table_exists?
#-----------------------------------------------------------------------
def table_exists?(tablename)
return File.exists?(File.join(@db.path, tablename.to_s + @db.ext))
end
@@ -658,122 +894,22 @@
}
return list
end
#-----------------------------------------------------------------------
- # build_header_field_string
- #-----------------------------------------------------------------------
- def build_header_field_string(field_name_def, field_type_def)
- # Put field name at start of string definition.
- temp_field_def = field_name_def.to_s + ':'
-
- # if field type is a hash, that means that it is not just a
- # simple field. Either is is being used in an index, it is a
- # Lookup field, it is a Link_many field, or it is a Calculated
- # field. This next bit of code is to piece together a proper
- # string so that it can be written out to the header rec.
- 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?
- temp_field_def += ':Default->' + \
- KBTable.convert_to_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 = @db.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 @db.table_exists?(field_type_def)
- tbl = @db.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
-
- #-----------------------------------------------------------------------
# new_table
#-----------------------------------------------------------------------
#++
# Create physical file holding table. This table should not be directly
# called in your application, but only called by #create_table.
#
def new_table(name, field_defs, encrypt, record_class)
- # Can't create a table that already exists!
- raise "Table already exists!" if table_exists?(name)
-
- raise 'Must have a field type for each field name' \
- unless field_defs.size.remainder(2) == 0
- temp_field_defs = []
- (0...field_defs.size).step(2) do |x|
- temp_field_defs << build_header_field_string(field_defs[x],
- field_defs[x+1])
- end
-
# Header rec consists of last record no. used, delete count, and
# all field names/types. Here, I am inserting the 'recno' field
# at the beginning of the fields.
header_rec = ['000000', '000000', record_class, 'recno:Integer',
- temp_field_defs].join('|')
+ 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')
@@ -786,13 +922,13 @@
#-----------------------------------------------------------------------
# delete_table
#-----------------------------------------------------------------------
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)
- File.delete(File.join(@db.path, tablename.to_s + @db.ext))
return true
end
end
#----------------------------------------------------------------------
@@ -805,12 +941,12 @@
#-----------------------------------------------------------------------
# reset_recno_ctr
#-----------------------------------------------------------------------
def reset_recno_ctr(table)
with_write_locked_table(table) do |fptr|
- last_rec_no, rest_of_line = get_header_record(table, fptr
- ).split('|', 2)
+ 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
@@ -818,11 +954,11 @@
#-----------------------------------------------------------------------
# get_header_vars
#-----------------------------------------------------------------------
def get_header_vars(table)
with_table(table) do |fptr|
- line = get_header_record(table, 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
@@ -836,22 +972,24 @@
x.split(':')[2..-1].each do |y|
if y =~ /Index/
field_indexes[i] = y
elsif y =~ /Default/
field_defaults[i] = \
- convert_to(field_types[i], y.split('->')[1])
+ convert_to_native_type(field_types[i],
+ y.split('->')[1])
elsif y =~ /Required/
field_requireds[i] = \
- convert_to(:Boolean, y.split('->')[1])
+ convert_to_native_type(:Boolean,
+ y.split('->')[1])
else
field_extras[i][y.split('->')[0]] = \
y.split('->')[1]
end
end
end
end
- return [table.encrypted?, last_rec_no.to_i, del_ctr.to_i,
+ 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
@@ -867,25 +1005,16 @@
# Skip header rec.
fptr.readline
# Loop through table.
while true
- # Record current position in table. Then read first
- # detail record.
+ # Record current position in table.
fpos = fptr.tell
- line = fptr.readline
- line.chomp!
- line_length = line.length
+ rec, line_length = line_to_rec(fptr.readline, encrypted)
- line = unencrypt_str(line) if encrypted
- line.strip!
+ next if rec.empty?
- # If blank line (i.e. 'deleted'), skip it.
- next if line == ''
-
- # Split the line up into fields.
- rec = line.split('|', -1)
rec << fpos << line_length
recs << rec
end
# Here's how we break out of the loop...
rescue EOFError
@@ -910,22 +1039,14 @@
# to them, and sort by file position, so that when we seek
# through the physical file we are going in ascending file
# position order, which should be fastest.
recnos.collect { |r| [recno_idx[r], r] }.sort.each do |r|
fptr.seek(r[0])
- line = fptr.readline
- line.chomp!
- line_length = line.length
+ rec, line_length = line_to_rec(fptr.readline, encrypted)
- line = unencrypt_str(line) if encrypted
- line.strip!
+ next if rec.empty?
- # If blank line (i.e. 'deleted'), skip it.
- next if line == ''
-
- # Split the line up into fields.
- rec = line.split('|', -1)
raise "Index Corrupt!" unless rec[0].to_i == r[1]
rec << r[0] << line_length
recs << rec
end
return recs
@@ -941,29 +1062,37 @@
return nil unless recno_idx.has_key?(recno)
with_table(table) do |fptr|
fptr.seek(recno_idx[recno])
- line = fptr.readline
- line.chomp!
- line_length = line.length
+ rec, line_length = line_to_rec(fptr.readline, encrypted)
- line = unencrypt_str(line) if encrypted
- line.strip!
+ raise "Recno Index Corrupt for table %s!" % table.name if \
+ rec.empty?
- return nil if line == ''
+ raise "Recno Index Corrupt for table %s!" % table.name unless \
+ rec[0].to_i == recno
- # Split the line up into fields.
- rec = line.split('|', -1)
-
- raise "Index Corrupt!" unless rec[0].to_i == recno
rec << recno_idx[recno] << line_length
return rec
end
end
#-----------------------------------------------------------------------
+ # line_to_rec
+ #-----------------------------------------------------------------------
+ def line_to_rec(line, encrypted)
+ line.chomp!
+ line_length = line.length
+ line = unencrypt_str(line) if encrypted
+ line.strip!
+
+ # Convert line to rec and return rec and line length.
+ return line.split('|', -1), line_length
+ end
+
+ #-----------------------------------------------------------------------
# insert_record
#-----------------------------------------------------------------------
def insert_record(table, rec)
with_write_locked_table(table) do |fptr|
# Auto-increment the record number field.
@@ -1128,45 +1257,42 @@
end
#-----------------------------------------------------------------------
# add_column
#-----------------------------------------------------------------------
- def add_column(table, col_name, col_type, after)
- temp_field_def = build_header_field_string(col_name, col_type)
-
- if after.nil?
+ def add_column(table, field_def, after)
+ # Find the index position of where to insert the column, either at
+ # the end (-1) or after the field specified.
+ if after.nil? or table.field_names.last == after
insert_after = -1
else
- if table.field_names.last == after
- insert_after = -1
- else
- insert_after = table.field_names.index(after)+1
- end
+ 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('|')
- if insert_after == -1
- header_rec.insert(insert_after, temp_field_def)
- else
- header_rec.insert(insert_after+3, temp_field_def)
- end
+ else
+ header_rec = line.split('|')
+ end
+
+ if insert_after == -1
+ header_rec.insert(insert_after, field_def)
+ else
+ # Need to account for recno ctr, delete ctr, record class.
+ 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
- header_rec = line.split('|')
- if insert_after == -1
- header_rec.insert(insert_after, temp_field_def)
- else
- header_rec.insert(insert_after+3, temp_field_def)
- end
new_fptr.write(header_rec.join('|') + "\n")
end
begin
while true
@@ -1176,11 +1302,11 @@
temp_line = unencrypt_str(line)
else
temp_line = line
end
- rec = temp_line.split('|')
+ rec = temp_line.split('|', -1)
rec.insert(insert_after, '')
if table.encrypted?
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
else
@@ -1229,11 +1355,11 @@
temp_line = unencrypt_str(line)
else
temp_line = line
end
- rec = temp_line.split('|')
+ rec = temp_line.split('|', -1)
rec.delete_at(col_index)
if table.encrypted?
new_fptr.write(encrypt_str(rec.join('|')) + "\n")
else
@@ -1695,23 +1821,25 @@
# get_header_record
#----------------------------------------------------------------------
def get_header_record(table, fptr)
fptr.seek(0)
- if table.encrypted?
- return unencrypt_str(fptr.readline[1..-1].chomp)
+ line = fptr.readline.chomp
+
+ if line[0..0] == 'Z'
+ return [true, unencrypt_str(line[1..-1])]
else
- return fptr.readline.chomp
+ return [false, line]
end
end
#-----------------------------------------------------------------------
# incr_rec_no_ctr
#-----------------------------------------------------------------------
def incr_rec_no_ctr(table, fptr)
- last_rec_no, rest_of_line = get_header_record(table, fptr).split(
- '|', 2)
+ 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('|'))
@@ -1721,81 +1849,28 @@
#-----------------------------------------------------------------------
# incr_del_ctr
#-----------------------------------------------------------------------
def incr_del_ctr(table, fptr)
- last_rec_no, del_ctr, rest_of_line = get_header_record(table,
- fptr).split('|', 3)
+ 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
-
- #-----------------------------------------------------------------------
- # encrypt_str
- #-----------------------------------------------------------------------
- def encrypt_str(s)
- # Returns an encrypted string, using the Vignere Cipher.
-
- new_str = ''
- i_key = -1
- s.each_byte do |c|
- if i_key < EN_KEY_LEN - 1
- i_key += 1
- else
- i_key = 0
- end
-
- if EN_STR.index(c.chr).nil?
- new_str << c.chr
- next
- end
-
- i_from_str = EN_STR.index(EN_KEY[i_key]) + EN_STR.index(c.chr)
- i_from_str = i_from_str - EN_STR_LEN if i_from_str >= EN_STR_LEN
- new_str << EN_STR[i_from_str]
- end
- return new_str
- end
-
- #-----------------------------------------------------------------------
- # unencrypt_str
- #-----------------------------------------------------------------------
- def unencrypt_str(s)
- # Returns an unencrypted string, using the Vignere Cipher.
-
- new_str = ''
- i_key = -1
- s.each_byte do |c|
- if i_key < EN_KEY_LEN - 1
- i_key += 1
- else
- i_key = 0
- end
-
- if EN_STR.index(c.chr).nil?
- new_str << c.chr
- next
- end
-
- i_from_str = EN_STR.index(c.chr) - EN_STR.index(EN_KEY[i_key])
- i_from_str = i_from_str + EN_STR_LEN if i_from_str < 0
- new_str << EN_STR[i_from_str]
- end
- return new_str
- end
end
#---------------------------------------------------------------------------
# KBTable
#---------------------------------------------------------------------------
class KBTable
include DRb::DRbUndumped
+ include KBTypeConversionsMixin
# Make constructor private. KBTable instances should only be created
# from KirbyBase#get_table.
private_class_method :new
@@ -1804,18 +1879,15 @@
VALID_DEFAULT_TYPES = [:String, :Integer, :Float, :Boolean, :Date,
:Time, :DateTime, :YAML]
VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
:DateTime]
- # Regular expression used to determine if field needs to be
- # encoded.
- ENCODE_RE = /&|\n|\r|\032|\|/
+ attr_reader :filename, :name, :table_class, :db, :lookup_key, \
+ :last_rec_no, :del_ctr
- attr_reader :filename, :name, :table_class, :db, :lookup_key
-
#-----------------------------------------------------------------------
- # KBTable.valid_field_type
+ # KBTable.valid_field_type?
#-----------------------------------------------------------------------
#++
# Return true if valid field type.
#
# *field_type*:: Symbol specifying field type.
@@ -1823,23 +1895,60 @@
def KBTable.valid_field_type?(field_type)
VALID_FIELD_TYPES.include?(field_type)
end
#-----------------------------------------------------------------------
- # KBTable.valid_default_type
+ # KBTable.valid_data_type?
#-----------------------------------------------------------------------
#++
+ # Return true if data is correct type, false otherwise.
+ #
+ # *data_type*:: Symbol specifying data type.
+ # *value*:: Value to convert to String.
+ #
+ 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
+
+ #-----------------------------------------------------------------------
+ # KBTable.valid_default_type?
+ #-----------------------------------------------------------------------
+ #++
# Return true if valid default type.
#
# *field_type*:: Symbol specifying field type.
#
def KBTable.valid_default_type?(field_type)
VALID_DEFAULT_TYPES.include?(field_type)
end
#-----------------------------------------------------------------------
- # KBTable.valid_index_type
+ # KBTable.valid_index_type?
#-----------------------------------------------------------------------
#++
# Return true if valid index type.
#
# *field_type*:: Symbol specifying field type.
@@ -1847,47 +1956,10 @@
def KBTable.valid_index_type?(field_type)
VALID_INDEX_TYPES.include?(field_type)
end
#-----------------------------------------------------------------------
- # KBTable.convert_to_string
- #-----------------------------------------------------------------------
- #++
- # Return value converted to String object.
- #
- # *data_type*:: Symbol specifying data type.
- # *value*:: Value to convert to String.
- #
- def KBTable.convert_to_string(data_type, value)
- case data_type
- when :YAML
- y = value.to_yaml
- if y =~ ENCODE_RE
- return y.gsub("&", '&').gsub("\n", '&linefeed;').gsub(
- "\r", '&carriage_return;').gsub("\032", '&substitute;'
- ).gsub("|", '&pipe;')
- else
- return y
- end
- when :String
- if value =~ ENCODE_RE
- return value.gsub("&", '&').gsub("\n", '&linefeed;'
- ).gsub("\r", '&carriage_return;').gsub("\032",
- '&substitute;').gsub("|", '&pipe;')
- else
- return value
- end
- when :Memo
- return value.filepath
- when :Blob
- return value.filepath
- else
- return value.to_s
- end
- end
-
- #-----------------------------------------------------------------------
# create_called_from_database_instance
#-----------------------------------------------------------------------
#++
# Return a new instance of KBTable. Should never be called directly by
# your application. Should only be called from KirbyBase#get_table.
@@ -2015,51 +2087,50 @@
'proc for method #insert!' if data.empty? and insert_proc.nil?
# Update the header variables.
update_header_vars
- # Convert input, which could be an array, a hash, or a Struct
- # into a common format (i.e. hash).
+ # Convert input, which could be a proc, an array, a hash, or a
+ # Struct into a common format (i.e. hash).
if data.empty?
input_rec = convert_input_data(insert_proc)
else
input_rec = convert_input_data(data)
end
# Check the field values to make sure they are proper types.
validate_input(input_rec)
- if @field_types.include?(:Memo)
- input_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
- end
-
- if @field_types.include?(:Blob)
- input_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
- end
-
-
+ input_rec = Struct.new(*field_names).new(*field_names.zip(
+ @field_defaults).collect do |fn, fd|
+ if input_rec[fn].nil?
+ fd
+ else
+ input_rec[fn]
+ end
+ end)
- return @db.engine.insert_record(self, @field_names.zip(@field_types,
- @field_defaults).collect do |fn, ft, fd|
- if input_rec.has_key?(fn)
- if input_rec[fn].nil?
- if fd.nil?
- ''
- else
- KBTable.convert_to_string(ft, fd)
- end
- else
- KBTable.convert_to_string(ft, input_rec[fn])
- 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|
+ if input_rec[fn].nil?
+ ''
else
- if fd.nil?
- ''
- else
- KBTable.convert_to_string(ft, fd)
- end
+ convert_to_encoded_string(ft, input_rec[fn])
end
end)
+
+ # If there are any associated memo/blob fields, save their values.
+ 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
#-----------------------------------------------------------------------
# update_all
#-----------------------------------------------------------------------
@@ -2067,12 +2138,27 @@
# Return array of records (Structs) to be updated, in this case all
# records.
#
# *updates*:: Hash or Struct containing updates.
#
- def update_all(*updates)
- update(*updates) { true }
+ 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?
+
+ # Depending on whether the user supplied an array/hash/struct or a
+ # block as update criteria, we are going to call updates in one of
+ # two ways.
+ if updates.empty?
+ update { true }.set &update_proc
+ else
+ update(*updates) { true }
+ end
end
#-----------------------------------------------------------------------
# update
#-----------------------------------------------------------------------
@@ -2092,12 +2178,17 @@
# Get all records that match the selection criteria and
# return them in an array.
result_set = get_matches(:update, @field_names, select_cond)
+ # If updates is empty, this means that the user must have specified
+ # the updates in KBResultSet#set, i.e.
+ # tbl.update {|r| r.recno == 1}.set(:name => 'Bob')
return result_set if updates.empty?
+ # Call KBTable#set and pass it the records to be updated and the
+ # updated criteria.
set(result_set, updates)
end
#-----------------------------------------------------------------------
# []=
@@ -2121,41 +2212,68 @@
#
# *recs*:: Array of records (Structs) that will be updated.
# *data*:: Hash, Struct, Proc containing updates.
#
def set(recs, data)
- # Convert updates, which could be an array, a hash, or a Struct
- # into a common format (i.e. hash).
- update_rec = convert_input_data(data)
+ # If updates are not in the form of a Proc, convert updates, which
+ # could be an array, a hash, or a Struct into a common format (i.e.
+ # hash).
+ update_rec = convert_input_data(data) unless data.is_a?(Proc)
- # Make sure all of the fields of the update rec are of the proper
- # type.
- validate_input(update_rec)
-
- if @field_types.include?(:Memo)
- update_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
- end
-
- if @field_types.include?(:Blob)
- update_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
- end
-
updated_recs = []
# For each one of the recs that matched the update query, apply the
# updates to it and write it back to the database table.
recs.each do |rec|
- updated_rec = {}
- updated_rec[:rec] = \
- @field_names.zip(@field_types).collect do |fn, ft|
- KBTable.convert_to_string(ft,
- update_rec.fetch(fn, rec.send(fn)))
+ 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
- updated_rec[:fpos] = rec.fpos
- updated_rec[:line_length] = rec.line_length
- updated_recs << updated_rec
+
+ # Is the user trying to change something they shouldn't?
+ 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
+
+ # Are the data types of the updates correct?
+ validate_input(temp_rec)
+
+ check_required_fields(temp_rec)
+
+ check_against_input_for_specials(temp_rec)
+
+ # Apply updates to the record and add it to an array holding
+ # updated records. We need the fpos and line_length because
+ # the engine will use them to determine where to write the
+ # update and whether the updated record will fit in the old
+ # record's spot.
+ 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 }
+
+
+ # Update any associated blob/memo fields.
+ 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
+
+ # Take all of the update records and write them back out to the
+ # table's file.
@db.engine.update_records(self, updated_recs)
# Return the number of records updated.
return recs.size
end
@@ -2191,14 +2309,16 @@
#
# *reset_recno_ctr*:: true/false specifying whether recno counter should
# be reset to 0.
#
def clear(reset_recno_ctr=true)
- delete { true }
+ recs_deleted = delete { true }
pack
@db.engine.reset_recno_ctr(self) if reset_recno_ctr
+ update_header_vars
+ return recs_deleted
end
#-----------------------------------------------------------------------
# []
#-----------------------------------------------------------------------
@@ -2276,10 +2396,13 @@
#-----------------------------------------------------------------------
#++
# Remove blank records from table, return total removed.
#
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)
@@ -2384,22 +2507,27 @@
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)
+
# Does this new column have field extras (i.e. Index, Lookup, etc.)
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)
- @db.engine.add_column(self, col_name, col_type, after)
+ field_def = @db.build_header_field_string(col_name, col_type)
+ @db.engine.add_column(self, field_def, after)
+
# Need to reinitialize the table instance and associated indexes.
@db.engine.remove_recno_index(@name)
@db.engine.remove_indexes(@name)
update_header_vars
@@ -2545,11 +2673,11 @@
if value.nil?
@db.engine.change_column_default_value(self, col_name, nil)
else
@db.engine.change_column_default_value(self, col_name,
- KBTable.convert_to_string(
+ convert_to_encoded_string(
@field_types[@field_names.index(col_name)], value))
end
# Need to reinitialize the table instance and associated indexes.
@db.engine.remove_recno_index(@name)
@@ -2714,17 +2842,20 @@
return @#{field_name}
end
END_OF_STRING
set_meth_str = <<-END_OF_STRING
def #{field_name}=(s)
- @#{field_name} = convert_to(:#{field_type}, s)
+ @#{field_name} = convert_to_native_type(:#{field_type}, s)
end
END_OF_STRING
# If this is a Lookup field, modify the get_method.
if field_extra.has_key?('Lookup')
lookup_table, key_field = field_extra['Lookup'].split('.')
+
+ # If joining to recno field of lookup table use the
+ # KBTable[] method to get the record from the lookup table.
if key_field == 'recno'
get_meth_str = <<-END_OF_STRING
def #{field_name}
table = @tbl.db.get_table(:#{lookup_table})
return table[@#{field_name}]
@@ -2739,19 +2870,19 @@
get_meth_str = <<-END_OF_STRING
def #{field_name}
table = @tbl.db.get_table(:#{lookup_table})
return table.select_by_#{key_field}_index { |r|
- r.#{key_field} == @#{field_name} }.first
+ r.#{key_field} == @#{field_name} }[0]
end
END_OF_STRING
rescue RuntimeError
get_meth_str = <<-END_OF_STRING
def #{field_name}
table = @tbl.db.get_table(:#{lookup_table})
return table.select { |r|
- r.#{key_field} == @#{field_name} }.first
+ r.#{key_field} == @#{field_name} }[0]
end
END_OF_STRING
end
end
end
@@ -2843,110 +2974,99 @@
#-----------------------------------------------------------------------
#++
# Convert data passed to #input, #update, or #set to a common format.
#
def convert_input_data(values)
- if values.class == Proc
- tbl_struct = Struct.new(*@field_names[1..-1])
- tbl_rec = tbl_struct.new
+ temp_hash = {}
+
+ # This only applies to Procs in #insert, Procs in #update are
+ # handled in #set.
+ 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
- temp_hash = {}
- @field_names[1..-1].collect { |f|
+
+ @field_names[1..-1].each do |f|
temp_hash[f] = tbl_rec[f] unless tbl_rec[f].nil?
- }
- return temp_hash
- elsif values[0].class.to_s == @record_class or \
- values[0].class == @table_class
- temp_hash = {}
- @field_names[1..-1].collect { |f|
- temp_hash[f] = values[0].send(f) if values[0].respond_to?(f)
- }
- return temp_hash
- elsif values[0].class == Hash
- return values[0].dup
- elsif values[0].kind_of?(Struct)
- temp_hash = {}
- @field_names[1..-1].collect { |f|
- temp_hash[f] = values[0][f] if values[0].members.include?(
- f.to_s)
- }
- return temp_hash
- elsif values[0].class == Array
+ end
+
+ # Is input data an instance of custom record class, Struct, or
+ # KBTableRec?
+ 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
+
+ # Is input data a hash?
+ elsif values.first.is_a?(Hash)
+ temp_hash = values.first.dup
+
+ # Is input data an array?
+ elsif values.is_a?(Array)
raise ArgumentError, 'Must specify all fields in input array!' \
- unless values[0].size == @field_names[1..-1].size
- temp_hash = {}
- @field_names[1..-1].collect { |f|
- temp_hash[f] = values[0][@field_names.index(f)-1]
- }
- return temp_hash
- elsif values.class == Array
- raise ArgumentError, 'Must specify all fields in input array!' \
unless values.size == @field_names[1..-1].size
- temp_hash = {}
- @field_names[1..-1].collect { |f|
+
+ @field_names[1..-1].each do |f|
temp_hash[f] = values[@field_names.index(f)-1]
- }
- return temp_hash
+ end
else
raise(ArgumentError, 'Invalid type for values container!')
end
+
+ return temp_hash
end
#-----------------------------------------------------------------------
+ # check_required_fields
+ #-----------------------------------------------------------------------
+ #++
+ # Check that all required fields have values.
+ #
+ 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
+
+ #-----------------------------------------------------------------------
+ # check_against_input_for_specials
+ #-----------------------------------------------------------------------
+ #++
+ # Check that no special field types (i.e. calculated or link_many
+ # fields)
+ # have been given values.
+ #
+ 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
+
+ #-----------------------------------------------------------------------
# validate_input
#-----------------------------------------------------------------------
#++
# Check input data to ensure proper data types.
#
def validate_input(data)
- raise 'Cannot insert/update recno field!' if data.has_key?(:recno)
-
@field_names[1..-1].each do |f|
- next unless data.has_key?(f)
+ next if data[f].nil?
- if data[f].nil?
- raise 'A value for this field is required: %s' % f if \
- @field_requireds[@field_names.index(f)]
- next
- end
-
- case @field_types[@field_names.index(f)]
- when /:String|:Blob/
- raise 'Invalid String value for: %s' % f unless \
- data[f].respond_to?(:to_str)
- when :Memo
- raise 'Invalid Memo value for: %s' % f unless \
- data[f].is_a?(KBMemo)
- when :Blob
- raise 'Invalid Blob value for: %s' % f unless \
- data[f].is_a?(KBBlob)
- when :Boolean
- raise 'Invalid Boolean value for: %s' % f unless \
- data[f].is_a?(TrueClass) or data[f].kind_of?(FalseClass)
- when :Integer
- raise 'Invalid Integer value for: %s' % f unless \
- data[f].respond_to?(:to_int)
- when :Float
- raise 'Invalid Float value for: %s' % f unless \
- data[f].respond_to?(:to_f)
- when :Time
- raise 'Invalid Time value for: %s' % f unless \
- data[f].is_a?(Time)
- when :Date
- raise 'Invalid Date value for: %s' % f unless \
- data[f].is_a?(Date)
- when :DateTime
- raise 'Invalid DateTime value for: %s' % f unless \
- data[f].is_a?(DateTime)
- when :YAML
- raise 'Invalid YAML value for: %s' % f unless \
- data[f].respond_to?(:to_yaml)
- end
+ 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
#-----------------------------------------------------------------------
# update_header_vars
@@ -2961,10 +3081,13 @@
end
#-----------------------------------------------------------------------
# get_result_struct
#-----------------------------------------------------------------------
+ #++
+ # Return Struct object that will hold result record.
+ #
def get_result_struct(query_type, filter)
case query_type
when :select
return Struct.new(*filter) if @record_class == 'Struct'
when :update
@@ -2976,10 +3099,13 @@
end
#-----------------------------------------------------------------------
# create_result_rec
#-----------------------------------------------------------------------
+ #++
+ # Return Struct/custom class populated with table row data.
+ #
def create_result_rec(query_type, filter, result_struct, tbl_rec, rec)
# If this isn't a select query or if it is a select query, but
# the table record class is simply a Struct, then we will use
# a Struct for the result record type.
if query_type != :select
@@ -3034,15 +3160,15 @@
tbl_rec = @table_class.new(self)
# Loop through table.
@db.engine.get_recs(self).each do |rec|
tbl_rec.populate(rec)
- next unless select_cond.call(tbl_rec) unless select_cond.nil?
+ 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
#-----------------------------------------------------------------------
@@ -3097,20 +3223,18 @@
# Return records from table that match select condition using the
# table's recno index instead of searching the whole file.
#
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))
+ good_matches << key if select_cond.call(idx_struct.new(key))
end
rescue NoMethodError
- raise "Field name in select block not part of index!"
+ 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
@@ -3148,11 +3272,10 @@
@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)
@@ -3175,19 +3298,26 @@
@db = db
@filepath = filepath
@contents = contents
end
+ #-----------------------------------------------------------------------
+ # read_from_file
+ #-----------------------------------------------------------------------
def read_from_file
@contents = @db.engine.read_memo_file(@filepath)
end
+ #-----------------------------------------------------------------------
+ # write_to_file
+ #-----------------------------------------------------------------------
def write_to_file
@db.engine.write_memo_file(@filepath, @contents)
end
end
+
#---------------------------------------------------------------------------
# KBBlob
#---------------------------------------------------------------------------
class KBBlob
attr_accessor :filepath, :contents
@@ -3199,14 +3329,20 @@
@db = db
@filepath = filepath
@contents = contents
end
+ #-----------------------------------------------------------------------
+ # read_from_file
+ #-----------------------------------------------------------------------
def read_from_file
@contents = @db.engine.read_blob_file(@filepath)
end
+ #-----------------------------------------------------------------------
+ # write_to_file
+ #-----------------------------------------------------------------------
def write_to_file
@db.engine.write_blob_file(@filepath, @contents)
end
end
@@ -3214,13 +3350,12 @@
#---------------------------------------------------------------------------
# KBIndex
#---------------------------------------------------------------------------
class KBIndex
include KBTypeConversionsMixin
+ include KBEncryptionMixin
- UNENCODE_RE = /&(?:amp|linefeed|carriage_return|substitute|pipe);/
-
#-----------------------------------------------------------------------
# initialize
#-----------------------------------------------------------------------
def initialize(table, index_fields)
@last_update = Time.new
@@ -3275,11 +3410,12 @@
# Create the index record by pulling out the record fields
# that make up this index and converting them to their
# native types.
idx_rec = []
@col_poss.zip(@col_types).each do |col_pos, col_type|
- idx_rec << convert_to(col_type, rec[col_pos])
+ idx_rec << convert_to_native_type(col_type,
+ rec[col_pos])
end
# Were all the index fields for this record equal to NULL?
# Then don't add this index record to index array; skip to
# next record.
@@ -3301,11 +3437,11 @@
#-----------------------------------------------------------------------
# add_index_rec
#-----------------------------------------------------------------------
def add_index_rec(rec)
@idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type|
- convert_to(col_type, rec[col_pos])
+ convert_to_native_type(col_type, rec[col_pos])
end + [rec.first.to_i]
@last_update = Time.new
end
@@ -3330,11 +3466,11 @@
#---------------------------------------------------------------------------
# KBRecnoIndex
#---------------------------------------------------------------------------
class KBRecnoIndex
-# include DRb::DRbUndumped
+ include KBEncryptionMixin
#-----------------------------------------------------------------------
# initialize
#-----------------------------------------------------------------------
def initialize(table)
@@ -3343,10 +3479,10 @@
end
#-----------------------------------------------------------------------
# get_idx
#-----------------------------------------------------------------------
- def get_idx
+ def get_idx
return @idx_hash
end
#-----------------------------------------------------------------------
# rebuild