sequel 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/CHANGELOG +1551 -4
  2. data/README +306 -19
  3. data/Rakefile +84 -56
  4. data/bin/sequel +106 -0
  5. data/doc/cheat_sheet.rdoc +225 -0
  6. data/doc/dataset_filtering.rdoc +182 -0
  7. data/lib/sequel_core.rb +136 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
  9. data/lib/sequel_core/adapters/ado.rb +80 -0
  10. data/lib/sequel_core/adapters/db2.rb +148 -0
  11. data/lib/sequel_core/adapters/dbi.rb +117 -0
  12. data/lib/sequel_core/adapters/informix.rb +78 -0
  13. data/lib/sequel_core/adapters/jdbc.rb +186 -0
  14. data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
  15. data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
  16. data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
  17. data/lib/sequel_core/adapters/mysql.rb +231 -0
  18. data/lib/sequel_core/adapters/odbc.rb +155 -0
  19. data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/openbase.rb +64 -0
  21. data/lib/sequel_core/adapters/oracle.rb +170 -0
  22. data/lib/sequel_core/adapters/postgres.rb +199 -0
  23. data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
  24. data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
  25. data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
  26. data/lib/sequel_core/adapters/sqlite.rb +138 -0
  27. data/lib/sequel_core/connection_pool.rb +194 -0
  28. data/lib/sequel_core/core_ext.rb +203 -0
  29. data/lib/sequel_core/core_sql.rb +184 -0
  30. data/lib/sequel_core/database.rb +471 -0
  31. data/lib/sequel_core/database/schema.rb +156 -0
  32. data/lib/sequel_core/dataset.rb +457 -0
  33. data/lib/sequel_core/dataset/callback.rb +13 -0
  34. data/lib/sequel_core/dataset/convenience.rb +245 -0
  35. data/lib/sequel_core/dataset/pagination.rb +96 -0
  36. data/lib/sequel_core/dataset/query.rb +41 -0
  37. data/lib/sequel_core/dataset/schema.rb +15 -0
  38. data/lib/sequel_core/dataset/sql.rb +889 -0
  39. data/lib/sequel_core/deprecated.rb +26 -0
  40. data/lib/sequel_core/exceptions.rb +42 -0
  41. data/lib/sequel_core/migration.rb +187 -0
  42. data/lib/sequel_core/object_graph.rb +216 -0
  43. data/lib/sequel_core/pretty_table.rb +71 -0
  44. data/lib/sequel_core/schema.rb +2 -0
  45. data/lib/sequel_core/schema/generator.rb +239 -0
  46. data/lib/sequel_core/schema/sql.rb +325 -0
  47. data/lib/sequel_core/sql.rb +812 -0
  48. data/lib/sequel_model.rb +5 -1
  49. data/lib/sequel_model/association_reflection.rb +3 -8
  50. data/lib/sequel_model/base.rb +15 -10
  51. data/lib/sequel_model/inflector.rb +3 -5
  52. data/lib/sequel_model/plugins.rb +1 -1
  53. data/lib/sequel_model/record.rb +11 -3
  54. data/lib/sequel_model/schema.rb +4 -4
  55. data/lib/sequel_model/validations.rb +6 -1
  56. data/spec/adapters/ado_spec.rb +17 -0
  57. data/spec/adapters/informix_spec.rb +96 -0
  58. data/spec/adapters/mysql_spec.rb +764 -0
  59. data/spec/adapters/oracle_spec.rb +222 -0
  60. data/spec/adapters/postgres_spec.rb +441 -0
  61. data/spec/adapters/spec_helper.rb +7 -0
  62. data/spec/adapters/sqlite_spec.rb +400 -0
  63. data/spec/integration/dataset_test.rb +51 -0
  64. data/spec/integration/eager_loader_test.rb +702 -0
  65. data/spec/integration/schema_test.rb +102 -0
  66. data/spec/integration/spec_helper.rb +44 -0
  67. data/spec/integration/type_test.rb +43 -0
  68. data/spec/rcov.opts +2 -0
  69. data/spec/sequel_core/connection_pool_spec.rb +363 -0
  70. data/spec/sequel_core/core_ext_spec.rb +156 -0
  71. data/spec/sequel_core/core_sql_spec.rb +427 -0
  72. data/spec/sequel_core/database_spec.rb +964 -0
  73. data/spec/sequel_core/dataset_spec.rb +2977 -0
  74. data/spec/sequel_core/expression_filters_spec.rb +346 -0
  75. data/spec/sequel_core/migration_spec.rb +261 -0
  76. data/spec/sequel_core/object_graph_spec.rb +234 -0
  77. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  78. data/spec/sequel_core/schema_generator_spec.rb +122 -0
  79. data/spec/sequel_core/schema_spec.rb +497 -0
  80. data/spec/sequel_core/spec_helper.rb +51 -0
  81. data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
  82. data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
  83. data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
  84. data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
  85. data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
  86. data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
  87. data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
  88. data/spec/sequel_model/inflector_spec.rb +119 -0
  89. data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
  90. data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
  91. data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
  92. data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
  93. data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
  94. data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
  95. data/spec/spec_config.rb +9 -0
  96. data/spec/spec_config.rb.example +10 -0
  97. metadata +110 -37
  98. data/spec/inflector_spec.rb +0 -34
@@ -54,7 +54,7 @@ module Sequel
54
54
  # sti_dataset, and sti_key. You should not usually need to
55
55
  # access these directly.
56
56
  # * The following class level attr_accessors are created: raise_on_save_failure,
57
- # strict_param_setting, and typecast_on_assignment:
57
+ # strict_param_setting, typecast_empty_string_to_nil, and typecast_on_assignment:
58
58
  #
59
59
  # # Don't raise an error if a validation attempt fails in
60
60
  # # save/create/save_changes/etc.
@@ -71,6 +71,10 @@ module Sequel
71
71
  # m = Model.new
72
72
  # m.number = '10'
73
73
  # m.number # => '10' instead of 10
74
+ # # Don't typecast empty string to nil for non-string, non-blob columns.
75
+ # Model.typecast_empty_string_to_nil = false
76
+ # m.number = ''
77
+ # m.number # => '' instead of nil
74
78
  #
75
79
  # * The following class level method aliases are defined:
76
80
  # * Model.dataset= => set_dataset
@@ -70,14 +70,14 @@ module Sequel
70
70
 
71
71
  # Name symbol for default join table
72
72
  def default_join_table
