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