tramp 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+