sequel 4.34.0 → 4.35.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +30 -0
  3. data/Rakefile +14 -17
  4. data/doc/object_model.rdoc +4 -4
  5. data/doc/release_notes/4.35.0.txt +130 -0
  6. data/doc/schema_modification.rdoc +8 -3
  7. data/doc/security.rdoc +3 -3
  8. data/lib/sequel/adapters/ado.rb +2 -2
  9. data/lib/sequel/adapters/ado/access.rb +6 -6
  10. data/lib/sequel/adapters/ado/mssql.rb +2 -2
  11. data/lib/sequel/adapters/amalgalite.rb +6 -6
  12. data/lib/sequel/adapters/cubrid.rb +4 -4
  13. data/lib/sequel/adapters/do.rb +2 -2
  14. data/lib/sequel/adapters/do/mysql.rb +1 -1
  15. data/lib/sequel/adapters/do/postgres.rb +1 -1
  16. data/lib/sequel/adapters/do/sqlite3.rb +1 -1
  17. data/lib/sequel/adapters/ibmdb.rb +6 -6
  18. data/lib/sequel/adapters/jdbc.rb +15 -15
  19. data/lib/sequel/adapters/jdbc/db2.rb +1 -1
  20. data/lib/sequel/adapters/jdbc/derby.rb +3 -3
  21. data/lib/sequel/adapters/jdbc/h2.rb +3 -3
  22. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -2
  23. data/lib/sequel/adapters/jdbc/mssql.rb +1 -1
  24. data/lib/sequel/adapters/jdbc/mysql.rb +1 -1
  25. data/lib/sequel/adapters/jdbc/oracle.rb +1 -1
  26. data/lib/sequel/adapters/jdbc/postgresql.rb +2 -2
  27. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +1 -1
  28. data/lib/sequel/adapters/jdbc/sqlite.rb +1 -1
  29. data/lib/sequel/adapters/jdbc/transactions.rb +10 -10
  30. data/lib/sequel/adapters/mock.rb +1 -1
  31. data/lib/sequel/adapters/mysql.rb +2 -2
  32. data/lib/sequel/adapters/mysql2.rb +2 -2
  33. data/lib/sequel/adapters/odbc.rb +2 -2
  34. data/lib/sequel/adapters/odbc/mssql.rb +2 -2
  35. data/lib/sequel/adapters/oracle.rb +9 -9
  36. data/lib/sequel/adapters/postgres.rb +3 -3
  37. data/lib/sequel/adapters/shared/mssql.rb +36 -8
  38. data/lib/sequel/adapters/shared/oracle.rb +15 -0
  39. data/lib/sequel/adapters/shared/postgres.rb +22 -1
  40. data/lib/sequel/adapters/sqlanywhere.rb +1 -1
  41. data/lib/sequel/adapters/sqlite.rb +7 -7
  42. data/lib/sequel/adapters/swift.rb +3 -3
  43. data/lib/sequel/adapters/swift/mysql.rb +1 -1
  44. data/lib/sequel/adapters/swift/postgres.rb +1 -1
  45. data/lib/sequel/adapters/swift/sqlite.rb +1 -1
  46. data/lib/sequel/adapters/tinytds.rb +5 -7
  47. data/lib/sequel/database/logging.rb +18 -3
  48. data/lib/sequel/database/misc.rb +19 -8
  49. data/lib/sequel/database/schema_generator.rb +7 -2
  50. data/lib/sequel/database/schema_methods.rb +9 -2
  51. data/lib/sequel/database/transactions.rb +52 -18
  52. data/lib/sequel/dataset/actions.rb +24 -19
  53. data/lib/sequel/dataset/features.rb +5 -0
  54. data/lib/sequel/dataset/query.rb +6 -0
  55. data/lib/sequel/extensions/_pretty_table.rb +1 -1
  56. data/lib/sequel/extensions/error_sql.rb +3 -3
  57. data/lib/sequel/extensions/pg_range.rb +10 -1
  58. data/lib/sequel/extensions/schema_dumper.rb +8 -5
  59. data/lib/sequel/extensions/server_logging.rb +61 -0
  60. data/lib/sequel/extensions/sql_comments.rb +91 -0
  61. data/lib/sequel/model/associations.rb +40 -8
  62. data/lib/sequel/model/base.rb +19 -5
  63. data/lib/sequel/plugins/class_table_inheritance.rb +12 -0
  64. data/lib/sequel/plugins/delay_add_association.rb +1 -0
  65. data/lib/sequel/plugins/json_serializer.rb +10 -2
  66. data/lib/sequel/version.rb +1 -1
  67. data/spec/adapter_spec.rb +4 -0
  68. data/spec/adapters/mysql_spec.rb +1 -1
  69. data/spec/adapters/postgres_spec.rb +3 -2
  70. data/spec/core/connection_pool_spec.rb +2 -0
  71. data/spec/core/database_spec.rb +49 -0
  72. data/spec/core/dataset_spec.rb +25 -1
  73. data/spec/core/mock_adapter_spec.rb +3 -1
  74. data/spec/core/schema_generator_spec.rb +1 -1
  75. data/spec/core_model_spec.rb +2 -0
  76. data/spec/core_spec.rb +1 -0
  77. data/spec/extensions/delay_add_association_spec.rb +22 -0
  78. data/spec/extensions/json_serializer_spec.rb +6 -0
  79. data/spec/extensions/pg_range_spec.rb +30 -2
  80. data/spec/extensions/schema_dumper_spec.rb +3 -2
  81. data/spec/extensions/server_logging_spec.rb +45 -0
  82. data/spec/extensions/sql_comments_spec.rb +27 -0
  83. data/spec/files/reversible_migrations/006_reversible.rb +10 -0
  84. data/spec/files/reversible_migrations/007_reversible.rb +10 -0
  85. data/spec/integration/dataset_test.rb +28 -2
  86. data/spec/integration/migrator_test.rb +23 -1
  87. data/spec/integration/schema_test.rb +12 -32
  88. data/spec/integration/transaction_test.rb +10 -0
  89. data/spec/integration/type_test.rb +1 -1
  90. data/spec/model/eager_loading_spec.rb +16 -0
  91. data/spec/model/record_spec.rb +9 -0
  92. data/spec/model_no_assoc_spec.rb +1 -0
  93. data/spec/model_spec.rb +1 -0
  94. data/spec/plugin_spec.rb +1 -0
  95. metadata +16 -2
