SEQUEL_ADAPTER_TEST = :mysql

require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper.rb')

unless defined?(MYSQL_SOCKET_FILE)
  MYSQL_SOCKET_FILE = '/tmp/mysql.sock'
end
MYSQL_URI = URI.parse(DB.uri)

def DB.sqls
  (@sqls ||= [])
end
logger = Object.new
def logger.method_missing(m, msg)
  DB.sqls << msg
end
DB.loggers = [logger]
DB.drop_table?(:items, :dolls, :booltest)

SQL_BEGIN = 'BEGIN'
SQL_ROLLBACK = 'ROLLBACK'
SQL_COMMIT = 'COMMIT'

describe "MySQL", '#create_table' do
  before do
    @db = DB
    @db.test_connection
    DB.sqls.clear
  end
  after do
    @db.drop_table?(:dolls)
  end

  it "should allow to specify options for MySQL" do
    @db.create_table(:dolls, :engine => 'MyISAM', :charset => 'latin2'){text :name}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `dolls` (`name` text) ENGINE=MyISAM DEFAULT CHARSET=latin2"]
    end
  end

  it "should create a temporary table" do
    @db.create_table(:tmp_dolls, :temp => true, :engine => 'MyISAM', :charset => 'latin2'){text :name}
    check_sqls do
      @db.sqls.must_equal ["CREATE TEMPORARY TABLE `tmp_dolls` (`name` text) ENGINE=MyISAM DEFAULT CHARSET=latin2"]
    end
  end

  it "should not use a default for a String :text=>true type" do
    @db.create_table(:dolls){String :name, :text=>true, :default=>'blah'}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `dolls` (`name` text)"]
    end
  end

  it "should not use a default for a File type" do
    @db.create_table(:dolls){File :name, :default=>'blah'}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `dolls` (`name` blob)"]
    end
  end

  it "should respect the size option for File type" do
    @db.create_table(:dolls) do
      File :n1
      File :n2, :size=>:tiny
      File :n3, :size=>:medium
      File :n4, :size=>:long
      File :n5, :size=>255
    end
    @db.schema(:dolls).map{|k, v| v[:db_type]}.must_equal %w"blob tinyblob mediumblob longblob blob"
  end

  it "should include an :auto_increment schema attribute if auto incrementing" do
    @db.create_table(:dolls) do
      primary_key :n4
      Integer :n2
      String :n3
    end
    @db.schema(:dolls).map{|k, v| v[:auto_increment]}.must_equal [true, nil, nil]
  end

  it "should support collate with various other column options" do
    @db.create_table!(:dolls){ String :name, :size=>128, :collate=>:utf8_bin, :default=>'foo', :null=>false, :unique=>true}
    @db[:dolls].insert
    @db[:dolls].select_map(:name).must_equal ["foo"]
  end

  it "should be able to parse the default value for set and enum types" do
    @db.create_table!(:dolls){column :t, "set('a', 'b', 'c', 'd')", :default=>'a,b'}
    @db.schema(:dolls).first.last[:ruby_default].must_equal 'a,b'
    @db.create_table!(:dolls){column :t, "enum('a', 'b', 'c', 'd')", :default=>'b'}
    @db.schema(:dolls).first.last[:ruby_default].must_equal 'b'
  end

  it "should allow setting auto_increment for existing column" do
    @db.create_table(:dolls){Integer :a, :primary_key=>true}
    @db.schema(:dolls).first.last[:auto_increment].must_equal false
    @db.set_column_type :dolls, :a, Integer, :auto_increment=>true
    @db.schema(:dolls).first.last[:auto_increment].must_equal true
  end
end

if [:mysql, :mysql2].include?(DB.adapter_scheme)
  describe "Sequel::MySQL::Database#convert_tinyint_to_bool" do
    before do
      @db = DB
      @db.create_table(:booltest){column :b, 'tinyint(1)'; column :i, 'tinyint(4)'}
      @ds = @db[:booltest]
    end
    after do
      @db.convert_tinyint_to_bool = true
      @db.drop_table?(:booltest)
    end

    it "should consider tinyint(1) datatypes as boolean if set, but not larger tinyints" do
      @db.schema(:booltest, :reload=>true).map{|_, s| s[:type]}.must_equal [:boolean, :integer]
      @db.convert_tinyint_to_bool = false
      @db.schema(:booltest, :reload=>true).map{|_, s| s[:type]}.must_equal [:integer, :integer]
    end

    it "should return tinyint(1)s as bools and tinyint(4)s as integers when set" do
      @db.convert_tinyint_to_bool = true
      @ds.delete
      @ds << {:b=>true, :i=>10}
      @ds.all.must_equal [{:b=>true, :i=>10}]
      @ds.delete
      @ds << {:b=>false, :i=>0}
      @ds.all.must_equal [{:b=>false, :i=>0}]
      @ds.delete
      @ds << {:b=>true, :i=>1}
      @ds.all.must_equal [{:b=>true, :i=>1}]
    end

    it "should return all tinyints as integers when unset" do
      @db.convert_tinyint_to_bool = false
      @ds.delete
      @ds << {:b=>true, :i=>10}
      @ds.all.must_equal [{:b=>1, :i=>10}]
      @ds.delete
      @ds << {:b=>false, :i=>0}
      @ds.all.must_equal [{:b=>0, :i=>0}]

      @ds.delete
      @ds << {:b=>1, :i=>10}
      @ds.all.must_equal [{:b=>1, :i=>10}]
      @ds.delete
      @ds << {:b=>0, :i=>0}
      @ds.all.must_equal [{:b=>0, :i=>0}]
    end

    it "should allow disabling the conversion on a per-dataset basis" do
      @db.convert_tinyint_to_bool = true
      ds = @ds.with_extend do
        def cast_tinyint_integer?(f) true end #mysql
        def convert_tinyint_to_bool?() false end #mysql2
      end
      ds.delete
      ds << {:b=>true, :i=>10}
      ds.all.must_equal [{:b=>1, :i=>10}]
      @ds.all.must_equal [{:b=>true, :i=>10}]
    end
  end
end

