sequel 4.9.0 → 4.10.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +79 -1
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/Rakefile +2 -12
  6. data/bin/sequel +1 -0
  7. data/doc/advanced_associations.rdoc +82 -25
  8. data/doc/association_basics.rdoc +21 -22
  9. data/doc/core_extensions.rdoc +1 -1
  10. data/doc/opening_databases.rdoc +7 -0
  11. data/doc/release_notes/4.10.0.txt +226 -0
  12. data/doc/security.rdoc +1 -0
  13. data/doc/testing.rdoc +7 -7
  14. data/doc/transactions.rdoc +8 -0
  15. data/lib/sequel/adapters/jdbc.rb +160 -168
  16. data/lib/sequel/adapters/jdbc/db2.rb +17 -18
  17. data/lib/sequel/adapters/jdbc/derby.rb +5 -28
  18. data/lib/sequel/adapters/jdbc/h2.rb +11 -22
  19. data/lib/sequel/adapters/jdbc/hsqldb.rb +31 -18
  20. data/lib/sequel/adapters/jdbc/jtds.rb +0 -15
  21. data/lib/sequel/adapters/jdbc/oracle.rb +36 -35
  22. data/lib/sequel/adapters/jdbc/postgresql.rb +72 -90
  23. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +18 -16
  24. data/lib/sequel/adapters/jdbc/sqlite.rb +7 -0
  25. data/lib/sequel/adapters/jdbc/sqlserver.rb +10 -30
  26. data/lib/sequel/adapters/jdbc/transactions.rb +5 -6
  27. data/lib/sequel/adapters/openbase.rb +1 -7
  28. data/lib/sequel/adapters/postgres.rb +1 -1
  29. data/lib/sequel/adapters/shared/access.rb +3 -6
  30. data/lib/sequel/adapters/shared/cubrid.rb +24 -9
  31. data/lib/sequel/adapters/shared/db2.rb +13 -5
  32. data/lib/sequel/adapters/shared/firebird.rb +16 -16
  33. data/lib/sequel/adapters/shared/informix.rb +2 -5
  34. data/lib/sequel/adapters/shared/mssql.rb +72 -63
  35. data/lib/sequel/adapters/shared/mysql.rb +72 -40
  36. data/lib/sequel/adapters/shared/oracle.rb +27 -15
  37. data/lib/sequel/adapters/shared/postgres.rb +24 -44
  38. data/lib/sequel/adapters/shared/progress.rb +1 -5
  39. data/lib/sequel/adapters/shared/sqlanywhere.rb +26 -18
  40. data/lib/sequel/adapters/shared/sqlite.rb +21 -6
  41. data/lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb +8 -1
  42. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +8 -2
  43. data/lib/sequel/adapters/utils/split_alter_table.rb +8 -0
  44. data/lib/sequel/core.rb +14 -9
  45. data/lib/sequel/database/dataset_defaults.rb +1 -0
  46. data/lib/sequel/database/misc.rb +12 -0
  47. data/lib/sequel/database/query.rb +4 -1
  48. data/lib/sequel/database/schema_methods.rb +3 -2
  49. data/lib/sequel/database/transactions.rb +47 -17
  50. data/lib/sequel/dataset/features.rb +12 -2
  51. data/lib/sequel/dataset/mutation.rb +2 -0
  52. data/lib/sequel/dataset/placeholder_literalizer.rb +12 -4
  53. data/lib/sequel/dataset/prepared_statements.rb +6 -0
  54. data/lib/sequel/dataset/query.rb +1 -1
  55. data/lib/sequel/dataset/sql.rb +132 -70
  56. data/lib/sequel/extensions/columns_introspection.rb +1 -1
  57. data/lib/sequel/extensions/null_dataset.rb +8 -4
  58. data/lib/sequel/extensions/pg_array.rb +4 -4
  59. data/lib/sequel/extensions/pg_row.rb +1 -0
  60. data/lib/sequel/model/associations.rb +468 -188
  61. data/lib/sequel/model/base.rb +88 -13
  62. data/lib/sequel/plugins/association_pks.rb +23 -64
  63. data/lib/sequel/plugins/auto_validations.rb +3 -2
  64. data/lib/sequel/plugins/dataset_associations.rb +1 -3
  65. data/lib/sequel/plugins/many_through_many.rb +18 -65
  66. data/lib/sequel/plugins/pg_array_associations.rb +97 -86
  67. data/lib/sequel/plugins/prepared_statements.rb +2 -1
  68. data/lib/sequel/plugins/prepared_statements_associations.rb +36 -27
  69. data/lib/sequel/plugins/rcte_tree.rb +12 -16
  70. data/lib/sequel/plugins/sharding.rb +21 -3
  71. data/lib/sequel/plugins/single_table_inheritance.rb +2 -1
  72. data/lib/sequel/plugins/subclasses.rb +1 -9
  73. data/lib/sequel/plugins/tactical_eager_loading.rb +9 -0
  74. data/lib/sequel/plugins/tree.rb +2 -2
  75. data/lib/sequel/plugins/validation_class_methods.rb +1 -1
  76. data/lib/sequel/version.rb +1 -1
  77. data/spec/adapters/mssql_spec.rb +57 -15
  78. data/spec/adapters/mysql_spec.rb +11 -0
  79. data/spec/bin_spec.rb +2 -2
  80. data/spec/core/database_spec.rb +38 -4
  81. data/spec/core/dataset_spec.rb +45 -7
  82. data/spec/core/placeholder_literalizer_spec.rb +17 -0
  83. data/spec/core/schema_spec.rb +6 -1
  84. data/spec/extensions/active_model_spec.rb +18 -9
  85. data/spec/extensions/association_pks_spec.rb +20 -18
  86. data/spec/extensions/association_proxies_spec.rb +9 -9
  87. data/spec/extensions/auto_validations_spec.rb +6 -0
  88. data/spec/extensions/columns_introspection_spec.rb +1 -0
  89. data/spec/extensions/constraint_validations_spec.rb +3 -1
  90. data/spec/extensions/many_through_many_spec.rb +191 -111
  91. data/spec/extensions/pg_array_associations_spec.rb +133 -103
  92. data/spec/extensions/prepared_statements_associations_spec.rb +23 -4
  93. data/spec/extensions/rcte_tree_spec.rb +35 -27
  94. data/spec/extensions/sequel_3_dataset_methods_spec.rb +0 -1
  95. data/spec/extensions/sharding_spec.rb +2 -2
  96. data/spec/extensions/tactical_eager_loading_spec.rb +4 -0
  97. data/spec/extensions/to_dot_spec.rb +1 -0
  98. data/spec/extensions/touch_spec.rb +2 -2
  99. data/spec/integration/associations_test.rb +130 -37
  100. data/spec/integration/dataset_test.rb +17 -0
  101. data/spec/integration/model_test.rb +17 -0
  102. data/spec/integration/schema_test.rb +14 -0
  103. data/spec/integration/transaction_test.rb +25 -1
  104. data/spec/model/association_reflection_spec.rb +63 -24
  105. data/spec/model/associations_spec.rb +104 -57
  106. data/spec/model/base_spec.rb +14 -1
  107. data/spec/model/class_dataset_methods_spec.rb +1 -0
  108. data/spec/model/eager_loading_spec.rb +221 -74
  109. data/spec/model/model_spec.rb +119 -1
  110. metadata +4 -2
