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