sequel 3.28.0 → 3.29.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (148) hide show
  1. data/CHANGELOG +119 -3
  2. data/Rakefile +5 -3
  3. data/bin/sequel +1 -5
  4. data/doc/model_hooks.rdoc +9 -1
  5. data/doc/opening_databases.rdoc +49 -40
  6. data/doc/prepared_statements.rdoc +27 -6
  7. data/doc/release_notes/3.28.0.txt +2 -2
  8. data/doc/release_notes/3.29.0.txt +459 -0
  9. data/doc/sharding.rdoc +7 -1
  10. data/doc/testing.rdoc +18 -9
  11. data/doc/transactions.rdoc +41 -1
  12. data/lib/sequel/adapters/ado.rb +28 -17
  13. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  14. data/lib/sequel/adapters/amalgalite.rb +11 -7
  15. data/lib/sequel/adapters/db2.rb +122 -70
  16. data/lib/sequel/adapters/dbi.rb +15 -15
  17. data/lib/sequel/adapters/do.rb +5 -36
  18. data/lib/sequel/adapters/do/mysql.rb +0 -5
  19. data/lib/sequel/adapters/do/postgres.rb +0 -5
  20. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  21. data/lib/sequel/adapters/firebird.rb +3 -6
  22. data/lib/sequel/adapters/ibmdb.rb +24 -16
  23. data/lib/sequel/adapters/informix.rb +2 -4
  24. data/lib/sequel/adapters/jdbc.rb +47 -11
  25. data/lib/sequel/adapters/jdbc/as400.rb +5 -24
  26. data/lib/sequel/adapters/jdbc/db2.rb +0 -5
  27. data/lib/sequel/adapters/jdbc/derby.rb +217 -0
  28. data/lib/sequel/adapters/jdbc/firebird.rb +0 -5
  29. data/lib/sequel/adapters/jdbc/h2.rb +10 -12
  30. data/lib/sequel/adapters/jdbc/hsqldb.rb +166 -0
  31. data/lib/sequel/adapters/jdbc/informix.rb +0 -5
  32. data/lib/sequel/adapters/jdbc/jtds.rb +0 -5
  33. data/lib/sequel/adapters/jdbc/mysql.rb +0 -10
  34. data/lib/sequel/adapters/jdbc/oracle.rb +70 -3
  35. data/lib/sequel/adapters/jdbc/postgresql.rb +0 -11
  36. data/lib/sequel/adapters/jdbc/sqlite.rb +0 -5
  37. data/lib/sequel/adapters/jdbc/sqlserver.rb +0 -5
  38. data/lib/sequel/adapters/jdbc/transactions.rb +56 -7
  39. data/lib/sequel/adapters/mock.rb +315 -0
  40. data/lib/sequel/adapters/mysql.rb +64 -51
  41. data/lib/sequel/adapters/mysql2.rb +15 -9
  42. data/lib/sequel/adapters/odbc.rb +13 -6
  43. data/lib/sequel/adapters/odbc/db2.rb +0 -4
  44. data/lib/sequel/adapters/odbc/mssql.rb +0 -5
  45. data/lib/sequel/adapters/openbase.rb +2 -4
  46. data/lib/sequel/adapters/oracle.rb +333 -51
  47. data/lib/sequel/adapters/postgres.rb +80 -27
  48. data/lib/sequel/adapters/shared/access.rb +0 -6
  49. data/lib/sequel/adapters/shared/db2.rb +13 -15
  50. data/lib/sequel/adapters/shared/firebird.rb +6 -6
  51. data/lib/sequel/adapters/shared/mssql.rb +23 -18
  52. data/lib/sequel/adapters/shared/mysql.rb +6 -6
  53. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
  54. data/lib/sequel/adapters/shared/oracle.rb +185 -30
  55. data/lib/sequel/adapters/shared/postgres.rb +35 -18
  56. data/lib/sequel/adapters/shared/progress.rb +0 -6
  57. data/lib/sequel/adapters/shared/sqlite.rb +116 -37
  58. data/lib/sequel/adapters/sqlite.rb +16 -8
  59. data/lib/sequel/adapters/swift.rb +5 -5
  60. data/lib/sequel/adapters/swift/mysql.rb +0 -5
  61. data/lib/sequel/adapters/swift/postgres.rb +0 -5
  62. data/lib/sequel/adapters/swift/sqlite.rb +6 -4
  63. data/lib/sequel/adapters/tinytds.rb +13 -10
  64. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +8 -0
  65. data/lib/sequel/core.rb +40 -0
  66. data/lib/sequel/database/connecting.rb +1 -2
  67. data/lib/sequel/database/dataset.rb +3 -3
  68. data/lib/sequel/database/dataset_defaults.rb +58 -0
  69. data/lib/sequel/database/misc.rb +62 -2
  70. data/lib/sequel/database/query.rb +113 -49
  71. data/lib/sequel/database/schema_methods.rb +7 -2
  72. data/lib/sequel/dataset/actions.rb +37 -19
  73. data/lib/sequel/dataset/features.rb +24 -0
  74. data/lib/sequel/dataset/graph.rb +7 -6
  75. data/lib/sequel/dataset/misc.rb +11 -3
  76. data/lib/sequel/dataset/mutation.rb +2 -3
  77. data/lib/sequel/dataset/prepared_statements.rb +6 -4
  78. data/lib/sequel/dataset/query.rb +46 -15
  79. data/lib/sequel/dataset/sql.rb +28 -4
  80. data/lib/sequel/extensions/named_timezones.rb +5 -0
  81. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  82. data/lib/sequel/model.rb +2 -1
  83. data/lib/sequel/model/associations.rb +115 -33
  84. data/lib/sequel/model/base.rb +91 -31
  85. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  86. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  87. data/lib/sequel/plugins/force_encoding.rb +6 -6
  88. data/lib/sequel/plugins/identity_map.rb +1 -1
  89. data/lib/sequel/plugins/many_through_many.rb +6 -10
  90. data/lib/sequel/plugins/prepared_statements.rb +12 -1
  91. data/lib/sequel/plugins/prepared_statements_associations.rb +1 -1
  92. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  93. data/lib/sequel/plugins/serialization.rb +6 -1
  94. data/lib/sequel/plugins/sharding.rb +0 -5
  95. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  96. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  97. data/lib/sequel/plugins/update_primary_key.rb +1 -1
  98. data/lib/sequel/timezones.rb +42 -42
  99. data/lib/sequel/version.rb +1 -1
  100. data/spec/adapters/mssql_spec.rb +29 -29
  101. data/spec/adapters/mysql_spec.rb +86 -104
  102. data/spec/adapters/oracle_spec.rb +48 -76
  103. data/spec/adapters/postgres_spec.rb +98 -33
  104. data/spec/adapters/spec_helper.rb +0 -5
  105. data/spec/adapters/sqlite_spec.rb +24 -21
  106. data/spec/core/connection_pool_spec.rb +9 -15
  107. data/spec/core/core_sql_spec.rb +20 -31
  108. data/spec/core/database_spec.rb +491 -227
  109. data/spec/core/dataset_spec.rb +638 -1051
  110. data/spec/core/expression_filters_spec.rb +0 -1
  111. data/spec/core/mock_adapter_spec.rb +378 -0
  112. data/spec/core/object_graph_spec.rb +48 -114
  113. data/spec/core/schema_generator_spec.rb +3 -3
  114. data/spec/core/schema_spec.rb +51 -114
  115. data/spec/core/spec_helper.rb +3 -90
  116. data/spec/extensions/class_table_inheritance_spec.rb +1 -1
  117. data/spec/extensions/dataset_associations_spec.rb +199 -0
  118. data/spec/extensions/instance_hooks_spec.rb +71 -0
  119. data/spec/extensions/named_timezones_spec.rb +22 -2
  120. data/spec/extensions/nested_attributes_spec.rb +3 -0
  121. data/spec/extensions/schema_spec.rb +1 -1
  122. data/spec/extensions/serialization_modification_detection_spec.rb +1 -0
  123. data/spec/extensions/serialization_spec.rb +5 -8
  124. data/spec/extensions/spec_helper.rb +4 -0
  125. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  126. data/spec/extensions/typecast_on_load_spec.rb +1 -6
  127. data/spec/integration/associations_test.rb +123 -12
  128. data/spec/integration/dataset_test.rb +140 -47
  129. data/spec/integration/eager_loader_test.rb +19 -21
  130. data/spec/integration/model_test.rb +80 -1
  131. data/spec/integration/plugin_test.rb +179 -128
  132. data/spec/integration/prepared_statement_test.rb +92 -91
  133. data/spec/integration/schema_test.rb +42 -23
  134. data/spec/integration/spec_helper.rb +25 -31
  135. data/spec/integration/timezone_test.rb +38 -12
  136. data/spec/integration/transaction_test.rb +161 -34
  137. data/spec/integration/type_test.rb +3 -3
  138. data/spec/model/association_reflection_spec.rb +83 -7
  139. data/spec/model/associations_spec.rb +393 -676
  140. data/spec/model/base_spec.rb +186 -116
  141. data/spec/model/dataset_methods_spec.rb +7 -27
  142. data/spec/model/eager_loading_spec.rb +343 -867
  143. data/spec/model/hooks_spec.rb +160 -79
  144. data/spec/model/model_spec.rb +118 -165
  145. data/spec/model/plugins_spec.rb +7 -13
  146. data/spec/model/record_spec.rb +138 -207
  147. data/spec/model/spec_helper.rb +10 -73
  148. metadata +14 -8
