begin require 'kirbybase' rescue Object => ex Logger.error "KirbyBase is not installed. Please run 'gem install KirbyBase'" Logger.error ex end require 'fileutils' require 'og/store/sql' module Og #Fix for schema inheritance. Instead of inferring the property on each #access of the database we add it explicitly when the marker module #is imported. Currently this fix is applied for this store only. The #other stores should be similarly altered. module SchemaInheritanceBase property :ogtype, String end # A Store that persists objects into a KirbyBase database. # To read documentation about the methods, consult the documentation # for SqlStore and Store. class KirbyStore < SqlStore Text = :Text # Override if needed. def self.base_dir(options) options[:base_dir] || './kirbydb' end def self.destroy(options) begin FileUtils.rm_rf(base_dir(options)) super rescue Object Logger.info 'Cannot drop database!' end end def initialize(options) super @typemap = { Integer => :Integer, Fixnum => :Integer, Float => :Float, String => :String, Time => :Time, Date => :Date, TrueClass => :Boolean, FalseClass => :Boolean, Object => :Object, Array => :String, Hash => :String } mode = options[:mode] || :local if mode == :client # Use a client/server configuration. @conn = KirbyBase.new(:client, options[:address], options[:port]) else # Use an in-process configuration. base_dir = self.class.base_dir(options) FileUtils.mkdir_p(base_dir) unless File.exist?(base_dir) @conn = KirbyBase.new( :local, nil, nil, base_dir, options[:ext] || '.tbl' ) end end def close # Nothing to do. super end def enchant(klass, manager) klass.send(:attr_accessor, :oid) klass.send(:alias_method, :recno, :oid) klass.send(:alias_method, :recno=, :oid=) super end def get_table(klass) @conn.get_table(klass.table.to_sym) end # :section: Lifecycle methods. def load(pk, klass) convert_to_instance(klass, get_table(klass)[pk.to_i]) end #convert the given record into an instance of the given class. #Supports schema_inheritance. def convert_to_instance(klass, record) return nil unless record if(klass.schema_inheritance?) obj = eval("#{record.ogtype}.allocate") elsif obj = klass.allocate end record.each_pair do |k, v| assign_sym = (k.to_s + '=').to_sym if (v && obj.respond_to?(assign_sym)) obj.method(assign_sym).call(v) end end obj end alias_method :exist?, :load def reload(obj, pk) raise 'Cannot reload unmanaged object' unless obj.saved? new_obj = load(pk, obj.class) #TODO: replace obj with new_obj end def find(options) #FIXME: this currently overrides options with scope; this should #work the other way around if scope = options[:class].get_scope scope = scope.dup options.update(scope) end query(options) end def find_one(options) query(options).first end #-- # FIXME: optimize me! #++ def count(options) find(options).size end #-- # FIXME: optimize me! # #++ def aggregate(term = 'COUNT(*)', options = {}) case term.upcase when /COUNT/ count(options) when /MIN/ term =~ /MIN\((\w*)\)/ find_column_values($1, options){|result| result.min} when /MAX/ term =~ /MAX\((\w*)\)/ find_column_values($1, options) {|result| result.max} when /AVG/ term =~ /AVG\((\w*)\)/ values = find_column_values($1, options) do |results| results.inject(0.0){|sum, result| sum + result} / results.size end when /SUM/ term =~ /SUM\((\w*)\)/ values = find_column_values($1, options) do |results| results.inject(0.0){|sum, result| sum + result} end end end alias_method :calculate, :aggregate #find an array of values for the given column; or a hash of arrays #if there is a group by expression. Takes an optional block #that operates on each value in the array. def find_column_values(column_name, options) options[:column] = column_name results = find(options) if (block_given?) if (options[:group]) #hash of arrays results.values.collect { |group| yield group} else yield results end end end #needs refactoring, badly def query(options) Logger.debug "Querying with #{options.inspect}." if $DBG klass = options[:class] table = get_table(klass) result_set = [] if (klass.schema_inheritance_child?) type_condition = 'ogtype = \'' + klass.name + '\'' condition = options[:condition] if (condition) #if there is already an ogtype condition do nothing if (condition !~ /ogtype/) options[:condition] = condition + " and #{type_condition}" end else options[:condition] = type_condition end end #FIXME: add support for select statements with mathematics (tc_select) #as well as 'from table' statements if condition = options[:condition] || options[:where] condition = condition.is_a?(Array) ? prepare_statement(condition) : condition condition.gsub!(/ogtc_resolveoptions\w*\./, '') condition.gsub!(/([^><])=/, '\1==') condition.gsub!(/ OR /, ' or ') condition.gsub!(/ AND /, ' and ') condition.gsub!(/LIKE '(.*?)'/, '=~ /\1/') condition.gsub!(/\%/, '') condition.gsub!(/(\w*?)\s?([=><])(.)/, 'o.\1 \2\3') condition.gsub!(/o.oid/, 'o.recno') if(condition =~ /LIMIT (\d+)/) condition.gsub!(/LIMIT (\d+)/, '') options[:limit] = $1.to_i end result_set = eval("table.select do |o| #{condition} end") else result_set = table.select end if (order = options[:order]) order = order.to_s desc = (order =~ /DESC/) order = order.gsub(/DESC/, '').gsub(/ASC/, '') eval("result_set.sort { |x, y| x.#{order} <=> y.#{order} }") result_set.reverse! if desc end if (options[:limit]) limit = options[:limit] if (result_set.size > limit) result_set = result_set[0, limit] end end if (column = options[:column]) if (!result_set.methods.include?(column)) return [] end begin #if there is a group by expression on a valid column #gather arrays of values if ((group = options[:group]) && result_set.method(options[:group])) #store an empty array under the key if there is no value groups = Hash.new {|groups, key| groups[key] = []} result_set.each do |object| groups[object.method(group).call] << object.method(column).call end return groups end rescue NameError => error Logger.warn 'Attempt to group by missing column:' Logger.warn 'Column \'' + options[:group].to_s + '\' does not exist on table \'' + klass.name + '\'' raise error end #when called on a resultset a column method returns an array of that #column's values return result_set.method(column).call() end result_set.map { |object| convert_to_instance(klass, object) } end def start # nop end # Commit a transaction. def commit # nop, not supported? end # Rollback a transaction. def rollback # nop, not supported? end def sql_update(sql) # nop, not supported. end #FIXME: this method relies on naming conventions and does not account #for more than one set of '::' marks. def join(obj1, obj2, table, options = nil) first, second = join_object_ordering(obj1, obj2) options[get_key(first)] = first.pk options[get_key(second)] = second.pk @conn.get_table(table.to_sym).insert(options) end #retrieve the join table key name for the object def get_key(obj) obj.class.name =~ /::(\w*)/ ($1.downcase + '_oid').to_sym end def unjoin(obj1, obj2, table) first, second = join_object_ordering(obj1, obj2) @conn.get_table(table.to_sym).delete do |r| r.send(get_key(first)) == first.pk and r.send(get_key(second)) == second.pk end end private def typemap(key) @typemap[key] end def create_table(klass) if @conn.table_exists?(klass.table.to_sym) get_table(klass).pack # Kirby specific method of database cleanup. field_names = field_names_for_class(klass) actual_fields = get_table(klass).field_names field_names.each do |needed_field| next if actual_fields.include?(needed_field) if @options[:evolve_schema] == true Logger.debug "Adding field '#{needed_field}' to '#{klass.table}'" if $DBG field_type = typemap(klass.properties[needed_field].klass) if get_table(klass).respond_to?(:add_column) get_table(klass).add_column(needed_field, field_type) else @conn.add_table_column(klass.table, needed_field, field_type) end else Logger.warn "Table '#{klass.table}' is missing field '#{needed_field}' and :evolve_schema is not set to true!" end end actual_fields.each do |obsolete_field| next if field_names.include?(obsolete_field) || obsolete_field == :recno if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass.table}'" if $DBG if get_table(klass).respond_to?(:drop_column) get_table(klass).drop_column(obsolete_field) else @conn.drop_table_column(klass.table, obsolete_field) end else Logger.warn "You have an obsolete field '#{obsolete_field}' on table '#{klass.table}' and :evolve_schema is not set or is in cautious mode!" end end else Logger.debug "Creating table '#{klass.table}'" if $DBG fields = fields_for_class(klass) table = @conn.create_table(klass.table.to_sym, *fields) end =begin # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if join_tables = klass.ann.self[:join_tables] for info in join_tables unless @conn.table_exists?(info[:table].to_sym) @conn.create_table(info[:table].to_sym, *create_join_table_sql(info)) Logger.debug "Created jointable '#{info[:table]}'." if $DBG end end end =end end def create_join_table_sql(join_info) [join_info[:first_key].to_sym, :Integer, join_info[:second_key].to_sym, :Integer] end def drop_table(klass) @conn.drop_table(klass.table) if @conn.table_exists?(klass.table) end def field_names_for_class(klass) properties = get_properties_for_class(klass) properties.values.map {|p| p.symbol } end def fields_for_class(klass) fields = [] get_properties_for_class(klass).values.each do |p| klass.index(p.symbol) if p.index fields << p.symbol type = typemap(p.klass) fields << type end fields end def eval_og_insert(klass) pk = klass.primary_key.symbol fields = get_properties_for_class(klass).keys fields = fields.map { |f| ":#{f} => (self.respond_to?(:#{f}) ? self.#{f} : nil)"} field_string = fields.join(', ') klass.class_eval %{ def og_insert(store) @ogtype = #{klass} #{::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} Logger.debug "Inserting \#{self}." if $DBG @#{pk} = store.get_table(#{klass}).insert({#{field_string}}) #{::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} end } end # Compile the og_update method for the class. def eval_og_update(klass) pk = klass.pk_symbol updates = klass.properties.keys.collect { |p| ":#{p} => @#{p}" } klass.module_eval %{ def og_update(store, options = nil) #{::Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} store.get_table(#{klass}).update { |r| r.recno == #{pk} }.set(#{updates.join(', ')}) #{::Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)} end } end def eval_og_read(klass) klass.module_eval %{ def og_read(res, row = 0, offset = 0) #{::Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)} #{::Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)} end } end def eval_og_delete(klass) klass.module_eval %{ def og_delete(store, pk, cascade = true) pk ||= self.pk pk = pk.to_i #{::Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} table = store.get_table(self.class) transaction do |tx| table.delete { |r| r.recno == pk } if cascade and #{klass}.ann.self[:descendants] #{klass}.ann.self.descendants.each do |dclass, foreign_key| dtable = store.get_table(dclass) dtable.delete { |r| foreign_key == pk } end end end #{::Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)} end } end end end # * George Moschovitis