begin require 'lib/vendor/kirbybase' rescue Object => ex Logger.error 'KirbyBase is not installed!' Logger.error ex end require 'og/store/sql' module Og # A Store that persists objects into an KirbyBase database. # KirbyBase is a pure-ruby database implementation. # To read documentation about the methods, consult the # documentation for SqlStore and Store. class KirbyStore < SqlStore def self.db_dir(options) "#{options[:name]}_db" end def self.destroy(options) begin FileUtils.rm_rf(db_dir(options)) super rescue Object Logger.info "Cannot drop '#{options[:name]}'!" end end def initialize(options) super if options[:embedded] name = self.class.db_dir(options) FileUtils.mkdir_p(name) @conn = KirbyBase.new(:local, nil, nil, name) else # TODO end end def close super end def enchant(klass, manager) klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY' super end def query(sql) Logger.debug sql if $DBG return @conn.query(sql) rescue => ex handle_sql_exception(ex, sql) end def exec(sql) Logger.debug sql if $DBG @conn.query(sql).close rescue => ex handle_sql_exception(ex, sql) end def start # nop end def commit # nop end def rollback # nop end private def create_table(klass) fields = fields_for_class(klass) table = @conn.create_table(klass::OGTABLE, *fields) { |obj| obj.encrypt = false } # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if klass.__meta and join_tables = klass.__meta[:join_tables] for join_table in join_tables begin @conn.create_table(join_table[:table], join_table[:first_key], :Integer, join_table[:second_key], :Integer) # KirbyBase doesn't support indices. rescue RuntimeError => error # Unfortunately, KirbyBase just throws RuntimeErrors # with no extra information, so we just have to look # for the error message it uses. if error.message =~ /table #{join_table[:table]} already exists/i Logger.debug 'Join table already exists' if $DBG else raise end end end end end def drop_table(klass) @conn.drop_table(klass.table) if @conn.table_exists?(klass.table) end def fields_for_class(klass) fields = [] klass.properties.each do |p| klass.index(p.symbol) if p.meta[:index] fields << p.symbol type = p.klass.name.intern type = :Integer if type == :Fixnum fields << type end return fields end def create_field_map(klass) map = {} fields = @conn.get_table(klass.table).field_names fields.size.times do |i| map[fields[i]] = i end return map end # Return an sql string evaluator for the property. # No need to optimize this, used only to precalculate code. # YAML is used to store general Ruby objects to be more # portable. #-- # FIXME: add extra handling for float. #++ def write_prop(p) if p.klass.ancestors.include?(Integer) return "@#{p.symbol} || nil" elsif p.klass.ancestors.include?(Float) return "@#{p.symbol} || nil" elsif p.klass.ancestors.include?(String) return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : nil| elsif p.klass.ancestors.include?(Time) return %|@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : nil| elsif p.klass.ancestors.include?(Date) return %|@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : nil| elsif p.klass.ancestors.include?(TrueClass) return "@#{p.symbol} ? \"'t'\" : nil" else # gmosx: keep the '' for nil symbols. return %|@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"| end end # Return an evaluator for reading the property. # No need to optimize this, used only to precalculate code. def read_prop(p, col) if p.klass.ancestors.include?(Integer) return "#{self.class}.parse_int(res[#{col} + offset])" elsif p.klass.ancestors.include?(Float) return "#{self.class}.parse_float(res[#{col} + offset])" elsif p.klass.ancestors.include?(String) return "res[#{col} + offset]" elsif p.klass.ancestors.include?(Time) return "#{self.class}.parse_timestamp(res[#{col} + offset])" elsif p.klass.ancestors.include?(Date) return "#{self.class}.parse_date(res[#{col} + offset])" elsif p.klass.ancestors.include?(TrueClass) return "('0' != res[#{col} + offset])" else return "YAML::load(res[#{col} + offset])" end end # :section: Lifecycle method compilers. # Compile the og_update method for the class. def eval_og_insert(klass) pk = klass.pk_symbol props = klass.properties data = props.collect {|p| ":#{p.symbol} => #{write_prop(p)}"}.join(', ') # data.gsub!(/#|\{|\}/, '') klass.module_eval %{ def og_insert(store) #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} store.conn.get_table('#{klass.table}').insert(#{data}) #{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 props = klass.properties.reject { |p| pk == p.symbol } updates = props.collect { |p| "#{p.symbol}=#{write_prop(p)}" } sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}" klass.module_eval %{ def og_update(store) #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} store.exec "#{sql}" #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)} end } end # Compile the og_read method for the class. This method is # used to read (deserialize) the given class from the store. # In order to allow for changing field/attribute orders a # field mapping hash is used. def eval_og_read(klass) code = [] props = klass.properties field_map = create_field_map(klass) props.each do |p| if col = field_map[p.symbol] code << "@#{p.symbol} = #{read_prop(p, col)}" end end code = code.join('; ') 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)} #{code} #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)} end } end #-- # FIXME: is pk needed as parameter? #++ def eval_og_delete(klass) klass.module_eval %{ def og_delete(store, pk, cascade = true) #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} pk ||= @#{klass.pk_symbol} transaction do |tx| tx.exec "DELETE FROM #{klass.table} WHERE #{klass.pk_symbol}=\#{pk}" if cascade and #{klass}.__meta[:descendants] #{klass}.__meta[:descendants].each do |dclass, foreign_key| tx.exec "DELETE FROM \#{dclass::OGTABLE} WHERE \#{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