@@ -9,11 +9,6 @@ module Sequel
9
9
  module DatabaseMethods
10
10
  include Sequel::MySQL::DatabaseMethods
11
11
 
12
- # Return instance of Sequel::Swift::MySQL::Dataset with the given opts.
13
- def dataset(opts=nil)
14
- Sequel::Swift::MySQL::Dataset.new(self, opts)
15
- end
16
-
17
12
  private
18
13
 
19
14
  # The database name for the given database.
@@ -41,11 +41,6 @@ module Sequel
41
41
  end
42
42
  end
43
43
 
44
- # Return instance of Sequel::Swift::Postgres::Dataset with the given opts.
45
- def dataset(opts=nil)
46
- Sequel::Swift::Postgres::Dataset.new(self, opts)
47
- end
48
-
49
44
  # Run the SELECT SQL on the database and yield the rows
50
45
  def execute(sql, opts={})
51
46
  synchronize(opts[:server]) do |conn|
@@ -8,10 +8,12 @@ module Sequel
8
8
  # Database instance methods for SQLite databases accessed via Swift.
9
9
  module DatabaseMethods
10
10
  include Sequel::SQLite::DatabaseMethods
11
-
12
- # Return instance of Sequel::Swift::SQL::Dataset with the given opts.
13
- def dataset(opts=nil)
14
- Sequel::Swift::SQLite::Dataset.new(self, opts)
11
+
12
+ # Set the correct pragmas on the connection.
13
+ def connect(opts)
14
+ c = super
15
+ connection_pragmas.each{|s| log_yield(s){c.execute(s)}}
16
+ c
15
17
  end