73
- ([self[:class_name].demodulize, self[:model].name.demodulize]. \
73
+ ([self[:class_name].demodulize, self[:model].name.to_s.demodulize]. \
74
74
  map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
75
75
  end
76
76
 
77
77
  # Default foreign key name symbol for key in associated table that points to
78
78
  # current table's primary key.
79
79
  def default_left_key
80
- :"#{self[:model].name.demodulize.underscore}_id"
80
+ :"#{self[:model].name.to_s.demodulize.underscore}_id"
81
81
  end
82
82
 
83
83
  # Default foreign key name symbol for foreign key in current model's table that points to
@@ -86,14 +86,9 @@ module Sequel
86
86
  :"#{self[:type] == :many_to_one ? self[:name] : self[:name].to_s.singularize}_id"
87
87
  end
88
88
 
89
- # Name symbol for _dataset association method
90
- def eager_dataset_method
91
- :"#{self[:name]}_eager_dataset"
92
- end
93
-
94
89
  # Whether to eagerly graph a lazy dataset
95
90
  def eager_graph_lazy_dataset?
96
- self[:type] != :many_to_one or opts[:key]
91
+ self[:type] != :many_to_one or self[:key].nil?
97
92
  end
98
93
 
99
94
  # Whether the associated object needs a primary key to be added/removed
@@ -14,6 +14,7 @@ module Sequel
14
14
  @sti_dataset = nil
15
15
  @sti_key = nil
16
16
  @strict_param_setting = true
17
+ @typecast_empty_string_to_nil = true
17
18
  @typecast_on_assignment = true
18
19
 
19
20
  # Which columns should be the only columns allowed in a call to set
@@ -47,12 +48,16 @@ module Sequel
47
48
  # access is restricted to it).
48
49
  metaattr_accessor :strict_param_setting
49
50
 
51
+ # Whether to typecast the empty string ('') to nil for columns that
52
+ # are not string or blob.
53
+ metaattr_accessor :typecast_empty_string_to_nil
54
+
50
55
  # Whether to typecast attribute values on assignment (default: true)
51
56
  metaattr_accessor :typecast_on_assignment
52
57
 
53
58
  # Dataset methods to proxy via metaprogramming
54
59
  DATASET_METHODS = %w'<< all avg count delete distinct eager eager_graph each each_page
55
- empty? except exclude filter first from_self full_outer_join get graph
60
+ empty? except exclude filter first from from_self full_outer_join get graph
56
61
  group group_and_count group_by having import inner_join insert
57
62
  insert_multiple intersect interval invert_order join join_table last
58
63
  left_outer_join limit map multi_insert naked order order_by order_more
@@ -65,7 +70,7 @@ module Sequel
65
70
  :@cache_ttl=>nil, :@dataset_methods=>:dup, :@primary_key=>nil,
66
71
  :@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
67
72
  :@sti_dataset=>nil, :@sti_key=>nil, :@strict_param_setting=>nil,
68
- :@typecast_on_assignment=>nil}
73
+ :@typecast_empty_string_to_nil=>nil, :@typecast_on_assignment=>nil}
69
74
 
70
75
  # Returns the first record from the database matching the conditions.
71
76
  # If a hash is given, it is used as the conditions. If another
@@ -190,7 +195,7 @@ module Sequel
190
195
  # from the parent class.
191
196
  def self.inherited(subclass)
192
197
  sup_class = subclass.superclass
193
- ivs = subclass.instance_variables
198
+ ivs = subclass.instance_variables.collect{|x| x.to_s}
194
199
  INHERITED_INSTANCE_VARIABLES.each do |iv, dup|
195
200
  next if ivs.include?(iv.to_s)
196
201
  sup_class_value = sup_class.instance_variable_get(iv)
@@ -200,11 +205,12 @@ module Sequel
200
205
  unless ivs.include?("@dataset")
201
206
  begin
202
207
  if sup_class == Model
203
- subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.empty?
208
+ subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.blank?
204
209
  elsif ds = sup_class.instance_variable_get(:@dataset)
205
- subclass.set_dataset(sup_class.sti_key ? sup_class.sti_dataset.filter(sup_class.sti_key=>subclass.name) : ds.clone)
210
+ subclass.set_dataset(sup_class.sti_key ? sup_class.sti_dataset.filter(sup_class.sti_key=>subclass.name.to_s) : ds.clone)
206
211
  end
207
212
  rescue
213
+ nil
208
214
  end
209
215
  end
210
216
  end
@@ -318,9 +324,8 @@ module Sequel
318
324
  @dataset.extend(Associations::EagerLoading)
319
325
  @dataset.transform(@transform) if @transform
320
326
  @dataset_methods.each{|meth, block| @dataset.meta_def(meth, &block)} if @dataset_methods
321
- begin
322
- (@db_schema = get_db_schema) unless @@lazy_load_schema
323
- rescue
327
+ unless @@lazy_load_schema
328
+ (@db_schema = get_db_schema) rescue nil
324
329
  end
325
330
  self
326
331
  end
@@ -375,7 +380,7 @@ module Sequel
375
380
  @sti_key = key
376
381
  @sti_dataset = dataset
377
382
  dataset.set_model(key, Hash.new{|h,k| h[k] = (k.constantize rescue m)})
378
- before_create(:set_sti_key){send("#{key}=", model.name)}
383
+ before_create(:set_sti_key){send("#{key}=", model.name.to_s)}
379
384
  end
380
385
 
381
386
  # Returns the columns as a list of frozen strings instead
@@ -422,7 +427,7 @@ module Sequel
422
427
  # Create the column accessors
423
428
  def self.def_column_accessor(*columns) # :nodoc:
424
429
  columns.each do |column|
425
- im = instance_methods
430
+ im = instance_methods.collect{|x| x.to_s}
426
431
  meth = "#{column}="
427
432
  define_method(column){self[column]} unless im.include?(column.to_s)
428
433
  unless im.include?(meth)
@@ -148,11 +148,9 @@ class String
148
148
  # "active_record/errors".camelize #=> "ActiveRecord::Errors"
149
149
  # "active_record/errors".camelize(:lower) #=> "activeRecord::Errors"
150
150
  def camelize(first_letter_in_uppercase = :upper)
151
- if first_letter_in_uppercase == :upper
152
- gsub(/\/(.?)/){|x| "::#{x[-1..-1].upcase unless x == '/'}"}.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
153
- else
154
- "#{first}#{camelize[1..-1]}"
155
- end
151
+ s = gsub(/\/(.?)/){|x| "::#{x[-1..-1].upcase unless x == '/'}"}.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
152
+ s[0...1] = s[0...1].downcase unless first_letter_in_uppercase == :upper
153
+ s
156
154
  end
157
155
  alias_method :camelcase, :camelize
158
156
 
@@ -34,7 +34,7 @@ module Sequel
34
34
  end
35
35
  if m.const_defined?("DatasetMethods")
36
36
  dataset.meta_def(:"#{plugin}_opts") {args.first}
37
- dataset.metaclass.send(:include, m::DatasetMethods)
37
+ dataset.extend(m::DatasetMethods)
38
38
  def_dataset_method(*m::DatasetMethods.instance_methods)
39
39
  end
40
40
  end
@@ -2,7 +2,7 @@ module Sequel
2
2
  class Model
3
3
  # The setter methods (methods ending with =) that are never allowed
4
4
  # to be called automatically via set.
5
- RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_on_assignment= strict_param_setting= raise_on_save_failure="
5
+ RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_empty_string_to_nil= typecast_on_assignment= strict_param_setting= raise_on_save_failure="
6
6
 
7
7
  # The current cached associations. A hash with the keys being the
8
8
  # association name symbols and the values being the associated object
@@ -22,6 +22,10 @@ module Sequel
22
22
  # doesn't exist or access to it is denied.
