begin require 'mysql' rescue Object => ex Logger.error 'Ruby-Mysql bindings are not installed!' Logger.error 'Trying to use the pure-Ruby binding included in Og' begin # Attempt to use the included pure ruby version. require 'og/vendor/mysql' rescue Object => ex Logger.error ex end end # Helper for scripts. # # === Example # # mysql "-u root -p", <<-END # drop database if exists weblog_development; # create database weblog_development; # grant all on weblog_development.* to #{`id -un`.strip}@localhost; # END def mysql(opts, stream) IO.popen("mysql #{opts}", 'w') { |io| io.puts stream } end require 'og/store/sql' #-- # Customize the standard mysql resultset to make # more compatible with Og. #++ class Mysql::Result # :nodoc: all def blank? 0 == num_rows end alias_method :next, :fetch_row def each_row each do |row| yield(row, 0) end end def first_value val = fetch_row[0] free return val end alias_method :close, :free def fields fetch_fields.map { |f| f.name } end end module Og module MysqlUtils include SqlUtils def escape(str) return nil unless str return Mysql.quote(str) end end # A Store that persists objects into a MySQL database. # To read documentation about the methods, consult # the documentation for SqlStore and Store. # # Here is some useful code to initialize your MySQL # RDBMS for development. You probably want to be # more careful with provileges on your production # environment. # # mysql> GRANT ALL PRIVELEGES # ON keystone.* # TO <$sys_dbuser name>@localhost # IDENTIFIED BY '(password)' # WITH GRANT OPTION; class MysqlStore < SqlStore extend MysqlUtils include MysqlUtils DefaultPort = 3306 def self.create(options) options[:port] ||= DefaultPort # gmosx: system is used to avoid shell expansion. system 'mysqladmin', '-f', "--user=#{options[:user]}", "--password=#{options[:password]}", "--host=#{options[:address]}", "--port=#{options[:port]}", 'create', options[:name] super end def self.destroy(options) options[:port] ||= DefaultPort system 'mysqladmin', '-f', "--user=#{options[:user]}", "--password=#{options[:password]}", 'drop', "--host=#{options[:address]}", "--port=#{options[:port]}", options[:name] super end # Initialize the MySQL store. # # === Options # # * :address, the addres where the server is listening. # * :socket, is useful when the pure ruby driver is used. # this is the location of mysql.sock. For Ubuntu/Debian # this is '/var/run/mysqld/mysqld.sock'. You can find # the location for your system in my.cnf def initialize(options) super @typemap.update(TrueClass => 'tinyint', Time => 'datetime') @conn = Mysql.connect( options[:address] || 'localhost', options[:user], options[:password], options[:name], options[:port], options[:socket] ) # You should set recconect to true to avoid MySQL has # gone away errors. if @conn.respond_to? :reconnect options[:reconnect] = true unless options.has_key?(:reconnect) @conn.reconnect = options[:reconnect] end rescue => ex if ex.errno == 1049 # database does not exist. Logger.info "Database '#{options[:name]}' not found!" self.class.create(options) retry end raise end def close @conn.close super end def enchant(klass, manager) if klass.ann.self.primary_key.symbol == :oid unless klass.properties.include? :oid klass.property :oid, Fixnum, :sql => 'integer AUTO_INCREMENT PRIMARY KEY' end end super end def query(sql) Logger.debug sql if $DBG @conn.query_with_result = true return @conn.query(sql) rescue => ex handle_sql_exception(ex, sql) end def exec(sql) Logger.debug sql if $DBG @conn.query_with_result = false @conn.query(sql) rescue => ex handle_sql_exception(ex, sql) end def start # nop # FIXME: InnoDB supports transactions. end # Commit a transaction. def commit # nop, not supported? # FIXME: InnoDB supports transactions. end # Rollback a transaction. def rollback # nop, not supported? # FIXME: InnoDB supports transactions. end def sql_update(sql) exec(sql) @conn.affected_rows end # Deserialize one object from the ResultSet. def read_one(res, klass, options = nil) return nil if res.blank? if options and join_relations = options[:include] join_relations = [join_relations].flatten.collect do |n| klass.relation(n) end end res_row = res.next # causes STI classes to come back as the correct child class # if accessed from the superclass. klass = Og::Entity::entity_from_string(res_row[0]) if klass.schema_inheritance? obj = klass.og_allocate(res_row, 0) if options and options[:select] read_row(obj, res, res_row, 0) else obj.og_read(res_row) read_join_relations(obj, res_row, 0, join_relations) if join_relations end return obj ensure res.close end private def create_table(klass) # rp: fixes problems when user doesn't have # write access to db. # THINK, should a method more like this be # used instead of catching database exceptions # for 'table exists'? fields = fields_for_class(klass) if @conn.list_tables.include?(klass::OGTABLE) actual_fields = conn.list_fields(klass::OGTABLE).fetch_fields.map {|f| f.name } # Make new ones always - don't destroy by default because # it might contain data you want back. need_fields = fields.each do |needed_field| field_name = needed_field[0..(needed_field.index(' ')-1)] next if actual_fields.include?(field_name) if @options[:evolve_schema] == true Logger.debug "Adding field '#{needed_field}' to '#{klass::OGTABLE}'" if $DBG sql = "ALTER TABLE #{klass::OGTABLE} ADD COLUMN #{needed_field}" @conn.query(sql) else Logger.info "WARNING: Table '#{klass::OGTABLE}' is missing field '#{needed_field}' and :evolve_schema is not set to true!" end end #Drop old ones needed_fields = fields.map {|f| f =~ /^([^ ]+)/; $1} actual_fields.each do |obsolete_field| next if needed_fields.include?(obsolete_field) if @options[:evolve_schema] == true and @options[:evolve_schema_cautious] == false sql = "ALTER TABLE #{klass::OGTABLE} DROP COLUMN #{obsolete_field}" Logger.debug "Removing obsolete field '#{obsolete_field}' from '#{klass::OGTABLE}'" if $DBG @conn.query(sql) else Logger.info "WARNING: You have an obsolete field '#{obsolete_field}' on table '#{klass::OGTABLE}' and :evolve_schema is not set or is in cautious mode!" end end return end sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}" # Create table constraints. if constraints = klass.ann.self[:sql_constraint] sql << ", #{constraints.join(', ')}" end if table_type = @options[:table_type] sql << ") TYPE = #{table_type};" else sql << ");" end # Create indices. if indices = klass.ann.self[: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(/\(.*\)/, "") sql << " CREATE #{pre_sql} INDEX #{klass::OGTABLE}_#{idxname}_idx #{post_sql} ON #{klass::OGTABLE} (#{idx});" end end @conn.query_with_result = false begin @conn.query(sql) Logger.info "Created table '#{klass::OGTABLE}'." rescue => ex if ex.errno == 1050 # table already exists. Logger.debug 'Table already exists' if $DBG return else raise end end # Create join tables if needed. Join tables are used in # 'many_to_many' relations. if join_tables = klass.ann.self[:join_tables] for info in join_tables begin create_join_table_sql(info).each do |sql| @conn.query sql end Logger.debug "Created jointable '#{info[:table]}'." if $DBG rescue => ex if ex.respond_to?(:errno) and ex.errno == 1050 # table already exists. Logger.debug 'Join table already exists' if $DBG else raise end end end end end def create_field_map(klass) conn.query_with_result = true res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1" map = {} # Check if the field should be ignored. ignore = klass.ann[:self][:ignore_field] || klass.ann[:self][:ignore_fields] || klass.ann[:self][:ignore_columns] res.num_fields.times do |i| field_name = res.fetch_field.name.to_sym unless (ignore and ignore.include?(field_name)) map[field_name] = i end end return map ensure res.close if res end def write_prop(p) if p.klass.ancestors.include?(Integer) return "#\{@#{p} || 'NULL'\}" elsif p.klass.ancestors.include?(Float) return "#\{@#{p} || 'NULL'\}" elsif p.klass.ancestors.include?(String) return %|#\{@#{p} ? "'#\{#{self.class}.escape(@#{p})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(Time) return %|#\{@#{p} ? "'#\{#{self.class}.timestamp(@#{p})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(Date) return %|#\{@#{p} ? "'#\{#{self.class}.date(@#{p})\}'" : 'NULL'\}| elsif p.klass.ancestors.include?(TrueClass) return "#\{@#{p} ? \"'1'\" : 'NULL' \}" elsif p.klass.ancestors.include?(Og::Blob) return %|#\{@#{p} ? "'#\{#{self.class}.escape(#{self.class}.blob(@#{p}))\}'" : 'NULL'\}| else # gmosx: keep the '' for nil symbols. return %|#\{@#{p} ? "'#\{#{self.class}.escape(@#{p}.to_yaml)\}'" : "''"\}| end end def eval_og_insert(klass) props = klass.properties.values values = props.collect { |p| write_prop(p) }.join(',') if klass.schema_inheritance? props << Property.new(:symbol => :ogtype, :klass => String) values << ", '#{klass}'" end sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| field_for_property(p)}.join(',')}) VALUES (#{values})" klass.class_eval %{ def og_insert(store) #{::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} store.conn.query_with_result = false store.conn.query "#{sql}" @#{klass.pk_symbol} = store.conn.insert_id #{::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} end } end end end # * George Moschovitis