16
18
  end
17
19
 
@@ -16,11 +16,6 @@ module Sequel
16
16
  TinyTds::Client.new(opts)
17
17
  end
18
18
 
19
- # Return instance of Sequel::TinyTDS::Dataset with the given options.
20
- def dataset(opts = nil)
21
- TinyTDS::Dataset.new(self, opts)
22
- end
23
-
24
19
  # Execute the given +sql+ on the server. If the :return option
25
20
  # is present, its value should be a method symbol that is called
26
21
  # on the TinyTds::Result object returned from executing the
@@ -150,6 +145,8 @@ module Sequel
150
145
 
151
146
  class Dataset < Sequel::Dataset
152
147
  include Sequel::MSSQL::DatasetMethods
148
+
149
+ Database::DatasetClass = self
153
150
 
154
151
  # SQLite already supports named bind arguments, so use directly.
155
152
  module ArgumentMapper
@@ -208,22 +205,28 @@ module Sequel
208
205
  def fetch_rows(sql)
209
206
  execute(sql) do |result|
210
207
  each_opts = {:cache_rows=>false}
211
- each_opts[:timezone] = :utc if Sequel.database_timezone == :utc
212
- offset = @opts[:offset]
213
- @columns = cols = result.fields.map{|c| output_identifier(c)}
208
+ each_opts[:timezone] = :utc if db.timezone == :utc
209
+ rn = row_number_column if @opts[:offset]
210
+ columns = cols = result.fields.map{|c| output_identifier(c)}
211
+ if opts[:offset]
212
+ rn = row_number_column
213
+ columns = columns.dup
214
+ columns.delete(rn)
215
+ end
216
+ @columns = columns
214
217
  if identifier_output_method
215
218
  each_opts[:as] = :array
216
219
  result.each(each_opts) do |r|
217
220
  h = {}
218
221
  cols.zip(r).each{|k, v| h[k] = v}
219
- h.delete(row_number_column) if offset
222
+ h.delete(rn) if rn
220
223
  yield h
221
224
  end
222
225
  else
223
226
  each_opts[:symbolize_keys] = true
224
227
  if offset
225
228
  result.each(each_opts) do |r|
226
- r.delete(row_number_column) if offset
229
+ r.delete(rn) if rn
227
230
  yield r
228
231
  end
229
232
  else
@@ -51,5 +51,13 @@ module Sequel
51
51
  limit(@opts[:limit]).
52
52
  where(SQL::Identifier.new(rn) > o))
53
53
  end
54
+
55
+ private
56
+
57
+ # This emulation adds an extra row number column that should be
58
+ # eliminated.
59
+ def offset_returns_row_number_column?
60
+ true
61
+ end
54
62
  end
55
63
  end
data/lib/sequel/core.rb CHANGED
@@ -240,6 +240,46 @@ module Sequel
240
240
  end
241
241
  end
242
242
 