@@ -0,0 +1,61 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The server_logging extension makes the logger include the server/shard
4
+ # the query was issued on. This makes it easier to use the logs when
5
+ # using sharding.
6
+ #
7
+ # Example:
8
+ #
9
+ # DB.opts[:server]
10
+ # # {:read_only=>{}, :b=>{}}
11
+ # DB.extension :server_logging
12
+ # DB[:a].all
13
+ # # (0.000005s) (conn: 1014942550, server: read_only) SELECT * FROM a
14
+ # DB[:a].server(:b).all
15
+ # # (0.000004s) (conn: 997304100, server: b) SELECT * FROM a
16
+ # DB[:a].insert
17
+ # # (0.000004s) (conn: 1014374750, server: default) INSERT INTO a DEFAULT VALUES
18
+ #
19
+ # In order for the server/shard to be correct for all connections, you need to
20
+ # use this before connections to the database are made, or you need to call
21
+ # <tt>Database#disconnect</tt> after loading this extension.
22
+ #
23
+ # Related module: Sequel::ServerLogging
24
+
25
+ #
26
+ module Sequel
27
+ module ServerLogging
28
+ # Initialize the hash mapping connections to shards, and turn on logging
29
+ # of connection info unless it has specifically been turned off.
30
+ def self.extended(db)
31
+ db.instance_exec do
32
+ @server_connection_map ||= {}
33
+ self.log_connection_info = true if log_connection_info.nil?
34
+ end
35
+ end
36
+
37
+ # When setting up a new connection, associate the connection with the
38
+ # shard.
39
+ def connect(server)
40
+ conn = super
41
+ Sequel.synchronize{@server_connection_map[conn] = server}
42
+ conn
43
+ end
44
+
45
+ # When disconnecting a connection, remove the related connection from the mapping.
46
+ def disconnect_connection(conn)
47
+ super
48
+ ensure
49
+ Sequel.synchronize{@server_connection_map.delete(conn)}
50
+ end
51
+
52
+ private
53
+
54
+ # Include the server with the connection's id.
55
+ def connection_info(conn)
56
+ "(conn: #{conn.__id__}, server: #{Sequel.synchronize{@server_connection_map[conn]}}) "
57
+ end
58
+ end
59
+
60
+ Database.register_extension(:server_logging, ServerLogging)
61
+ end
@@ -0,0 +1,91 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The sql_comments extension adds Dataset#comment to the datasets,
4
+ # allowing you to set SQL comments in the resulting query. These
5
+ # comments are appended to the end of the SQL query:
6
+ #
7
+ # ds = DB[:table].comment("Some Comment").all
8
+ # # SELECT * FROM table -- Some Comment
9
+ # #
10
+ #
11
+ # As you can see, this uses single line SQL comments (--) suffixed
12
+ # by a newline. This # plugin transforms all consecutive
13
+ # whitespace in the comment to # a single string:
14
+ #
15
+ # ds = DB[:table].comment("Some\r\nComment Here").all
16
+ # # SELECT * FROM table -- Some Comment Here
17
+ # #
18
+ #
19
+ # The reason for the prefixing and suffixing by newlines is to
20
+ # work correctly when used in subqueries:
21
+ #
22
+ # ds = DB[:table].comment("Some\r\nComment Here")
23
+ # ds.where(:id=>ds).all
24
+ # # SELECT * FROM table WHERE (id IN (SELECT * FROM table -- Some Comment Here
25
+ # # )) -- Some Comment Here
26
+ # #
27
+ #
28
+ # In addition to working on SELECT queries, it also works when
29
+ # inserting, updating, and deleting.
30
+ #
31
+ # Due to the use of single line SQL comments and converting all
32
+ # whitespace to spaces, this should correctly handle even
33
+ # malicious input. However, it would be unwise to rely on that,
34
+ # you should probably attempt to ensure that the argument given
35
+ # to Dataset#comment is not derived from user input.
36
+ #
37
+ # You can load this extension into specific datasets:
38
+ #
39
+ # ds = DB[:table]
40
+ # ds = ds.extension(:sql_comments)
41
+ #
42
+ # Or you can load it into all of a database's datasets, which
43
+ # is probably the desired behavior if you are using this extension:
44
+ #
45
+ # DB.extension(:sql_comments)
46
+ #
47
+ # Note that Microsoft Access does not support inline comments,
48
+ # and attempting to use comments on it will result in SQL syntax
49
+ # errors.
50
+ #
51
+ # Related module: Sequel::SQLComments
52
+
53
+ #
54
+ module Sequel
55
+ module SQLComments
56
+ # Return a modified copy of the dataset that will use the given comment.
57
+ # To uncomment a commented dataset, pass nil as the argument.
58
+ def comment(comment)
59
+ clone(:comment=>(format_sql_comment(comment) if comment))
60
+ end
61
+
62
+ %w'select insert update delete'.each do |type|
63
+ define_method(:"#{type}_sql") do |*a|
64
+ sql = super(*a)
65
+ if comment = @opts[:comment]
66
+ # This assumes that the comment stored in the dataset has
67
+ # already been formatted. If not, this could result in SQL
68
+ # injection.
69
+ #
70
+ # Additionally, due to the use of an SQL comment, if any
71
+ # SQL is appened to the query after the comment is added,
72
+ # it will become part of the comment unless it is preceded
73
+ # by a newline.
74
+ sql << comment
75
+ end
76
+ sql
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Format the comment. For maximum compatibility, this uses a
83
+ # single line SQL comment, and converts all consecutive whitespace
84
+ # in the comment to a single space.
85
+ def format_sql_comment(comment)
86
+ " -- #{comment.to_s.gsub(/\s+/, ' ')}\n"
87
+ end
88
+ end
89
+
90
+ Dataset.register_extension(:sql_comments, SQLComments)
91
+ end
@@ -2194,14 +2194,7 @@ module Sequel
2194
2194
 
