# * George Moschovitis # (c) 2004-2005 Navel, all rights reserved. # $Id: backend.rb 202 2005-01-17 10:44:13Z gmosx $ require 'yaml' require 'og/connection' class Og # Abstract backend. A backend communicates with the RDBMS. # This is the base class for the various backend implementations. class Backend # The actual connection to the database attr_accessor :conn # Intitialize the connection to the RDBMS. def initialize(config) raise "Not implemented" end # Close the connection to the RDBMS. def close() @conn.close() end # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # :section: Utilities # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Encode the name of the klass as an sql safe string. # The Module separators are replaced with _ and NOT stripped # out so that we can convert back to the original notation if # needed. The leading module if available is removed. def self.encode(klass) "#{klass.name.gsub(/^.*::/, "")}".gsub(/::/, "_").downcase end # The name of the SQL table where objects of this class # are stored. def self.table(klass) "_#{Og.table_prefix}#{encode(klass)}" end # The name of the join table for the two given classes. def self.join_table(klass1, klass2) "_#{Og.table_prefix}j_#{encode(klass1)}_#{encode(klass2)}" end # Returns the props that will be included in the insert query. # For some backends the oid should be stripped. def self.props_for_insert(klass) klass.__props end # Precompile the insert code for the given class. # The generated code sets the oid when inserting! def self.eval_og_insert(klass) props = props_for_insert(klass) values = props.collect { |p| write_prop(p) } sql = "INSERT INTO #{table(klass)} (#{props.collect {|p| p.name}.join(',')}) VALUES (#{values.join(',')})" if klass.instance_methods.include?("og_pre_insert") pre_cb = "og_pre_insert(conn);" else pre_cb = "" end if klass.instance_methods.include?("og_post_insert") post_cb = "og_post_insert(conn);" else post_cb = "" end if klass.instance_methods.include?("og_pre_insert_update") pre_cb << "og_pre_insert_update(conn);" end if klass.instance_methods.include?("og_post_insert_update") post_cb << "og_post_insert_update(conn);" end klass.class_eval %{ def og_insert(conn) #{insert_code(klass, sql, pre_cb, post_cb)} end } end # Precompile the update code for the given class. # Ignore the oid when updating! def self.eval_og_update(klass) props = klass.__props.reject { |p| :oid == p.symbol } updates = props.collect { |p| "#{p.name}=#{write_prop(p)}" } sql = "UPDATE #{klass::DBTABLE} SET #{updates.join(', ')} WHERE oid=#\{@oid\}" if klass.instance_methods.include?("og_pre_update") pre_cb = "og_pre_update(conn);" else pre_cb = "" end if klass.instance_methods.include?("og_post_update") post_cb = "og_post_update(conn);" else post_cb = "" end if klass.instance_methods.include?("og_pre_insert_update") pre_cb << "og_pre_insert_update(conn);" end if klass.instance_methods.include?("og_post_insert_update") post_cb << "og_post_insert_update(conn);" end klass.class_eval %{ def og_update(conn) #{pre_cb} conn.exec "#{sql}" #{post_cb} end } end # Precompile the code to read objects of the given class # from the backend. In order to allow for changing # field/attribute orders we have to use a field mapping hash. def self.eval_og_deserialize(klass, og) calc_field_index(klass, og) props = klass.__props code = [] props.each do |p| if idx = og.managed_classes[klass].field_index[p.name] # more fault tolerant if a new field is added and it # doesnt exist in the database. code << "@#{p.name} = #{read_prop(p, idx)}" end end klass.class_eval %{ def og_deserialize(res, tuple = nil) #{code.join('; ')} end } end # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # :section: Connection methods. # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - # Create the database. def self.create_db(database, user = nil, password = nil) Logger.info "Creating database '#{database}'." end # Drop the database. def self.drop_db(database, user = nil, password = nil) Logger.info "Dropping database '#{database}'." end # Execute an SQL query and return the result. def query(sql) raise "Not implemented" end # Execute an SQL query, no result returned. def exec(sql) raise "Not implemented" end # Execute an SQL query and return the result. Wrapped in a # rescue block. def safe_query(sql) raise "Not implemented" end # Execute an SQL query, no result returned. Wrapped in a # rescue block. def safe_exec(sql) raise "Not implemented" end # Check if it is a valid resultset. def valid?(res) raise "Not implemented" end # Start a new transaction. def start exec "START TRANSACTION" end # Commit a transaction. def commit exec "COMMIT" end # Rollback transaction. def rollback exec "ROLLBACK" end # Create the fields that correpsond to the klass properties. # The generated fields array is used in create_table. # If the property has an :sql metadata this overrides the # default mapping. If the property has an :extra_sql metadata # the extra sql is appended after the default mapping. def create_fields(klass, typemap) fields = [] klass.__props.each do |p| klass.sql_index(p.symbol) if p.meta[:sql_index] field = "#{p.symbol}" if p.meta and p.meta[:sql] field << " #{p.meta[:sql]}" else field << " #{typemap[p.klass]}" # attach extra sql if p.meta and extra_sql = p.meta[:extra_sql] field << " #{extra_sql}" end end fields << field end return fields end # Create the managed object table. The properties of the # object are mapped to the table columns. Additional sql relations # and constrains are created (indicices, sequences, etc). def create_table(klass) return if query("SELECT * FROM #{klass::DBTABLE} LIMIT 1") end # Drop the managed object table def drop_table(klass) exec "DROP TABLE #{klass::DBTABLE}" end # Deserialize one row of the resultset. def deserialize_one(res, klass) raise 'Not implemented' end # Deserialize all rows of the resultset. def deserialize_all(res, klass) raise 'Not implemented' end # Return a single integer value from the resultset. def get_int(res, idx = 0) raise 'Not implemented' end end end