23
23
  attr_writer :strict_param_setting
24
24
 
25
+ # Whether this model instance should typecast the empty string ('') to
26
+ # nil for columns that are non string or blob.
27
+ attr_writer :typecast_empty_string_to_nil
28
+
25
29
  # Whether this model instance should typecast on attribute assignment
26
30
  attr_writer :typecast_on_assignment
27
31
 
@@ -49,6 +53,7 @@ module Sequel
49
53
  @raise_on_save_failure = model.raise_on_save_failure
50
54
  @strict_param_setting = model.strict_param_setting
51
55
  @typecast_on_assignment = model.typecast_on_assignment
56
+ @typecast_empty_string_to_nil = model.typecast_empty_string_to_nil
52
57
  if from_db
53
58
  @new = false
54
59
  @values = values
@@ -247,7 +252,8 @@ module Sequel
247
252
  end
248
253
 
249
254
  # Updates the instance with the supplied values with support for virtual
250
- # attributes, ignoring any values for which no setter method is available.
255
+ # attributes, raising an exception if a value is used that doesn't have
256
+ # a setter method (or ignoring it if strict_param_setting = false).
251
257
  # Does not save the record.
252
258
  #
253
259
  # If no columns have been set for this model (very unlikely), assume symbol
@@ -479,6 +485,7 @@ module Sequel
479
485
  raise Error, "method #{m} doesn't exist or access is restricted to it"
480
486
  end
481
487
  end
488
+ self
482
489
  end
483
490
 
484
491
  # Returns all methods that can be used for attribute
@@ -503,7 +510,7 @@ module Sequel
503
510
  if only
504
511
  only.map{|x| "#{x}="}
505
512
  else
506
- meths = methods.grep(/=\z/) - RESTRICTED_SETTER_METHODS
513
+ meths = methods.collect{|x| x.to_s}.grep(/=\z/) - RESTRICTED_SETTER_METHODS
507
514
  meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && model.restrict_primary_key?
508
515
  meths -= except.map{|x| "#{x}="} if except
509
516
  meths
@@ -515,6 +522,7 @@ module Sequel
515
522
  # for database specific column types.
516
523
  def typecast_value(column, value)
517
524
  return value unless @typecast_on_assignment && @db_schema && (col_schema = @db_schema[column])
525
+ value = nil if value == '' and @typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type])
518
526
  raise(Error, "nil/NULL is not allowed for the #{column} column") if value.nil? && (col_schema[:allow_null] == false)
519
527
  model.db.typecast_value(col_schema[:type], value)
520
528
  end
@@ -2,27 +2,27 @@ module Sequel
2
2
  class Model
3
3
  # Creates table.
4
4
  def self.create_table
5
- db.create_table_sql_list(table_name, *schema.create_info).each {|s| db << s}
5
+ db.create_table(table_name, @schema)
6
6
  @db_schema = get_db_schema(true) unless @@lazy_load_schema
7
7
  columns
8
8
  end
9
9
 
10
10
  # Drops the table if it exists and then runs create_table.
11
11
  def self.create_table!
12
- drop_table if table_exists?
12
+ drop_table rescue nil
13
13
  create_table
14
14
  end
15
15
 
16
16
  # Drops table.
17
17
  def self.drop_table
18
- db.execute db.drop_table_sql(table_name)
18
+ db.drop_table(table_name)
19
19
  end
20
20
 
21
21
  # Returns table schema created with set_schema for direct descendant of Model.
22
22
  # Does not retreive schema information from the database, see db_schema if you
23
23
  # want that.
24
24
  def self.schema
25
- @schema || ((superclass != Model) && (superclass.schema))
25
+ @schema || (superclass.schema unless superclass == Model)
26
26
  end
27
27
 
28
28
  # Defines a table schema (see Schema::Generator for more information).
@@ -223,7 +223,12 @@ module Sequel
223
223
  :wrong_length => 'is the wrong length'
224
224
  }.merge!(atts.extract_options!)
225
225
 
226
- atts << {:if=>opts[:if], :tag=>opts[:tag]||:length}
226
+ tag = if opts[:tag]
227
+ opts[:tag]
228
+ else
229
+ ([:length] + [:maximum, :minimum, :is, :within].reject{|x| !opts.include?(x)}).join('-').to_sym
230
+ end
231
+ atts << {:if=>opts[:if], :tag=>tag}
227
232
  validates_each(*atts) do |o, a, v|
228
233
  next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