2195
2195
  # Add the given associated object to the given association
2196
2196
  def add_associated_object(opts, o, *args)
2197
- klass = opts.associated_class
2198
- if o.is_a?(Hash)
2199
- o = klass.new(o)
2200
- elsif o.is_a?(Integer) || o.is_a?(String) || o.is_a?(Array)
2201
- o = klass.with_pk!(o)
2202
- elsif !o.is_a?(klass)
2203
- raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}")
2204
- end
2197
+ o = make_add_associated_object(opts, o)
2205
2198
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
2206
2199
  ensure_associated_primary_key(opts, o, *args)
2207
2200
  return if run_association_callbacks(opts, :before_add, o) == false
@@ -2317,6 +2310,25 @@ module Sequel
2317
2310
  opts.send(:cached_fetch, :many_to_one_pk_lookup){opts.primary_key == opts.associated_class.primary_key}
2318
2311
  end
2319
2312
 
2313
+ # Convert the input of the add_* association method into an associated object. For
2314
+ # hashes, this creates a new object using the hash. For integers, strings, and arrays,
2315
+ # assume the value specifies a primary key, and lookup an existing object with that primary key.
2316
+ # Otherwise, if the object is not already an instance of the class, raise an exception.
2317
+ def make_add_associated_object(opts, o)
2318
+ klass = opts.associated_class
2319
+
2320
+ case o
2321
+ when Hash
2322
+ klass.new(o)
2323
+ when Integer, String, Array
2324
+ klass.with_pk!(o)
2325
+ when klass
2326
+ o
2327
+ else
2328
+ raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}")
2329
+ end
2330
+ end
2331
+
2320
2332
  # Remove all associated objects from the given association