describe "A MySQL dataset" do
  before do
    DB.create_table(:items){String :name; Integer :value}
    @d = DB[:items]
    DB.sqls.clear
  end
  after do
    DB.drop_table?(:items)
  end

  it "should quote columns and tables using back-ticks if quoting identifiers" do
    @d = @d.with_quote_identifiers(true)
    @d.select(:name).sql.must_equal 'SELECT `name` FROM `items`'
    @d.select(Sequel.lit('COUNT(*)')).sql.must_equal 'SELECT COUNT(*) FROM `items`'
    @d.select(Sequel.function(:max, :value)).sql.must_equal 'SELECT max(`value`) FROM `items`'
    @d.select(Sequel.function(:NOW)).sql.must_equal 'SELECT NOW() FROM `items`'
    @d.select(Sequel.function(:max, Sequel[:items][:value])).sql.must_equal 'SELECT max(`items`.`value`) FROM `items`'
    @d.order(Sequel.expr(:name).desc).sql.must_equal 'SELECT * FROM `items` ORDER BY `name` DESC'
    @d.select(Sequel.lit('items.name AS item_name')).sql.must_equal 'SELECT items.name AS item_name FROM `items`'
    @d.select(Sequel.lit('`name`')).sql.must_equal 'SELECT `name` FROM `items`'
    @d.select(Sequel.lit('max(items.`name`) AS `max_name`')).sql.must_equal 'SELECT max(items.`name`) AS `max_name` FROM `items`'
    @d.select(Sequel.function(:test, :abc, 'hello')).sql.must_equal "SELECT test(`abc`, 'hello') FROM `items`"
    @d.select(Sequel.function(:test, Sequel[:abc][:def], 'hello')).sql.must_equal "SELECT test(`abc`.`def`, 'hello') FROM `items`"
    @d.select(Sequel.function(:test, Sequel[:abc][:def], 'hello').as(:x2)).sql.must_equal "SELECT test(`abc`.`def`, 'hello') AS `x2` FROM `items`"
    @d.insert_sql(:value => 333).must_equal 'INSERT INTO `items` (`value`) VALUES (333)'
    @d.insert_sql(:x => :y).must_equal 'INSERT INTO `items` (`x`) VALUES (`y`)'
  end

  it "should quote fields correctly when reversing the order" do
    @d = @d.with_quote_identifiers(true)
    @d.reverse_order(:name).sql.must_equal 'SELECT * FROM `items` ORDER BY `name` DESC'
    @d.reverse_order(Sequel.desc(:name)).sql.must_equal 'SELECT * FROM `items` ORDER BY `name` ASC'
    @d.reverse_order(:name, Sequel.desc(:test)).sql.must_equal 'SELECT * FROM `items` ORDER BY `name` DESC, `test` ASC'
    @d.reverse_order(Sequel.desc(:name), :test).sql.must_equal 'SELECT * FROM `items` ORDER BY `name` ASC, `test` DESC'
  end

  it "should support ORDER clause in UPDATE statements" do
    @d.order(:name).update_sql(:value => 1).must_equal 'UPDATE `items` SET `value` = 1 ORDER BY `name`'
  end

  it "should support LIMIT clause in UPDATE statements" do
    @d.limit(10).update_sql(:value => 1).must_equal 'UPDATE `items` SET `value` = 1 LIMIT 10'
  end

  it "should support regexps" do
    @d << {:name => 'abc', :value => 1}
    @d << {:name => 'bcd', :value => 2}
    @d.filter(:name => /bc/).count.must_equal 2
    @d.filter(:name => /^bc/).count.must_equal 1
  end

  it "should have explain output" do
    @d.explain.must_be_kind_of(String)
    @d.explain(:extended=>true).must_be_kind_of(String)
    @d.explain.wont_equal @d.explain(:extended=>true)
  end

  it "should correctly literalize strings with comment backslashes in them" do
    @d.delete
    @d << {:name => ':\\'}

    @d.first[:name].must_equal ':\\'
  end

  it "should handle prepared statements with on_duplicate_key_update" do
    @d.db.add_index :items, :value, :unique=>true
    ds = @d.on_duplicate_key_update
    ps = ds.prepare(:insert, :insert_user_id_feature_name, :value => :$v, :name => :$n)
    ps.call(:v => 1, :n => 'a')
    ds.all.must_equal [{:value=>1, :name=>'a'}]
    ps.call(:v => 1, :n => 'b')
    ds.all.must_equal [{:value=>1, :name=>'b'}]
  end
end

describe "MySQL datasets" do
  before do
    @d = DB[:orders]
  end

  it "should correctly quote column references" do
    @d = @d.with_quote_identifiers(true)
    market = 'ICE'
    ack_stamp = Time.now - 15 * 60 # 15 minutes ago
    @d.select(:market, Sequel.function(:minute, Sequel.function(:from_unixtime, :ack)).as(:minute)).
      where{(ack > ack_stamp) & {:market => market}}.
      group_by(Sequel.function(:minute, Sequel.function(:from_unixtime, :ack))).sql.must_equal \
      "SELECT `market`, minute(from_unixtime(`ack`)) AS `minute` FROM `orders` WHERE ((`ack` > #{@d.literal(ack_stamp)}) AND (`market` = 'ICE')) GROUP BY minute(from_unixtime(`ack`))"
  end
end

describe "Dataset#distinct" do
  before do
    @db = DB
    @db.create_table!(:a) do
      Integer :a
      Integer :b
    end
    @ds = @db[:a]
  end
  after do
    @db.drop_table?(:a)
  end

  it "#distinct with arguments should return results distinct on those arguments" do
    @ds.insert(20, 10)
    @ds.insert(30, 10)
    @ds.order(:b, :a).distinct.map(:a).must_equal [20, 30]
    @ds.order(:b, Sequel.desc(:a)).distinct.map(:a).must_equal [30, 20]
    # MySQL doesn't respect orders when using the nonstandard GROUP BY
    [[20], [30]].must_include(@ds.order(:b, :a).distinct(:b).map(:a))
  end
end

describe "MySQL join expressions" do
  before do
    @ds = DB[:nodes]
  end

  it "should raise error for :full_outer join requests." do
    lambda{@ds.join_table(:full_outer, :nodes)}.must_raise(Sequel::Error)
  end
  it "should support natural left joins" do
    @ds.join_table(:natural_left, :nodes).sql.must_equal 'SELECT * FROM `nodes` NATURAL LEFT JOIN `nodes`'
  end
  it "should support natural right joins" do
    @ds.join_table(:natural_right, :nodes).sql.must_equal 'SELECT * FROM `nodes` NATURAL RIGHT JOIN `nodes`'
  end
  it "should support natural left outer joins" do
    @ds.join_table(:natural_left_outer, :nodes).sql.must_equal 'SELECT * FROM `nodes` NATURAL LEFT OUTER JOIN `nodes`'
  end
  it "should support natural right outer joins" do
    @ds.join_table(:natural_right_outer, :nodes).sql.must_equal 'SELECT * FROM `nodes` NATURAL RIGHT OUTER JOIN `nodes`'
  end
  it "should support natural inner joins" do
    @ds.join_table(:natural_inner, :nodes).sql.must_equal 'SELECT * FROM `nodes` NATURAL LEFT JOIN `nodes`'
  end
  it "should support cross joins" do
    @ds.join_table(:cross, :nodes).sql.must_equal 'SELECT * FROM `nodes` CROSS JOIN `nodes`'
  end
  it "should support cross joins as inner joins if conditions are used" do
    @ds.join_table(:cross, :nodes, :id=>:id).sql.must_equal 'SELECT * FROM `nodes` INNER JOIN `nodes` ON (`nodes`.`id` = `nodes`.`id`)'
  end
  it "should support straight joins (force left table to be read before right)" do
    @ds.join_table(:straight, :nodes).sql.must_equal 'SELECT * FROM `nodes` STRAIGHT_JOIN `nodes`'
  end
  it "should support natural joins on multiple tables." do
    @ds.join_table(:natural_left_outer, [:nodes, :branches]).sql.must_equal 'SELECT * FROM `nodes` NATURAL LEFT OUTER JOIN (`nodes`, `branches`)'
  end
  it "should support straight joins on multiple tables." do
    @ds.join_table(:straight, [:nodes,:branches]).sql.must_equal 'SELECT * FROM `nodes` STRAIGHT_JOIN (`nodes`, `branches`)'
  end