@@ -10,7 +10,7 @@ module Sequel
10
10
  end
11
11
 
12
12
  module DatasetMethods
13
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select limit distinct columns from join where group order having compounds')
13
+ Dataset.def_sql_method(self, :select, %w'select limit distinct columns from join where group order having compounds')
14
14
 
15
15
  # Progress requires SQL standard datetimes
16
16
  def requires_sql_standard_datetimes?
@@ -24,10 +24,6 @@ module Sequel
24
24
 
25
25
  private
26
26
 
27
- def select_clause_methods
28
- SELECT_CLAUSE_METHODS
29
- end
30
-
31
27
  # Progress uses TOP for limit, but it is only supported in Progress 10.
32
28
  # The Progress adapter targets Progress 9, so it silently ignores the option.
33
29
  def select_limit_sql(sql)
@@ -240,8 +240,6 @@ module Sequel
240
240
  module DatasetMethods
241
241
  BOOL_TRUE = '1'.freeze
242
242
  BOOL_FALSE = '0'.freeze
243
- INSERT_CLAUSE_METHODS = Dataset.clause_methods(:insert, %w'with insert into columns values')
244
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'with select distinct limit columns into from join where group having order compounds lock')
245
243
  WILDCARD = LiteralString.new('%').freeze
246
244
  TOP = " TOP ".freeze
