sequel 3.28.0 → 3.29.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 (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