2321
2333
  def remove_all_associated_objects(opts, *args)
2322
2334
  raise(Sequel::Error, "model object #{inspect} does not have a primary key") if opts.dataset_need_primary_key? && !pk
@@ -2650,6 +2662,26 @@ END
2650
2662
  end
2651
2663
  end
2652
2664
 
2665
+ # If the dataset is being eagerly loaded, default to calling all
2666
+ # instead of each.
2667
+ def to_hash(key_column=nil, value_column=nil, opts=OPTS)
2668
+ if (@opts[:eager_graph] || @opts[:eager]) && !opts.has_key?(:all)
2669
+ opts = Hash[opts]
2670
+ opts[:all] = true
2671
+ end
2672
+ super
2673
+ end
2674
+
2675
+ # If the dataset is being eagerly loaded, default to calling all
2676
+ # instead of each.
2677
+ def to_hash_groups(key_column, value_column=nil, opts=OPTS)
2678
+ if (@opts[:eager_graph] || @opts[:eager]) && !opts.has_key?(:all)
2679
+ opts = Hash[opts]
2680
+ opts[:all] = true
2681
+ end
2682
+ super
2683
+ end
2684
+
2653
2685
  # Do not attempt to split the result set into associations,
2654
2686
  # just return results as simple objects. This is useful if you
2655
2687
  # want to use eager_graph as a shortcut to have all of the joins
@@ -1402,17 +1402,31 @@ module Sequel
1402
1402
  @values.keys
1403
1403
  end
1404
1404
 
1405
- # Refresh this record using +for_update+ unless this is a new record. Returns self.
1406
- # This can be used to make sure no other process is updating the record at the
1407
- # same time.
1405
+ # Refresh this record using +for_update+ (by default, or the specified style when given)
1406
+ # unless this is a new record. Returns self. This can be used to make sure no other
1407
+ # process is updating the record at the same time.
1408
+ #
1409
+ # If style is a string, it will be used directly. You should never pass a string
1410
+ # to this method that is derived from user input, as that can lead to
1411
+ # SQL injection.
1412
+ #
1413
+ # A symbol may be used for database independent locking behavior, but
1414
+ # all supported symbols have separate methods (e.g. for_update).
1415
+ #
1408
1416
  #