247
245
  START_AT = " START AT ".freeze
@@ -261,6 +259,10 @@ module Sequel
261
259
  HSTAR = "H*".freeze
262
260
  CROSS_APPLY = 'CROSS APPLY'.freeze
263
261
  OUTER_APPLY = 'OUTER APPLY'.freeze
262
+ ONLY_OFFSET = " TOP 2147483647".freeze
263
+
264
+ Dataset.def_sql_method(self, :insert, %w'with insert into columns values')
265
+ Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from join where group having order compounds lock')
264
266
 
265
267
  # Whether to convert smallint to boolean arguments for this dataset.
266
268
  # Defaults to the SqlAnywhere module setting.
@@ -271,6 +273,10 @@ module Sequel
271
273
  # Override the default SqlAnywhere.convert_smallint_to_bool setting for this dataset.
272
274
  attr_writer :convert_smallint_to_bool
273
275
 
276
+ def supports_cte?(type=:select)
277
+ type == :select || type == :insert
278
+ end
279
+
274
280
  def supports_multiple_column_in?
275
281
  false
276
282
  end
@@ -401,14 +407,9 @@ module Sequel
401
407
  end
402
408
  end
403
409
 
404
- # Sybase supports the OUTPUT clause for INSERT statements.
405
- # It also allows prepending a WITH clause.
406
- def insert_clause_methods
407
- INSERT_CLAUSE_METHODS
408
- end
409
-
410
- def select_clause_methods
411
- SELECT_CLAUSE_METHODS
410
+ # Sybase supports multiple rows in INSERT.
411
+ def multi_insert_sql_strategy
412
+ :values
412
413
  end
413
414
 
414
415
  def select_into_sql(sql)
@@ -425,14 +426,21 @@ module Sequel
425
426
  # Sybase uses TOP N for limit. For Sybase TOP (N) is used
426
427
  # to allow the limit to be a bound variable.
427
428
  def select_limit_sql(sql)
428
- if l = @opts[:limit]
429
- sql << TOP
430
- literal_append(sql, l)
431
- end
432
- if o = @opts[:offset]
433
- sql << START_AT + "("
434
- literal_append(sql, o)
435
- sql << " + 1)"
429
+ l = @opts[:limit]
430
+ o = @opts[:offset]
431
+ if l || o
432
+ if l
433
+ sql << TOP
434
+ literal_append(sql, l)
435
+ else
436
+ sql << ONLY_OFFSET
437
+ end
438
+
439
+ if o
440
+ sql << START_AT + "("
441
+ literal_append(sql, o)
442
+ sql << " + 1)"
443
+ end
436
444
  end
437
445
  end
438
446
 
@@ -132,7 +132,7 @@ module Sequel
132
132
  def sqlite_version
133
133
  return @sqlite_version if defined?(@sqlite_version)
134
134
  @sqlite_version = begin
135
- v = get{sqlite_version{}}
135
+ v = fetch('SELECT sqlite_version()').single_value
136
136
  [10000, 100, 1].zip(v.split('.')).inject(0){|a, m| a + m[0] * Integer(m[1])}
137
137
  rescue
138
138
  0
@@ -497,7 +497,6 @@ module Sequel
497
497
  module DatasetMethods
498
498
  include Dataset::Replace
499
499
 
500
- SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select distinct columns from join where group having compounds order limit')
501
500
  CONSTANT_MAP = {:CURRENT_DATE=>"date(CURRENT_TIMESTAMP, 'localtime')".freeze, :CURRENT_TIMESTAMP=>"datetime(CURRENT_TIMESTAMP, 'localtime')".freeze, :CURRENT_TIME=>"time(CURRENT_TIMESTAMP, 'localtime')".freeze}
502
501
  EMULATED_FUNCTION_MAP = {:char_length=>'length'.freeze}
503
502
  EXTRACT_MAP = {:year=>"'%Y'", :month=>"'%m'", :day=>"'%d'", :hour=>"'%H'", :minute=>"'%M'", :second=>"'%f'"}
@@ -517,6 +516,11 @@ module Sequel
517
516
  HSTAR = "H*".freeze
518
517
  DATE_OPEN = "date(".freeze
