tramp 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009-2010 Pratik Naik
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,49 @@
1
+ require 'eventmachine'
2
+ EM.epoll
3
+
4
+ require 'active_support'
5
+ require 'active_support/core_ext/class/inheritable_attributes'
6
+ require 'active_support/core_ext/class/attribute_accessors'
7
+ require 'active_support/core_ext/module/aliasing'
8
+ require 'active_support/core_ext/module/attribute_accessors'
9
+ require 'active_support/core_ext/kernel/reporting'
10
+ require 'active_support/concern'
11
+ require 'active_support/core_ext/hash/indifferent_access'
12
+ require 'active_support/buffered_logger'
13
+
14
+ require 'tramp/evented_mysql'
15
+ require 'tramp/emysql_ext'
16
+ require 'mysqlplus'
17
+ require 'arel'
18
+ require 'tramp/arel_monkey_patches'
19
+ require 'active_model'
20
+
21
+ module Tramp
22
+ VERSION = '0.1'
23
+
24
+ mattr_accessor :logger
25
+
26
+ autoload :Quoting, "tramp/quoting"
27
+ autoload :Engine, "tramp/engine"
28
+ autoload :Column, "tramp/column"
29
+ autoload :Relation, "tramp/relation"
30
+
31
+ autoload :Base, "tramp/base"
32
+ autoload :Finders, "tramp/finders"
33
+ autoload :Attribute, "tramp/attribute"
34
+ autoload :AttributeMethods, "tramp/attribute_methods"
35
+ autoload :Status, "tramp/status"
36
+ autoload :Callbacks, "tramp/callbacks"
37
+
38
+ def self.init(settings)
39
+ Arel::Table.engine = Tramp::Engine.new(settings)
40
+ end
41
+
42
+ def self.select(query, callback = nil, &block)
43
+ callback ||= block
44
+
45
+ EventedMysql.select(query) do |rows|
46
+ callback.arity == 1 ? callback.call(rows) : callback.call if callback
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,66 @@
1
+ class Arel::Session
2
+ def create(insert, &block)
3
+ insert.call(&block)
4
+ end
5
+
6
+ def read(select, &block)
7
+ select.call(&block)
8
+ end
9
+
10
+ def update(update, &block)
11
+ update.call(&block)
12
+ end
13
+
14
+ def delete(delete, &block)
15
+ delete.call(&block)
16
+ end
17
+
18
+ end
19
+
20
+ module Arel::Relation
21
+ def call(&block)
22
+ engine.read(self, &block)
23
+ end
24
+
25
+ def all(&block)
26
+ session.read(self) {|rows| block.call(rows) }
27
+ end
28
+
29
+ def first(&block)
30
+ session.read(self) {|rows| block.call(rows[0]) }
31
+ end
32
+
33
+ def each(&block)
34
+ session.read(self) {|rows| rows.each {|r| block.call(r) } }
35
+ end
36
+
37
+ def insert(record, &block)
38
+ session.create(Arel::Insert.new(self, record), &block)
39
+ end
40
+
41
+ def update(assignments, &block)
42
+ session.update(Arel::Update.new(self, assignments), &block)
43
+ end
44
+
45
+ def delete(&block)
46
+ session.delete(Arel::Deletion.new(self), &block)
47
+ end
48
+ end
49
+
50
+ class Arel::Deletion
51
+ def call(&block)
52
+ engine.delete(self, &block)
53
+ end
54
+ end
55
+
56
+ class Arel::Insert
57
+ def call(&block)
58
+ engine.create(self, &block)
59
+ end
60
+ end
61
+
62
+ class Arel::Update
63
+ def call(&block)
64
+ engine.update(self, &block)
65
+ end
66
+ end
@@ -0,0 +1,101 @@
1
+ # Copyright (c) 2009 Koziarski Software Ltd
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ module Tramp
16
+ class Attribute
17
+
18
+ FORMATS = {}
19
+ FORMATS[Date] = /^\d{4}\/\d{2}\/\d{2}$/
20
+ FORMATS[Integer] = /^-?\d+$/
21
+ FORMATS[Float] = /^-?\d*\.\d*$/
22
+ FORMATS[Time] = /\A\s*
23
+ -?\d+-\d\d-\d\d
24
+ T
25
+ \d\d:\d\d:\d\d
26
+ (\.\d*)?
27
+ (Z|[+-]\d\d:\d\d)?
28
+ \s*\z/ix # lifted from the implementation of Time.xmlschema
29
+
30
+ CONVERTERS = {}
31
+ CONVERTERS[Date] = Proc.new do |str|
32
+ Date.strptime(str, "%Y/%m/%d")
33
+ end
34
+
35
+ CONVERTERS[Integer] = Proc.new do |str|
36
+ Integer(str)
37
+ end
38
+
39
+ CONVERTERS[Float] = Proc.new do |str|
40
+ Float(str)
41
+ end
42
+
43
+ CONVERTERS[Time] = Proc.new do |str|
44
+ Time.xmlschema(str)
45
+ end
46
+
47
+ attr_reader :name
48
+ def initialize(name, owner_class, options)
49
+ @name = name.to_s
50
+ @owner_class = owner_class
51
+ @options = options
52
+
53
+ # append_validations!
54
+ define_methods!
55
+ end
56
+
57
+ # I think this should live somewhere in Amo
58
+ def check_value!(value)
59
+ # Allow nil and Strings to fall back on the validations for typecasting
60
+ # Everything else gets checked with is_a?
61
+ if value.nil?
62
+ nil
63
+ elsif value.is_a?(String)
64
+ value
65
+ elsif value.is_a?(expected_type)
66
+ value
67
+ else
68
+ raise TypeError, "Expected #{expected_type.inspect} but got #{value.inspect}"
69
+ end
70
+ end
71
+
72
+ def expected_type
73
+ @options[:type] || String
74
+ end
75
+
76
+ def type_cast(value)
77
+ if value.is_a?(expected_type)
78
+ value
79
+ elsif (converter = CONVERTERS[expected_type]) && (value =~ FORMATS[expected_type])
80
+ converter.call(value)
81
+ else
82
+ value
83
+ end
84
+ end
85
+
86
+ def append_validations!
87
+ if f = FORMATS[expected_type]
88
+ @owner_class.validates_format_of @name, :with => f, :unless => lambda {|obj| obj.send(name).is_a? expected_type }, :allow_nil => @options[:allow_nil]
89
+ end
90
+ end
91
+
92
+ def define_methods!
93
+ @owner_class.define_attribute_methods(true)
94
+ end
95
+
96
+ def primary_key?
97
+ @options[:primary_key]
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2009 Koziarski Software Ltd
2
+ #
3
+ # Permission to use, copy, modify, and/or distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ module Tramp
16
+ module AttributeMethods
17
+
18
+ extend ActiveSupport::Concern
19
+ include ActiveModel::AttributeMethods
20
+
21
+ module ClassMethods
22
+ def attribute(name, options = {})
23
+ write_inheritable_hash(:model_attributes, {name => Attribute.new(name, self, options)})
24
+ end
25
+
26
+ def define_attribute_methods(force = false)
27
+ return unless model_attributes
28
+ undefine_attribute_methods if force
29
+ super(model_attributes.keys)
30
+ end
31
+ end
32
+
33
+ included do
34
+ class_inheritable_hash :model_attributes
35
+ undef_method(:id) if method_defined?(:id)
36
+
37
+ attribute_method_suffix("", "=")
38
+ end
39
+
40
+ def write_attribute(name, value)
41
+ if ma = self.class.model_attributes[name.to_sym]
42
+ value = ma.check_value!(value)
43
+ end
44
+ if(@attributes[name] != value)
45
+ send "#{name}_will_change!".to_sym
46
+ @attributes[name] = value
47
+ end
48
+ end
49
+
50
+ def read_attribute(name)
51
+ if ma = self.class.model_attributes[name]
52
+ ma.type_cast(@attributes[name])
53
+ else
54
+ @attributes[name]
55
+ end
56
+ end
57
+
58
+ def attributes=(attributes)
59
+ attributes.each do |(name, value)|
60
+ send("#{name}=", value)
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ def attribute_method?(name)
67
+ @attributes.include?(name.to_sym) || model_attributes[name.to_sym]
68
+ end
69
+
70
+ private
71
+
72
+ def attribute(name)
73
+ read_attribute(name.to_sym)
74
+ end
75
+
76
+ def attribute=(name, value)
77
+ write_attribute(name.to_sym, value)
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,117 @@
1
+ module Tramp
2
+ class Base
3
+
4
+ extend Finders
5
+ include AttributeMethods
6
+ include ActiveModel::Validations
7
+ include ActiveModel::Dirty
8
+ include Callbacks
9
+
10
+ class << self
11
+ def columns
12
+ @columns ||= arel_table.columns
13
+ end
14
+
15
+ def column_names
16
+ columns.map(&:name)
17
+ end
18
+
19
+ def primary_key
20
+ @primary_key ||= model_attributes.detect {|k, v| v.primary_key? }[0]
21
+ end
22
+
23
+ def instantiate(record)
24
+ object = allocate
25
+ object.instance_variable_set("@attributes", record.with_indifferent_access)
26
+ object
27
+ end
28
+ end
29
+
30
+ attr_reader :attributes
31
+
32
+ def initialize(attributes = {})
33
+ @new_record = true
34
+ @attributes = {}.with_indifferent_access
35
+ self.attributes = attributes
36
+ end
37
+
38
+ def new_record?
39
+ @new_record
40
+ end
41
+
42
+ def save(callback = nil, &block)
43
+ callback ||= block
44
+
45
+ if valid?
46
+ new_record? ? create_record(callback) : update_record(callback)
47
+ else
48
+ callback.arity == 1 ? callback.call(Status.new(self, false)) : callback.call if callback
49
+ end
50
+ end
51
+
52
+ def destroy(callback = nil, &block)
53
+ callback ||= block
54
+
55
+ relation.delete do
56
+ status = Status.new(self, true)
57
+ after_destroy_callbacks status
58
+ callback.arity == 1 ? callback.call(status) : callback.call if callback
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def create_record(callback = nil, &block)
65
+ callback ||= block
66
+
67
+ self.class.arel_table.insert(arel_attributes(true)) do |new_id|
68
+ if new_id.present?
69
+ self.id = new_id
70
+ saved = true
71
+ @new_record = false
72
+ else
73
+ saved = false
74
+ end
75
+
76
+ status = Status.new(self, saved)
77
+ after_save status
78
+ callback.arity == 1 ? callback.call(status) : callback.call if callback
79
+ end
80
+ end
81
+
82
+ def update_record(callback = nil, &block)
83
+ callback ||= block
84
+
85
+ relation.update(arel_attributes) do |updated_rows|
86
+ status = Status.new(self, true)
87
+ after_save status
88
+ callback.arity == 1 ? callback.call(status) : callback.call if callback
89
+ end
90
+ end
91
+
92
+ def relation
93
+ self.class.arel_table.where(self.class[self.class.primary_key].eq(send(self.class.primary_key)))
94
+ end
95
+
96
+ def after_save(status)
97
+ if status.success?
98
+ @previously_changed = changes
99
+ changed_attributes.clear
100
+ end
101
+
102
+ after_save_callbacks status
103
+ end
104
+
105
+ def arel_attributes(exclude_primary_key = true, attribute_names = @attributes.keys)
106
+ attrs = {}
107
+
108
+ attribute_names.each do |name|
109
+ value = read_attribute(name)
110
+ attrs[self.class.arel_table[name]] = value
111
+ end
112
+
113
+ attrs
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,39 @@
1
+ module Tramp
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_inheritable_accessor :after_save_callback_names
7
+ class_inheritable_accessor :after_destroy_callback_names
8
+
9
+ self.after_save_callback_names = []
10
+ self.after_destroy_callback_names = []
11
+ end
12
+
13
+ module ClassMethods
14
+ def after_save(*method_names)
15
+ self.after_save_callback_names += method_names
16
+ end
17
+
18
+ def after_destroy(*method_names)
19
+ self.after_destroy_callback_names += method_names
20
+ end
21
+ end
22
+
23
+ private
24
+ def after_save_callbacks(result)
25
+ after_save_callback_names.collect do |callback_name|
26
+ callback = method callback_name
27
+ callback.arity == 1 ? callback.call(result) : callback.call if callback
28
+ end
29
+ end
30
+
31
+ def after_destroy_callbacks(result)
32
+ after_destroy_callback_names.collect do |callback_name|
33
+ callback = method callback_name
34
+ callback.arity == 1 ? callback.call(result) : callback.call if callback
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ # Some of it yanked from Rails
2
+
3
+ # Copyright (c) 2004-2009 David Heinemeier Hansson
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module Tramp
25
+ class Column < Struct.new(:name, :default, :sql_type, :null)
26
+ attr_reader :type
27
+
28
+ def initialize(name, default, sql_type, null)
29
+ super
30
+ @type = simplified_type(sql_type)
31
+ end
32
+
33
+ private
34
+
35
+ def simplified_type(field_type)
36
+ case field_type
37
+ when /int/i
38
+ :integer
39
+ when /float|double/i
40
+ :float
41
+ when /decimal|numeric|number/i
42
+ extract_scale(field_type) == 0 ? :integer : :decimal
43
+ when /datetime/i
44
+ :datetime
45
+ when /timestamp/i
46
+ :timestamp
47
+ when /time/i
48
+ :time
49
+ when /date/i
50
+ :date
51
+ when /clob/i, /text/i
52
+ :text
53
+ when /blob/i, /binary/i
54
+ :binary
55
+ when /char/i, /string/i
56
+ :string
57
+ when /boolean/i
58
+ :boolean
59
+ end
60
+ end
61
+
62
+ def extract_scale(sql_type)
63
+ case sql_type
64
+ when /^(numeric|decimal|number)\((\d+)\)/i then 0
65
+ when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,21 @@
1
+ class EventedMysql
2
+ def self.execute_now(query)
3
+ @n ||= 0
4
+ connection = connection_pool[@n]
5
+ @n = 0 if (@n+=1) >= connection_pool.size
6
+ connection.execute_now(query)
7
+ end
8
+
9
+ def execute_now(sql)
10
+ log 'mysql sending', sql
11
+ @mysql.query(sql)
12
+ rescue Mysql::Error => e
13
+ log 'mysql error', e.message
14
+ if DisconnectErrors.include? e.message
15
+ @@queue << [response, sql, cblk, eblk]
16
+ return close
17
+ else
18
+ raise e
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ require 'active_support/core_ext/module/attribute_accessors'
2
+
3
+ module Tramp
4
+ class Engine
5
+ autoload :Connection, "tramp/engine/connection"
6
+
7
+ include Quoting
8
+
9
+ def initialize(settings)
10
+ @connection = Connection.new settings
11
+ @quoted_column_names, @quoted_table_names = {}, {}
12
+ end
13
+
14
+ def create(relation, &block)
15
+ query = relation.to_sql
16
+ log_query(query)
17
+ @connection.insert(query) {|rows| yield(rows) if block_given? }
18
+ end
19
+
20
+ def read(relation, &block)
21
+ query = relation.to_sql
22
+ log_query(query)
23
+ @connection.select(query) {|rows| yield(rows) }
24
+ end
25
+
26
+ def update(relation)
27
+ query = relation.to_sql
28
+ log_query(query)
29
+ @connection.update(query) {|rows| yield(rows) if block_given? }
30
+ end
31
+
32
+ def delete(relation)
33
+ query = relation.to_sql
34
+ log_query(query)
35
+ @connection.delete(relation.to_sql) {|rows| yield(rows) if block_given? }
36
+ end
37
+
38
+ def adapter_name
39
+ "mysql"
40
+ end
41
+
42
+ def connection
43
+ # Arel apparently uses this method to check whether the engine is connected or not
44
+ @connection
45
+ end
46
+
47
+ def tables
48
+ sql = "SHOW TABLES"
49
+ tables = []
50
+ result = @connection.execute_now(sql)
51
+
52
+ result.each { |field| tables << field[0] }
53
+ result.free
54
+ tables
55
+ end
56
+
57
+ def columns(table_name, name = nil)
58
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
59
+ columns = []
60
+ result = @connection.execute_now(sql)
61
+
62
+ result.each { |field| columns << Column.new(field[0], field[4], field[1], field[2] == "YES") }
63
+ result.free
64
+ columns
65
+ end
66
+
67
+ protected
68
+
69
+ def log_query(sql)
70
+ Tramp.logger.info("[QUERY] #{sql}") if Tramp.logger
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,30 @@
1
+ module Tramp
2
+ class Engine
3
+ class Connection
4
+ def initialize(settings)
5
+ EventedMysql.settings.update(settings)
6
+ end
7
+
8
+ def execute_now(sql)
9
+ EventedMysql.execute_now sql
10
+ end
11
+
12
+ def insert(sql, &block)
13
+ EventedMysql.insert sql, block
14
+ end
15
+
16
+ def select(sql, &block)
17
+ EventedMysql.select sql, block
18
+ end
19
+
20
+ def update(sql, &block)
21
+ EventedMysql.update sql, block
22
+ end
23
+
24
+ def delete(sql, &block)
25
+ EventedMysql.delete sql, block
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,298 @@
1
+ # Async MySQL driver for Ruby/EventMachine
2
+ # (c) 2008 Aman Gupta (tmm1)
3
+ # http://github.com/tmm1/em-mysql
4
+
5
+ require 'eventmachine'
6
+ require 'fcntl'
7
+
8
+ class Mysql
9
+ def result
10
+ @cur_result
11
+ end
12
+ end
13
+
14
+ class EventedMysql < EM::Connection
15
+ def initialize mysql, opts
16
+ @mysql = mysql
17
+ @fd = mysql.socket
18
+ @opts = opts
19
+ @current = nil
20
+ @@queue ||= []
21
+ @processing = false
22
+ @connected = true
23
+
24
+ log 'mysql connected'
25
+
26
+ self.notify_readable = true
27
+ EM.add_timer(0){ next_query }
28
+ end
29
+ attr_reader :processing, :connected, :opts
30
+ alias :settings :opts
31
+
32
+ DisconnectErrors = [
33
+ 'query: not connected',
34
+ 'MySQL server has gone away',
35
+ 'Lost connection to MySQL server during query'
36
+ ] unless defined? DisconnectErrors
37
+
38
+ def notify_readable
39
+ log 'readable'
40
+ if item = @current
41
+ @current = nil
42
+ start, response, sql, cblk, eblk = item
43
+ log 'mysql response', Time.now-start, sql
44
+ arg = case response
45
+ when :raw
46
+ result = @mysql.get_result
47
+ @mysql.instance_variable_set('@cur_result', result)
48
+ @mysql
49
+ when :select
50
+ ret = []
51
+ result = @mysql.get_result
52
+ result.each_hash{|h| ret << h }
53
+ log 'mysql result', ret
54
+ ret
55
+ when :update
56
+ result = @mysql.get_result
57
+ @mysql.affected_rows
58
+ when :insert
59
+ result = @mysql.get_result
60
+ @mysql.insert_id
61
+ else
62
+ result = @mysql.get_result
63
+ log 'got a result??', result if result
64
+ nil
65
+ end
66
+
67
+ @processing = false
68
+ # result.free if result.is_a? Mysql::Result
69
+ next_query
70
+ cblk.call(arg) if cblk
71
+ else
72
+ log 'readable, but nothing queued?! probably an ERROR state'
73
+ return close
74
+ end
75
+ rescue Mysql::Error => e
76
+ log 'mysql error', e.message
77
+ if e.message =~ /Deadlock/
78
+ @@queue << [response, sql, cblk, eblk]
79
+ @processing = false
80
+ next_query
81
+ elsif DisconnectErrors.include? e.message
82
+ @@queue << [response, sql, cblk, eblk]
83
+ return close
84
+ elsif cb = (eblk || @opts[:on_error])
85
+ cb.call(e)
86
+ @processing = false
87
+ next_query
88
+ else
89
+ raise e
90
+ end
91
+ # ensure
92
+ # res.free if res.is_a? Mysql::Result
93
+ # @processing = false
94
+ # next_query
95
+ end
96
+
97
+ def unbind
98
+ log 'mysql disconnect', $!
99
+ # cp = EventedMysql.instance_variable_get('@connection_pool') and cp.delete(self)
100
+ @connected = false
101
+
102
+ # XXX wait for the next tick until the current fd is removed completely from the reactor
103
+ #
104
+ # XXX in certain cases the new FD# (@mysql.socket) is the same as the old, since FDs are re-used
105
+ # XXX without next_tick in these cases, unbind will get fired on the newly attached signature as well
106
+ #
107
+ # XXX do _NOT_ use EM.next_tick here. if a bunch of sockets disconnect at the same time, we want
108
+ # XXX reconnects to happen after all the unbinds have been processed
109
+ EM.add_timer(0) do
110
+ log 'mysql reconnecting'
111
+ @processing = false
112
+ @mysql = EventedMysql._connect @opts
113
+ @fd = @mysql.socket
114
+
115
+ @signature = EM.attach_fd @mysql.socket, true
116
+ EM.set_notify_readable @signature, true
117
+ log 'mysql connected'
118
+ EM.instance_variable_get('@conns')[@signature] = self
119
+ @connected = true
120
+ make_socket_blocking
121
+ next_query
122
+ end
123
+ end
124
+
125
+ def execute sql, response = nil, cblk = nil, eblk = nil, &blk
126
+ cblk ||= blk
127
+
128
+ begin
129
+ unless @processing or !@connected
130
+ # begin
131
+ # log 'mysql ping', @mysql.ping
132
+ # # log 'mysql stat', @mysql.stat
133
+ # # log 'mysql errno', @mysql.errno
134
+ # rescue
135
+ # log 'mysql ping failed'
136
+ # @@queue << [response, sql, blk]
137
+ # return close
138
+ # end
139
+
140
+ @processing = true
141
+
142
+ log 'mysql sending', sql
143
+ @mysql.send_query(sql)
144
+ else
145
+ @@queue << [response, sql, cblk, eblk]
146
+ return
147
+ end
148
+ rescue Mysql::Error => e
149
+ log 'mysql error', e.message
150
+ if DisconnectErrors.include? e.message
151
+ @@queue << [response, sql, cblk, eblk]
152
+ return close
153
+ else
154
+ raise e
155
+ end
156
+ end
157
+
158
+ log 'queuing', response, sql
159
+ @current = [Time.now, response, sql, cblk, eblk]
160
+ end
161
+
162
+ def close
163
+ @connected = false
164
+ fd = detach
165
+ log 'detached fd', fd
166
+ end
167
+
168
+ private
169
+
170
+ def next_query
171
+ if @connected and !@processing and pending = @@queue.shift
172
+ response, sql, cblk, eblk = pending
173
+ execute(sql, response, cblk, eblk)
174
+ end
175
+ end
176
+
177
+ def log *args
178
+ return unless @opts[:logging]
179
+ p [Time.now, @fd, (@signature[-4..-1] if @signature), *args]
180
+ end
181
+
182
+ public
183
+
184
+ def self.connect opts
185
+ unless EM.respond_to?(:watch) and Mysql.method_defined?(:socket)
186
+ raise RuntimeError, 'mysqlplus and EM.watch are required for EventedMysql'
187
+ end
188
+
189
+ if conn = _connect(opts)
190
+ EM.watch conn.socket, self, conn, opts
191
+ else
192
+ EM.add_timer(5){ connect opts }
193
+ end
194
+ end
195
+
196
+ self::Mysql = ::Mysql unless defined? self::Mysql
197
+
198
+ # stolen from sequel
199
+ def self._connect opts
200
+ opts = settings.merge(opts)
201
+
202
+ conn = Mysql.init
203
+
204
+ # set encoding _before_ connecting
205
+ if charset = opts[:charset] || opts[:encoding]
206
+ conn.options(Mysql::SET_CHARSET_NAME, charset)
207
+ end
208
+
209
+ conn.options(Mysql::OPT_LOCAL_INFILE, 'client')
210
+
211
+ conn.real_connect(
212
+ opts[:host] || 'localhost',
213
+ opts[:user] || opts[:username] || 'root',
214
+ opts[:password],
215
+ opts[:database],
216
+ opts[:port],
217
+ opts[:socket],
218
+ 0 +
219
+ # XXX multi results require multiple callbacks to parse
220
+ # Mysql::CLIENT_MULTI_RESULTS +
221
+ # Mysql::CLIENT_MULTI_STATEMENTS +
222
+ (opts[:compress] == false ? 0 : Mysql::CLIENT_COMPRESS)
223
+ )
224
+
225
+ # increase timeout so mysql server doesn't disconnect us
226
+ # this is especially bad if we're disconnected while EM.attach is
227
+ # still in progress, because by the time it gets to EM, the FD is
228
+ # no longer valid, and it throws a c++ 'bad file descriptor' error
229
+ # (do not use a timeout of -1 for unlimited, it does not work on mysqld > 5.0.60)
230
+ conn.query("set @@wait_timeout = #{opts[:timeout] || 2592000}")
231
+
232
+ # we handle reconnecting (and reattaching the new fd to EM)
233
+ conn.reconnect = false
234
+
235
+ # By default, MySQL 'where id is null' selects the last inserted id
236
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
237
+ conn.query("set SQL_AUTO_IS_NULL=0")
238
+
239
+ # get results for queries
240
+ conn.query_with_result = true
241
+
242
+ conn
243
+ rescue Mysql::Error => e
244
+ if cb = opts[:on_error]
245
+ cb.call(e)
246
+ nil
247
+ else
248
+ raise e
249
+ end
250
+ end
251
+ end
252
+
253
+ class EventedMysql
254
+ def self.settings
255
+ @settings ||= { :connections => 4, :logging => false }
256
+ end
257
+
258
+ def self.execute query, type = nil, cblk = nil, eblk = nil, &blk
259
+ unless nil#connection = connection_pool.find{|c| not c.processing and c.connected }
260
+ @n ||= 0
261
+ connection = connection_pool[@n]
262
+ @n = 0 if (@n+=1) >= connection_pool.size
263
+ end
264
+
265
+ connection.execute(query, type, cblk, eblk, &blk)
266
+ end
267
+
268
+ %w[ select insert update delete raw ].each do |type| class_eval %[
269
+
270
+ def self.#{type} query, cblk = nil, eblk = nil, &blk
271
+ execute query, :#{type}, cblk, eblk, &blk
272
+ end
273
+
274
+ ] end
275
+
276
+ def self.all query, type = nil, &blk
277
+ responses = 0
278
+ connection_pool.each do |c|
279
+ c.execute(query, type) do
280
+ responses += 1
281
+ blk.call if blk and responses == @connection_pool.size
282
+ end
283
+ end
284
+ end
285
+
286
+ def self.connection_pool
287
+ @connection_pool ||= (1..settings[:connections]).map{ EventedMysql.connect(settings) }
288
+ # p ['connpool', settings[:connections], @connection_pool.size]
289
+ # (1..(settings[:connections]-@connection_pool.size)).each do
290
+ # @connection_pool << EventedMysql.connect(settings)
291
+ # end unless settings[:connections] == @connection_pool.size
292
+ # @connection_pool
293
+ end
294
+
295
+ def self.reset_connection_pool!
296
+ @connection_pool = nil
297
+ end
298
+ end
@@ -0,0 +1,25 @@
1
+ module Tramp
2
+ module Finders
3
+
4
+ delegate :all, :first, :each, :where, :select, :group, :order, :limit, :offset, :to => :relation
5
+
6
+ def [](attribute)
7
+ arel_table[attribute]
8
+ end
9
+
10
+ def arel_table
11
+ @arel_table ||= Arel::Table.new(table_name)
12
+ end
13
+
14
+ def relation
15
+ Relation.new(self, arel_table)
16
+ end
17
+
18
+ private
19
+
20
+ def table_name
21
+ self.to_s.demodulize.underscore.pluralize
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,102 @@
1
+ # Yanked from Rails
2
+
3
+ # Copyright (c) 2004-2009 David Heinemeier Hansson
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module Tramp
25
+ module Quoting
26
+ def quote_column_name(name)
27
+ @quoted_column_names[name] ||= "`#{name}`"
28
+ end
29
+
30
+ def quote_table_name(name)
31
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
32
+ end
33
+
34
+ def quote(value, column = nil)
35
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
36
+ s = value.unpack("H*")[0]
37
+ "x'#{s}'"
38
+ elsif value.kind_of?(BigDecimal)
39
+ value.to_s("F")
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ def quote(value, column = nil)
46
+ # records are quoted as their primary key
47
+ return value.quoted_id if value.respond_to?(:quoted_id)
48
+
49
+ case value
50
+ when String, ActiveSupport::Multibyte::Chars
51
+ value = value.to_s
52
+ if column && column.type == :binary && column.class.respond_to?(:string_to_binary)
53
+ "#{quoted_string_prefix}'#{quote_string(column.class.string_to_binary(value))}'" # ' (for ruby-mode)
54
+ elsif column && [:integer, :float].include?(column.type)
55
+ value = column.type == :integer ? value.to_i : value.to_f
56
+ value.to_s
57
+ else
58
+ "#{quoted_string_prefix}'#{quote_string(value)}'" # ' (for ruby-mode)
59
+ end
60
+ when NilClass then "NULL"
61
+ when TrueClass then (column && column.type == :integer ? '1' : quoted_true)
62
+ when FalseClass then (column && column.type == :integer ? '0' : quoted_false)
63
+ when Float, Fixnum, Bignum then value.to_s
64
+ # BigDecimals need to be output in a non-normalized form and quoted.
65
+ when BigDecimal then value.to_s('F')
66
+ else
67
+ if value.acts_like?(:date) || value.acts_like?(:time)
68
+ "'#{quoted_date(value)}'"
69
+ else
70
+ "#{quoted_string_prefix}'#{quote_string(value.to_yaml)}'"
71
+ end
72
+ end
73
+ end
74
+
75
+ # Quotes a string, escaping any ' (single quote) and \ (backslash)
76
+ # characters.
77
+ def quote_string(s)
78
+ s.gsub(/\\/, '\&\&').gsub(/'/, "''") # ' (for ruby-mode)
79
+ end
80
+
81
+ def quoted_true
82
+ "'t'"
83
+ end
84
+
85
+ def quoted_false
86
+ "'f'"
87
+ end
88
+
89
+ def quoted_date(value)
90
+ if value.acts_like?(:time)
91
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
92
+ value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
93
+ else
94
+ value
95
+ end.to_s(:db)
96
+ end
97
+
98
+ def quoted_string_prefix
99
+ ''
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,60 @@
1
+ module Tramp
2
+ class Relation
3
+
4
+ def initialize(klass, relation)
5
+ @klass, @relation = klass, relation
6
+ end
7
+
8
+ def each(callback = nil, &block)
9
+ callback ||= block
10
+
11
+ @relation.each do |row|
12
+ object = @klass.instantiate(row)
13
+ callback.call(object)
14
+ end
15
+ end
16
+
17
+ def all(callback = nil, &block)
18
+ callback ||= block
19
+
20
+ @relation.all do |rows|
21
+ objects = rows.map {|r| @klass.instantiate(r) }
22
+ callback.call(objects)
23
+ end
24
+ end
25
+
26
+ def first(callback = nil, &block)
27
+ callback ||= block
28
+
29
+ @relation.first do |row|
30
+ object = row ? @klass.instantiate(row) : nil
31
+ callback.call(object)
32
+ end
33
+ end
34
+
35
+ def where(*conditions)
36
+ Relation.new(@klass, @relation.where(*conditions))
37
+ end
38
+
39
+ def select(*selects)
40
+ Relation.new(@klass, @relation.project(*selects))
41
+ end
42
+
43
+ def group(groups)
44
+ Relation.new(@klass, @relation.group(groups))
45
+ end
46
+
47
+ def order(orders)
48
+ Relation.new(@klass, @relation.order(orders))
49
+ end
50
+
51
+ def limit(limits)
52
+ Relation.new(@klass, @relation.take(limits))
53
+ end
54
+
55
+ def offset(offsets)
56
+ Relation.new(@klass, @relation.skip(offsets))
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ module Tramp
2
+ class Status
3
+
4
+ attr_reader :record
5
+
6
+ def initialize(record, success)
7
+ @record = record
8
+ @success = success
9
+ end
10
+
11
+ def success?
12
+ @success
13
+ end
14
+
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tramp
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Pratik Naik
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-07-17 00:00:00 +01:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: activesupport
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ segments:
27
+ - 3
28
+ - 0
29
+ - 0
30
+ - beta4
31
+ version: 3.0.0.beta4
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: activemodel
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 3
43
+ - 0
44
+ - 0
45
+ - beta4
46
+ version: 3.0.0.beta4
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: arel
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ - 4
59
+ - 0
60
+ version: 0.4.0
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: mysqlplus
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ - 1
73
+ - 1
74
+ version: 0.1.1
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ description: " Tramp provides asynchronous ORM layer.\n"
78
+ email: pratiknaik@gmail.com
79
+ executables: []
80
+
81
+ extensions: []
82
+
83
+ extra_rdoc_files: []
84
+
85
+ files:
86
+ - MIT-LICENSE
87
+ - lib/tramp/arel_monkey_patches.rb
88
+ - lib/tramp/attribute.rb
89
+ - lib/tramp/attribute_methods.rb
90
+ - lib/tramp/base.rb
91
+ - lib/tramp/callbacks.rb
92
+ - lib/tramp/column.rb
93
+ - lib/tramp/emysql_ext.rb
94
+ - lib/tramp/engine/connection.rb
95
+ - lib/tramp/engine.rb
96
+ - lib/tramp/evented_mysql.rb
97
+ - lib/tramp/finders.rb
98
+ - lib/tramp/quoting.rb
99
+ - lib/tramp/relation.rb
100
+ - lib/tramp/status.rb
101
+ - lib/tramp.rb
102
+ has_rdoc: true
103
+ homepage: http://m.onkey.org
104
+ licenses: []
105
+
106
+ post_install_message:
107
+ rdoc_options: []
108
+
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ requirements: []
126
+
127
+ rubyforge_project:
128
+ rubygems_version: 1.3.6
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Async ORM layer.
132
+ test_files: []
133
+