1409
1417
  # a = Artist[1]
1410
1418
  # Artist.db.transaction do
1411
1419
  # a.lock!
1412
1420
  # a.update(:name=>'A')
1413
1421
  # end
1414
- def lock!
1415
- _refresh(this.for_update) unless new?
1422
+ #
1423
+ # a = Artist[2]
1424
+ # Artist.db.transaction do
1425
+ # a.lock!('FOR NO KEY UPDATE')
1426
+ # a.update(:name=>'B')
1427
+ # end
1428
+ def lock!(style=:update)
1429
+ _refresh(this.lock_style(style)) unless new?
1416
1430
  self
1417
1431
  end
1418
1432
 
@@ -84,6 +84,18 @@ module Sequel
84
84
  # a.values # {:id=>1, name=>'S', :kind=>'CEO'}
85
85
  # a.refresh.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
86
86
  #
87
+ # You can also load directly from a subclass:
88
+ #
89
+ # a = Executive.first
90
+ # a.values # {:id=>1, name=>'S', :kind=>'Executive', :num_staff=>4, :num_managers=>2}
91
+ #
92
+ # Note that when loading from a subclass, because the subclass dataset uses a JOIN,
93
+ # if you are referencing the primary key column, you need to disambiguate the reference
94
+ # by explicitly qualifying it:
95
+ #
96
+ # a = Executive.where(:id=>1).first # database error
97
+ # a = Executive.where(:executives__id=>1).first # no error
98
+ #
87
99
  # = Usage
88
100
  #
89
101
  # # Use the default of storing the class name in the sti_key
@@ -38,6 +38,7 @@ module Sequel
38
38
  # current object.
39
39
  def add_associated_object(opts, o, *args)
40
40
  if opts.dataset_need_primary_key? && new?
41
+ o = make_add_associated_object(opts, o)
41
42
  delay_validate_associated_object(opts, o)
42
43
  send(opts[:name]) << o
43
44
  after_create_hook{super(opts, o, *args)}
@@ -300,8 +300,16 @@ module Sequel
300
300
  if inc.is_a?(Hash)
301
301
  inc.each do |k, v|
302
302
  v = v.empty? ? [] : [v]
303
- h[k.to_s] = case objs = send(k)
304
- when Array
303
+
304
+ objs = send(k)
305
+
306
+ is_array = if r = model.association_reflection(k)
307
+ r.returns_array?
308
+ else
309
+ objs.is_a?(Array)
310
+ end
311
+
312
+ h[k.to_s] = if is_array
305
313
  objs.map{|obj| Literal.new(Sequel.object_to_json(obj, *v))}
306
314
  else
307
315
  Literal.new(Sequel.object_to_json(objs, *v))
@@ -5,7 +5,7 @@ module Sequel
5
5
  MAJOR = 4
6
6
  # The minor version of Sequel. Bumped for every non-patch level
7
7
  # release, generally around once a month.
8
- MINOR = 34
8
+ MINOR = 35
9
9
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
10
10
  # releases that fix regressions from previous versions.
11
11
  TINY = 0
@@ -0,0 +1,4 @@
1
+ if !ARGV.empty? && ARGV.first != 'none'
2
+ require "./spec/adapters/#{ARGV.first}_spec.rb"
3
+ end
4
+ Dir['./spec/integration/*_test.rb'].each{|f| require f}
@@ -528,7 +528,7 @@ describe "A MySQL database" do
528
528
 
529
529
  it "should have set_column_type support keep existing options" do
530
530
  @db.create_table(:items){Integer :id, :null=>false, :default=>5}
531
- @db.alter_table(:items){set_column_type :id, Bignum}
531
+ @db.alter_table(:items){set_column_type :id, :Bignum}
532
532
  check_sqls do
533
533
  @db.sqls.must_equal ["CREATE TABLE `items` (`id` integer NOT NULL DEFAULT 5)", "DESCRIBE `items`", "ALTER TABLE `items` CHANGE COLUMN `id` `id` bigint NOT NULL DEFAULT 5"]
534
534
  end
