Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 0fbb3b38c2e59e80f397fcd83fcc50efbd050bd5 0 parents
@lifo authored
14 example.rb
@@ -0,0 +1,14 @@
+require 'rubygems'
+
+$: << File.join(File.dirname(__FILE__), "lib")
+require 'cramp'
+require 'cramp/model'
+
+Arel::Table.engine = Cramp::Model::Engine.new(:username => 'root', :database => 'arel_test')
+
+EM.run do
+ users = Table(:users)
+ users.where(users[:name].eq('lifo')).each {|x| puts x.inspect }
+ EM.stop
+end
+
2  lib/cramp.rb
@@ -0,0 +1,2 @@
+module Cramp
+end
18 lib/cramp/model.rb
@@ -0,0 +1,18 @@
+require 'cramp/vendor/evented_mysql'
+require 'eventmachine'
+require 'mysqlplus'
+
+$: << File.join(File.dirname(__FILE__), 'vendor/arel/lib')
+require 'arel'
+
+module Cramp
+ module Model
+ autoload :Quoting, "cramp/model/quoting"
+ autoload :Engine, "cramp/model/engine"
+ autoload :Column, "cramp/model/column"
+ end
+end
+
+require 'cramp/model/monkey_patches'
+require 'cramp/model/emysql_ext'
+
72 lib/cramp/model/column.rb
@@ -0,0 +1,72 @@
+# Some of it yanked from Rails
+
+# Copyright (c) 2004-2009 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+module Cramp
+ module Model
+ class Column < Struct.new(:name, :default, :sql_type, :null)
+ attr_reader :type
+
+ def initialize(name, default, sql_type, null)
+ super
+ @type = simplified_type(sql_type)
+ end
+
+ private
+
+ def simplified_type(field_type)
+ case field_type
+ when /int/i
+ :integer
+ when /float|double/i
+ :float
+ when /decimal|numeric|number/i
+ extract_scale(field_type) == 0 ? :integer : :decimal
+ when /datetime/i
+ :datetime
+ when /timestamp/i
+ :timestamp
+ when /time/i
+ :time
+ when /date/i
+ :date
+ when /clob/i, /text/i
+ :text
+ when /blob/i, /binary/i
+ :binary
+ when /char/i, /string/i
+ :string
+ when /boolean/i
+ :boolean
+ end
+ end
+
+ def extract_scale(sql_type)
+ case sql_type
+ when /^(numeric|decimal|number)\((\d+)\)/i then 0
+ when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
+ end
+ end
+
+ end
+ end
+end
21 lib/cramp/model/emysql_ext.rb
@@ -0,0 +1,21 @@
+class EventedMysql
+ def self.execute_now(query)
+ @n ||= 0
+ connection = connection_pool[@n]
+ @n = 0 if (@n+=1) >= connection_pool.size
+ connection.execute_now(query)
+ end
+
+ def execute_now(sql)
+ log 'mysql sending', sql
+ @mysql.query(sql)
+ rescue Mysql::Error => e
+ log 'mysql error', e.message
+ if DisconnectErrors.include? e.message
+ @@queue << [response, sql, cblk, eblk]
+ return close
+ else
+ raise e
+ end
+ end
+end
45 lib/cramp/model/engine.rb
@@ -0,0 +1,45 @@
+module Cramp
+ module Model
+ class Engine
+ include Quoting
+
+ def initialize(settings)
+ @settings = settings
+ @quoted_column_names, @quoted_table_names = {}, {}
+
+ EventedMysql.settings.update(settings) {|r| yield(r) }
+ end
+
+ def create(relation)
+ EventedMysql.insert(relation.to_sql) {|r| yield(r) }
+ end
+
+ def read(relation, &block)
+ EventedMysql.select(relation.to_sql) {|rows| block.call(rows) }
+ end
+
+ def update(relation)
+ EventedMysql.update(relation.to_sql) {|r| yield(r) }
+ end
+
+ def delete(relation)
+ EventedMysql.delete(relation.to_sql) {|r| yield(r) }
+ end
+
+ def adapter_name
+ "Cramp MySQL Async Adapter"
+ end
+
+ def columns(table_name, name = nil)
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
+ columns = []
+ result = EventedMysql.execute_now(sql)
+
+ result.each { |field| columns << Column.new(field[0], field[4], field[1], field[2] == "YES") }
+ result.free
+ columns
+ end
+
+ end
+ end
+end
19 lib/cramp/model/monkey_patches.rb
@@ -0,0 +1,19 @@
+class Arel::Session
+ def read(select, &block)
+ select.call(&block)
+ end
+end
+
+class Arel::Relation
+ def call(&block)
+ engine.read(self, &block)
+ end
+
+ def each(&block)
+ session.read(self) {|rows| rows.each(&block)}
+ end
+
+ def each(&block)
+ session.read(self) {|rows| rows.each {|r| block.call(r) } }
+ end
+end
114 lib/cramp/model/quoting.rb
@@ -0,0 +1,114 @@
+# Yanked from Rails
+
+# Copyright (c) 2004-2009 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+module Cramp
+ module Model
+ module Quoting
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= "`#{name}`"
+ end
+
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
+ end
+
+ def quote(value, column = nil)
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
+ s = value.unpack("H*")[0]
+ "x'#{s}'"
+ elsif value.kind_of?(BigDecimal)
+ value.to_s("F")
+ else
+ super
+ end
+ end
+
+ def quote(value, column = nil)
+ # records are quoted as their primary key
+ return value.quoted_id if value.respond_to?(:quoted_id)
+
+ case value
+ when String, ActiveSupport::Multibyte::Chars
+ value = value.to_s
+ if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
+ "#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
+ elsif column && [:integer, :float].include?(column.type)
+ value = column.type == :integer ? value.to_i : value.to_f
+ value.to_s
+ else
+ "#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
+ end
+ when NilClass then "NULL"
+ when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
+ when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
+ when Float, Fixnum, Bignum then value.to_s
+ # BigDecimals need to be output in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ else
+ if value.acts_like?(:date) || value.acts_like?(:time)
+ "'#{quoted_date(value)}'"
+ else
+ "#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
+ end
+ end
+ end
+
+ # Quotes a string, escaping any ' (single quote) and \ (backslash)
+ # characters.
+ def quote_string(s)
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
+ end
+
+ # Quotes the column name. Defaults to no quoting.
+ def quote_column_name(column_name)
+ column_name
+ end
+
+ # Quotes the table name. Defaults to column name quoting.
+ def quote_table_name(table_name)
+ quote_column_name(table_name)
+ end
+
+ def quoted_true
+ "'t'"
+ end
+
+ def quoted_false
+ "'f'"
+ end
+
+ def quoted_date(value)
+ if value.acts_like?(:time)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+ value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
+ else
+ value
+ end.to_s(:db)
+ end
+
+ def quoted_string_prefix
+ ''
+ end
+ end
+ end
+end
298 lib/cramp/vendor/evented_mysql.rb
@@ -0,0 +1,298 @@
+# Async MySQL driver for Ruby/EventMachine
+# (c) 2008 Aman Gupta (tmm1)
+# http://github.com/tmm1/em-mysql
+
+require 'eventmachine'
+require 'fcntl'
+
+class Mysql
+ def result
+ @cur_result
+ end
+end
+
+class EventedMysql < EM::Connection
+ def initialize mysql, opts
+ @mysql = mysql
+ @fd = mysql.socket
+ @opts = opts
+ @current = nil
+ @@queue ||= []
+ @processing = false
+ @connected = true
+
+ log 'mysql connected'
+
+ self.notify_readable = true
+ EM.add_timer(0){ next_query }
+ end
+ attr_reader :processing, :connected, :opts
+ alias :settings :opts
+
+ DisconnectErrors = [
+ 'query: not connected',
+ 'MySQL server has gone away',
+ 'Lost connection to MySQL server during query'
+ ] unless defined? DisconnectErrors
+
+ def notify_readable
+ log 'readable'
+ if item = @current
+ @current = nil
+ start, response, sql, cblk, eblk = item
+ log 'mysql response', Time.now-start, sql
+ arg = case response
+ when :raw
+ result = @mysql.get_result
+ @mysql.instance_variable_set('@cur_result', result)
+ @mysql
+ when :select
+ ret = []
+ result = @mysql.get_result
+ result.each_hash{|h| ret << h }
+ log 'mysql result', ret
+ ret
+ when :update
+ result = @mysql.get_result
+ @mysql.affected_rows
+ when :insert
+ result = @mysql.get_result
+ @mysql.insert_id
+ else
+ result = @mysql.get_result
+ log 'got a result??', result if result
+ nil
+ end
+
+ @processing = false
+ # result.free if result.is_a? Mysql::Result
+ next_query
+ cblk.call(arg) if cblk
+ else
+ log 'readable, but nothing queued?! probably an ERROR state'
+ return close
+ end
+ rescue Mysql::Error => e
+ log 'mysql error', e.message
+ if e.message =~ /Deadlock/
+ @@queue << [response, sql, cblk, eblk]
+ @processing = false
+ next_query
+ elsif DisconnectErrors.include? e.message
+ @@queue << [response, sql, cblk, eblk]
+ return close
+ elsif cb = (eblk || @opts[:on_error])
+ cb.call(e)
+ @processing = false
+ next_query
+ else
+ raise e
+ end
+ # ensure
+ # res.free if res.is_a? Mysql::Result
+ # @processing = false
+ # next_query
+ end
+
+ def unbind
+ log 'mysql disconnect', $!
+ # cp = EventedMysql.instance_variable_get('@connection_pool') and cp.delete(self)
+ @connected = false
+
+ # XXX wait for the next tick until the current fd is removed completely from the reactor
+ #
+ # XXX in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
+ # XXX without next_tick in these cases, unbind will get fired on the newly attached signature as well
+ #
+ # XXX do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
+ # XXX reconnects to happen after all the unbinds have been processed
+ EM.add_timer(0) do
+ log 'mysql reconnecting'
+ @processing = false
+ @mysql = EventedMysql._connect @opts
+ @fd = @mysql.socket
+
+ @signature = EM.attach_fd @mysql.socket, true
+ EM.set_notify_readable @signature, true
+ log 'mysql connected'
+ EM.instance_variable_get('@conns')[@signature] = self
+ @connected = true
+ make_socket_blocking
+ next_query
+ end
+ end
+
+ def execute sql, response = nil, cblk = nil, eblk = nil, &blk
+ cblk ||= blk
+
+ begin
+ unless @processing or !@connected
+ # begin
+ # log 'mysql ping', @mysql.ping
+ # # log 'mysql stat', @mysql.stat
+ # # log 'mysql errno', @mysql.errno
+ # rescue
+ # log 'mysql ping failed'
+ # @@queue << [response, sql, blk]
+ # return close
+ # end
+
+ @processing = true
+
+ log 'mysql sending', sql
+ @mysql.send_query(sql)
+ else
+ @@queue << [response, sql, cblk, eblk]
+ return
+ end
+ rescue Mysql::Error => e
+ log 'mysql error', e.message
+ if DisconnectErrors.include? e.message
+ @@queue << [response, sql, cblk, eblk]
+ return close
+ else
+ raise e
+ end
+ end
+
+ log 'queuing', response, sql
+ @current = [Time.now, response, sql, cblk, eblk]
+ end
+
+ def close
+ @connected = false
+ fd = detach
+ log 'detached fd', fd
+ end
+
+ private
+
+ def next_query
+ if @connected and !@processing and pending = @@queue.shift
+ response, sql, cblk, eblk = pending
+ execute(sql, response, cblk, eblk)
+ end
+ end
+
+ def log *args
+ return unless @opts[:logging]
+ p [Time.now, @fd, (@signature[-4..-1] if @signature), *args]
+ end
+
+ public
+
+ def self.connect opts
+ unless EM.respond_to?(:watch) and Mysql.method_defined?(:socket)
+ raise RuntimeError, 'mysqlplus and EM.watch are required for EventedMysql'
+ end
+
+ if conn = _connect(opts)
+ EM.watch conn.socket, self, conn, opts
+ else
+ EM.add_timer(5){ connect opts }
+ end
+ end
+
+ self::Mysql = ::Mysql unless defined? self::Mysql
+
+ # stolen from sequel
+ def self._connect opts
+ opts = settings.merge(opts)
+
+ conn = Mysql.init
+
+ # set encoding _before_ connecting
+ if charset = opts[:charset] || opts[:encoding]
+ conn.options(Mysql::SET_CHARSET_NAME, charset)
+ end
+
+ conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
+
+ conn.real_connect(
+ opts[:host] || 'localhost',
+ opts[:user] || opts[:username] || 'root',
+ opts[:password],
+ opts[:database],
+ opts[:port],
+ opts[:socket],
+ 0 +
+ # XXX multi results require multiple callbacks to parse
+ # Mysql::CLIENT_MULTI_RESULTS +
+ # Mysql::CLIENT_MULTI_STATEMENTS +
+ (opts[:compress] == false ? 0 : Mysql::CLIENT_COMPRESS)
+ )
+
+ # increase timeout so mysql server doesn't disconnect us
+ # this is especially bad if we're disconnected while EM.attach is
+ # still in progress, because by the time it gets to EM, the FD is
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
+ conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
+
+ # we handle reconnecting (and reattaching the new fd to EM)
+ conn.reconnect = false
+
+ # By default, MySQL 'where id is null' selects the last inserted id
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
+ conn.query("set SQL_AUTO_IS_NULL=0")
+
+ # get results for queries
+ conn.query_with_result = true
+
+ conn
+ rescue Mysql::Error => e
+ if cb = opts[:on_error]
+ cb.call(e)
+ nil
+ else
+ raise e
+ end
+ end
+end
+
+class EventedMysql
+ def self.settings
+ @settings ||= { :connections => 4, :logging => false }
+ end
+
+ def self.execute query, type = nil, cblk = nil, eblk = nil, &blk
+ unless nil#connection = connection_pool.find{|c| not c.processing and c.connected }
+ @n ||= 0
+ connection = connection_pool[@n]
+ @n = 0 if (@n+=1) >= connection_pool.size
+ end
+
+ connection.execute(query, type, cblk, eblk, &blk)
+ end
+
+ %w[ select insert update delete raw ].each do |type| class_eval %[
+
+ def self.#{type} query, cblk = nil, eblk = nil, &blk
+ execute query, :#{type}, cblk, eblk, &blk
+ end
+
+ ] end
+
+ def self.all query, type = nil, &blk
+ responses = 0
+ connection_pool.each do |c|
+ c.execute(query, type) do
+ responses += 1
+ blk.call if blk and responses == @connection_pool.size
+ end
+ end
+ end
+
+ def self.connection_pool
+ @connection_pool ||= (1..settings[:connections]).map{ EventedMysql.connect(settings) }
+ # p ['connpool', settings[:connections], @connection_pool.size]
+ # (1..(settings[:connections]-@connection_pool.size)).each do
+ # @connection_pool << EventedMysql.connect(settings)
+ # end unless settings[:connections] == @connection_pool.size
+ # @connection_pool
+ end
+
+ def self.reset_connection_pool!
+ @connection_pool = nil
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.