sequel 2.2.0 → 2.3.0

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