end

describe "Joined MySQL dataset" do
  before do
    @ds = DB[:nodes]
  end

  it "should quote fields correctly" do
    @ds.with_quote_identifiers(true).join(:attributes, :node_id => :id).sql.must_equal "SELECT * FROM `nodes` INNER JOIN `attributes` ON (`attributes`.`node_id` = `nodes`.`id`)"
  end

  it "should put a having clause before an order by clause" do
    @ds.order(:aaa).having(:bbb => :ccc).sql.must_equal "SELECT * FROM `nodes` HAVING (`bbb` = `ccc`) ORDER BY `aaa`"
  end
end

describe "A MySQL database" do
  after do
    DB.drop_table?(:test_innodb)
  end

  it "should handle the creation and dropping of an InnoDB table with foreign keys" do
    DB.create_table!(:test_innodb, :engine=>:InnoDB){primary_key :id; foreign_key :fk, :test_innodb, :key=>:id}
  end
end

describe "A MySQL database" do
  before(:all) do
    @db = DB
    @db.create_table! :test2 do
      text :name
      integer :value
    end
  end
  after(:all) do
    @db.drop_table?(:test2)
  end

  it "should provide the server version" do
    @db.server_version.must_be :>=,  40000
  end

  it "should cache the server version" do
    # warm cache:
    @db.server_version
    @db.sqls.clear
    3.times{@db.server_version}
    @db.sqls.must_be :empty?
  end

  it "should support for_share" do
    @db[:test2].delete
    @db.transaction{@db[:test2].for_share.all.must_equal []}
  end

  it "should support column operations" do
    @db.add_column :test2, :xyz, :text

    @db[:test2].columns.must_equal [:name, :value, :xyz]
    @db[:test2] << {:name => 'mmm', :value => 111, :xyz => '000'}
    @db[:test2].first[:xyz].must_equal '000'

    @db[:test2].columns.must_equal [:name, :value, :xyz]
    @db.drop_column :test2, :xyz

    @db[:test2].columns.must_equal [:name, :value]

    @db[:test2].delete
    @db.add_column :test2, :xyz, :text
    @db[:test2] << {:name => 'mmm', :value => 111, :xyz => 'qqqq'}

    @db[:test2].columns.must_equal [:name, :value, :xyz]
    @db.rename_column :test2, :xyz, :zyx, :type => :text
    @db[:test2].columns.must_equal [:name, :value, :zyx]
    @db[:test2].first[:zyx].must_equal 'qqqq'

    @db[:test2].delete
    @db.add_column :test2, :tre, :text
    @db[:test2] << {:name => 'mmm', :value => 111, :tre => 'qqqq'}

    @db[:test2].columns.must_equal [:name, :value, :zyx, :tre]
    @db.rename_column :test2, :tre, :ert, :type => :varchar, :size=>255
    @db[:test2].columns.must_equal [:name, :value, :zyx, :ert]
    @db[:test2].first[:ert].must_equal 'qqqq'

    @db.add_column :test2, :xyz, :float
    @db[:test2].delete
    @db[:test2] << {:name => 'mmm', :value => 111, :xyz => 56.78}
    @db.set_column_type :test2, :xyz, :integer

    @db[:test2].first[:xyz].must_equal 57

    @db.alter_table :test2 do
      add_index :value, :unique=>true
      add_foreign_key :value2, :test2, :key=>:value
    end
    @db[:test2].columns.must_equal [:name, :value, :zyx, :ert, :xyz, :value2]

    @db.alter_table :test2 do
      drop_foreign_key :value2
      drop_index :value
    end
  end
end  

describe "A MySQL database with table options" do
  before do
    @options = {:engine=>'MyISAM', :charset=>'latin1', :collate => 'latin1_swedish_ci'}

    Sequel::MySQL.default_engine = 'InnoDB'
    Sequel::MySQL.default_charset = 'utf8'
    Sequel::MySQL.default_collate = 'utf8_general_ci'

    @db = DB
    @db.drop_table?(:items)

    DB.sqls.clear
  end
  after do
    @db.drop_table?(:items)

    Sequel::MySQL.default_engine = nil
    Sequel::MySQL.default_charset = nil
    Sequel::MySQL.default_collate = nil
  end

  it "should allow to pass custom options (engine, charset, collate) for table creation" do
    @db.create_table(:items, @options){Integer :size; text :name}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`size` integer, `name` text) ENGINE=MyISAM DEFAULT CHARSET=latin1 DEFAULT COLLATE=latin1_swedish_ci"]
    end
  end

  it "should use default options if specified (engine, charset, collate) for table creation" do
    @db.create_table(:items){Integer :size; text :name}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`size` integer, `name` text) ENGINE=InnoDB DEFAULT CHARSET=utf8 DEFAULT COLLATE=utf8_general_ci"]
    end
  end

  it "should not use default if option has a nil value" do
    @db.create_table(:items, :engine=>nil, :charset=>nil, :collate=>nil){Integer :size; text :name}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`size` integer, `name` text)"]
    end
  end
end

