add mysql support

add mysql support

diff --git a/.travis.yml b/.travis.yml
index 3793cca..9d75b49 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,11 +9,16 @@ before_install:
 cache: bundler
 sudo: false
 
+services:
+  - mysql
+
 addons:
   postgresql: 9.6
+  mysql: 5.7
 
 install:
   - createdb test_mini_sql
+  - mysql -e 'CREATE DATABASE test_mini_sql;'
   - bundle install
 
 matrix:
diff --git a/lib/mini_sql.rb b/lib/mini_sql.rb
index d437382..414b9a2 100644
--- a/lib/mini_sql.rb
+++ b/lib/mini_sql.rb
@@ -26,5 +26,10 @@ module MiniSql
       autoload :Connection, "mini_sql/sqlite/connection"
       autoload :DeserializerCache, "mini_sql/sqlite/deserializer_cache"
     end
+
+    module Mysql
+      autoload :Connection, "mini_sql/mysql/connection"
+      autoload :DeserializerCache, "mini_sql/mysql/deserializer_cache"
+    end
   end
 end
diff --git a/lib/mini_sql/builder.rb b/lib/mini_sql/builder.rb
index 36dc986..c8e1404 100644
--- a/lib/mini_sql/builder.rb
+++ b/lib/mini_sql/builder.rb
@@ -57,7 +57,6 @@ class MiniSql::Builder
       def #{m}(hash_args = nil)
         hash_args = @args.merge(hash_args) if hash_args && @args
         hash_args ||= @args
-
         if hash_args
           @connection.#{m}(to_sql, hash_args)
         else
diff --git a/lib/mini_sql/connection.rb b/lib/mini_sql/connection.rb
index f5c702c..c833412 100644
--- a/lib/mini_sql/connection.rb
+++ b/lib/mini_sql/connection.rb
@@ -10,6 +10,8 @@ module MiniSql
         Postgres::Connection.new(raw_connection, options)
       elsif (defined? ::SQLite3::Database) && (SQLite3::Database === raw_connection)
         Sqlite::Connection.new(raw_connection, options)
+      elsif (defined? ::Mysql2::Client) && (Mysql2::Client === raw_connection)
+        Mysql::Connection.new(raw_connection, options)
       else
         raise ArgumentError, 'unknown connection type!'
       end
diff --git a/lib/mini_sql/mysql/connection.rb b/lib/mini_sql/mysql/connection.rb
new file mode 100644
index 0000000..105828a
--- /dev/null
+++ b/lib/mini_sql/mysql/connection.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module MiniSql
+  module Mysql
+    class Connection < MiniSql::Connection
+      attr_reader :param_encoder, :raw_connection, :deserializer_cache
+
+      def initialize(raw_connection, args = nil)
+        @raw_connection = raw_connection
+        @param_encoder = (args && args[:param_encoder]) || InlineParamEncoder.new(self)
+        @deserializer_cache = (args && args[:deserializer_cache]) || DeserializerCache.new
+      end
+
+      def query_single(sql, *params)
+        if params && params.length > 0
+          sql = param_encoder.encode(sql, *params)
+        end
+        raw_connection.query(sql, as: :array).to_a.flatten!
+      end
+
+      def query_hash(sql, *params)
+        result = run(sql, params)
+        result.to_a
+      end
+
+      def exec(sql, *params)
+        run(sql, params)
+        raw_connection.affected_rows
+      end
+
+      def query(sql, *params)
+        result = run(sql, params)
+        @deserializer_cache.materialize(result)
+      end
+
+      def escape_string(str)
+        raw_connection.escape(str)
+      end
+
+      def build(sql)
+        Builder.new(self, sql)
+      end
+
+      private
+
+      def run(sql, params)
+        if params && params.length > 0
+          sql = param_encoder.encode(sql, *params)
+        end
+        raw_connection.query(sql)
+      end
+    end
+  end
+end
diff --git a/lib/mini_sql/mysql/deserializer_cache.rb b/lib/mini_sql/mysql/deserializer_cache.rb
new file mode 100644
index 0000000..3e95c95
--- /dev/null
+++ b/lib/mini_sql/mysql/deserializer_cache.rb
@@ -0,0 +1,60 @@
+module MiniSql
+  module Mysql
+    class DeserializerCache
+
+      DEFAULT_MAX_SIZE = 500
+
+      def initialize(max_size = nil)
+        @cache = {}
+        @max_size = max_size || DEFAULT_MAX_SIZE
+      end
+
+      def materialize(result)
+
+        key = result.fields
+
+        # trivial fast LRU implementation
+        materializer = @cache.delete(key)
+        if materializer
+          @cache[key] = materializer
+        else
+          materializer = @cache[key] = new_row_matrializer(result)
+          @cache.shift if @cache.length > @max_size
+        end
+
+        result.map do |data|
+          materializer.materialize(data)
+        end
+      end
+
+      private
+
+      def new_row_matrializer(result)
+        fields = result.fields
+
+        Class.new do
+          attr_accessor(*fields)
+
+          # AM serializer support
+          alias :read_attribute_for_serialization :send
+
+          def to_h
+            r = {}
+            instance_variables.each do |f|
+              r[f.to_s.sub('@','').to_sym] = instance_variable_get(f)
+            end
+            r
+          end
+
+          instance_eval <<~RUBY
+            def materialize(data)
+              r = self.new
+              #{fields.map{|f| "r.#{f} = data[#{f.inspect}]"}.join("; ")}
+              r
+            end
+          RUBY
+        end
+      end
+    end
+  end
+end
diff --git a/mini_sql.gemspec b/mini_sql.gemspec
index 523baa4..e3f7903 100644
--- a/mini_sql.gemspec
+++ b/mini_sql.gemspec
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
     spec.add_development_dependency "activerecord-jdbcpostgresql-adapter", "~> 52.2"
   else
     spec.add_development_dependency "pg", "> 1"
