lib/og/store/kirby.rb in og-0.23.0 vs lib/og/store/kirby.rb in og-0.24.0

- old
+ new

@@ -1,281 +1,264 @@ begin - require 'lib/vendor/kirbybase' + require 'vendor/kirbybase' rescue Object => ex Logger.error 'KirbyBase is not installed!' Logger.error ex end +require 'fileutils' + 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. +# 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 - def self.db_dir(options) - "#{options[:name]}_db" + # Override if needed. + + def self.base_dir(options) + options[:base_dir] || './kirbydb' end def self.destroy(options) begin - FileUtils.rm_rf(db_dir(options)) + FileUtils.rm_rf(base_dir(options)) super rescue Object - Logger.info "Cannot drop '#{options[:name]}'!" + Logger.info 'Cannot drop database!' end end def initialize(options) super + mode = options[:mode] || :local - if options[:embedded] - name = self.class.db_dir(options) - FileUtils.mkdir_p(name) - @conn = KirbyBase.new(:local, nil, nil, name) + if mode == :client + # Use a client/server configuration. + @conn = KirbyBase.new(:client, options[:address], options[:port]) else - # TODO + # 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.property :oid, Fixnum, :sql => 'integer PRIMARY KEY' + klass.send :attr_accessor, :recno + klass.send :alias_method, :oid, :recno + klass.send :alias_method, :oid=, :recno= + + symbols = klass.properties.keys + + klass.module_eval %{ + def self.kb_create(recno, #{symbols.join(', ')}) + obj = self.allocate + obj.recno = recno + #{ symbols.map { |s| "obj.#{s} = #{s}; "} } + return obj + end + } + super end - def query(sql) - Logger.debug sql if $DBG - return @conn.query(sql) - rescue => ex - handle_sql_exception(ex, sql) + def get_table(klass) + @conn.get_table(klass.table.to_sym) end + + # :section: Lifecycle methods. - def exec(sql) - Logger.debug sql if $DBG - @conn.query(sql).close - rescue => ex - handle_sql_exception(ex, sql) + def load(pk, klass) + get_table(klass)[pk.to_i] end + alias_method :exist?, :load - def start + def reload(obj, pk) + raise 'Cannot reload unmanaged object' unless obj.saved? + new_obj = load(pk, obj.class) + obj.clone(new_obj) + end + + def find(options) + query(options) + end + + def find_one(options) + query(options).first + end + + #-- + # FIXME: optimize me! + #++ + + def count(options) + find(options).size + end + + def query(options) + Logger.debug "Querying with #{options.inspect}." if $DBG + + klass = options[:class] + table = get_table(klass) + + objects = [] + + if condition = options[:condition] || options[:where] + condition.gsub!(/=/, '==') + condition.gsub!(/LIKE '(.*?)'/, '=~ /\1/') + condition.gsub!(/\%/, '') + condition.gsub!(/(\w*?)\s?=(.)/, 'o.\1 =\2') + objects = eval("table.select { |o| #{condition} }") + else + objects = table.select + end + + if order = options[:order] + desc = (order =~ /DESC/) + order = order.gsub(/DESC/, '').gsub(/ASC/, '') + eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }" + objects.reverse! if desc + end + + return objects + end + + def start # nop end + # Commit a transaction. + def commit - # nop + # nop, not supported? end + # Rollback a transaction. + def rollback - # nop + # nop, not supported? end + def sql_update(sql) + # nop, not supported. + 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 + begin + table = @conn.create_table(klass.table.to_sym, *fields) do |t| + t.record_class = klass end + rescue Object => ex + # gmosx: any idea how to better test this? + if ex.to_s =~ /already exists/i + Logger.debug "Table for '#{klass}' already exists!" + return + else + raise + end end end def drop_table(klass) - @conn.drop_table(klass.table) if @conn.table_exists?(klass.table) + @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] + klass.properties.values.each do |p| + klass.index(p.symbol) if p.index fields << p.symbol - type = p.klass.name.intern + type = p.klass.name.to_sym 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 + def eval_og_insert(klass) + pk = klass.primary_key.symbol - fields.size.times do |i| - map[fields[i]] = i + if klass.schema_inheritance? + props << Property.new(:symbol => :ogtype, :klass => String) + values << ", '#{klass}'" 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 %{ + klass.class_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}) + Logger.debug "Inserting \#{self}." if $DBG + @#{pk} = store.get_table(#{klass}).insert(self) #{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)}" - } + updates = klass.properties.keys.collect { |p| ":#{p} => @#{p}" } - sql = "UPDATE #{klass::OGTABLE} SET #{updates.join(', ')} WHERE #{pk}=#\{@#{pk}\}" - klass.module_eval %{ - def og_update(store) + def og_update(store, options = nil) #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} - store.exec "#{sql}" + 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 - - # 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) + pk ||= self.pk + pk = pk.to_i #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} - pk ||= @#{klass.pk_symbol} + table = store.get_table(self.class) 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}" + table.delete { |r| r.recno == pk } + if cascade and #{klass}.ann.this[:descendants] + #{klass}.ann.this.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