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('&amp;', "&") + 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('&amp;', "&") + 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("&", '&amp;').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("&", '&amp;').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('&amp;', "&") - 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('&amp;', "&") - 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("&", '&amp;').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("&", '&amp;').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