243
+ # Uses a transaction on all given databases with the given options. This:
244
+ #
245
+ # Sequel.transaction([DB1, DB2, DB3]){...}
246
+ #
247
+ # is equivalent to:
248
+ #
249
+ # DB1.transaction do
250
+ # DB2.transaction do
251
+ # DB3.transaction do
252
+ # ...
253
+ # end
254
+ # end
255
+ # end
256
+ #
257
+ # except that if Sequel::Rollback is raised by the block, the transaction is
258
+ # rolled back on all databases instead of just the last one.
259
+ #
260
+ # Note that this method cannot guarantee that all databases will commit or
261
+ # rollback. For example, if DB3 commits but attempting to commit on DB2
262
+ # fails (maybe because foreign key checks are deferred), there is no way
263
+ # to uncommit the changes on DB3. For that kind of support, you need to
264
+ # have two-phase commit/prepared transactions (which Sequel supports on
265
+ # some databases).
266
+ def self.transaction(dbs, opts={}, &block)
267
+ unless opts[:rollback]
268
+ rescue_rollback = true
269
+ opts = opts.merge(:rollback=>:reraise)
270
+ end
271
+ pr = dbs.reverse.inject(block){|bl, db| proc{db.transaction(opts, &bl)}}
272
+ if rescue_rollback
273
+ begin
274
+ pr.call
275
+ rescue Sequel::Rollback => e
276
+ nil
277
+ end
278
+ else
279
+ pr.call
280
+ end
281
+ end
282
+
243
283
  # Same as Sequel.require, but wrapped in a mutex in order to be thread safe.
244
284
  def self.ts_require(*args)
245
285
  check_requiring_thread{require(*args)}
@@ -6,7 +6,7 @@ module Sequel
6
6
  # ---------------------
7
7
 
8
8
  # Array of supported database adapters
9
- ADAPTERS = %w'ado amalgalite db2 dbi do firebird ibmdb informix jdbc mysql mysql2 odbc openbase oracle postgres sqlite swift tinytds'.collect{|x| x.to_sym}
9
+ ADAPTERS = %w'ado amalgalite db2 dbi do firebird ibmdb informix jdbc mock mysql mysql2 odbc openbase oracle postgres sqlite swift tinytds'.collect{|x| x.to_sym}
10
10
 
11
11
  # Whether to use the single threaded connection pool by default
12
12
  @@single_threaded = false
@@ -218,7 +218,6 @@ module Sequel
218
218
  # If a server option is given, acquires a connection for that specific
219
219
  # server, instead of the :default server.
220
220
  #
221
- #
222
221
  # DB.synchronize do |conn|
223
222
  # ...
224
223
  # end
@@ -24,10 +24,10 @@ module Sequel
24
24
  #
25
25
  # DB.dataset # SELECT *
26
26
  # DB.dataset.from(:items) # SELECT * FROM items
27
- def dataset
28
- ds = Sequel::Dataset.new(self)
27
+ def dataset(opts=nil)
28
+ @dataset_class.new(self, opts)
29
29
  end
30
-
30
+
31
31
  # Fetches records for an arbitrary SQL statement. If a block is given,
32
32
  # it is used to iterate over the records:
33
33
  #
@@ -5,6 +5,9 @@ module Sequel
5
5
  # This methods change the default behavior of this database's datasets.
6
6
  # ---------------------
7
7
 
8
+ # The default class to use for datasets
9
+ DatasetClass = Sequel::Dataset
10
+
8
11
  # The identifier input method to use by default
9
12
  @@identifier_input_method = nil
10
13
 
@@ -42,9 +45,59 @@ module Sequel
42
45
  @@quote_identifiers = value
43
46
  end
44
47
 
48
+ # The class to use for creating datasets. Should respond to
49
+ # new with the Database argument as the first argument, and
50
+ # an optional options hash.
51
+ attr_reader :dataset_class
52
+
45
53
  # The default schema to use, generally should be nil.
46
54
  attr_accessor :default_schema
47
55
 
56
+ # If the database has any dataset modules associated with it,
57
+ # use a subclass of the given class that includes the modules
58
+ # as the dataset class.
59
+ def dataset_class=(c)
60
+ unless @dataset_modules.empty?
61
+ c = Class.new(c)
62
+ @dataset_modules.each{|m| c.send(:include, m)}
63
+ end
64
+ @dataset_class = c
65
+ end
66
+
67
+ # Equivalent to extending all datasets produced by the database with a
68
+ # module. What it actually does is use a subclass of the current dataset_class
69
+ # as the new dataset_class, and include the module in the subclass.
70
+ # Instead of a module, you can provide a block that is used to create an
71
+ # anonymous module.
72
+ #
73
+ # This allows you to override any of the dataset methods even if they are
74
+ # defined directly on the dataset class that this Database object uses.
75
+ #
76
+ # Examples:
77
+ #
78
+ # # Introspec columns for all of DB's datasets
79
+ # DB.extend_datasets(Sequel::ColumnsIntrospection)
80
+ #
81
+ # # Trace all SELECT queries by printing the SQL and the full backtrace
82
+ # DB.extend_datasets do
83
+ # def fetch_rows(sql)
84
+ # puts sql
85
+ # puts caller
86
+ # super
87
+ # end
88
+ # end
89
+ def extend_datasets(mod=nil, &block)
90
+ raise(Error, "must provide either mod or block, not both") if mod && block
91
+ mod = Module.new(&block) if block
92
+ if @dataset_modules.empty?
93
+ @dataset_modules = [mod]
94
+ @dataset_class = Class.new(@dataset_class)
95
+ else
96
+ @dataset_modules << mod
97
+ end
98
+ @dataset_class.send(:include, mod)
99
+ end
100
+
48
101
  # The method to call on identifiers going into the database