519
518
  DATETIME_OPEN = "datetime(".freeze
519
+ ONLY_OFFSET = " LIMIT -1 OFFSET ".freeze
520
+
521
+ Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
522
+ Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert into columns values'], ["else", %w'insert into columns values']])
523
+ Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
520
524
 
521
525
  def cast_sql_append(sql, expr, type)
522
526
  if type == Time or type == DateTime
@@ -604,6 +608,11 @@ module Sequel
604
608
  end
605
609
  end
606
610
 
611
+ # SQLite 3.8.3+ supports common table expressions.
612
+ def supports_cte?(type=:select)
613
+ db.sqlite_version >= 30803
614
+ end
615
+
607
616
  # SQLite does not support INTERSECT ALL or EXCEPT ALL
608
617
  def supports_intersect_except_all?
609
618
  false
@@ -677,11 +686,12 @@ module Sequel
677
686
  @db.integer_booleans ? '1' : "'t'"
678
687
  end
679
688
 
680
- # SQLite does not support the SQL WITH clause
681
- def select_clause_methods
682
- SELECT_CLAUSE_METHODS
689
+ # SQLite only supporting multiple rows in the VALUES clause
690
+ # starting in 3.7.11. On older versions, fallback to using a UNION.
691
+ def multi_insert_sql_strategy
692
+ db.sqlite_version >= 30711 ? :values : :union
683
693
  end
684
-
694
+
685
695
  # SQLite does not support FOR UPDATE, but silently ignore it
686
696
  # instead of raising an error for compatibility with other
687
697
  # databases.
@@ -689,6 +699,11 @@ module Sequel
689
699
  super unless @opts[:lock] == :update
690
700
  end
691
701
 
702
+ def select_only_offset_sql(sql)
703
+ sql << ONLY_OFFSET
704
+ literal_append(sql, @opts[:offset])
705
+ end
706
+
692
707
  # SQLite supports quoted function names.
693
708
  def supports_quoted_function_names?
694
709
  true
@@ -18,6 +18,7 @@ module Sequel
18
18
  # reversed in the subselect. Note that the order needs to be unambiguous
19
19
  # to work correctly, and you must select all columns that you are ordering on.
20
20
  def select_sql
21
+ return super if @opts[:sql]
21
22
  return super unless o = @opts[:offset]
22
23
 
23
24
  order = @opts[:order] || default_offset_order
@@ -26,7 +27,7 @@ module Sequel
26
27
  end
27
28
 
28
29
  ds = unlimited
29
- row_count = @opts[:offset_total_count] || ds.clone(:append_sql=>'').count
30
+ row_count = @opts[:offset_total_count] || ds.clone(:append_sql=>'', :placeholder_literal_null=>true).count
30
31
  dsa1 = dataset_alias(1)
31
32
 
32
33
  if o.is_a?(Symbol) && @opts[:bind_vars] && (match = Sequel::Dataset::PreparedStatementMethods::PLACEHOLDER_RE.match(o.to_s))
@@ -57,6 +58,12 @@ module Sequel
57
58
  sql
58
59
  end
59
60
 
61
+ # This does not support offsets in correlated subqueries, as it requires a query to get
62
+ # a count that will be invalid if a correlated subquery is used.
63
+ def supports_offsets_in_correlated_subqueries?
64
+ false
65
+ end
66
+
60
67
  private
61
68
 
62
69
  # The default order to use for datasets with offsets, if no order is defined.
@@ -20,7 +20,7 @@ module Sequel
20
20
  end
21
21
  end
22
22
 
23
- columns = clone(:append_sql=>'').columns
23
+ columns = clone(:append_sql=>'', :placeholder_literal_null=>true).columns
24
24
  dsa1 = dataset_alias(1)
25
25
  rn = row_number_column
26
26
  sql = @opts[:append_sql] || ''
@@ -35,6 +35,12 @@ module Sequel
35
35
  sql
36
36
  end
37
37
 
38
+ # This does not support offsets in correlated subqueries, as it requires a query to get
39
+ # the columns that will be invalid if a correlated subquery is used.
40
+ def supports_offsets_in_correlated_subqueries?
41
+ false
42
+ end
43
+
38
44
  private
39
45
 
40
46
  # The default order to use for datasets with offsets, if no order is defined.
@@ -50,7 +56,7 @@ module Sequel
50
56
 