@@ -945,7 +945,7 @@ describe "A PostgreSQL database" do
945
945
  @db.create_table!(:posts){primary_key :a, :type=>Fixnum}
946
946
  @db[:posts].insert.must_equal 1
947
947
  @db[:posts].insert.must_equal 2
948
- @db.create_table!(:posts){primary_key :a, :type=>Bignum}
948
+ @db.create_table!(:posts){primary_key :a, :type=>:Bignum}
949
949
  @db[:posts].insert.must_equal 1
950
950
  @db[:posts].insert.must_equal 2
951
951
  end
@@ -973,6 +973,7 @@ describe "A PostgreSQL database" do
973
973
  @db[:posts].insert(:title=>'ruby scooby', :body=>'x')
974
974
 
975
975
  @db[:posts].full_text_search(:title, 'rails').all.must_equal [{:title=>'ruby rails', :body=>'yowsa'}]
976
+ @db[:posts].full_text_search(:title, 'rails', :headline=>true).all.must_equal [{:title=>'ruby rails', :body=>'yowsa', :headline=>'ruby <b>rails</b>'}]
976
977
  @db[:posts].full_text_search([:title, :body], ['yowsa', 'rails']).all.must_equal [:title=>'ruby rails', :body=>'yowsa']
977
978
  @db[:posts].full_text_search(:title, 'scooby', :language => 'french').all.must_equal [{:title=>'ruby scooby', :body=>'x'}]
978
979
 
@@ -997,7 +998,7 @@ describe "A PostgreSQL database" do
997
998
  @db[:posts].insert(:title=>t1)
998
999
  @db[:posts].insert(:title=>t2)
999
1000
  @db[:posts].full_text_search(:title, 'ruby & sequel', :rank=>true).select_map(:title).must_equal [t2, t1]
1000
- end
1001
+ end if DB.server_version >= 80300
1001
1002
 
1002
1003
  it "should support spatial indexes" do
1003
1004
  @db.create_table(:posts){box :geom; spatial_index [:geom]}
@@ -8,6 +8,8 @@ mock_db = lambda do |*a, &b|
8
8
  if b2 = a.shift
9
9
  (class << db; self end).send(:define_method, :disconnect_connection){|c| b2.arity == 1 ? b2.call(c) : b2.call}
10
10
  end
11
+ # Work around JRuby Issue #3854
12
+ (class << db; self end).send(:public, :connect, :disconnect_connection)
11
13
  db
12
14
  end
13
15
 
@@ -341,6 +341,35 @@ describe "Database#log_yield" do
341
341
  end
342
342
  end
343
343
 
344
+ describe "Database#log_connection_yield" do
345
+ before do
346
+ @o = Object.new
347
+ def @o.logs; @logs || []; end
348
+ def @o.to_ary; [self]; end
349
+ def @o.method_missing(*args); (@logs ||= []) << args; end
350
+ @conn = Object.new
351
+ @db = Sequel::Database.new(:logger=>@o)
352
+ end
353
+
354
+ it "should log SQL to the loggers" do
355
+ @db.log_connection_yield("some SQL", @conn){}
356
+ @o.logs.length.must_equal 1
357
+ @o.logs.first.length.must_equal 2
358
+ @o.logs.first.first.must_equal :info
359
+ @o.logs.first.last.must_match(/some SQL\z/)
360
+ @o.logs.first.last.wont_match(/\(conn: -?\d+\) some SQL\z/)
361
+ end
362
+
363
+ it "should include connection information when logging" do
364
+ @db.log_connection_info = true
365
+ @db.log_connection_yield("some SQL", @conn){}
366
+ @o.logs.length.must_equal 1
367
+ @o.logs.first.length.must_equal 2
368
+ @o.logs.first.first.must_equal :info
369
+ @o.logs.first.last.must_match(/\(conn: -?\d+\) some SQL\z/)
370
+ end
371
+ end
372
+
344
373
  describe "Database#uri" do
345
374
  before do
346
375
  @c = Class.new(Sequel::Database) do
@@ -993,6 +1022,15 @@ describe "Database#transaction with savepoint support" do
993
1022
  a.must_equal [1, 1]
