# * George Moschovitis # (c) 2004-2005 Navel, all rights reserved. # $Id: mysql.rb 326 2005-03-28 11:07:17Z gmosx $ begin require 'mysql' rescue Object => ex Logger.error 'Ruby-Mysql bindings are not installed!' Logger.error ex end require 'og/adapter' require 'og/connection' require 'glue/attribute' module Og # The MySQL adapter. This adapter communicates with # an MySQL rdbms. For extra documentation see # lib/og/adapter.rb class MysqlAdapter < Adapter def initialize super @typemap.update(TrueClass => 'tinyint') @typecast.update(TrueClass => "#\{:s: ? \"1\" : 'NULL' \}") end def self.escape(str) return nil unless str return Mysql.quote(str) end def self.timestamp(time = Time.now) return nil unless time return time.strftime("%Y%m%d%H%M%S") end def self.date(date) return nil unless date return "#{date.year}-#{date.month}-#{date.mday}" end def write_prop(p) if p.klass.ancestors.include?(Integer) return "#\{@#{p.symbol} || 'NULL'\}" elsif p.klass.ancestors.include?(Float) return "#\{@#{p.symbol} || 'NULL'\}" elsif p.klass.ancestors.include?(String) return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(Time) return %|#\{@#{p.symbol} ? "'#\{#{self.class}.timestamp(@#{p.symbol})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(Date) return %|#\{@#{p.symbol} ? "'#\{#{self.class}.date(@#{p.symbol})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(TrueClass) return "#\{@#{p.symbol} ? 1 : 0 \}" else # gmosx: keep the '' for nil symbols. return %|#\{@#{p.symbol} ? "'#\{#{self.class}.escape(@#{p.symbol}.to_yaml)\}'" : "''"\}| end end def read_prop(p, idx) if p.klass.ancestors.include?(Integer) return "res[#{idx}].to_i" elsif p.klass.ancestors.include?(Float) return "res[#{idx}].to_f" elsif p.klass.ancestors.include?(String) return "res[#{idx}]" elsif p.klass.ancestors.include?(Time) return "#{self.class}.parse_timestamp(res[#{idx}])" elsif p.klass.ancestors.include?(Date) return "#{self.class}.parse_date(res[#{idx}])" elsif p.klass.ancestors.include?(TrueClass) return "('0' != res[#{idx}])" else return "YAML::load(res[#{idx}])" end end def create_db(database, user = nil, password = nil) # gmosx: system is used to avoid shell expansion. system 'mysqladmin', '-f', "--user=#{user}", "--password=#{password}", 'create', database super end def drop_db(database, user = nil, password = nil) system 'mysqladmin', '-f', "--user=#{user}", "--password=#{password}", 'drop', database super end def props_for_insert(klass) klass.__props.reject { |p| :oid == p.symbol } end def insert_code(klass, db, pre_cb, post_cb) props = props_for_insert(klass) values = props.collect { |p| write_prop(p) }.join(',') sql = "INSERT INTO #{klass::DBTABLE} (#{props.collect {|p| p.name}.join(',')}) VALUES (#{values})" %{ #{pre_cb} conn.store.query_with_result = false conn.store.query "#{sql}" @oid = conn.store.insert_id() #{post_cb} } end def new_connection(db) return MysqlConnection.new(db) end def calc_field_index(klass, db) res = db.query "SELECT * FROM #{klass::DBTABLE} LIMIT 1" meta = db.managed_classes[klass] for idx in (0...res.num_fields) meta.field_index[res.fetch_field.name] = idx end ensure res.free if res end def create_fields(klass) 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]}" if p.meta # set default value (gmosx: not that useful in the # current implementation). if default = p.meta[:default] field << " DEFAULT #{default.inspect} NOT NULL" end # set unique # FIXME: correctly handle UNIQUE constrain. # field << " UNIQUE" if p.meta[:unique] # attach extra sql if extra_sql = p.meta[:extra_sql] field << " #{extra_sql}" end end end fields << field end return fields end def create_table(klass, db) conn = db.get_connection fields = create_fields(klass) sql = "CREATE TABLE #{klass::DBTABLE} (#{fields.join(', ')}" conn.store.query_with_result = true # Create table constrains if klass.__meta and constrains = klass.__meta[:sql_constrain] sql << ", #{constrains.join(', ')}" end sql << ');' begin conn.store.query(sql) Logger.info "Created table '#{klass::DBTABLE}'." rescue => ex if ex.errno == 1050 # table already exists. Logger.debug "Table already exists" if $DBG return else raise end end # Create indices if klass.__meta and indices = klass.__meta[:sql_index] for data in indices idx, options = *data idx = idx.to_s pre_sql, post_sql = options[:pre], options[:post] idxname = idx.gsub(/ /, "").gsub(/,/, "_").gsub(/\(.*\)/, "") conn.store.query("CREATE #{pre_sql} INDEX #{klass::DBTABLE}_#{idxname}_idx #{post_sql} ON #{klass::DBTABLE} (#{idx})") end end # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if klass.__meta and joins = klass.__meta[:sql_join] for data in joins # the class to join to and some options. join_name, join_class, options = *data # gmosx: dont use DBTABLE here, perhaps the join class # is not managed yet. join_table = "#{self.class.join_table(klass, join_class, join_name)}" join_src = "#{self.class.encode(klass)}_oid" join_dst = "#{self.class.encode(join_class)}_oid" begin conn.store.query("CREATE TABLE #{join_table} ( key1 integer NOT NULL, key2 integer NOT NULL )") conn.store.query("CREATE INDEX #{join_table}_key1_idx ON #{join_table} (key1)") conn.store.query("CREATE INDEX #{join_table}_key2_idx ON #{join_table} (key2)") rescue => ex if ex.errno == 1050 # table already exists. Logger.debug "Join table already exists" if $DBG else raise end end end end ensure db.put_connection end def eval_og_oid(klass) klass.class_eval %{ prop_accessor :oid, Fixnum, :sql => 'integer AUTO_INCREMENT PRIMARY KEY' } end end # The MySQL connection. class MysqlConnection < Connection def initialize(db) super config = db.config @store = Mysql.connect( config[:address] || 'localhost', config[:user], config[:password], config[:database] ) rescue => ex if ex.errno == 1049 # database does not exist. Logger.info "Database '#{config[:database]}' not found!" @db.adapter.create_db(config[:database], config[:user], config[:password]) retry end raise end def close @store.close super end def prepare(sql) raise 'Not implemented!' end def query(sql) Logger.debug sql if $DBG begin @store.query_with_result = true return @store.query(sql) rescue => ex handle_db_exception(ex, sql) end end def exec(sql) Logger.debug sql if $DBG begin @store.query_with_result = false @store.query(sql) rescue => ex handle_db_exception(ex, sql) end end def start # @store.transaction end def commit # @store.commit end def rollback # @store.rollback end def valid_res?(res) return !(res.nil? or 0 == res.num_rows) end def read_one(res, klass) return nil unless valid_res?(res) row = res.fetch_row obj = klass.new obj.og_read(row) res.free return obj end def read_all(res, klass) return [] unless valid_res?(res) objects = [] for tuple in (0...res.num_rows) row = res.fetch_row obj = klass.new obj.og_read(row) objects << obj end res.free return objects end def read_int(res, idx = 0) val = res.fetch_row[idx].to_i res.free return val end def get_row(res) res.fetch_row end end end