describe "A MySQL database" do
  before do
    @db = DB
    @db.drop_table?(:items)
    DB.sqls.clear
  end
  after do
    @db.drop_table?(:items, :users)
  end

  it "should support defaults for boolean columns" do
    @db.create_table(:items){TrueClass :active1, :default=>true; FalseClass :active2, :default => false}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`active1` tinyint(1) DEFAULT 1, `active2` tinyint(1) DEFAULT 0)"]
    end
  end

  it "should correctly format CREATE TABLE statements with foreign keys" do
    @db.create_table(:items){primary_key :id; foreign_key :p_id, :items, :key => :id, :null => false, :on_delete => :cascade}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer PRIMARY KEY AUTO_INCREMENT, `p_id` integer NOT NULL, UNIQUE (`id`), FOREIGN KEY (`p_id`) REFERENCES `items`(`id`) ON DELETE CASCADE)"]
    end
  end

  it "should correctly format CREATE TABLE statements with foreign keys, when :key != the default (:id)" do
    @db.create_table(:items){primary_key :id; Integer :other_than_id; foreign_key :p_id, :items, :key => :other_than_id, :null => false, :on_delete => :cascade}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer PRIMARY KEY AUTO_INCREMENT, `other_than_id` integer, `p_id` integer NOT NULL, UNIQUE (`other_than_id`), FOREIGN KEY (`p_id`) REFERENCES `items`(`other_than_id`) ON DELETE CASCADE)"]
    end
  end

  it "should correctly format ALTER TABLE statements with foreign keys" do
    @db.create_table(:items){Integer :id}
    @db.create_table(:users){primary_key :id}
    @db.alter_table(:items){add_foreign_key :p_id, :users, :key => :id, :null => false, :on_delete => :cascade}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer)",
        "CREATE TABLE `users` (`id` integer PRIMARY KEY AUTO_INCREMENT)",
        "ALTER TABLE `items` ADD COLUMN `p_id` integer NOT NULL, ADD FOREIGN KEY (`p_id`) REFERENCES `users`(`id`) ON DELETE CASCADE"]
    end
  end

  it "should correctly format ALTER TABLE statements with named foreign keys" do
    @db.create_table(:items){Integer :id}
    @db.create_table(:users){primary_key :id}
    @db.alter_table(:items){add_foreign_key :p_id, :users, :key => :id, :null => false, :on_delete => :cascade, :foreign_key_constraint_name => :pk_items__users }
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer)",
        "CREATE TABLE `users` (`id` integer PRIMARY KEY AUTO_INCREMENT)",
        "ALTER TABLE `items` ADD COLUMN `p_id` integer NOT NULL, ADD CONSTRAINT `pk_items__users` FOREIGN KEY (`p_id`) REFERENCES `users`(`id`) ON DELETE CASCADE"]
    end
  end

  it "should correctly handle add_column :after option" do
    @db.create_table(:items){Integer :id; Integer :value}
    @db.alter_table(:items){add_column :name, String, :after=>:id}
    @db[:items].columns.must_equal [:id, :name, :value]
  end

  it "should correctly handle add_column :first option" do
    @db.create_table(:items){Integer :id; Integer :value}
    @db.alter_table(:items){add_column :name, String, :first => true}
    @db[:items].columns.must_equal [:name, :id, :value]
  end

  it "should correctly handle add_foreign_key :first option" do
    @db.create_table(:items){primary_key :id; Integer :value}
    @db.alter_table(:items){add_foreign_key :parent_id, :items, :first => true}
    @db[:items].columns.must_equal [:parent_id, :id, :value]
  end

  it "should have rename_column support keep existing options" do
    @db.create_table(:items){String :id, :null=>false, :default=>'blah'}
    @db.alter_table(:items){rename_column :id, :nid}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` varchar(255) NOT NULL DEFAULT 'blah')", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `nid` varchar(255) NOT NULL DEFAULT 'blah'"]
    end
    @db[:items].insert
    @db[:items].all.must_equal [{:nid=>'blah'}]
    proc{@db[:items].insert(:nid=>nil)}.must_raise(Sequel::NotNullConstraintViolation)
  end

  it "should have set_column_type support keep existing options" do
    @db.create_table(:items){Integer :id, :null=>false, :default=>5}
    @db.alter_table(:items){set_column_type :id, :Bignum}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer NOT NULL DEFAULT 5)", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `id` bigint NOT NULL DEFAULT 5"]
    end
    @db[:items].insert
    @db[:items].all.must_equal [{:id=>5}]
    proc{@db[:items].insert(:id=>nil)}.must_raise(Sequel::NotNullConstraintViolation)
    @db[:items].delete
    @db[:items].insert(2**40)
    @db[:items].all.must_equal [{:id=>2**40}]
  end

  it "should have set_column_type pass through options" do
    @db.create_table(:items){integer :id; enum :list, :elements=>%w[one]}
    @db.alter_table(:items){set_column_type :id, :int, :unsigned=>true, :size=>8; set_column_type :list, :enum, :elements=>%w[two]}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer, `list` enum('one'))", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `id` int(8) UNSIGNED NULL, CHANGE COLUMN `list` `list` enum('two') NULL"]
    end
  end

  it "should have set_column_default support keep existing options" do
    @db.create_table(:items){Integer :id, :null=>false, :default=>5}
    @db.alter_table(:items){set_column_default :id, 6}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer NOT NULL DEFAULT 5)", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `id` int(11) NOT NULL DEFAULT 6"]
    end
    @db[:items].insert
    @db[:items].all.must_equal [{:id=>6}]
    proc{@db[:items].insert(:id=>nil)}.must_raise(Sequel::NotNullConstraintViolation)
  end

  it "should have set_column_allow_null support keep existing options" do
    @db.create_table(:items){Integer :id, :null=>false, :default=>5}
    @db.alter_table(:items){set_column_allow_null :id, true}
    check_sqls do
      @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer NOT NULL DEFAULT 5)", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `id` int(11) NULL DEFAULT 5"]
    end
    @db[:items].insert
    @db[:items].all.must_equal [{:id=>5}]
    @db[:items].insert(:id=>nil)
  end

  it "should accept repeated raw sql statements using Database#<<" do
    @db.create_table(:items){String :name; Integer :value}
    @db << 'DELETE FROM items'
    @db[:items].count.must_equal 0

    @db << "INSERT INTO items (name, value) VALUES ('tutu', 1234)"
    @db[:items].first.must_equal(:name => 'tutu', :value => 1234)

    @db << 'DELETE FROM items'
    @db[:items].first.must_be_nil
  end
end  

# Socket tests should only be run if the MySQL server is on localhost
if %w'localhost 127.0.0.1 ::1'.include?(MYSQL_URI.host) and DB.adapter_scheme == :mysql
  describe "A MySQL database" do
    it "should accept a socket option" do
      db = Sequel.mysql(DB.opts[:database], :host => 'localhost', :user => DB.opts[:user], :password => DB.opts[:password], :socket => MYSQL_SOCKET_FILE)
      db.test_connection
    end

    it "should accept a socket option without host option" do
      db = Sequel.mysql(DB.opts[:database], :user => DB.opts[:user], :password => DB.opts[:password], :socket => MYSQL_SOCKET_FILE)
      db.test_connection
    end

    it "should fail to connect with invalid socket" do
      db = Sequel.mysql(DB.opts[:database], :user => DB.opts[:user], :password => DB.opts[:password], :socket =>'blah')
      proc{db.test_connection}.must_raise Sequel::DatabaseConnectionError
    end
  end
end

describe "A MySQL database" do
  it "should accept a read_timeout option when connecting" do
    db = Sequel.connect(DB.opts.merge(:read_timeout=>22342))
    db.test_connection
  end

  it "should accept a connect_timeout option when connecting" do
    db = Sequel.connect(DB.opts.merge(:connect_timeout=>22342))
    db.test_connection
  end
end

describe "MySQL foreign key support" do
  after do
    DB.drop_table?(:testfk, :testpk)
  end

  it "should create table without :key" do
    DB.create_table!(:testpk){primary_key :id}
    DB.create_table!(:testfk){foreign_key :fk, :testpk}
  end

  it "should create table with composite keys without :key" do
    DB.create_table!(:testpk){Integer :id; Integer :id2; primary_key([:id, :id2])}
    DB.create_table!(:testfk){Integer :fk; Integer :fk2; foreign_key([:fk, :fk2], :testpk)}
  end

  it "should create table with self referential without :key" do
    DB.create_table!(:testfk){primary_key :id; foreign_key :fk, :testfk}
  end

  it "should create table with self referential with non-autoincrementing key without :key" do
    DB.create_table!(:testfk){Integer :id, :primary_key=>true; foreign_key :fk, :testfk}
  end

  it "should create table with self referential with composite keys without :key" do
    DB.create_table!(:testfk){Integer :id; Integer :id2; Integer :fk; Integer :fk2; primary_key([:id, :id2]); foreign_key([:fk, :fk2], :testfk)}
  end

  it "should alter table without :key" do
    DB.create_table!(:testpk){primary_key :id}
    DB.create_table!(:testfk){Integer :id}
    DB.alter_table(:testfk){add_foreign_key :fk, :testpk}
  end

  it "should alter table with composite keys without :key" do
    DB.create_table!(:testpk){Integer :id; Integer :id2; primary_key([:id, :id2])}
    DB.create_table!(:testfk){Integer :fk; Integer :fk2}
    DB.alter_table(:testfk){add_foreign_key([:fk, :fk2], :testpk)}
  end

  it "should alter table with self referential without :key" do
    DB.create_table!(:testfk){primary_key :id}
    DB.alter_table(:testfk){add_foreign_key :fk, :testfk}
  end

  it "should alter table with self referential with composite keys without :key" do
    DB.create_table!(:testfk){Integer :id; Integer :id2; Integer :fk; Integer :fk2; primary_key([:id, :id2])}
    DB.alter_table(:testfk){add_foreign_key [:fk, :fk2], :testfk}
  end
end

describe "A grouped MySQL dataset" do
  before do
    DB.create_table! :test2 do
      text :name
      integer :value
    end
    DB[:test2] << {:name => '11', :value => 10}
    DB[:test2] << {:name => '11', :value => 20}
    DB[:test2] << {:name => '11', :value => 30}
    DB[:test2] << {:name => '12', :value => 10}
    DB[:test2] << {:name => '12', :value => 20}
    DB[:test2] << {:name => '13', :value => 10}
  end
  after do
    DB.drop_table?(:test2)
  end

  it "should return the correct count for raw sql query" do
    ds = DB["select name FROM test2 WHERE name = '11' GROUP BY name"]
    ds.count.must_equal 1
  end

  it "should return the correct count for a normal dataset" do
    ds = DB[:test2].select(:name).where(:name => '11').group(:name)
    ds.count.must_equal 1
  end
end

describe "A MySQL database" do
  before do
    @db = DB
    @db.drop_table?(:posts)
    @db.sqls.clear
  end
  after do
    @db.drop_table?(:posts)
  end

  it "should support fulltext indexes and full_text_search" do
    @db.create_table(:posts, :engine=>:MyISAM){text :title; text :body; full_text_index :title; full_text_index [:title, :body]}
    check_sqls do
      @db.sqls.must_equal [
        "CREATE TABLE `posts` (`title` text, `body` text) ENGINE=MyISAM",
        "CREATE FULLTEXT INDEX `posts_title_index` ON `posts` (`title`)",
        "CREATE FULLTEXT INDEX `posts_title_body_index` ON `posts` (`title`, `body`)"
      ]
    end

    @db[:posts].insert(:title=>'ruby rails', :body=>'y')
    @db[:posts].insert(:title=>'sequel', :body=>'ruby')
    @db[:posts].insert(:title=>'ruby scooby', :body=>'x')
    @db.sqls.clear

    @db[:posts].full_text_search(:title, 'rails').all.must_equal [{:title=>'ruby rails', :body=>'y'}]
    @db[:posts].full_text_search([:title, :body], ['sequel', 'ruby']).all.must_equal [{:title=>'sequel', :body=>'ruby'}]
    @db[:posts].full_text_search(:title, '+ruby -rails', :boolean => true).all.must_equal [{:title=>'ruby scooby', :body=>'x'}]
    check_sqls do
      @db.sqls.must_equal [
        "SELECT * FROM `posts` WHERE (MATCH (`title`) AGAINST ('rails'))",
        "SELECT * FROM `posts` WHERE (MATCH (`title`, `body`) AGAINST ('sequel ruby'))",
        "SELECT * FROM `posts` WHERE (MATCH (`title`) AGAINST ('+ruby -rails' IN BOOLEAN MODE))"]
    end

    @db[:posts].full_text_search(:title, :$n).call(:select, :n=>'rails').must_equal [{:title=>'ruby rails', :body=>'y'}]
    @db[:posts].full_text_search(:title, :$n).prepare(:select, :fts_select).call(:n=>'rails').must_equal [{:title=>'ruby rails', :body=>'y'}]
  end

  it "should support spatial indexes" do
    @db.create_table(:posts, :engine=>:MyISAM){point :geom, :null=>false; spatial_index [:geom]}
    check_sqls do
      @db.sqls.must_equal [
        "CREATE TABLE `posts` (`geom` point NOT NULL) ENGINE=MyISAM",
        "CREATE SPATIAL INDEX `posts_geom_index` ON `posts` (`geom`)"
      ]
    end
  end

  it "should support indexes with index type" do
    @db.create_table(:posts){Integer :id; index :id, :type => :btree}
    check_sqls do
      @db.sqls.must_equal [
        "CREATE TABLE `posts` (`id` integer)",
        "CREATE INDEX `posts_id_index` USING btree ON `posts` (`id`)"
      ]
    end
  end

  it "should support unique indexes with index type" do
    @db.create_table(:posts){Integer :id; index :id, :type => :btree, :unique => true}
    check_sqls do
      @db.sqls.must_equal [
        "CREATE TABLE `posts` (`id` integer)",
        "CREATE UNIQUE INDEX `posts_id_index` USING btree ON `posts` (`id`)"
      ]
    end
  end

  it "should not dump partial indexes" do
    @db.create_table(:posts){text :id}
    @db << "CREATE INDEX posts_id_index ON posts (id(10))"
    @db.indexes(:posts).must_equal({})
  end

  it "should dump partial indexes if :partial option is set to true" do
    @db.create_table(:posts){text :id}
    @db << "CREATE INDEX posts_id_index ON posts (id(10))"
    @db.indexes(:posts, :partial => true).must_equal(:posts_id_index => {:columns => [:id], :unique => false})
  end
end

describe "MySQL::Dataset#insert and related methods" do
  before do
    DB.create_table(:items){String :name; Integer :value}
    @d = DB[:items]
    DB.sqls.clear
  end
  after do
    DB.drop_table?(:items)
  end

  it "#insert should insert record with default values when no arguments given" do
    @d.insert
    check_sqls do
      DB.sqls.must_equal ["INSERT INTO `items` () VALUES ()"]
    end
    @d.all.must_equal [{:name => nil, :value => nil}]
  end

  it "#insert  should insert record with default values when empty hash given" do
    @d.insert({})
    check_sqls do
      DB.sqls.must_equal ["INSERT INTO `items` () VALUES ()"]
    end
    @d.all.must_equal [{:name => nil, :value => nil}]
  end

  it "#insert should insert record with default values when empty array given" do
    @d.insert []
    check_sqls do
      DB.sqls.must_equal ["INSERT INTO `items` () VALUES ()"]
    end
    @d.all.must_equal [{:name => nil, :value => nil}]
  end

  it "#on_duplicate_key_update should work with regular inserts" do
    DB.add_index :items, :name, :unique=>true
    DB.sqls.clear
    @d.insert(:name => 'abc', :value => 1)
    @d.on_duplicate_key_update(:name, :value => 6).insert(:name => 'abc', :value => 1)
    @d.on_duplicate_key_update(:name, :value => 6).insert(:name => 'def', :value => 2)

    check_sqls do
      DB.sqls.length.must_equal 3
      DB.sqls[0].must_match(/\AINSERT INTO `items` \(`(name|value)`, `(name|value)`\) VALUES \(('abc'|1), (1|'abc')\)\z/)
      DB.sqls[1].must_match(/\AINSERT INTO `items` \(`(name|value)`, `(name|value)`\) VALUES \(('abc'|1), (1|'abc')\) ON DUPLICATE KEY UPDATE `name`=VALUES\(`name`\), `value`=6\z/)
      DB.sqls[2].must_match(/\AINSERT INTO `items` \(`(name|value)`, `(name|value)`\) VALUES \(('def'|2), (2|'def')\) ON DUPLICATE KEY UPDATE `name`=VALUES\(`name`\), `value`=6\z/)
    end

    @d.all.must_equal [{:name => 'abc', :value => 6}, {:name => 'def', :value => 2}]
  end

  it "#multi_replace should insert multiple records in a single statement" do
    @d.multi_replace([{:name => 'abc'}, {:name => 'def'}])

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "REPLACE INTO `items` (`name`) VALUES ('abc'), ('def')",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => nil}, {:name => 'def', :value => nil}
    ]
  end

  it "#multi_replace should split the list of records into batches if :commit_every option is given" do
    @d.multi_replace([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
      :commit_every => 2)

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "REPLACE INTO `items` (`value`) VALUES (1), (2)",
        SQL_COMMIT,
        SQL_BEGIN,
        "REPLACE INTO `items` (`value`) VALUES (3), (4)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => nil, :value => 1}, 
      {:name => nil, :value => 2},
      {:name => nil, :value => 3}, 
      {:name => nil, :value => 4}
    ]
  end

  it "#multi_replace should split the list of records into batches if :slice option is given" do
    @d.multi_replace([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
      :slice => 2)

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "REPLACE INTO `items` (`value`) VALUES (1), (2)",
        SQL_COMMIT,
        SQL_BEGIN,
        "REPLACE INTO `items` (`value`) VALUES (3), (4)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => nil, :value => 1}, 
      {:name => nil, :value => 2},
      {:name => nil, :value => 3}, 
      {:name => nil, :value => 4}
    ]
  end

  it "#multi_insert should insert multiple records in a single statement" do
    @d.multi_insert([{:name => 'abc'}, {:name => 'def'}])

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT INTO `items` (`name`) VALUES ('abc'), ('def')",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => nil}, {:name => 'def', :value => nil}
    ]
  end

  it "#multi_insert should split the list of records into batches if :commit_every option is given" do
    @d.multi_insert([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
      :commit_every => 2)

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT INTO `items` (`value`) VALUES (1), (2)",
        SQL_COMMIT,
        SQL_BEGIN,
        "INSERT INTO `items` (`value`) VALUES (3), (4)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => nil, :value => 1}, 
      {:name => nil, :value => 2},
      {:name => nil, :value => 3}, 
      {:name => nil, :value => 4}
    ]
  end

  it "#multi_insert should split the list of records into batches if :slice option is given" do
    @d.multi_insert([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
      :slice => 2)

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT INTO `items` (`value`) VALUES (1), (2)",
        SQL_COMMIT,
        SQL_BEGIN,
        "INSERT INTO `items` (`value`) VALUES (3), (4)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => nil, :value => 1}, 
      {:name => nil, :value => 2},
      {:name => nil, :value => 3}, 
      {:name => nil, :value => 4}
    ]
  end

  it "#import should support inserting using columns and values arrays" do
    @d.import([:name, :value], [['abc', 1], ['def', 2]])

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT INTO `items` (`name`, `value`) VALUES ('abc', 1), ('def', 2)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => 1}, 
      {:name => 'def', :value => 2}
    ]
  end

  it "#insert_ignore should add the IGNORE keyword when inserting" do
    @d.insert_ignore.multi_insert([{:name => 'abc'}, {:name => 'def'}])

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT IGNORE INTO `items` (`name`) VALUES ('abc'), ('def')",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => nil}, {:name => 'def', :value => nil}
    ]
  end

  it "#insert_ignore should add the IGNORE keyword for single inserts" do
    @d.insert_ignore.insert(:name => 'ghi')
    check_sqls do
      DB.sqls.must_equal ["INSERT IGNORE INTO `items` (`name`) VALUES ('ghi')"]
    end
    @d.all.must_equal [{:name => 'ghi', :value => nil}]
  end

  it "#on_duplicate_key_update should add the ON DUPLICATE KEY UPDATE and ALL columns when no args given" do
    @d.on_duplicate_key_update.import([:name,:value], [['abc', 1], ['def',2]])

    check_sqls do
      DB.sqls.must_equal [
        "SELECT * FROM `items` LIMIT 1",
        SQL_BEGIN,
        "INSERT INTO `items` (`name`, `value`) VALUES ('abc', 1), ('def', 2) ON DUPLICATE KEY UPDATE `name`=VALUES(`name`), `value`=VALUES(`value`)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => 1}, {:name => 'def', :value => 2}
    ]
  end

  it "#on_duplicate_key_update should add the ON DUPLICATE KEY UPDATE and columns specified when args are given" do
    @d.on_duplicate_key_update(:value).import([:name,:value], 
      [['abc', 1], ['def',2]]
    )

    check_sqls do
      DB.sqls.must_equal [
        SQL_BEGIN,
        "INSERT INTO `items` (`name`, `value`) VALUES ('abc', 1), ('def', 2) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)",
        SQL_COMMIT
      ]
    end

    @d.all.must_equal [
      {:name => 'abc', :value => 1}, {:name => 'def', :value => 2}
    ]
  end

end

describe "MySQL::Dataset#update and related methods" do
  before do
    DB.create_table(:items){String :name; Integer :value; index :name, :unique=>true}
    @d = DB[:items]
  end
  after do
    DB.drop_table?(:items)
  end

  it "#update_ignore should not raise error where normal update would fail" do
    @d.insert(:name => 'cow', :value => 0)
    @d.insert(:name => 'cat', :value => 1)
    proc{@d.where(:value => 1).update(:name => 'cow')}.must_raise(Sequel::UniqueConstraintViolation)
    DB.sqls.clear
    @d.update_ignore.where(:value => 1).update(:name => 'cow')
    check_sqls do
      DB.sqls.must_equal ["UPDATE IGNORE `items` SET `name` = 'cow' WHERE (`value` = 1)"]
    end
    @d.order(:name).all.must_equal [{:name => 'cat', :value => 1}, {:name => 'cow', :value => 0}]
  end
end

describe "MySQL::Dataset#replace" do
  before do
    DB.create_table(:items){Integer :id, :unique=>true; Integer :value}
    @d = DB[:items]
    DB.sqls.clear
  end
  after do
    DB.drop_table?(:items)
  end

  it "should use default values if they exist" do
    DB.alter_table(:items){set_column_default :id, 1; set_column_default :value, 2}
    @d.replace
    @d.all.must_equal [{:id=>1, :value=>2}]
    @d.replace([])
    @d.all.must_equal [{:id=>1, :value=>2}]
    @d.replace({})
    @d.all.must_equal [{:id=>1, :value=>2}]
  end
end

describe "MySQL::Dataset#complex_expression_sql" do
  before do
    @d = DB.dataset
  end

  it "should handle string concatenation with CONCAT if more than one record" do
    @d.literal(Sequel.join([:x, :y])).must_equal "CONCAT(`x`, `y`)"
    @d.literal(Sequel.join([:x, :y], ' ')).must_equal "CONCAT(`x`, ' ', `y`)"
    @d.literal(Sequel.join([Sequel.function(:x, :y), 1, Sequel.lit('z')], Sequel.subscript(:y, 1))).must_equal "CONCAT(x(`y`), `y`[1], '1', `y`[1], z)"
  end

  it "should handle string concatenation as simple string if just one record" do
    @d.literal(Sequel.join([:x])).must_equal "`x`"
    @d.literal(Sequel.join([:x], ' ')).must_equal "`x`"
  end
end

describe "MySQL::Dataset#calc_found_rows" do
  before do
    DB.create_table!(:items){Integer :a}
  end
  after do
    DB.drop_table?(:items)
  end

  it "should add the SQL_CALC_FOUND_ROWS keyword when selecting" do
    DB[:items].select(:a).calc_found_rows.limit(1).sql.must_equal \
      'SELECT SQL_CALC_FOUND_ROWS `a` FROM `items` LIMIT 1'
  end

  it "should count matching rows disregarding LIMIT clause" do
    DB[:items].multi_insert([{:a => 1}, {:a => 1}, {:a => 2}])
    DB.sqls.clear

    DB.synchronize do
      DB[:items].calc_found_rows.filter(:a => 1).limit(1).all.must_equal [{:a => 1}]
      DB.dataset.select(Sequel.function(:FOUND_ROWS).as(:rows)).all.must_equal [{:rows => 2 }]
    end

    check_sqls do
      DB.sqls.must_equal [
        'SELECT SQL_CALC_FOUND_ROWS * FROM `items` WHERE (`a` = 1) LIMIT 1',
        'SELECT FOUND_ROWS() AS `rows`',
      ]
    end
  end
end

if DB.adapter_scheme == :mysql or DB.adapter_scheme == :jdbc or DB.adapter_scheme == :mysql2
  describe "MySQL Stored Procedures" do
    before do
      DB.create_table(:items){Integer :id; Integer :value}
      @d = DB[:items]
      DB.sqls.clear
    end
    after do
      DB.drop_table?(:items)
      DB.execute('DROP PROCEDURE test_sproc')
    end

    it "should be callable on the database object" do
      DB.execute_ddl('CREATE PROCEDURE test_sproc() BEGIN DELETE FROM items; END')
      DB[:items].delete
      DB[:items].insert(:value=>1)
      DB[:items].count.must_equal 1
      DB.call_sproc(:test_sproc)
      DB[:items].count.must_equal 0
    end

    # Mysql2 doesn't support stored procedures that return result sets, probably because
    # CLIENT_MULTI_RESULTS is not set.
    unless DB.adapter_scheme == :mysql2
      it "should be callable on the dataset object" do
        DB.execute_ddl('CREATE PROCEDURE test_sproc(a INTEGER) BEGIN SELECT *, a AS b FROM items; END')
        DB[:items].delete
        @d = DB[:items]
        @d.call_sproc(:select, :test_sproc, 3).must_equal []
        @d.insert(:value=>1)
        @d.call_sproc(:select, :test_sproc, 4).must_equal [{:id=>nil, :value=>1, :b=>4}]
        @d = @d.with_row_proc(proc{|r| r.keys.each{|k| r[k] *= 2 if r[k].is_a?(Integer)}; r})
        @d.call_sproc(:select, :test_sproc, 3).must_equal [{:id=>nil, :value=>2, :b=>6}]
      end

      it "should be callable on the dataset object with multiple arguments" do
        DB.execute_ddl('CREATE PROCEDURE test_sproc(a INTEGER, c INTEGER) BEGIN SELECT *, a AS b, c AS d FROM items; END')
        DB[:items].delete
        @d = DB[:items]
        @d.call_sproc(:select, :test_sproc, 3, 4).must_equal []
        @d.insert(:value=>1)
        @d.call_sproc(:select, :test_sproc, 4, 5).must_equal [{:id=>nil, :value=>1, :b=>4, :d=>5}]
        @d = @d.with_row_proc(proc{|r| r.keys.each{|k| r[k] *= 2 if r[k].is_a?(Integer)}; r})
        @d.call_sproc(:select, :test_sproc, 3, 4).must_equal [{:id=>nil, :value=>2, :b=>6, :d => 8}]
      end
    end

    it "should deal with nil values" do
      DB.execute_ddl('CREATE PROCEDURE test_sproc(i INTEGER, v INTEGER) BEGIN INSERT INTO items VALUES (i, v); END')
      DB[:items].delete
      DB.call_sproc(:test_sproc, :args=>[1, nil])
      DB[:items].all.must_equal [{:id=>1, :value=>nil}]
    end
  end
end

if DB.adapter_scheme == :mysql
  describe "MySQL bad date/time conversions" do
    after do
      DB.convert_invalid_date_time = false
    end

    it "should raise an exception when a bad date/time is used and convert_invalid_date_time is false" do
      DB.convert_invalid_date_time = false
      proc{DB["SELECT CAST('0000-00-00' AS date)"].single_value}.must_raise(Sequel::InvalidValue)
      proc{DB["SELECT CAST('0000-00-00 00:00:00' AS datetime)"].single_value}.must_raise(Sequel::InvalidValue)
      proc{DB["SELECT CAST('25:00:00' AS time)"].single_value}.must_raise(Sequel::InvalidValue)
    end

    it "should not use a nil value bad date/time is used and convert_invalid_date_time is nil or :nil" do
      DB.convert_invalid_date_time = nil
      DB["SELECT CAST('0000-00-00' AS date)"].single_value.must_be_nil
      DB["SELECT CAST('0000-00-00 00:00:00' AS datetime)"].single_value.must_be_nil
      DB["SELECT CAST('25:00:00' AS time)"].single_value.must_be_nil
      DB.convert_invalid_date_time = :nil
      DB["SELECT CAST('0000-00-00' AS date)"].single_value.must_be_nil
      DB["SELECT CAST('0000-00-00 00:00:00' AS datetime)"].single_value.must_be_nil
      DB["SELECT CAST('25:00:00' AS time)"].single_value.must_be_nil
    end

    it "should not use a nil value bad date/time is used and convert_invalid_date_time is :string" do
      DB.convert_invalid_date_time = :string
      DB["SELECT CAST('0000-00-00' AS date)"].single_value.must_equal '0000-00-00'
      DB["SELECT CAST('0000-00-00 00:00:00' AS datetime)"].single_value.must_equal '0000-00-00 00:00:00'
      DB["SELECT CAST('25:00:00' AS time)"].single_value.must_equal '25:00:00'
    end
  end

  describe "MySQL multiple result sets" do
    before do
      DB.create_table!(:a){Integer :a}
      DB.create_table!(:b){Integer :b}
      @ds = DB['SELECT * FROM a; SELECT * FROM b']
      DB[:a].insert(10)
      DB[:a].insert(15)
      DB[:b].insert(20)
      DB[:b].insert(25)
    end
    after do
      DB.drop_table?(:a, :b)
    end

    it "should combine all results by default" do
      @ds.all.must_equal [{:a=>10}, {:a=>15}, {:b=>20}, {:b=>25}]
    end

    it "should work with Database#run" do
      DB.run('SELECT * FROM a; SELECT * FROM b')
      DB.run('SELECT * FROM a; SELECT * FROM b')
    end

    it "should work with Database#run and other statements" do
      DB.run('UPDATE a SET a = 1; SELECT * FROM a; DELETE FROM b')
      DB[:a].select_order_map(:a).must_equal [1, 1]
      DB[:b].all.must_equal []
    end

    it "should split results returned into arrays if split_multiple_result_sets is used" do
      @ds.split_multiple_result_sets.all.must_equal [[{:a=>10}, {:a=>15}], [{:b=>20}, {:b=>25}]]
    end

    it "should have regular row_procs work when splitting multiple result sets" do
      @ds = @ds.with_row_proc(proc{|x| x[x.keys.first] *= 2; x})
      @ds.split_multiple_result_sets.all.must_equal [[{:a=>20}, {:a=>30}], [{:b=>40}, {:b=>50}]]
    end

    it "should use the columns from the first result set when splitting result sets" do
      @ds.split_multiple_result_sets.columns.must_equal [:a]
    end

    it "should not allow graphing a dataset that splits multiple statements" do
      proc{@ds.split_multiple_result_sets.graph(:b, :b=>:a)}.must_raise(Sequel::Error)
    end

    it "should not allow splitting a graphed dataset" do
      proc{DB[:a].graph(:b, :b=>:a).split_multiple_result_sets}.must_raise(Sequel::Error)
    end
  end
end

if DB.adapter_scheme == :mysql2
  describe "Mysql2 streaming" do
    before(:all) do
      DB.create_table!(:a){Integer :a}
      DB.transaction do
        1000.times do |i|
          DB[:a].insert(i)
        end
      end
      @ds = DB[:a].stream.order(:a)
    end
    after(:all) do
      DB.drop_table?(:a)
    end

    it "should correctly stream results" do
      @ds.map(:a).must_equal((0...1000).to_a)
    end

    it "should correctly handle early returning when streaming results" do
      3.times{@ds.each{|r| break r[:a]}.must_equal 0}
    end
  end
end

describe "MySQL joined datasets" do
  before do
    @db = DB
    @db.create_table!(:a) do
      Integer :id
    end
    @db.create_table!(:b) do
      Integer :id
      Integer :a_id
    end
    @db[:a].insert(1)
    @db[:a].insert(2)
    @db[:b].insert(3, 1)
    @db[:b].insert(4, 1)
    @db[:b].insert(5, 2)
    @ds = @db[:a].join(:b, :a_id=>:id)
  end
  after do
    @db.drop_table?(:a, :b)
  end

  it "should support deletions from a single table" do
    @ds.where(Sequel[:a][:id]=>1).delete
    @db[:a].select_order_map(:id).must_equal [2]
    @db[:b].select_order_map(:id).must_equal [3, 4, 5]
  end

  it "should support deletions from multiple tables" do
    @ds.delete_from(:a, :b).where(Sequel[:a][:id]=>1).delete
    @db[:a].select_order_map(:id).must_equal [2]
    @db[:b].select_order_map(:id).must_equal [5]
  end
end