994
1023
  end
995
1024
 
1025
+ it "should automatically use a savepoint if :rollback=>:always given inside a transaction" do
1026
+ @db.transaction do
1027
+ @db.transaction(:rollback=>:always) do
1028
+ @db.get(1)
1029
+ end
1030
+ end
1031
+ @db.sqls.must_equal ["BEGIN", "SAVEPOINT autopoint_1", "SELECT 1 AS v LIMIT 1", "ROLLBACK TO SAVEPOINT autopoint_1", "COMMIT"]
1032
+ end
1033
+
996
1034
  it "should support :retry_on option for automatically retrying transactions inside an :auto_savepoint transaction" do
997
1035
  a = []
998
1036
  @db.transaction(:auto_savepoint=>true) do
@@ -1058,6 +1096,17 @@ describe "Database#transaction without savepoint support" do
1058
1096
  @db.sqls.must_equal ['BEGIN', 'COMMIT']
1059
1097
  end
1060
1098
 
1099
+ it "should automatically use a savepoint if :rollback=>:always given inside a transaction" do
1100
+ proc do
1101
+ @db.transaction do
1102
+ @db.transaction(:rollback=>:always) do
1103
+ @db.get(1)
1104
+ end
1105
+ end
1106
+ end.must_raise Sequel::Error
1107
+ @db.sqls.must_equal ["BEGIN", "ROLLBACK"]
1108
+ end
1109
+
1061
1110
  include DatabaseTransactionSpecs
1062
1111
  end
1063
1112
 
@@ -1822,10 +1822,17 @@ describe "Dataset#to_hash_groups" do
1822
1822
  @d.to_hash_groups([:a, :b]).must_equal([1, 2] => [{:a => 1, :b => 2}], [3, 4] => [{:a => 3, :b => 4}], [1, 6] => [{:a => 1, :b => 6}], [7, 4] => [{:a => 7, :b => 4}])
1823
1823
  end
1824
1824
 
1825
- it "should accept an optional :hash parameter into which entries can be merged" do
1825
+ it "should accept a :hash option into which entries can be merged" do
1826
1826
  @d.to_hash_groups(:a, :b, :hash => (tmp = {})).must_be_same_as(tmp)
1827
1827
  end
1828
1828
 
1829
+ it "should accept an :all option to use all into which entries can be merged" do
1830
+ called = false
1831
+ meta_def(@d, :post_load){|_| called = true}
1832
+ @d.to_hash_groups(:a, :b, :all=>true)
1833
+ called.must_equal true
1834
+ end
1835
+
1829
1836
  it "should not call the row_proc if two arguments are given" do
1830
1837
  @d.row_proc = proc{|r| h = {}; r.keys.each{|k| h[k] = r[k] * 2}; h}
1831
1838
  @d.to_hash_groups(:a, :b).must_equal(1 => [2, 6], 3 => [4], 7 => [4])
@@ -4534,6 +4541,23 @@ describe "Dataset#lock_style and for_update" do
4534
4541
  end
4535
4542
  end
4536
4543
 
4544
+ describe "Dataset#skip_locked" do
4545
+ before do
4546
+ @ds = Sequel.mock.dataset.from(:t).for_update
4547
+ end
4548
+
4549
+ it "should raise an error if not supported" do
4550
+ proc{@ds.skip_locked}.must_raise Sequel::Error
4551
+ end
4552
+
4553
+ it "should skipped locked rows if supported" do
4554
+ def @ds.supports_skip_locked?; true end
4555
+ def @ds.select_lock_sql(sql) super; sql << " SKIP LOCKED" if @opts[:skip_locked] end
4556
+ @ds.sql.must_equal "SELECT * FROM t FOR UPDATE"
4557
+ @ds.skip_locked.sql.must_equal "SELECT * FROM t FOR UPDATE SKIP LOCKED"
4558
+ end
4559
+ end
4560
+
4537
4561
  describe "Custom ASTTransformer" do
4538
4562
  it "should transform given objects" do
4539
4563
  c = Class.new(Sequel::ASTTransformer) do