lib/kirbybase.rb in KirbyBase-2.5.2 vs lib/kirbybase.rb in KirbyBase-2.6

- old
+ new

@@ -1,11 +1,16 @@ require 'date' require 'time' require 'drb' -require 'csv' require 'fileutils' require 'yaml' + +begin # first choice--for speed + require 'faster_csv' +rescue LoadError # second choice--slower but standard + require 'csv' +end # # :main:KirbyBase # :title:KirbyBase Class Documentation # KirbyBase is a class that allows you to create and manipulate simple, @@ -150,15 +155,36 @@ # * Changed the behavior of KBTable#insert method. If user explicitly # specifies nil for a field value and there is a default value for that # field, the default value will no longer override the user specified nil # value. Thanks to Assaph Mehr for suggesting this. # +# 2006-06-27:: Version 2.6 +# * Removed NilClass#method_missing. I have replaced it with a new class +# called KBNilClass. Thanks to a host of people for help on this, +# including: Assaph Mehr, James Edward Gray II, Matthew Desmarais, +# Logan Capaldo, Trans, John Carter, Dave Burt and anyone else I missed. +# * Added conditional require logic so that KirbyBase will use FasterCVS if +# it is available. Thanks to James Edward Gray II for this. +# * You can now delay index creation in local mode. Thanks to Nicholas Rahn +# for this. +# * Added ability to allow for a custom record class with no kb_create or +# kb_defaults methods. KirbyBase will return each result record as an +# instance of the custom record class, and will attempt to set attributes +# with the same names as the table's field names equal to the values of +# the table record's fields. Thanks to Hal Fulton for this idea. # #--------------------------------------------------------------------------- # KBTypeConversionsMixin #--------------------------------------------------------------------------- module KBTypeConversionsMixin + # Constant that will represent a kb_nil in the physical table file. + # If you think you might need to write the value 'kb_nil' to a field + # yourself, comment out the following line and un-comment the line + # below that to use an alternative representation for kb_nil. + KB_NIL = 'kb_nil' + #KB_NIL = '&kb_nil;' + # 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|\|/ @@ -168,12 +194,16 @@ #----------------------------------------------------------------------- #++ # 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? + return kb_nil if s == KB_NIL + # I added this line to keep KBTable#import_csv working after I made + # the kb_nil changes. + return nil if 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;', "|" @@ -234,10 +264,12 @@ #----------------------------------------------------------------------- #++ # Return value converted to encoded String object suitable for storage. # def convert_to_encoded_string(data_type, value) + return KB_NIL if value.nil? + case data_type when :YAML y = value.to_yaml if y =~ ENCODE_RE return y.gsub("&", '&amp;').gsub("\n", '&linefeed;').gsub( @@ -340,11 +372,11 @@ #--------------------------------------------------------------------------- class KirbyBase include DRb::DRbUndumped include KBTypeConversionsMixin - VERSION = "2.5.2" + VERSION = "2.6" attr_reader :engine attr_accessor(:connect_type, :host, :port, :path, :ext, :memo_blob_path, :delay_index_creation) @@ -420,17 +452,13 @@ # 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 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. You can turn this off by specifying true for - # the delay_index_creation argument. - if server? and @delay_index_creation + # You can delay index creation until the first time the index is + # used. + if @delay_index_creation else @engine.tables.each do |tbl| @table_hash[tbl] = \ KBTable.create_called_from_database_instance(self, tbl, File.join(@path, tbl.to_s + @ext)) @@ -2119,15 +2147,11 @@ 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 - convert_to_encoded_string(ft, input_rec[fn]) - end + convert_to_encoded_string(ft, input_rec[fn]) 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) @@ -2749,11 +2773,13 @@ # def import_csv(csv_filename) records_inserted = 0 tbl_rec = @table_class.new(self) - CSV.open(csv_filename, 'r') do |row| + # read with FasterCSV if loaded, or the standard CSV otherwise + (defined?(FasterCSV) ? FasterCSV : CSV).foreach(csv_filename + ) do |row| tbl_rec.populate([nil] + row) insert(tbl_rec) records_inserted += 1 end return records_inserted @@ -2921,16 +2947,16 @@ END_OF_STRING end get_meth_upd_res_str = <<-END_OF_STRING def #{field_name}_upd_res - return nil + return kb_nil end END_OF_STRING set_meth_str = <<-END_OF_STRING def #{field_name}=(s) - @#{field_name} = nil + @#{field_name} = kb_nil end END_OF_STRING end # If this is a Calculated field, modify the get/set methods. @@ -2942,16 +2968,16 @@ return #{calculation} end END_OF_STRING get_meth_upd_res_str = <<-END_OF_STRING def #{field_name}_upd_res() - return nil + return kb_nil end END_OF_STRING set_meth_str = <<-END_OF_STRING def #{field_name}=(s) - @#{field_name} = nil + @#{field_name} = kb_nil end END_OF_STRING end @table_class.class_eval(get_meth_str) @@ -3079,13 +3105,21 @@ #----------------------------------------------------------------------- #++ # Read header record and update instance variables. # def update_header_vars - @encrypted, @last_rec_no, @del_ctr, @record_class, @field_names, \ - @field_types, @field_indexes, @field_defaults, @field_requireds, \ - @field_extras = @db.engine.get_header_vars(self) + @encrypted, @last_rec_no, @del_ctr, @record_class, @col_names, \ + @col_types, @col_indexes, @col_defaults, @col_requireds, \ + @col_extras = @db.engine.get_header_vars(self) + + # These are deprecated. + @field_names = @col_names + @field_types = @col_types + @field_indexes = @col_indexes + @field_defaults = @col_defaults + @field_requireds = @col_requireds + @field_extras = @col_extras end #----------------------------------------------------------------------- # get_result_struct #----------------------------------------------------------------------- @@ -3116,34 +3150,55 @@ # a Struct for the result record type. if query_type != :select result_rec = result_struct.new(*filter.collect { |f| tbl_rec.send("#{f}_upd_res".to_sym) }) elsif @record_class == 'Struct' - result_rec = result_struct.new(*filter.collect { |f| - tbl_rec.send(f) }) + result_rec = result_struct.new(*filter.collect do |f| + if tbl_rec.send(f).kb_nil? + nil + else + tbl_rec.send(f) + end + end) else if Object.full_const_get(@record_class).respond_to?(:kb_create) result_rec = Object.full_const_get(@record_class - ).kb_create(*@field_names.collect { |f| - # Just a warning here: If you specify a filter on - # a select, you are only going to get those fields - # you specified in the result set, EVEN IF - # record_class is a custom class instead of Struct. - if filter.include?(f) - tbl_rec.send(f) - else - nil - end - }) + ).kb_create(*@field_names.collect do |f| + # Just a warning here: If you specify a filter on + # a select, you are only going to get those fields + # you specified in the result set, EVEN IF + # record_class is a custom class instead of Struct. + if filter.include?(f) + if tbl_rec.send(f).kb_nil? + nil + else + tbl_rec.send(f) + end + else + nil + end + end) elsif Object.full_const_get(@record_class).respond_to?( :kb_defaults) result_rec = Object.full_const_get(@record_class).new( - *@field_names.collect { |f| - tbl_rec.send(f) || Object.full_const_get( - @record_class).kb_defaults[@field_names.index(f)] - } - ) + *@field_names.collect do |f| + if tbl_rec.send(f).kb_nil? + nil + else + tbl_rec.send(f) || Object.full_const_get( + @record_class).kb_defaults[@field_names.index(f)] + end + end) + else + result_rec = Object.full_const_get(@record_class).allocate + @field_names.each do |fn| + if tbl_rec.send(fn).kb_nil? + result_rec.send("#{fn}=", nil) + else + result_rec.send("#{fn}=", tbl_rec.send(fn)) + end + end end end unless query_type == :select result_rec.fpos = rec[-2] @@ -3411,46 +3466,24 @@ next if line == '' # Split the line up into fields. rec = line.split('|', @col_poss.max+2) - # 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_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. - next if idx_rec.compact.empty? - - # Add recno to the end of this index record. - idx_rec << rec.first.to_i - - # Add index record to index array. - @idx_arr << idx_rec + append_new_rec_to_index_array(rec) end # Here's how we break out of the loop... rescue EOFError end - + @last_update = Time.new end #----------------------------------------------------------------------- # add_index_rec #----------------------------------------------------------------------- def add_index_rec(rec) - @idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type| - convert_to_native_type(col_type, rec[col_pos]) - end + [rec.first.to_i] - - @last_update = Time.new + @last_upddate = Time.new if append_new_rec_to_index_array(rec) end #----------------------------------------------------------------------- # delete_index_rec #----------------------------------------------------------------------- @@ -3465,10 +3498,26 @@ #----------------------------------------------------------------------- def update_index_rec(rec) delete_index_rec(rec.first.to_i) add_index_rec(rec) end + + #----------------------------------------------------------------------- + # append_new_rec_to_index_array + #----------------------------------------------------------------------- + def append_new_rec_to_index_array(rec) + idx_rec = [] + @col_poss.zip(@col_types).each do |col_pos, col_type| + idx_rec << convert_to_native_type(col_type, rec[col_pos]) + end + + return false if idx_rec.uniq == [kb_nil] + + idx_rec << rec.first.to_i + @idx_arr << idx_rec + return true + end end #--------------------------------------------------------------------------- # KBRecnoIndex @@ -3571,11 +3620,11 @@ end end def clear @tbl.field_names.each do |fn| - send("#{fn}=", nil) + send("#{fn}=", kb_nil) end end end @@ -3678,10 +3727,11 @@ else x << a_value y << b_value end end + x <=> y } end #----------------------------------------------------------------------- @@ -3736,46 +3786,86 @@ end end #--------------------------------------------------------------------------- +# KBNilClass +#--------------------------------------------------------------------------- +class KBNilClass + include Comparable + + class << self + def new + @kb_nil ||= KBNilClass.allocate + end + end + + def inspect + 'kb_nil' + end + + def kb_nil? + true + end + + def to_s + "" + end + + def to_i + 0 + end + + def to_f + 0.0 + end + + def to_a + [] + end + + def <=>(other) + return 0 if other.kb_nil? + return -1 + end + + def coerce(other) + return [other, to_i] if other.kind_of? Fixnum + return [other, to_f] if other.kind_of? Float + + raise "Didn't know how to coerce kb_nil to a #{other.class}" + end + + def method_missing(sym, *args) + kb_nil + end +end + + +#--------------------------------------------------------------------------- +# Kernel +#--------------------------------------------------------------------------- +module Kernel + def kb_nil + KBNilClass.new + end +end + + +#--------------------------------------------------------------------------- # Object #--------------------------------------------------------------------------- class Object def full_const_get(name) list = name.split("::") obj = Object list.each {|x| obj = obj.const_get(x) } obj end -end - -#--------------------------------------------------------------------------- -# NilClass -#--------------------------------------------------------------------------- -class NilClass - #----------------------------------------------------------------------- - # method_missing - #----------------------------------------------------------------------- - # - # This code is necessary because if, inside a select condition code - # block, there is a case where you are trying to do an expression - # against a table field that is equal to nil, I don't want a method - # missing exception to occur. I just want the expression to be nil. I - # initially had this method returning false, but then I had an issue - # where I had a YAML field that was supposed to hold an Array. If the - # field was empty (i.e. nil) it was actually returning false when it - # should be returning nil. Since nil evaluates to false, it works if I - # return nil. - # Here's an example: - # #select { |r| r.speed > 300 } - # What happens if speed is nil (basically NULL in DBMS terms)? Without - # this code, an exception is going to be raised, which is not what we - # really want. We really want this expression to return nil. - def method_missing(method_id, *stuff) - return nil + def kb_nil? + false end end #--------------------------------------------------------------------------- @@ -3800,5 +3890,7 @@ # to specify ascending sort order. def +@ ("+"+self.to_s).to_sym end end + +