51
57
  # Whether to use ROW_NUMBER to emulate offsets
52
58
  def emulate_offset_with_row_number?
53
- @opts[:offset]
59
+ @opts[:offset] && !@opts[:sql]
54
60
  end
55
61
  end
56
62
  end
@@ -24,6 +24,9 @@ module Sequel::Database::SplitAlterTable
24
24
  modified_columns << op[:name] unless modified_columns.include?(op[:name])
25
25
  modified_columns << op[:new_name] unless modified_columns.include?(op[:new_name])
26
26
  end
27
+ if split_alter_table_op?(op)
28
+ op_groups << []
29
+ end
27
30
  op_groups.last << op
28
31
  end
29
32
 
@@ -33,4 +36,9 @@ module Sequel::Database::SplitAlterTable
33
36
  remove_cached_schema(name)
34
37
  end
35
38
  end
39
+
40
+ # Whether the given alter table op should start a new group.
41
+ def split_alter_table_op?(op)
42
+ false
43
+ end
36
44
  end
data/lib/sequel/core.rb CHANGED
@@ -219,6 +219,7 @@ module Sequel
219
219
  COLUMN_REF_RE1 = /\A((?:(?!__).)+)__((?:(?!___).)+)___(.+)\z/.freeze
220
220
  COLUMN_REF_RE2 = /\A((?:(?!___).)+)___(.+)\z/.freeze
221
221
  COLUMN_REF_RE3 = /\A((?:(?!__).)+)__(.+)\z/.freeze
222
+ SPLIT_SYMBOL_CACHE = {}
222
223
 
223
224
  # Splits the symbol into three parts. Each part will
224
225
  # either be a string or nil.
@@ -226,16 +227,20 @@ module Sequel
226
227
  # For columns, these parts are the table, column, and alias.
227
228
  # For tables, these parts are the schema, table, and alias.
228
229
  def self.split_symbol(sym)
229
- case s = sym.to_s
230
- when COLUMN_REF_RE1
231
- [$1, $2, $3]
232
- when COLUMN_REF_RE2
233
- [nil, $1, $2]
234
- when COLUMN_REF_RE3
235
- [$1, $2, nil]
236
- else
237
- [nil, s, nil]
230
+ unless v = Sequel.synchronize{SPLIT_SYMBOL_CACHE[sym]}
231
+ v = case s = sym.to_s
232
+ when COLUMN_REF_RE1
233
+ [$1.freeze, $2.freeze, $3.freeze].freeze
234
+ when COLUMN_REF_RE2
235
+ [nil, $1.freeze, $2.freeze].freeze
236
+ when COLUMN_REF_RE3
237
+ [$1.freeze, $2.freeze, nil].freeze
238
+ else
239
+ [nil, s.freeze, nil].freeze
240
+ end
241
+ Sequel.synchronize{SPLIT_SYMBOL_CACHE[sym] = v}
238
242
  end
243
+ v
239
244
  end
240
245
 
241
246
  # Converts the given +string+ into a +Date+ object.
@@ -137,6 +137,7 @@ module Sequel
137
137
  # create datasets. Usually done after changes to the identifier
138
138
  # mangling methods.
139
139
  def reset_default_dataset
140
+ Sequel.synchronize{@symbol_literal_cache.clear}
140
141
  @default_dataset = dataset
141
142
  end
142
143
 
@@ -131,6 +131,7 @@ module Sequel
131
131
  @dataset_class = dataset_class_default
132
132
  @cache_schema = typecast_value_boolean(@opts.fetch(:cache_schema, true))
133
133
  @dataset_modules = []
134
+ @symbol_literal_cache = {}
134
135
  @schema_type_classes = SCHEMA_TYPE_CLASSES.dup
135
136
  self.sql_log_level = @opts[:sql_log_level] ? @opts[:sql_log_level].to_sym : :info
136
137
  @pool = ConnectionPool.get_pool(self, @opts)
@@ -235,6 +236,17 @@ module Sequel
235
236
  schema_utility_dataset.literal(v)
236
237
  end
237
238
 