49
102
  def identifier_input_method
50
103
  case @identifier_input_method
@@ -109,6 +162,11 @@ module Sequel
109
162
 
110
163
  private
111
164
 
165
+ # The default dataset class to use for the database
166
+ def dataset_class_default
167
+ self.class.const_get(:DatasetClass)
168
+ end
169
+
112
170
  # The default value for default_schema.
113
171
  def default_schema_default
114
172
  nil
@@ -19,6 +19,9 @@ module Sequel
19
19
  # The options hash for this database
20
20
  attr_reader :opts
21
21
 
22
+ # Set the timezone to use for this database, overridding <tt>Sequel.database_timezone</tt>.
23
+ attr_writer :timezone
24
+
22
25
  # Constructs a new instance of a database connection with the specified
23
26
  # options hash.
24
27
  #
@@ -50,15 +53,46 @@ module Sequel
50
53
  @schemas = {}
51
54
  @default_schema = @opts.fetch(:default_schema, default_schema_default)
52
55
  @prepared_statements = {}
53
- @transactions = []
56
+ @transactions = {}
54
57
  @identifier_input_method = nil
55
58
  @identifier_output_method = nil
56
59
  @quote_identifiers = nil
60
+ @timezone = nil
61
+ @dataset_class = dataset_class_default
62
+ @dataset_modules = []
57
63
  self.sql_log_level = @opts[:sql_log_level] ? @opts[:sql_log_level].to_sym : :info
58
64
  @pool = ConnectionPool.get_pool(@opts, &block)
59
65
 
60
66
  ::Sequel::DATABASES.push(self)
61
67
  end
68
+
69
+ # If a transaction is not currently in process, yield to the block immediately.
70
+ # Otherwise, add the block to the list of blocks to call after the currently
71
+ # in progress transaction commits (and only if it commits).
72
+ def after_commit(opts={}, &block)
73
+ raise Error, "must provide block to after_commit" unless block
74
+ synchronize(opts) do |conn|
75
+ if h = @transactions[conn]
76
+ raise Error, "cannot call after_commit in a prepared transaction" if h[:prepare]
77
+ (h[:after_commit] ||= []) << block
78
+ else
79
+ yield
80
+ end
81
+ end
82
+ end
83
+
84
+ # If a transaction is not currently in progress, ignore the block.
85
+ # Otherwise, add the block to the list of the blocks to call after the currently
86
+ # in progress transaction rolls back (and only if it rolls back).
87
+ def after_rollback(opts={}, &block)
88
+ raise Error, "must provide block to after_rollback" unless block
89
+ synchronize(opts) do |conn|
90
+ if h = @transactions[conn]
91
+ raise Error, "cannot call after_rollback in a prepared transaction" if h[:prepare]
92
+ (h[:after_rollback] ||= []) << block
93
+ end
94
+ end
95
+ end
62
96
 
63
97
  # Cast the given type to a literal type
64
98
  #
@@ -68,6 +102,20 @@ module Sequel
68
102
  type_literal(:type=>type)
69
103
  end
70
104
 
105
+ # Convert the given timestamp from the application's timezone,
106
+ # to the databases's timezone or the default database timezone if
107
+ # the database does not have a timezone.
108
+ def from_application_timestamp(v)
109
+ Sequel.convert_output_timestamp(v, timezone)
110
+ end
111
+
112
+ # Return true if already in a transaction given the options,
113
+ # false otherwise. Respects the :server option for selecting
114
+ # a shard.
115
+ def in_transaction?(opts={})
116
+ synchronize(opts[:server]){|conn| !!@transactions[conn]}
117
+ end
118
+
71
119
  # Returns a string representation of the database object including the
72
120
  # class name and the connection URI (or the opts if the URI
73
121
  # cannot be constructed).
@@ -112,6 +160,18 @@ module Sequel
112
160
  false
113
161
  end
114
162
 
163
+ # The timezone to use for this database, defaulting to <tt>Sequel.database_timezone</tt>.
164
+ def timezone
165
+ @timezone || Sequel.database_timezone
166
+ end
167
+
168
+ # Convert the given timestamp to the application's timezone,
169
+ # from the databases's timezone or the default database timezone if
170
+ # the database does not have a timezone.
171
+ def to_application_timestamp(v)
172
+ Sequel.convert_timestamp(v, timezone)
173
+ end
174
+
115
175
  # Typecast the value to the given column_type. Calls