229
234
  if m = opts[:maximum]
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ unless defined?(ADO_DB)
4
+ ADO_DB = Sequel.connect(:adapter => 'ado', :driver => "{Microsoft Access Driver (*.mdb)}; DBQ=c:\\Nwind.mdb")
5
+ end
6
+
7
+ context "An ADO dataset" do
8
+ setup do
9
+ ADO_DB.create_table!(:items) { text :name }
10
+ end
11
+
12
+ specify "should not raise exceptions when working with empty datasets" do
13
+ lambda {
14
+ ADO_DB[:items].all
15
+ }.should_not raise_error
16
+ end
17
+ end
@@ -0,0 +1,96 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ unless defined?(INFORMIX_DB)
4
+ INFORMIX_DB = Sequel.connect('informix://localhost/mydb')
5
+ end
6
+
7
+ if INFORMIX_DB.table_exists?(:test)
8
+ INFORMIX_DB.drop_table :test
9
+ end
10
+ INFORMIX_DB.create_table :test do
11
+ text :name
12
+ integer :value
13
+
14
+ index :value
15
+ end
16
+
17
+ context "A Informix database" do
18
+ specify "should provide disconnect functionality" do
19
+ INFORMIX_DB.execute("select user from dual")
20
+ INFORMIX_DB.pool.size.should == 1
21
+ INFORMIX_DB.disconnect
22
+ INFORMIX_DB.pool.size.should == 0
23
+ end
24
+ end
25
+
26
+ context "A Informix dataset" do
27
+ setup do
28
+ @d = INFORMIX_DB[:test]
29
+ @d.delete # remove all records
30
+ end
31
+
32
+ specify "should return the correct record count" do
33
+ @d.count.should == 0
34
+ @d << {:name => 'abc', :value => 123}
35
+ @d << {:name => 'abc', :value => 456}
36
+ @d << {:name => 'def', :value => 789}
37
+ @d.count.should == 3
38
+ end
39
+
40
+ specify "should return the correct records" do
41
+ @d.to_a.should == []
42
+ @d << {:name => 'abc', :value => 123}
43
+ @d << {:name => 'abc', :value => 456}
44
+ @d << {:name => 'def', :value => 789}
45
+
46
+ @d.order(:value).to_a.should == [
47
+ {:name => 'abc', :value => 123},
48
+ {:name => 'abc', :value => 456},
49
+ {:name => 'def', :value => 789}
50
+ ]
51
+ end
52
+
53
+ specify "should update records correctly" do
54
+ @d << {:name => 'abc', :value => 123}
55
+ @d << {:name => 'abc', :value => 456}
56
+ @d << {:name => 'def', :value => 789}
57
+ @d.filter(:name => 'abc').update(:value => 530)
58
+
59
+ # the third record should stay the same
60
+ # floating-point precision bullshit
61
+ @d[:name => 'def'][:value].should == 789
62
+ @d.filter(:value => 530).count.should == 2
63
+ end
64
+
65
+ specify "should delete records correctly" do
66
+ @d << {:name => 'abc', :value => 123}
67
+ @d << {:name => 'abc', :value => 456}
68
+ @d << {:name => 'def', :value => 789}
69
+ @d.filter(:name => 'abc').delete
70
+
71
+ @d.count.should == 1
72
+ @d.first[:name].should == 'def'
73
+ end
74
+
75
+ specify "should be able to literalize booleans" do
76
+ proc {@d.literal(true)}.should_not raise_error
77
+ proc {@d.literal(false)}.should_not raise_error
78
+ end
79
+
80
+ specify "should support transactions" do
81
+ INFORMIX_DB.transaction do
82
+ @d << {:name => 'abc', :value => 1}
83
+ end
84
+
85
+ @d.count.should == 1
86
+ end
87
+
88
+ specify "should support #first and #last" do
89
+ @d << {:name => 'abc', :value => 123}
90
+ @d << {:name => 'abc', :value => 456}
91
+ @d << {:name => 'def', :value => 789}
92
+
93
+ @d.order(:value).first.should == {:name => 'abc', :value => 123}
94
+ @d.order(:value).last.should == {:name => 'def', :value => 789}
95
+ end
96
+ end
@@ -0,0 +1,764 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
+
3
+ unless defined?(MYSQL_USER)
4
+ MYSQL_USER = 'root'
5
+ end
6
+ unless defined?(MYSQL_DB)
7
+ MYSQL_URL = (ENV['SEQUEL_MY_SPEC_DB']||"mysql://#{MYSQL_USER}@localhost/sandbox") unless defined? MYSQL_URL
8
+ MYSQL_DB = Sequel.connect(MYSQL_URL)
9
+ end
10
+ unless defined?(MYSQL_SOCKET_FILE)
11
+ MYSQL_SOCKET_FILE = '/tmp/mysql.sock'
12
+ end
13
+
14
+ MYSQL_URI = URI.parse(MYSQL_DB.uri)
15
+ MYSQL_DB_NAME = (m = /\/(.*)/.match(MYSQL_URI.path)) && m[1]
16
+
17
+ MYSQL_DB.create_table! :items do
18
+ text :name
19
+ integer :value, :index => true
20
+ end
21
+ MYSQL_DB.create_table! :test2 do
22
+ text :name
23
+ integer :value
24
+ end
25
+ MYSQL_DB.create_table! :booltest do
26
+ tinyint :value
27
+ end
28
+ def MYSQL_DB.sqls
29
+ (@sqls ||= [])
30
+ end
31
+ logger = Object.new
32
+ def logger.method_missing(m, msg)
33
+ MYSQL_DB.sqls << msg
34
+ end
35
+ MYSQL_DB.logger = logger
36
+
37
+ context "A MySQL database" do
38
+ setup do
39
+ @db = MYSQL_DB
40
+ end
41
+ teardown do
42
+ Sequel.convert_tinyint_to_bool = true
43
+ end
44
+
45
+ specify "should provide disconnect functionality" do
46
+ @db.tables
47
+ @db.pool.size.should == 1
48
+ @db.disconnect
49
+ @db.pool.size.should == 0
50
+ end
51
+
52
+ specify "should provide the server version" do
53
+ @db.server_version.should >= 40000
54
+ end
55
+
56
+ specify "should support sequential primary keys" do
57
+ @db.create_table!(:with_pk) {primary_key :id; text :name}
58
+ @db[:with_pk] << {:name => 'abc'}
59
+ @db[:with_pk] << {:name => 'def'}
60
+ @db[:with_pk] << {:name => 'ghi'}
61
+ @db[:with_pk].order(:name).all.should == [
62
+ {:id => 1, :name => 'abc'},
63
+ {:id => 2, :name => 'def'},
64
+ {:id => 3, :name => 'ghi'}
65
+ ]
66
+ end
67
+
68
+ specify "should convert Mysql::Errors to Sequel::Errors" do
69
+ proc{@db << "SELECT 1 + blah;"}.should raise_error(Sequel::Error)
70
+ end
71
+
72
+ specify "should correctly parse the schema" do
73
+ @db.schema(:booltest, :reload=>true).should == [[:value, {:type=>:boolean, :allow_null=>true, :max_chars=>nil, :default=>nil, :db_type=>"tinyint", :numeric_precision=>3}]]
74
+
75
+ Sequel.convert_tinyint_to_bool = false
76
+ @db.schema(:booltest, :reload=>true).should == [[:value, {:type=>:integer, :allow_null=>true, :max_chars=>nil, :default=>nil, :db_type=>"tinyint", :numeric_precision=>3}]]
77
+ end
78
+
79
+ specify "should get the schema all database tables if no table name is used" do
80
+ @db.schema(:booltest, :reload=>true).should == @db.schema(nil, :reload=>true)[:booltest]
81
+ end
82
+ end
83
+
84
+ context "A MySQL dataset" do
85
+ setup do
86
+ @d = MYSQL_DB[:items]
87
+ @d.delete # remove all records
88
+ MYSQL_DB.sqls.clear
89
+ end
90
+
91
+ specify "should return the correct record count" do
92
+ @d.count.should == 0
93
+ @d << {:name => 'abc', :value => 123}
94
+ @d << {:name => 'abc', :value => 456}
95
+ @d << {:name => 'def', :value => 789}
96
+ @d.count.should == 3
97
+ end
98
+
99
+ specify "should return the correct records" do
100
+ @d.to_a.should == []
101
+ @d << {:name => 'abc', :value => 123}
102
+ @d << {:name => 'abc', :value => 456}
103
+ @d << {:name => 'def', :value => 789}
104
+
105
+ @d.order(:value).to_a.should == [
106
+ {:name => 'abc', :value => 123},
107
+ {:name => 'abc', :value => 456},
108
+ {:name => 'def', :value => 789}
109
+ ]
110
+ end
111
+
112
+ specify "should update records correctly" do
113
+ @d << {:name => 'abc', :value => 123}
114
+ @d << {:name => 'abc', :value => 456}
115
+ @d << {:name => 'def', :value => 789}
116
+ @d.filter(:name => 'abc').update(:value => 530)
117
+
118
+ # the third record should stay the same
119
+ # floating-point precision bullshit
120
+ @d[:name => 'def'][:value].should == 789
121
+ @d.filter(:value => 530).count.should == 2
122
+ end
123
+
124
+ specify "should delete records correctly" do
125
+ @d << {:name => 'abc', :value => 123}
126
+ @d << {:name => 'abc', :value => 456}
127
+ @d << {:name => 'def', :value => 789}
128
+ @d.filter(:name => 'abc').delete
129
+
130
+ @d.count.should == 1
131
+ @d.first[:name].should == 'def'
132
+ end
133
+
134
+ specify "should be able to literalize booleans" do
135
+ proc {@d.literal(true)}.should_not raise_error
136
+ proc {@d.literal(false)}.should_not raise_error
137
+ end
138
+
139
+ specify "should quote columns and tables using back-ticks if quoting identifiers" do
140
+ @d.quote_identifiers = true
141
+ @d.select(:name).sql.should == \
142
+ 'SELECT `name` FROM `items`'
143
+
144
+ @d.select('COUNT(*)'.lit).sql.should == \
145
+ 'SELECT COUNT(*) FROM `items`'
146
+
147
+ @d.select(:max[:value]).sql.should == \
148
+ 'SELECT max(`value`) FROM `items`'
149
+
150
+ @d.select(:NOW[]).sql.should == \
151
+ 'SELECT NOW() FROM `items`'
152
+
153
+ @d.select(:max[:items__value]).sql.should == \
154
+ 'SELECT max(`items`.`value`) FROM `items`'
155
+
156
+ @d.order(:name.desc).sql.should == \
157
+ 'SELECT * FROM `items` ORDER BY `name` DESC'
158
+
159
+ @d.select('items.name AS item_name'.lit).sql.should == \
160
+ 'SELECT items.name AS item_name FROM `items`'
161
+
162
+ @d.select('`name`'.lit).sql.should == \
163
+ 'SELECT `name` FROM `items`'
164
+
165
+ @d.select('max(items.`name`) AS `max_name`'.lit).sql.should == \
166
+ 'SELECT max(items.`name`) AS `max_name` FROM `items`'
167
+
168
+ @d.select(:test[:abc, 'hello']).sql.should == \
169
+ "SELECT test(`abc`, 'hello') FROM `items`"
170
+
171
+ @d.select(:test[:abc__def, 'hello']).sql.should == \
172
+ "SELECT test(`abc`.`def`, 'hello') FROM `items`"
173
+
174
+ @d.select(:test[:abc__def, 'hello'].as(:x2)).sql.should == \
175
+ "SELECT test(`abc`.`def`, 'hello') AS `x2` FROM `items`"
176
+
177
+ @d.insert_sql(:value => 333).should == \
178
+ 'INSERT INTO `items` (`value`) VALUES (333)'
179
+
180
+ @d.insert_sql(:x => :y).should == \
181
+ 'INSERT INTO `items` (`x`) VALUES (`y`)'
182
+ end
183
+
184
+ specify "should quote fields correctly when reversing the order" do
185
+ @d.quote_identifiers = true
186
+ @d.reverse_order(:name).sql.should == \
187
+ 'SELECT * FROM `items` ORDER BY `name` DESC'
188
+
189
+ @d.reverse_order(:name.desc).sql.should == \
190
+ 'SELECT * FROM `items` ORDER BY `name` ASC'
191
+
192
+ @d.reverse_order(:name, :test.desc).sql.should == \
193
+ 'SELECT * FROM `items` ORDER BY `name` DESC, `test` ASC'
194
+
195
+ @d.reverse_order(:name.desc, :test).sql.should == \
196
+ 'SELECT * FROM `items` ORDER BY `name` ASC, `test` DESC'
197
+ end
198
+
199
+ specify "should support ORDER clause in UPDATE statements" do
200
+ @d.order(:name).update_sql(:value => 1).should == \
201
+ 'UPDATE items SET value = 1 ORDER BY name'
202
+ end
203
+
204
+ specify "should support LIMIT clause in UPDATE statements" do
205
+ @d.limit(10).update_sql(:value => 1).should == \
206
+ 'UPDATE items SET value = 1 LIMIT 10'
207
+ end
208
+
209
+ specify "should support transactions" do
210
+ MYSQL_DB.transaction do
211
+ @d << {:name => 'abc', :value => 1}
212
+ end
213
+
214
+ @d.count.should == 1
215
+ end
216
+
217
+ specify "should correctly rollback transactions" do
218
+ proc do
219
+ MYSQL_DB.transaction do
220
+ @d << {:name => 'abc'}
221
+ raise Interrupt, 'asdf'
222
+ end
223
+ end.should raise_error(Interrupt)
224
+
225
+ MYSQL_DB.sqls.should == ['BEGIN', "INSERT INTO items (name) VALUES ('abc')", 'ROLLBACK']
226
+ end
227
+
228
+ specify "should handle returning inside of the block by committing" do
229
+ def MYSQL_DB.ret_commit
230
+ transaction do
231
+ self[:items] << {:name => 'abc'}
232
+ return
233
+ self[:items] << {:name => 'd'}
234
+ end
235
+ end
236
+ MYSQL_DB.ret_commit
237
+ MYSQL_DB.sqls.should == ['BEGIN', "INSERT INTO items (name) VALUES ('abc')", 'COMMIT']
238
+ end
239
+
240
+ specify "should support regexps" do
241
+ @d << {:name => 'abc', :value => 1}
242
+ @d << {:name => 'bcd', :value => 2}
243
+ @d.filter(:name => /bc/).count.should == 2
244
+ @d.filter(:name => /^bc/).count.should == 1
245
+ end
246
+
247
+ specify "should correctly literalize strings with comment backslashes in them" do
248
+ @d.delete
249
+ proc {@d << {:name => ':\\'}}.should_not raise_error
250
+
251
+ @d.first[:name].should == ':\\'
252
+ end
253
+ end
254
+
255
+ context "MySQL datasets" do
256
+ setup do
257
+ @d = MYSQL_DB[:orders]
258
+ end
259
+ teardown do
260
+ Sequel.convert_tinyint_to_bool = true
261
+ end
262
+
263
+ specify "should correctly quote column references" do
264
+ @d.quote_identifiers = true
265
+ market = 'ICE'
266
+ ack_stamp = Time.now - 15 * 60 # 15 minutes ago
267
+ @d.query do
268
+ select :market, :minute[:from_unixtime[:ack]].as(:minute)
269
+ where {(:ack > ack_stamp) & {:market => market}}
270
+ group_by :minute[:from_unixtime[:ack]]
271
+ end.sql.should == \
272
+ "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`))"
273
+ end
274
+
275
+ specify "should accept and return tinyints as bools or integers when configured to do so" do
276
+ MYSQL_DB[:booltest].delete
277
+ MYSQL_DB[:booltest] << {:value=>true}
278
+ MYSQL_DB[:booltest].all.should == [{:value=>true}]
279
+ MYSQL_DB[:booltest].delete
280
+ MYSQL_DB[:booltest] << {:value=>false}
281
+ MYSQL_DB[:booltest].all.should == [{:value=>false}]
282
+
283
+ Sequel.convert_tinyint_to_bool = false
284
+ MYSQL_DB[:booltest].delete
285
+ MYSQL_DB[:booltest] << {:value=>true}
286
+ MYSQL_DB[:booltest].all.should == [{:value=>1}]
287
+ MYSQL_DB[:booltest].delete
288
+ MYSQL_DB[:booltest] << {:value=>false}
289
+ MYSQL_DB[:booltest].all.should == [{:value=>0}]
290
+
291
+ MYSQL_DB[:booltest].delete
292
+ MYSQL_DB[:booltest] << {:value=>1}
293
+ MYSQL_DB[:booltest].all.should == [{:value=>1}]
294
+ MYSQL_DB[:booltest].delete
295
+ MYSQL_DB[:booltest] << {:value=>0}
296
+ MYSQL_DB[:booltest].all.should == [{:value=>0}]
297
+ end
298
+ end
299
+
300
+ # # Commented out because it was causing subsequent examples to fail for some reason
301
+ # context "Simple stored procedure test" do
302
+ # setup do
303
+ # # Create a simple stored procedure but drop it first if there
304
+ # MYSQL_DB.execute("DROP PROCEDURE IF EXISTS sp_get_server_id;")
305
+ # MYSQL_DB.execute("CREATE PROCEDURE sp_get_server_id() SQL SECURITY DEFINER SELECT @@SERVER_ID as server_id;")
306
+ # end
307
+ #
308
+ # specify "should return the server-id via a stored procedure call" do
309
+ # @server_id = MYSQL_DB["SELECT @@SERVER_ID as server_id;"].first[:server_id] # grab the server_id via a simple query
310
+ # @server_id_by_sp = MYSQL_DB["CALL sp_get_server_id();"].first[:server_id]
311
+ # @server_id_by_sp.should == @server_id # compare it to output from stored procedure
312
+ # end
313
+ # end
314
+ #
315
+ context "MySQL join expressions" do
316
+ setup do
317
+ @ds = MYSQL_DB[:nodes]
318
+ @ds.db.meta_def(:server_version) {50014}
319
+ end
320
+
321
+ specify "should raise error for :full_outer join requests." do
322
+ lambda{@ds.join_table(:full_outer, :nodes)}.should raise_error(Sequel::Error::InvalidJoinType)
323
+ end
324
+ specify "should support natural left joins" do
325
+ @ds.join_table(:natural_left, :nodes).sql.should == \
326
+ 'SELECT * FROM nodes NATURAL LEFT JOIN nodes'
327
+ end
328
+ specify "should support natural right joins" do
329
+ @ds.join_table(:natural_right, :nodes).sql.should == \
330
+ 'SELECT * FROM nodes NATURAL RIGHT JOIN nodes'
331
+ end
332
+ specify "should support natural left outer joins" do
333
+ @ds.join_table(:natural_left_outer, :nodes).sql.should == \
334
+ 'SELECT * FROM nodes NATURAL LEFT OUTER JOIN nodes'
335
+ end
336
+ specify "should support natural right outer joins" do
337
+ @ds.join_table(:natural_right_outer, :nodes).sql.should == \
338
+ 'SELECT * FROM nodes NATURAL RIGHT OUTER JOIN nodes'
339
+ end
340
+ specify "should support natural inner joins" do
341
+ @ds.join_table(:natural_inner, :nodes).sql.should == \
342
+ 'SELECT * FROM nodes NATURAL LEFT JOIN nodes'
343
+ end
344
+ specify "should support cross joins" do
345
+ @ds.join_table(:cross, :nodes).sql.should == \
346
+ 'SELECT * FROM nodes CROSS JOIN nodes'
347
+ end
348
+ specify "should support cross joins as inner joins if conditions are used" do
349
+ @ds.join_table(:cross, :nodes, :id=>:id).sql.should == \
350
+ 'SELECT * FROM nodes INNER JOIN nodes ON (nodes.id = nodes.id)'
351
+ end
352
+ specify "should support straight joins (force left table to be read before right)" do
353
+ @ds.join_table(:straight, :nodes).sql.should == \
354
+ 'SELECT * FROM nodes STRAIGHT_JOIN nodes'
355
+ end
356
+ specify "should support natural joins on multiple tables." do
357
+ @ds.join_table(:natural_left_outer, [:nodes, :branches]).sql.should == \
358
+ 'SELECT * FROM nodes NATURAL LEFT OUTER JOIN (nodes, branches)'
359
+ end
360
+ specify "should support straight joins on multiple tables." do
361
+ @ds.join_table(:straight, [:nodes,:branches]).sql.should == \
362
+ 'SELECT * FROM nodes STRAIGHT_JOIN (nodes, branches)'
363
+ end
364
+ end
365
+
366
+ context "Joined MySQL dataset" do
367
+ setup do
368
+ @ds = MYSQL_DB[:nodes]
369
+ end
370
+
371
+ specify "should quote fields correctly" do
372
+ @ds.quote_identifiers = true
373
+ @ds.join(:attributes, :node_id => :id).sql.should == \
374
+ "SELECT * FROM `nodes` INNER JOIN `attributes` ON (`attributes`.`node_id` = `nodes`.`id`)"
375
+ end
376
+
377
+ specify "should allow a having clause on ungrouped datasets" do
378
+ proc {@ds.having('blah')}.should_not raise_error
379
+
380
+ @ds.having('blah').sql.should == \
381
+ "SELECT * FROM nodes HAVING (blah)"
382
+ end
383
+
384
+ specify "should put a having clause before an order by clause" do
385
+ @ds.order(:aaa).having(:bbb => :ccc).sql.should == \
386
+ "SELECT * FROM nodes HAVING (bbb = ccc) ORDER BY aaa"
387
+ end
388
+ end
389
+
390
+ context "A MySQL database" do
391
+ setup do
392
+ @db = MYSQL_DB
393
+ end
394
+
395
+ specify "should support add_column operations" do
396
+ @db.add_column :test2, :xyz, :text
397
+
398
+ @db[:test2].columns.should == [:name, :value, :xyz]
399
+ @db[:test2] << {:name => 'mmm', :value => 111, :xyz => '000'}
400
+ @db[:test2].first[:xyz].should == '000'
401
+ end
402
+
403
+ specify "should support drop_column operations" do
404
+ @db[:test2].columns.should == [:name, :value, :xyz]
405
+ @db.drop_column :test2, :xyz
406
+
407
+ @db[:test2].columns.should == [:name, :value]
408
+ end
409
+
410
+ specify "should support rename_column operations" do
411
+ @db[:test2].delete
412
+ @db.add_column :test2, :xyz, :text
413
+ @db[:test2] << {:name => 'mmm', :value => 111, :xyz => 'qqqq'}
414
+
415
+ @db[:test2].columns.should == [:name, :value, :xyz]
416
+ @db.rename_column :test2, :xyz, :zyx, :type => :text
417
+ @db[:test2].columns.should == [:name, :value, :zyx]
418
+ @db[:test2].first[:zyx].should == 'qqqq'
419
+ end
420
+
421
+ specify "should support rename_column operations with types like varchar(255)" do
422
+ @db[:test2].delete
423
+ @db.add_column :test2, :tre, :text
424
+ @db[:test2] << {:name => 'mmm', :value => 111, :tre => 'qqqq'}
425
+
426
+ @db[:test2].columns.should == [:name, :value, :zyx, :tre]
427
+ @db.rename_column :test2, :tre, :ert, :type => :varchar[255]
428
+ @db[:test2].columns.should == [:name, :value, :zyx, :ert]
429
+ @db[:test2].first[:ert].should == 'qqqq'
430
+ end
431
+
432
+ specify "should support set_column_type operations" do
433
+ @db.add_column :test2, :xyz, :float
434
+ @db[:test2].delete
435
+ @db[:test2] << {:name => 'mmm', :value => 111, :xyz => 56.78}
436
+ @db.set_column_type :test2, :xyz, :integer
437
+
438
+ @db[:test2].first[:xyz].should == 57
439
+ end
440
+
441
+ specify "should support add_index" do
442
+ @db.add_index :test2, :value
443
+ end
444
+
445
+ specify "should support drop_index" do
446
+ @db.drop_index :test2, :value
447
+ end
448
+ end
449
+
450
+ context "A MySQL database" do
451
+ setup do
452
+ @db = MYSQL_DB
453
+ end
454
+
455
+ specify "should support defaults for boolean columns" do
456
+ g = Sequel::Schema::Generator.new(@db) do
457
+ boolean :active1, :default => true
458
+ boolean :active2, :default => false
459
+ end
460
+ statements = @db.create_table_sql_list(:items, *g.create_info)
461
+ statements.should == [
462
+ "CREATE TABLE items (active1 boolean DEFAULT 1, active2 boolean DEFAULT 0)"
463
+ ]
464
+ end
465
+
466
+ specify "should correctly format CREATE TABLE statements with foreign keys" do
467
+ g = Sequel::Schema::Generator.new(@db) do
468
+ foreign_key :p_id, :table => :users, :key => :id,
469
+ :null => false, :on_delete => :cascade
470
+ end
471
+ @db.create_table_sql_list(:items, *g.create_info).should == [
472
+ "CREATE TABLE items (p_id integer NOT NULL, FOREIGN KEY (p_id) REFERENCES users(id) ON DELETE CASCADE)"
473
+ ]
474
+ end
475
+
476
+ specify "should accept repeated raw sql statements using Database#<<" do
477
+ @db << 'DELETE FROM items'
478
+ @db[:items].count.should == 0
479
+
480
+ @db << "INSERT INTO items (name, value) VALUES ('tutu', 1234)"
481
+ @db[:items].first.should == {:name => 'tutu', :value => 1234}
482
+
483
+ @db << 'DELETE FROM items'
484
+ @db[:items].first.should == nil
485
+ end
486
+ end
487
+
488
+ # Socket tests should only be run if the MySQL server is on localhost
489
+ if %w'localhost 127.0.0.1 ::1'.include? MYSQL_URI.host
490
+ context "A MySQL database" do
491
+ specify "should accept a socket option" do
492
+ db = Sequel.mysql(MYSQL_DB_NAME, :host => 'localhost', :user => MYSQL_USER, :socket => MYSQL_SOCKET_FILE)
493
+ proc {db.test_connection}.should_not raise_error
494
+ end
495
+
496
+ specify "should accept a socket option without host option" do
497
+ db = Sequel.mysql(MYSQL_DB_NAME, :user => MYSQL_USER, :socket => MYSQL_SOCKET_FILE)
498
+ proc {db.test_connection}.should_not raise_error
499
+ end
500
+
501
+ specify "should fail to connect with invalid socket" do
502
+ db = Sequel.mysql(MYSQL_DB_NAME, :host => 'localhost', :user => MYSQL_USER, :socket => 'blah')
503
+ proc {db.test_connection}.should raise_error
504
+ end
505
+ end
506
+ end
507
+
508
+ context "A grouped MySQL dataset" do
509
+ setup do
510
+ MYSQL_DB[:test2].delete
511
+ MYSQL_DB[:test2] << {:name => '11', :value => 10}
512
+ MYSQL_DB[:test2] << {:name => '11', :value => 20}
513
+ MYSQL_DB[:test2] << {:name => '11', :value => 30}
514
+ MYSQL_DB[:test2] << {:name => '12', :value => 10}
515
+ MYSQL_DB[:test2] << {:name => '12', :value => 20}
516
+ MYSQL_DB[:test2] << {:name => '13', :value => 10}
517
+ end
518
+
519
+ specify "should return the correct count for raw sql query" do
520
+ ds = MYSQL_DB["select name FROM test2 WHERE name = '11' GROUP BY name"]
521
+ ds.count.should == 1
522
+ end
523
+
524
+ specify "should return the correct count for a normal dataset" do
525
+ ds = MYSQL_DB[:test2].select(:name).where(:name => '11').group(:name)
526
+ ds.count.should == 1
527
+ end
528
+ end
529
+
530
+ context "A MySQL database" do
531
+ setup do
532
+ end
533
+
534
+ specify "should support fulltext indexes" do
535
+ g = Sequel::Schema::Generator.new(MYSQL_DB) do
536
+ text :title
537
+ text :body
538
+ full_text_index [:title, :body]
539
+ end
540
+ MYSQL_DB.create_table_sql_list(:posts, *g.create_info).should == [
541
+ "CREATE TABLE posts (title text, body text)",
542
+ "CREATE FULLTEXT INDEX posts_title_body_index ON posts (title, body)"
543
+ ]
544
+ end
545
+
546
+ specify "should support full_text_search" do
547
+ MYSQL_DB[:posts].full_text_search(:title, 'ruby').sql.should ==
548
+ "SELECT * FROM posts WHERE (MATCH (title) AGAINST ('ruby'))"
549
+
550
+ MYSQL_DB[:posts].full_text_search([:title, :body], ['ruby', 'sequel']).sql.should ==
551
+ "SELECT * FROM posts WHERE (MATCH (title, body) AGAINST ('ruby', 'sequel'))"
552
+
553
+ MYSQL_DB[:posts].full_text_search(:title, '+ruby -rails', :boolean => true).sql.should ==
554
+ "SELECT * FROM posts WHERE (MATCH (title) AGAINST ('+ruby -rails' IN BOOLEAN MODE))"
555
+ end
556
+
557
+ specify "should support spatial indexes" do
558
+ g = Sequel::Schema::Generator.new(MYSQL_DB) do
559
+ point :geom
560
+ spatial_index [:geom]
561
+ end
562
+ MYSQL_DB.create_table_sql_list(:posts, *g.create_info).should == [
563
+ "CREATE TABLE posts (geom point)",
564
+ "CREATE SPATIAL INDEX posts_geom_index ON posts (geom)"
565
+ ]
566
+ end
567
+
568
+ specify "should support indexes with index type" do
569
+ g = Sequel::Schema::Generator.new(MYSQL_DB) do
570
+ text :title
571
+ index :title, :type => :hash
572
+ end
573
+ MYSQL_DB.create_table_sql_list(:posts, *g.create_info).should == [
574
+ "CREATE TABLE posts (title text)",
575
+ "CREATE INDEX posts_title_index ON posts (title) USING hash"
576
+ ]
577
+ end
578
+
579
+ specify "should support unique indexes with index type" do
580
+ g = Sequel::Schema::Generator.new(MYSQL_DB) do
581
+ text :title
582
+ index :title, :type => :hash, :unique => true
583
+ end
584
+ MYSQL_DB.create_table_sql_list(:posts, *g.create_info).should == [
585
+ "CREATE TABLE posts (title text)",
586
+ "CREATE UNIQUE INDEX posts_title_index ON posts (title) USING hash"
587
+ ]
588
+ end
589
+ end
590
+
591
+ context "MySQL::Dataset#insert" do
592
+ setup do
593
+ @d = MYSQL_DB[:items]
594
+ @d.delete # remove all records
595
+ MYSQL_DB.sqls.clear
596
+ end
597
+
598
+ specify "should insert record with default values when no arguments given" do
599
+ @d.insert
600
+
601
+ MYSQL_DB.sqls.should == [
602
+ "INSERT INTO items () VALUES ()"
603
+ ]
604
+
605
+ @d.all.should == [
606
+ {:name => nil, :value => nil}
607
+ ]
608
+ end
609
+
610
+ specify "should insert record with default values when empty hash given" do
611
+ @d.insert({})
612
+
613
+ MYSQL_DB.sqls.should == [
614
+ "INSERT INTO items () VALUES ()"
615
+ ]
616
+
617
+ @d.all.should == [
618
+ {:name => nil, :value => nil}
619
+ ]
620
+ end
621
+
622
+ specify "should insert record with default values when empty array given" do
623
+ @d.insert []
624
+
625
+ MYSQL_DB.sqls.should == [
626
+ "INSERT INTO items () VALUES ()"
627
+ ]
628
+
629
+ @d.all.should == [
630
+ {:name => nil, :value => nil}
631
+ ]
632
+ end
633
+ end
634
+
635
+ context "MySQL::Dataset#multi_insert" do
636
+ setup do
637
+ @d = MYSQL_DB[:items]
638
+ @d.delete # remove all records
639
+ MYSQL_DB.sqls.clear
640
+ end
641
+
642
+ specify "should insert multiple records in a single statement" do
643
+ @d.multi_insert([{:name => 'abc'}, {:name => 'def'}])
644
+
645
+ MYSQL_DB.sqls.should == [
646
+ 'BEGIN',
647
+ "INSERT INTO items (name) VALUES ('abc'), ('def')",
648
+ 'COMMIT'
649
+ ]
650
+
651
+ @d.all.should == [
652
+ {:name => 'abc', :value => nil}, {:name => 'def', :value => nil}
653
+ ]
654
+ end
655
+
656
+ specify "should split the list of records into batches if :commit_every option is given" do
657
+ @d.multi_insert([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
658
+ :commit_every => 2)
659
+
660
+ MYSQL_DB.sqls.should == [
661
+ 'BEGIN',
662
+ "INSERT INTO items (value) VALUES (1), (2)",
663
+ 'COMMIT',
664
+ 'BEGIN',
665
+ "INSERT INTO items (value) VALUES (3), (4)",
666
+ 'COMMIT'
667
+ ]
668
+
669
+ @d.all.should == [
670
+ {:name => nil, :value => 1},
671
+ {:name => nil, :value => 2},
672
+ {:name => nil, :value => 3},
673
+ {:name => nil, :value => 4}
674
+ ]
675
+ end
676
+
677
+ specify "should split the list of records into batches if :slice option is given" do
678
+ @d.multi_insert([{:value => 1}, {:value => 2}, {:value => 3}, {:value => 4}],
679
+ :slice => 2)
680
+
681
+ MYSQL_DB.sqls.should == [
682
+ 'BEGIN',
683
+ "INSERT INTO items (value) VALUES (1), (2)",
684
+ 'COMMIT',
685
+ 'BEGIN',
686
+ "INSERT INTO items (value) VALUES (3), (4)",
687
+ 'COMMIT'
688
+ ]
689
+
690
+ @d.all.should == [
691
+ {:name => nil, :value => 1},
692
+ {:name => nil, :value => 2},
693
+ {:name => nil, :value => 3},
694
+ {:name => nil, :value => 4}
695
+ ]
696
+ end
697
+
698
+ specify "should support inserting using columns and values arrays" do
699
+ @d.multi_insert([:name, :value], [['abc', 1], ['def', 2]])
700
+
701
+ MYSQL_DB.sqls.should == [
702
+ 'BEGIN',
703
+ "INSERT INTO items (name, value) VALUES ('abc', 1), ('def', 2)",
704
+ 'COMMIT'
705
+ ]
706
+
707
+ @d.all.should == [
708
+ {:name => 'abc', :value => 1},
709
+ {:name => 'def', :value => 2}
710
+ ]
711
+ end
712
+ end
713
+
714
+ context "MySQL::Dataset#replace" do
715
+ setup do
716
+ MYSQL_DB.drop_table(:items) if MYSQL_DB.table_exists?(:items)
717
+ MYSQL_DB.create_table :items do
718
+ integer :id, :unique => true
719
+ integer :value, :index => true
720
+ end
721
+ @d = MYSQL_DB[:items]
722
+ MYSQL_DB.sqls.clear
723
+ end
724
+
725
+ specify "should create a record if the condition is not met" do
726
+ @d.replace(:id => 111, :value => 333)
727
+ @d.all.should == [{:id => 111, :value => 333}]
728
+ end
729
+
730
+ specify "should update a record if the condition is met" do
731
+ @d << {:id => 111}
732
+ @d.all.should == [{:id => 111, :value => nil}]
733
+ @d.replace(:id => 111, :value => 333)
734
+ @d.all.should == [{:id => 111, :value => 333}]
735
+ end
736
+ end
737
+
738
+ context "MySQL::Dataset#complex_expression_sql" do
739
+ setup do
740
+ @d = MYSQL_DB.dataset
741
+ end
742
+
743
+ specify "should handle pattern matches correctly" do
744
+ @d.literal(:x.like('a')).should == "(x LIKE BINARY 'a')"
745
+ @d.literal(~:x.like('a')).should == "(x NOT LIKE BINARY 'a')"
746
+ @d.literal(:x.ilike('a')).should == "(x LIKE 'a')"
747
+ @d.literal(~:x.ilike('a')).should == "(x NOT LIKE 'a')"
748
+ @d.literal(:x.like(/a/)).should == "(x REGEXP BINARY 'a')"
749
+ @d.literal(~:x.like(/a/)).should == "(x NOT REGEXP BINARY 'a')"
750
+ @d.literal(:x.like(/a/i)).should == "(x REGEXP 'a')"
751
+ @d.literal(~:x.like(/a/i)).should == "(x NOT REGEXP 'a')"
752
+ end
753
+
754
+ specify "should handle string concatenation with CONCAT if more than one record" do
755
+ @d.literal([:x, :y].sql_string_join).should == "CONCAT(x, y)"
756
+ @d.literal([:x, :y].sql_string_join(' ')).should == "CONCAT(x, ' ', y)"
757
+ @d.literal([:x[:y], 1, 'z'.lit].sql_string_join(:y|1)).should == "CONCAT(x(y), y[1], '1', y[1], z)"
758
+ end
759
+
760
+ specify "should handle string concatenation as simple string if just one record" do
761
+ @d.literal([:x].sql_string_join).should == "x"
762
+ @d.literal([:x].sql_string_join(' ')).should == "x"
763
+ end
764
+ end