sequel 4.9.0 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
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