116
176
  # typecast_value_#{column_type} if the method exists,
117
177
  # otherwise returns the value.
@@ -205,7 +265,7 @@ module Sequel
205
265
  # Typecast the value to true, false, or nil
206
266
  def typecast_value_boolean(value)
207
267
  case value
208
- when false, 0, "0", /\Af(alse)?\z/i
268
+ when false, 0, "0", /\Af(alse)?\z/i, /\Ano?\z/i
209
269
  false
210
270
  else
211
271
  blank_object?(value) ? nil : true
@@ -37,9 +37,12 @@ module Sequel
37
37
  attr_accessor :transaction_isolation_level
38
38
 
39
39
  # Runs the supplied SQL statement string on the database server.
40
- # Alias for run.
40
+ # Returns self so it can be safely chained:
41
+ #
42
+ # DB << "UPADTE albums SET artist_id = NULL" << "DROP TABLE artists"
41
43
  def <<(sql)
42
44
  run(sql)
45
+ self
43
46
  end
44
47
 
45
48
  # Call the prepared statement with the given name with the given hash
@@ -112,14 +115,15 @@ module Sequel
112
115
 
113
116
  # Returns the schema for the given table as an array with all members being arrays of length 2,
114
117
  # the first member being the column name, and the second member being a hash of column information.
118
+ # The table argument can also be a dataset, as long as it only has one table.
115
119
  # Available options are:
116
120
  #
117
121
  # :reload :: Ignore any cached results, and get fresh information from the database.
118
122
  # :schema :: An explicit schema to use. It may also be implicitly provided
119
123
  # via the table name.
120
124
  #
121
- # If schema parsing is supported by the database, the column information should at least contain the
122
- # following columns:
125
+ # If schema parsing is supported by the database, the column information should hash at least contain the
126
+ # following entries:
123
127
  #
124
128
  # :allow_null :: Whether NULL is an allowed value for the column.
125
129
  # :db_type :: The database type for the column, as a database specific string.
@@ -152,9 +156,20 @@ module Sequel
152
156
  def schema(table, opts={})
153
157
  raise(Error, 'schema parsing is not implemented on this database') unless respond_to?(:schema_parse_table, true)
154
158
 
155
- sch, table_name = schema_and_table(table)
156
- quoted_name = quote_schema_table(table)
157
- opts = opts.merge(:schema=>sch) if sch && !opts.include?(:schema)
159
+ opts = opts.dup
160
+ if table.is_a?(Dataset)
161
+ o = table.opts
162
+ from = o[:from]
163
+ raise(Error, "can only parse the schema for a dataset with a single from table") unless from && from.length == 1 && !o.include?(:join) && !o.include?(:sql)
164
+ tab = table.first_source_table
165
+ sch, table_name = schema_and_table(tab)
166
+ quoted_name = table.literal(tab)
167
+ opts[:dataset] = table
168
+ else
169
+ sch, table_name = schema_and_table(table)
170
+ quoted_name = quote_schema_table(table)
171
+ end
172
+ opts[:schema] = sch if sch && !opts.include?(:schema)
158
173
 
159
174
  @schemas.delete(quoted_name) if opts[:reload]
160
175
  return @schemas[quoted_name] if @schemas[quoted_name]
@@ -199,6 +214,9 @@ module Sequel
199
214
  # :prepare :: A string to use as the transaction identifier for a
200
215
  # prepared transaction (two-phase commit), if the database/adapter
201
216
  # supports prepared transactions.
217
+ # :rollback :: Can the set to :reraise to reraise any Sequel::Rollback exceptions
218
+ # raised, or :always to always rollback even if no exceptions occur
219
+ # (useful for testing).
202
220
  # :server :: The server to use for the transaction.
203
221
  # :savepoint :: Whether to create a new savepoint for this transaction,
204
222
  # only respected if the database/adapter supports savepoints. By
@@ -225,40 +243,65 @@ module Sequel
225
243
  # not a Sequel::Rollback, the error will be reraised. If no exception occurs
226
244
  # inside the block, the transaction is commited.
227
245
  def _transaction(conn, opts={})
246
+ rollback = opts[:rollback]
228
247
  begin
229
- add_transaction
230
- t = begin_transaction(conn, opts)
231
- yield(conn)
248
+ add_transaction(conn, opts)
249
+ begin_transaction(conn, opts)
250
+ if rollback == :always
251
+ begin
252
+ yield(conn)
253
+ rescue Exception => e1
254
+ raise e1
255
+ ensure
256
+ raise ::Sequel::Rollback unless e1
257
+ end
258
+ else
259
+ yield(conn)
260
+ end
232
261
  rescue Exception => e