239
+ # Return the literalized version of the symbol if cached, or
240
+ # nil if it is not cached.
241
+ def literal_symbol(sym)
242
+ Sequel.synchronize{@symbol_literal_cache[sym]}
243
+ end
244
+
245
+ # Set the cached value of the literal symbol.
246
+ def literal_symbol_set(sym, lit)
247
+ Sequel.synchronize{@symbol_literal_cache[sym] = lit}
248
+ end
249
+
238
250
  # Synchronize access to the prepared statements cache.
239
251
  def prepared_statement(name)
240
252
  Sequel.synchronize{prepared_statements[name]}
@@ -279,7 +279,10 @@ module Sequel
279
279
 
280
280
  # Remove the cached schema for the given schema name
281
281
  def remove_cached_schema(table)
282
- Sequel.synchronize{@schemas.delete(quote_schema_table(table))} if @schemas
282
+ if @schemas
283
+ k = quote_schema_table(table)
284
+ Sequel.synchronize{@schemas.delete(k)}
285
+ end
283
286
  end
284
287
 
285
288
  # Match the database's column type to a ruby type via a
@@ -332,12 +332,13 @@ module Sequel
332
332
  # DB.drop_view(:cheap_items)
333
333
  # DB.drop_view(:cheap_items, :pricey_items)
334
334
  # DB.drop_view(:cheap_items, :pricey_items, :cascade=>true)
335
+ # DB.drop_view(:cheap_items, :pricey_items, :if_exists=>true)
335
336
  #
336
337
  # Options:
337
338
  # :cascade :: Also drop objects depending on this view.
339
+ # :if_exists :: Do not raise an error if the view does not exist.
338
340
  #
339
341
  # PostgreSQL specific options:
340
- # :if_exists :: Do not raise an error if the view does not exist.
341
342
  # :materialized :: Drop a materialized view.
342
343
  def drop_view(*names)
343
344
  options = names.last.is_a?(Hash) ? names.pop : {}
@@ -721,7 +722,7 @@ module Sequel
721
722
 
722
723
  # SQL DDL statement to drop a view with the given name.
723
724
  def drop_view_sql(name, options)
724
- "DROP VIEW #{quote_schema_table(name)}#{' CASCADE' if options[:cascade]}"
725
+ "DROP VIEW#{' IF EXISTS' if options[:if_exists]} #{quote_schema_table(name)}#{' CASCADE' if options[:cascade]}"
725
726
  end
726
727
 
727
728
  # Proxy the filter_expr call to the dataset, used for creating constraints.
@@ -36,6 +36,8 @@ module Sequel
36
36
  #
37
37
  # The following general options are respected:
38
38
  #
39
+ # :auto_savepoint :: Automatically use a savepoint for Database#transaction calls
40
+ # inside this transaction block.
39
41
  # :isolation :: The transaction isolation level to use for this transaction,
40
42
  # should be :uncommitted, :committed, :repeatable, or :serializable,
41
43
  # used if given and the database/adapter supports customizable
@@ -59,7 +61,8 @@ module Sequel
59
61
  # :savepoint :: Whether to create a new savepoint for this transaction,
60
62
  # only respected if the database/adapter supports savepoints. By
61
63
  # default Sequel will reuse an existing transaction, so if you want to
62
- # use a savepoint you must use this option.
64
+ # use a savepoint you must use this option. If the surrounding transaction
65
+ # uses :auto_savepoint, you can set this to false to not use a savepoint.
63
66
  #
64
67
  # PostgreSQL specific options:
65
68
  #
@@ -88,9 +91,14 @@ module Sequel
88
91
  if opts[:retrying]
89
92
  raise Sequel::Error, "cannot set :retry_on options if you are already inside a transaction"
90
93
  end
91
- return yield(conn)
94
+ if opts[:savepoint] != false && (stack = _trans(conn)[:savepoints]) && stack.last
95
+ _transaction(conn, opts.merge(:savepoint=>true), &block)
96
+ else
97
+ return yield(conn)
98
+ end
99
+ else
100
+ _transaction(conn, opts, &block)
92
101
  end
93
- _transaction(conn, opts, &block)
94
102
  end
95
103
  end
96
104
  end
@@ -151,18 +159,24 @@ module Sequel
151
159
 
152
160
  # Add the current thread to the list of active transactions
153
161
  def add_transaction(conn, opts)
162
+ hash = {}
163
+
154
164
  if supports_savepoints?