+    spec.add_development_dependency "mysql2"
     spec.add_development_dependency "sqlite3", "~> 1.3"
   end
 end
diff --git a/test/mini_sql/connection_tests.rb b/test/mini_sql/connection_tests.rb
index 1ed5130..5bb649a 100644
--- a/test/mini_sql/connection_tests.rb
+++ b/test/mini_sql/connection_tests.rb
@@ -56,7 +56,7 @@ module MiniSql::ConnectionTests
     assert_equal([1,2,3], r)
 
     r = @connection.query_single(
-      "select * from (select 1 x union select 2 union select 3) a where a.x in(?)",
+      "select * from (select 1 x union select 2 union select 3) a where a.x in(?) order by 1 asc",
       [[1,4,3]]
     )
 
diff --git a/test/mini_sql/mysql/builder_test.rb b/test/mini_sql/mysql/builder_test.rb
new file mode 100644
index 0000000..2995059
--- /dev/null
+++ b/test/mini_sql/mysql/builder_test.rb
@@ -0,0 +1,96 @@
+require 'test_helper'
+
+class MiniSql::Mysql::TestBuilder < MiniTest::Test
+  def setup
+    @connection = mysql_connection
+  end
+
+  include MiniSql::BuilderTests
+
+  def test_where
+    builder = @connection.build("select 1 as one from for_testing /*where*/")
+    builder.where("1 = :zero")
+    l = builder.query(zero: 0).length
+    assert_equal(0, l)
+  end
+
+  def test_join
+    @connection.exec("create TEMPORARY table ta(x int)")
+    @connection.exec("insert into ta(x) values(1),(2),(3)")
+
+    builder = @connection.build("select * from ta /*join*/")
+    builder.join("(select 1 as _one) as X on _one = x")
+    assert_equal(1, builder.exec)
+
+    builder.join("(select 2 as _two) as Y on _two = x")
+    assert_equal(0, builder.exec)
+  end
+
+  def test_mixing_params
+    builder = @connection.build("select * from for_testing /*where*/ limit 1")
+    builder.where("1 = ?", 1)
+    builder.where("1 = :one", one: 1)
+    assert_equal(1, builder.exec)
+  end
+
+  def test_accepts_params_at_end
+    builder = @connection.build("select :bob as a from for_testing /*where*/ limit 1")
+    builder.where('1 = :one', one: 1)
+    r = builder.query_hash(bob: 1)
+    assert_equal([{"a" => 1}], r)
+
+    r = builder.query_hash(bob: 1, one: 2)
+    assert_equal([], r)
+  end
+
+  def test_where2
+    builder = @connection.build("select 1 as one from for_testing /*where2*/")
+    builder.where2("1 = -1")
+    assert_equal(0, builder.exec)
+  end
+
+  def test_append_params
+    builder = @connection.build("select 1 as one from for_testing /*where*/")
+    builder.where("1 = :zero", zero: 0)
+    assert_equal(0, builder.exec)
+  end
+
+   def test_offset_limit
+    @connection.exec("create TEMPORARY table ta(x int)")
+    @connection.exec("insert into ta(x) values(1),(2),(3)")
+

[... diff too long, it was truncated ...]

GitHub sha: 282687e9