233
- rollback_transaction(t, opts) if t
234
- transaction_error(e, :conn=>conn)
262
+ rollback_transaction(conn, opts)
263
+ transaction_error(e, :conn=>conn, :rollback=>rollback)
235
264
  ensure
236
265
  begin
237
- commit_or_rollback_transaction(e, t, opts)
238
- rescue Exception => e
239
- raise_error(e, :classes=>database_error_classes, :conn=>conn)
266
+ committed = commit_or_rollback_transaction(e, conn, opts)
267
+ rescue Exception => e2
268
+ raise_error(e2, :classes=>database_error_classes, :conn=>conn)
240
269
  ensure
241
- remove_transaction(t)
270
+ remove_transaction(conn, committed)
242
271
  end
243
272
  end
244
273
  end
245
274
 
246
275
  # Add the current thread to the list of active transactions
247
- def add_transaction
248
- th = Thread.current
276
+ def add_transaction(conn, opts)
249
277
  if supports_savepoints?
250
- unless @transactions.include?(th)
251
- th[:sequel_transaction_depth] = 0
252
- @transactions << th
278
+ unless @transactions[conn]
279
+ @transactions[conn] = {:savepoint_level=>0}
280
+ @transactions[conn][:prepare] = opts[:prepare] if supports_prepared_transactions?
253
281
  end
254
282
  else
255
- @transactions << th
283
+ @transactions[conn] = {}
284
+ @transactions[conn][:prepare] = opts[:prepare] if supports_prepared_transactions?
256
285
  end
257
286
  end
258
287
 
288
+ # Call all stored after_commit blocks for the given transaction
289
+ def after_transaction_commit(conn)
290
+ if ary = @transactions[conn][:after_commit]
291
+ ary.each{|b| b.call}
292
+ end
293
+ end
294
+
295
+ # Call all stored after_rollback blocks for the given transaction
296
+ def after_transaction_rollback(conn)
297
+ if ary = @transactions[conn][:after_rollback]
298
+ ary.each{|b| b.call}
299
+ end
300
+ end
301
+
259
302
  # Whether the current thread/connection is already inside a transaction
260
303
  def already_in_transaction?(conn, opts)
261
- @transactions.include?(Thread.current) && (!supports_savepoints? || !opts[:savepoint])
304
+ @transactions.has_key?(conn) && (!supports_savepoints? || !opts[:savepoint])
262
305
  end
263
306
 
264
307
  # SQL to start a new savepoint
@@ -275,17 +318,16 @@ module Sequel
275
318
  # Start a new database transaction or a new savepoint on the given connection.
276
319
  def begin_transaction(conn, opts={})
277
320
  if supports_savepoints?
278
- th = Thread.current
279
- if (depth = th[:sequel_transaction_depth]) > 0
321
+ th = @transactions[conn]
322
+ if (depth = th[:savepoint_level]) > 0
280
323
  log_connection_execute(conn, begin_savepoint_sql(depth))
281
324
  else
282
325
  begin_new_transaction(conn, opts)
283
326
  end
284
- th[:sequel_transaction_depth] += 1
327
+ th[:savepoint_level] += 1
285
328
  else
286
329
  begin_new_transaction(conn, opts)
287
330
  end
288
- conn
289
331
  end
290
332
 
291
333
  # SQL to BEGIN a transaction.
@@ -348,12 +390,16 @@ module Sequel
348
390
  # Thread.current.status is checked because Thread#kill skips rescue
349
391
  # blocks (so exception would be nil), but the transaction should
350
392
  # still be rolled back.
351
- def commit_or_rollback_transaction(exception, thread, opts)
352
- unless exception
393
+ def commit_or_rollback_transaction(exception, conn, opts)
394
+ if exception
395
+ false
396
+ else
353
397
  if Thread.current.status == 'aborting'
354
- rollback_transaction(thread, opts)
398
+ rollback_transaction(conn, opts)
399
+ false
355
400
  else
356
- commit_transaction(thread, opts)
401
+ commit_transaction(conn, opts)
402
+ true
357
403
  end
358
404
  end
359
405
  end
@@ -361,8 +407,13 @@ module Sequel
361
407
  # Whether to commit the current transaction. On ruby 1.9 and JRuby,
362
408
  # transactions will be committed if Thread#kill is used on an thread
363
409
  # that has a transaction open, and there isn't a work around.
364
- def commit_or_rollback_transaction(exception, thread, opts)
365
- commit_transaction(thread, opts) unless exception
410
+ def commit_or_rollback_transaction(exception, conn, opts)
411
+ if exception
412
+ false
413
+ else
414
+ commit_transaction(conn, opts)
415
+ true
416
+ end
366
417
  end
367
418
  end
368
419
 