155
- unless _trans(conn)
165
+ if _trans(conn)
166
+ hash = nil
167
+ _trans(conn)[:savepoints].push(opts[:auto_savepoint])
168
+ else
169
+ hash[:savepoints] = [opts[:auto_savepoint]]
156
170
  if (prep = opts[:prepare]) && supports_prepared_transactions?
157
- Sequel.synchronize{@transactions[conn] = {:savepoint_level=>0, :prepare=>prep}}
158
- else
159
- Sequel.synchronize{@transactions[conn] = {:savepoint_level=>0}}
171
+ hash[:prepare] = prep
160
172
  end
161
173
  end
162
174
  elsif (prep = opts[:prepare]) && supports_prepared_transactions?
163
- Sequel.synchronize{@transactions[conn] = {:prepare => prep}}
164
- else
165
- Sequel.synchronize{@transactions[conn] = {}}
175
+ hash[:prepare] = prep
176
+ end
177
+
178
+ if hash
179
+ Sequel.synchronize{@transactions[conn] = hash}
166
180
  end
167
181
  end
168
182
 
@@ -199,13 +213,12 @@ module Sequel
199
213
  # Start a new database transaction or a new savepoint on the given connection.
200
214
  def begin_transaction(conn, opts=OPTS)
201
215
  if supports_savepoints?
202
- th = _trans(conn)
203
- if (depth = th[:savepoint_level]) > 0
204
- log_connection_execute(conn, begin_savepoint_sql(depth))
216
+ depth = savepoint_level(conn)
217
+ if depth > 1
218
+ log_connection_execute(conn, begin_savepoint_sql(depth-1))
205
219
  else
206
220
  begin_new_transaction(conn, opts)
207
221
  end
208
- th[:savepoint_level] += 1
209
222
  else
210
223
  begin_new_transaction(conn, opts)
211
224
  end
@@ -243,7 +256,7 @@ module Sequel
243
256
  # Commit the active transaction on the connection
244
257
  def commit_transaction(conn, opts=OPTS)
245
258
  if supports_savepoints?
246
- depth = _trans(conn)[:savepoint_level]
259
+ depth = savepoint_level(conn)
247
260
  log_connection_execute(conn, depth > 1 ? commit_savepoint_sql(depth-1) : commit_transaction_sql)
248
261
  else
249
262
  log_connection_execute(conn, commit_transaction_sql)
@@ -263,7 +276,7 @@ module Sequel
263
276
 
264
277
  # Remove the current thread from the list of active transactions
265
278
  def remove_transaction(conn, committed)
266
- if !supports_savepoints? || ((_trans(conn)[:savepoint_level] -= 1) <= 0)
279
+ if transaction_finished?(conn)
267
280
  begin
268
281
  if committed
269
282
  after_transaction_commit(conn)
@@ -284,7 +297,7 @@ module Sequel
284
297
  # Rollback the active transaction on the connection
285
298
  def rollback_transaction(conn, opts=OPTS)
286
299
  if supports_savepoints?
287
- depth = _trans(conn)[:savepoint_level]
300
+ depth = savepoint_level(conn)
288
301
  log_connection_execute(conn, depth > 1 ? rollback_savepoint_sql(depth-1) : rollback_transaction_sql)
289
302
  else
290
303
  log_connection_execute(conn, rollback_transaction_sql)
@@ -308,6 +321,11 @@ module Sequel
308
321
  "SET TRANSACTION ISOLATION LEVEL #{TRANSACTION_ISOLATION_LEVELS[level]}"
309
322
  end
310
323
 
324
+ # Current savepoint level.
325
+ def savepoint_level(conn)
326
+ _trans(conn)[:savepoints].length
327
+ end
328
+
311
329
  # Raise a database error unless the exception is an Rollback.
312
330
  def transaction_error(e, opts=OPTS)
313
331
  if e.is_a?(Rollback)
@@ -316,5 +334,17 @@ module Sequel
316
334
  raise_error(e, opts.merge(:classes=>database_error_classes))
317
335
  end
318
336
  end
337
+
338
+ # Finish a subtransaction. If savepoints are supported, pops the current
339
+ # tansaction off the savepoint stack.
340
+ def transaction_finished?(conn)
341
+ if supports_savepoints?
342
+ stack = _trans(conn)[:savepoints]
343
+ stack.pop
344
+ stack.empty?
345
+ else
346
+ true
347
+ end
348
+ end
319
349
  end
320
350
  end