@@ -374,7 +425,7 @@ module Sequel
374
425
  # Commit the active transaction on the connection
375
426
  def commit_transaction(conn, opts={})
376
427
  if supports_savepoints?
377
- depth = Thread.current[:sequel_transaction_depth]
428
+ depth = @transactions[conn][:savepoint_level]
378
429
  log_connection_execute(conn, depth > 1 ? commit_savepoint_sql(depth-1) : commit_transaction_sql)
379
430
  else
380
431
  log_connection_execute(conn, commit_transaction_sql)
@@ -395,8 +446,8 @@ module Sequel
395
446
  # Return a Method object for the dataset's output_identifier_method.
396
447
  # Used in metadata parsing to make sure the returned information is in the
397
448
  # correct format.
398
- def input_identifier_meth
399
- dataset.method(:input_identifier)
449
+ def input_identifier_meth(ds=nil)
450
+ (ds || dataset).method(:input_identifier)
400
451
  end
401
452
 
402
453
  # Return a dataset that uses the default identifier input and output methods
@@ -413,24 +464,28 @@ module Sequel
413
464
  # Return a Method object for the dataset's output_identifier_method.
414
465
  # Used in metadata parsing to make sure the returned information is in the
415
466
  # correct format.
416
- def output_identifier_meth
417
- dataset.method(:output_identifier)
467
+ def output_identifier_meth(ds=nil)
468
+ (ds || dataset).method(:output_identifier)
418
469
  end
419
470
 
420
- # SQL to ROLLBACK a transaction.
421
- def rollback_transaction_sql
422
- SQL_ROLLBACK
423
- end
424
-
425
471
  # Remove the cached schema for the given schema name
426
472
  def remove_cached_schema(table)
427
473
  @schemas.delete(quote_schema_table(table)) if @schemas
428
474
  end
429
475
 
430
476
  # Remove the current thread from the list of active transactions
431
- def remove_transaction(conn)
432
- th = Thread.current
433
- @transactions.delete(th) if !supports_savepoints? || ((th[:sequel_transaction_depth] -= 1) <= 0)
477
+ def remove_transaction(conn, committed)
478
+ if !supports_savepoints? || ((@transactions[conn][:savepoint_level] -= 1) <= 0)
479
+ begin
480
+ if committed
481
+ after_transaction_commit(conn)
482
+ else
483
+ after_transaction_rollback(conn)
484
+ end
485
+ ensure
486
+ @transactions.delete(conn)
487
+ end
488
+ end
434
489
  end
435
490
 
436
491
  # SQL to rollback to a savepoint
@@ -441,13 +496,18 @@ module Sequel
441
496
  # Rollback the active transaction on the connection
442
497
  def rollback_transaction(conn, opts={})
443
498
  if supports_savepoints?
444
- depth = Thread.current[:sequel_transaction_depth]
499
+ depth = @transactions[conn][:savepoint_level]
445
500
  log_connection_execute(conn, depth > 1 ? rollback_savepoint_sql(depth-1) : rollback_transaction_sql)
446
501
  else
447
502
  log_connection_execute(conn, rollback_transaction_sql)
448
503
  end
449
504
  end
450
505
 
506
+ # SQL to ROLLBACK a transaction.
507
+ def rollback_transaction_sql
508
+ SQL_ROLLBACK
509
+ end
510
+
451
511
  # Match the database's column type to a ruby type via a
452
512
  # regular expression, and return the ruby type as a symbol
453
513
  # such as :integer or :string.
@@ -469,8 +529,8 @@ module Sequel
469
529
  :boolean
470
530
  when /\A(real|float|double( precision)?)\z/io
471
531
  :float
472
- when /\A(?:(?:(?:num(?:ber|eric)?|decimal)(?:\(\d+,\s*(\d+)\))?)|(?:small)?money)\z/io
473
- $1 && $1 == '0' ? :integer : :decimal
532
+ when /\A(?:(?:(?:num(?:ber|eric)?|decimal)(?:\(\d+,\s*(\d+|false|true)\))?)|(?:small)?money)\z/io
533
+ $1 && ['0', 'false'].include?($1) ? :integer : :decimal
474
534
  when /bytea|[bc]lob|image|(var)?binary/io
475
535
  :blob
476
536
  when /\Aenum/io
@@ -492,7 +552,11 @@ module Sequel
492
552
 
493
553
  # Raise a database error unless the exception is an Rollback.
494
554
  def transaction_error(e, opts={})
495
- raise_error(e, opts.merge(:classes=>database_error_classes)) unless e.is_a?(Rollback)
555
+ if e.is_a?(Rollback)
556
+ raise e if opts[:rollback] == :reraise
557
+ else
558
+ raise_error(e, opts.merge(:classes=>database_error_classes))
559
+ end
496
560
  end
497
561
  end
498
562
  end