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
data/doc/sharding.rdoc CHANGED
@@ -10,7 +10,13 @@ that ship with Sequel.
10
10
 
11
11
  Both features use the :servers Database option. The :servers option should
12
12
  be a hash with symbol keys and values that are either hashes or procs that
13
- return hashes. Note that all servers should have the same schema for all
13
+ return hashes. These hashes are merged into the Database object's default
14
+ options hash to get the connection options for the shard, so you don't need
15
+ to override all options, just the ones that need to be modified. For example,
16
+ if you are using the same user, password, and database name and just the
17
+ host is changing, you only need a :host entry in each shard's hash.
18
+
19
+ Note that all servers should have the same schema for all
14
20
  tables you are accessing, unless you really know what you are doing.
15
21
 
16
22
  == Master and Slave Database Configurations
data/doc/testing.rdoc CHANGED
@@ -6,12 +6,14 @@ Whether or not you use Sequel in your application, you are usually going to want
6
6
 
7
7
  These run each test in its own transaction, the recommended way to test.
8
8
 
9
+ Make sure you are using Sequel 3.29.0 or above when using these examples, as older versions don't support the <tt>:rollback=>:always</tt> option.
10
+
9
11
  === RSpec 1
10
12
 
11
13
  class Spec::Example::ExampleGroup
12
14
  def execute(*args, &block)
13
15
  x = nil
14
- Sequel::Model.db.transaction{x = super(*args, &block); raise Sequel::Rollback}
16
+ Sequel::Model.db.transaction(:rollback=>:always){x = super(*args, &block)}
15
17
  x
16
18
  end
17
19
  end
@@ -20,7 +22,7 @@ These run each test in its own transaction, the recommended way to test.
20
22
 
21
23
  class Spec::Example::ExampleGroup
22
24
  around do |example|
23
- Sequel::Model.db.transaction{example.call; raise Sequel::Rollback}
25
+ Sequel::Model.db.transaction(:rollback=>:always){example.call}
24
26
  end
25
27
  end
26
28
 
@@ -29,13 +31,20 @@ These run each test in its own transaction, the recommended way to test.
29
31
  # Must use this class as the base class for your tests
30
32
  class SequelTestCase < Test::Unit::TestCase
31
33
  def run(*args, &block)
32
- Sequel::Model.db.transaction do
33
- super
34
- raise Sequel::Rollback
35
- end
34
+ Sequel::Model.db.transaction(:rollback=>:always){super}
36
35
  end
37
36
  end
38
37
 
38
+ == Transactional testing with multiple databases
39
+
40
+ You can use the Sequel.transaction method to run a transaction on multiple databases, rolling all of them back. Instead of:
41
+
42
+ Sequel::Model.db.transaction(:rollback=>:always)
43
+
44
+ Use Sequel.transaction with an array of databases:
45
+
46
+ Sequel.transaction([DB1, DB2, DB3], :rollback=>:always)
47
+
39
48
  == Nontransactional tests
40
49
 
41
50
  In some cases, it is not possible to use transactions. For example, if you are testing a web application that is running in a separate process, you don't have access to that process's database connections, so you can't run your examples in transactions. In that case, the best way to handle things is to cleanup after each test by deleting or truncating the database tables used in the test.
@@ -77,13 +86,13 @@ The +spec_plugin+ rake task runs the specs for the plugins and extensions that s
77
86
 
78
87
  == rake spec_<i>adapter</i> (e.g. rake spec_postgres)
79
88
 
80
- The <tt>spec_<i>adapter</i></tt> specs run against a real database connection with nothing mocked, and test for correct results. They are much slower than the standard specs, but they will catch errors that are mocked out by the default specs, as well show issues that only occur on a certain database, adapter, interpreter, or some combination of those.
89
+ The <tt>spec_<i>adapter</i></tt> specs run against a real database connection with nothing mocked, and test for correct results. They are slower than the standard specs, but they will catch errors that are mocked out by the default specs, as well as show issues that only occur on a certain database, adapter, or a combination of the two.
81
90
 
82
91
  These specs are broken down into two parts. For each database, there are specific specs that only apply to that database, and these are called the adapter specs. There are also shared specs that apply to all (or almost all) databases, these are called the integration specs.
83
92
 
84
93
  == Environment variables
85
94
 
86
- Sequel often uses environment variables when testing to specify either the database to be tested or specify how testing should be done.
95
+ Sequel often uses environment variables when testing to specify either the database to be tested or specify how testing should be done. You can also specify the databases to test by copying spec/spec_config.rb.example to spec/spec_config.rb and modifying it. See that file for details. It may be necessary to use spec_config.rb as opposed to an environment variable if you database connection cannot be specified by a connection string.
87
96
 
88
97
  === Connection Strings
89
98
 
@@ -101,6 +110,6 @@ The following environment variables specify Database connection URL strings:
101
110
 
102
111
  * SEQUEL_MSSQL_SPEC_REQUIRE: Separate file to require when running mssql adapter specs
103
112
  * SEQUEL_DB2_SPEC_REQUIRE: Separate file to require when running db2 adapter specs
104
- * SEQUEL_COLUMNS_INTROSPECTION: Whehter to run the specs with the columns_introspection extension loaded by default
113
+ * SEQUEL_COLUMNS_INTROSPECTION: Whether to run the specs with the columns_introspection extension loaded by default
105
114
  * SEQUEL_NO_PENDING: Don't mark any specs as pending, try running all specs
106
115
  * SKIPPED_TEST_WARN: Warn when skipping any tests because libraries aren't available
@@ -33,6 +33,46 @@ If any other exception is raised, the transaction is rolled back, and the except
33
33
  end # ROLLBACK
34
34
  # ArgumentError raised
35
35
 
36
+ If you want Sequel::Rollback exceptions to be reraised, use the <tt>:rollback => :reraise</tt> option:
37
+
38
+ DB.transaction(:rollback => :reraise) do # BEGIN
39
+ raise Sequel::Rollback
40
+ end # ROLLBACK
41
+ # Sequel::Rollback raised
42
+
43
+ If you always want to rollback (useful for testing), use the <tt>:rollback => :always</tt> option:
44
+
45
+ DB.transaction(:rollback => :always) do # BEGIN
46
+ DB[:foo].insert(1) # INSERT
47
+ end # ROLLBACK
48
+
49
+ If you want to check whether you are currently in a transaction, use the Database#in_transaction? method:
50
+
51
+ DB.in_transaction? # false
52
+ DB.transaction do
53
+ DB.in_transaction? # true
54
+ end
55
+
56
+ == Transaction Hooks
57
+
58
+ You can add hooks to an in progress transaction that are called after the transaction commits or rolls back:
59
+
60
+ x = nil
61
+ DB.transaction do
62
+ DB.after_commit{x = 1}
63
+ DB.after_rollback{x = 2}
64
+ x # nil
65
+ end
66
+ x # 1
67
+
68
+ x = nil
69
+ DB.transaction do
70
+ DB.after_commit{x = 1}
71
+ DB.after_rollback{x = 2}
72
+ raise Sequel::Rollback
73
+ end
74
+ x # 2
75
+
36
76
  == Nested Transaction Calls / Savepoints
37
77
 
38
78
  You can nest calls to transaction, which by default just reuses the existing transaction:
@@ -71,7 +111,7 @@ Other exceptions, unless rescued inside the outer transaction block, will rollba
71
111
 
72
112
  == Prepared Transactions / Two-Phase Commit
73
113
 
74
- Sequel supports database prepared transactions on PostreSQL, MySQL, and H2. With prepared transactions, at the end of the transaction, the transaction is not immediately committed (it acts like a rollback). Later, you can call +commit_prepared_transaction+ to commit the transaction or +rollback_prepared_transaction+ to roll the transaction back. Prepared transactions are usually used with distributed databases to make sure all databases commit the same transaction or none of them do.
114
+ Sequel supports database prepared transactions on PostgreSQL, MySQL, and H2. With prepared transactions, at the end of the transaction, the transaction is not immediately committed (it acts like a rollback). Later, you can call +commit_prepared_transaction+ to commit the transaction or +rollback_prepared_transaction+ to roll the transaction back. Prepared transactions are usually used with distributed databases to make sure all databases commit the same transaction or none of them do.
75
115
 
76
116
  To use prepared transactions in Sequel, you provide a string as the value of the :prepare option:
77
117
 
@@ -14,12 +14,14 @@ module Sequel
14
14
  when /Microsoft\.(Jet|ACE)\.OLEDB/io
15
15
  Sequel.ts_require 'adapters/shared/access'
16
16
  extend Sequel::Access::DatabaseMethods
17
+ extend_datasets(Sequel::Access::DatasetMethods)
17
18
  else
18
19
  @opts[:driver] ||= 'SQL Server'
19
20
  case @opts[:driver]
20
21
  when 'SQL Server'
21
22
  Sequel.ts_require 'adapters/ado/mssql'
22
23
  extend Sequel::ADO::MSSQL::DatabaseMethods
24
+ @dataset_class = ADO::MSSQL::Dataset
23
25
  set_mssql_unicode_strings
24
26
  end
25
27
  end
@@ -55,10 +57,6 @@ module Sequel
55
57
  handle
56
58
  end
57
59
 
58
- def dataset(opts = nil)
59
- ADO::Dataset.new(self, opts)
60
- end
61
-
62
60
  def execute(sql, opts={})
63
61
  synchronize(opts[:server]) do |conn|
64
62
  begin
@@ -77,18 +75,14 @@ module Sequel
77
75
  # The ADO adapter's default provider doesn't support transactions, since it
78
76
  # creates a new native connection for each query. So Sequel only attempts
79
77
  # to use transactions if an explicit :provider is given.
80
- def _transaction(conn, o={})
81
- return super if opts[:provider]
82
- th = Thread.current
83
- begin
84
- @transactions << th
85
- yield conn
86
- rescue Sequel::Rollback
87
- ensure
88
- @transactions.delete(th)
89
- end
78
+ def begin_transaction(conn, opts={})
79
+ super if @opts[:provider]
90
80
  end
91
-
81
+
82
+ def commit_transaction(conn, opts={})
83
+ super if @opts[:provider]
84
+ end
85
+
92
86
  def database_error_classes
93
87
  [::WIN32OLERuntimeError]
94
88
  end
@@ -100,13 +94,30 @@ module Sequel
100
94
  def disconnect_error?(e, opts)
101
95
  super || (e.is_a?(::WIN32OLERuntimeError) && e.message =~ DISCONNECT_ERROR_RE)
102
96
  end
97
+
98
+ def rollback_transaction(conn, opts={})
99
+ super if @opts[:provider]
100
+ end
103
101
  end
104
102
 
105
103
  class Dataset < Sequel::Dataset
104
+ Database::DatasetClass = self
105
+
106
106
  def fetch_rows(sql)
107
107
  execute(sql) do |s|
108
- @columns = cols = s.Fields.extend(Enumerable).map{|column| output_identifier(column.Name)}
109
- s.getRows.transpose.each{|r| yield cols.inject({}){|m,c| m[c] = r.shift; m}} unless s.eof
108
+ columns = cols = s.Fields.extend(Enumerable).map{|column| output_identifier(column.Name)}
109
+ if opts[:offset] && offset_returns_row_number_column?
110
+ rn = row_number_column
111
+ columns = columns.dup
112
+ columns.delete(rn)
113
+ end
114
+ @columns = columns
115
+ s.getRows.transpose.each do |r|
116
+ row = {}
117
+ cols.each{|c| row[c] = r.shift}
118
+ row.delete(rn) if rn
119
+ yield row
120
+ end unless s.eof
110
121
  end
111
122
  end
112
123
 
@@ -11,11 +11,6 @@ module Sequel
11
11
  # delete query.
12
12
  ROWS_AFFECTED = "SELECT @@ROWCOUNT AS AffectedRows"
13
13
 
14
- # Return instance of Sequel::ADO::MSSQL::Dataset with the given opts.
15
- def dataset(opts=nil)
16
- Sequel::ADO::MSSQL::Dataset.new(self, opts)
17
- end
18
-
19
14
  # Just execute so it doesn't attempt to return the number of rows modified.
20
15
  def execute_ddl(sql, opts={})
21
16
  execute(sql, opts)
@@ -37,11 +32,28 @@ module Sequel
37
32
  end
38
33
  end
39
34
  end
35
+
36
+ private
37
+
38
+ # The ADO adapter's default provider doesn't support transactions, since it
39
+ # creates a new native connection for each query. So Sequel only attempts
40
+ # to use transactions if an explicit :provider is given.
41
+ def begin_transaction(conn, opts={})
42
+ super if @opts[:provider]
43
+ end
44
+
45
+ def commit_transaction(conn, opts={})
46
+ super if @opts[:provider]
47
+ end
48
+
49
+ def rollback_transaction(conn, opts={})
50
+ super if @opts[:provider]
51
+ end
40
52
  end
41
53
 
42
54
  class Dataset < ADO::Dataset
43
55
  include Sequel::MSSQL::DatasetMethods
44
-
56
+
45
57
  # Use a nasty hack of multiple SQL statements in the same call and
46
58
  # having the last one return the most recently inserted id. This
47
59
  # is necessary as ADO's default :provider uses a separate native
@@ -14,6 +14,12 @@ module Sequel
14
14
  'float' => ['float', 'double', 'real', 'double precision'],
15
15
  'decimal' => %w'numeric decimal money'
16
16
  )
17
+
18
+ # Store the related database object, in order to be able to correctly
19
+ # handle the database timezone.
20
+ def initialize(db)
21
+ @db = db
22
+ end
17
23
 
18
24
  # Return blobs as instances of Sequel::SQL::Blob instead of
19
25
  # Amalgamite::Blob
@@ -29,7 +35,7 @@ module Sequel
29
35
 
30
36
  # Return datetime types as instances of Sequel.datetime_class
31
37
  def datetime(s)
32
- Sequel.database_to_application_timestamp(s)
38
+ @db.to_application_timestamp(s)
33
39
  end
34
40
 
35
41
  def time(s)
@@ -72,7 +78,8 @@ module Sequel
72
78
  opts[:database] = ':memory:' if blank_object?(opts[:database])
73
79
  db = ::Amalgalite::Database.new(opts[:database])
74
80
  db.busy_handler(::Amalgalite::BusyTimeout.new(opts.fetch(:timeout, 5000)/50, 50))
75
- db.type_map = SequelTypeMap.new
81
+ db.type_map = SequelTypeMap.new(self)
82
+ connection_pragmas.each{|s| log_yield(s){db.execute_batch(s)}}
76
83
  db
77
84
  end
78
85
 
@@ -81,11 +88,6 @@ module Sequel
81
88
  :sqlite
82
89
  end
83
90
 
84
- # Return instance of Sequel::Amalgalite::Dataset with the given options.
85
- def dataset(opts = nil)
86
- Amalgalite::Dataset.new(self, opts)
87
- end
88
-
89
91
  # Run the given SQL with the given arguments. Returns nil.
90
92
  def execute_ddl(sql, opts={})
91
93
  _execute(sql, opts){|conn| log_yield(sql){conn.execute_batch(sql)}}
@@ -155,6 +157,8 @@ module Sequel
155
157
  # Dataset class for SQLite datasets that use the amalgalite driver.
156
158
  class Dataset < Sequel::Dataset
157
159
  include ::Sequel::SQLite::DatasetMethods
160
+
161
+ Database::DatasetClass = self
158
162
 
159
163
  # Yield a hash for each row in the dataset.
160
164
  def fetch_rows(sql)
@@ -3,36 +3,60 @@ Sequel.require %w'shared/db2', 'adapters'
3
3
 
4
4
  module Sequel
5
5
  module DB2
6
+
7
+ @convert_smallint_to_bool = true
8
+
9
+ # Underlying error raised by Sequel, since ruby-db2 doesn't
10
+ # use exceptions.
11
+ class DB2Error < StandardError
12
+ end
13
+
14
+ class << self
15
+ # Whether to convert smallint values to bool, true by default.
16
+ # Can also be overridden per dataset.
17
+ attr_accessor :convert_smallint_to_bool
18
+ end
19
+
20
+ tt = Class.new do
21
+ def boolean(s) !s.to_i.zero? end
22
+ def date(s) Date.new(s.year, s.month, s.day) end
23
+ def time(s) Sequel::SQLTime.create(s.hour, s.minute, s.second) end
24
+ end.new
25
+
26
+ # Hash holding type translation methods, used by Dataset#fetch_rows.
27
+ DB2_TYPES = {
28
+ :boolean => tt.method(:boolean),
29
+ DB2CLI::SQL_BLOB => ::Sequel::SQL::Blob.method(:new),
30
+ DB2CLI::SQL_TYPE_DATE => tt.method(:date),
31
+ DB2CLI::SQL_TYPE_TIME => tt.method(:time),
32
+ DB2CLI::SQL_DECIMAL => ::BigDecimal.method(:new)
33
+ }
34
+ DB2_TYPES[DB2CLI::SQL_CLOB] = DB2_TYPES[DB2CLI::SQL_BLOB]
35
+
6
36
  class Database < Sequel::Database
7
37
  include DatabaseMethods
8
38
 
9
39
  set_adapter_scheme :db2
10
40
 
11
41
  TEMPORARY = 'GLOBAL TEMPORARY '.freeze
42
+ rc, NullHandle = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_ENV, DB2CLI::SQL_NULL_HANDLE)
12
43
 
13
- rc, @@env = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_ENV, DB2CLI::SQL_NULL_HANDLE)
14
- #check_error(rc, "Could not allocate DB2 environment")
44
+ # Hash of connection procs for converting
45
+ attr_reader :conversion_procs
46
+
47
+ def initialize(opts={})
48
+ super
49
+ @conversion_procs = DB2_TYPES.dup
50
+ @conversion_procs[DB2CLI::SQL_TYPE_TIMESTAMP] = method(:to_application_timestamp_db2)
51
+ end
15
52
 
16
53
  def connect(server)
17
54
  opts = server_opts(server)
18
- rc, dbc = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_DBC, @@env)
19
- check_error(rc, "Could not allocate database connection")
20
-
21
- rc = DB2CLI.SQLConnect(dbc, opts[:database], opts[:user], opts[:password])
22
- check_error(rc, "Could not connect to database")
23
-
55
+ dbc = checked_error("Could not allocate database connection"){DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_DBC, NullHandle)}
56
+ checked_error("Could not connect to database"){DB2CLI.SQLConnect(dbc, opts[:database], opts[:user], opts[:password])}
24
57
  dbc
25
58
  end
26
59
 
27
- def test_connection(server=nil)
28
- synchronize(server){|conn|}
29
- true
30
- end
31
-
32
- def dataset(opts = nil)
33
- DB2::Dataset.new(self, opts)
34
- end
35
-
36
60
  def execute(sql, opts={}, &block)
37
61
  synchronize(opts[:server]){|conn| log_connection_execute(conn, sql, &block)}
38
62
  end
@@ -43,11 +67,9 @@ module Sequel
43
67
  log_connection_execute(conn, sql)
44
68
  sql = "SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1"
45
69
  log_connection_execute(conn, sql) do |sth|
46
- rc, name, buflen, datatype, size, digits, nullable = DB2CLI.SQLDescribeCol(sth, 1, 256)
47
- check_error(rc, "Could not describe column")
48
- if (rc = DB2CLI.SQLFetch(sth)) != DB2CLI::SQL_NO_DATA_FOUND
49
- rc, v = DB2CLI.SQLGetData(sth, 1, datatype, size)
50
- check_error(rc, "Could not get data")
70
+ name, buflen, datatype, size, digits, nullable = checked_error("Could not describe column"){DB2CLI.SQLDescribeCol(sth, 1, 256)}
71
+ if DB2CLI.SQLFetch(sth) != DB2CLI::SQL_NO_DATA_FOUND
72
+ v, _ = checked_error("Could not get data"){DB2CLI.SQLGetData(sth, 1, datatype, size)}
51
73
  if v.is_a?(String)
52
74
  return v.to_i
53
75
  else
@@ -58,67 +80,101 @@ module Sequel
58
80
  end
59
81
  end
60
82
 
83
+ ERROR_MAP = {}
84
+ %w'SQL_INVALID_HANDLE SQL_STILL_EXECUTING SQL_ERROR'.each do |s|
85
+ ERROR_MAP[DB2CLI.const_get(s)] = s
86
+ end
61
87
  def check_error(rc, msg)
62
88
  case rc
63
- when DB2CLI::SQL_SUCCESS, DB2CLI::SQL_SUCCESS_WITH_INFO
89
+ when DB2CLI::SQL_SUCCESS, DB2CLI::SQL_SUCCESS_WITH_INFO, DB2CLI::SQL_NO_DATA_FOUND
64
90
  nil
91
+ when DB2CLI::SQL_INVALID_HANDLE, DB2CLI::SQL_STILL_EXECUTING
92
+ e = DB2Error.new("#{ERROR_MAP[rc]}: #{msg}")
93
+ e.set_backtrace(caller)
94
+ raise_error(e, :disconnect=>true)
65
95
  else
66
- raise DatabaseError, msg
96
+ e = DB2Error.new("#{ERROR_MAP[rc] || "Error code #{rc}"}: #{msg}")
97
+ e.set_backtrace(caller)
98
+ raise_error(e, :disconnect=>true)
67
99
  end
68
100
  end
69
101
 
102
+ def checked_error(msg)
103
+ rc, *ary= yield
104
+ check_error(rc, msg)
105
+ ary.length <= 1 ? ary.first : ary
106
+ end
107
+
108
+ def to_application_timestamp_db2(v)
109
+ to_application_timestamp(v.to_s)
110
+ end
111
+
70
112
  private
71
113
 
72
114
  def begin_transaction(conn, opts={})
73
115
  log_yield(TRANSACTION_BEGIN){DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_OFF)}
74
- conn
116
+ end
117
+
118
+ def remove_transaction(conn, committed)
119
+ DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_ON)
120
+ ensure
121
+ super
75
122
  end
76
123
 
77
124
  def rollback_transaction(conn, opts={})
78
125
  log_yield(TRANSACTION_ROLLBACK){DB2CLI.SQLEndTran(DB2CLI::SQL_HANDLE_DBC, conn, DB2CLI::SQL_ROLLBACK)}
79
- ensure
80
- DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_ON)
81
126
  end
82
127
 
83
128
  def commit_transaction(conn, opts={})
84
129
  log_yield(TRANSACTION_COMMIT){DB2CLI.SQLEndTran(DB2CLI::SQL_HANDLE_DBC, conn, DB2CLI::SQL_COMMIT)}
85
- ensure
86
- DB2CLI.SQLSetConnectAttr(conn, DB2CLI::SQL_ATTR_AUTOCOMMIT, DB2CLI::SQL_AUTOCOMMIT_ON)
87
130
  end
88
131
 
89
132
  def log_connection_execute(conn, sql)
90
- rc, sth = DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_STMT, conn)
91
- check_error(rc, "Could not allocate statement")
133
+ sth = checked_error("Could not allocate statement"){DB2CLI.SQLAllocHandle(DB2CLI::SQL_HANDLE_STMT, conn)}
92
134
 
93
135
  begin
94
- rc = log_yield(sql){DB2CLI.SQLExecDirect(sth, sql)}
95
- check_error(rc, "Could not execute statement: #{sql}")
136
+ checked_error("Could not execute statement: #{sql}"){log_yield(sql){DB2CLI.SQLExecDirect(sth, sql)}}
96
137
 
97
- yield(sth) if block_given?
98
-
99
- rc, rpc = DB2CLI.SQLRowCount(sth)
100
- check_error(rc, "Could not get RPC")
101
- rpc
138
+ if block_given?
139
+ yield(sth)
140
+ else
141
+ checked_error("Could not get RPC"){DB2CLI.SQLRowCount(sth)}
142
+ end
102
143
  ensure
103
- rc = DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_STMT, sth)
104
- check_error(rc, "Could not free statement")
144
+ checked_error("Could not free statement"){DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_STMT, sth)}
105
145
  end
106
146
  end
107
147
 
108
- def disconnect_connection(conn)
109
- rc = DB2CLI.SQLDisconnect(conn)
110
- check_error(rc, "Could not disconnect from database")
148
+ # Convert smallint type to boolean if convert_smallint_to_bool is true
149
+ def schema_column_type(db_type)
150
+ if DB2.convert_smallint_to_bool && db_type =~ /smallint/i
151
+ :boolean
152
+ else
153
+ super
154
+ end
155
+ end
111
156
 
112
- rc = DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_DBC, conn)
113
- check_error(rc, "Could not free Database handle")
157
+ def disconnect_connection(conn)
158
+ checked_error("Could not disconnect from database"){DB2CLI.SQLDisconnect(conn)}
159
+ checked_error("Could not free Database handle"){DB2CLI.SQLFreeHandle(DB2CLI::SQL_HANDLE_DBC, conn)}
114
160
  end
115
161
  end
116
162
 
117
163
  class Dataset < Sequel::Dataset
118
164
  include DatasetMethods
119
165
 
166
+ Database::DatasetClass = self
120
167
  MAX_COL_SIZE = 256
121
168
 
169
+ # Whether to convert smallint to boolean arguments for this dataset.
170
+ # Defaults to the DB2 module setting.
171
+ def convert_smallint_to_bool
172
+ defined?(@convert_smallint_to_bool) ? @convert_smallint_to_bool : (@convert_smallint_to_bool = DB2.convert_smallint_to_bool)
173
+ end
174
+
175
+ # Override the default DB2.convert_smallint_to_bool setting for this dataset.
176
+ attr_writer :convert_smallint_to_bool
177
+
122
178
  def fetch_rows(sql)
123
179
  execute(sql) do |sth|
124
180
  offset = @opts[:offset]
@@ -128,13 +184,19 @@ module Sequel
128
184
  cols = column_info.map{|c| c.at(1)}
129
185
  cols.delete(row_number_column) if offset
130
186
  @columns = cols
131
- while (rc = DB2CLI.SQLFetch(sth)) != DB2CLI::SQL_NO_DATA_FOUND
187
+ errors = [DB2CLI::SQL_NO_DATA_FOUND, DB2CLI::SQL_ERROR]
188
+ until errors.include?(rc = DB2CLI.SQLFetch(sth))
132
189
  db.check_error(rc, "Could not fetch row")
133
190
  row = {}
134
- column_info.each do |i, c, t, s|
135
- rc, v = DB2CLI.SQLGetData(sth, i, t, s)
136
- db.check_error(rc, "Could not get data")
137
- row[c] = convert_type(v)
191
+ column_info.each do |i, c, t, s, pr|
192
+ v, _ = db.checked_error("Could not get data"){DB2CLI.SQLGetData(sth, i, t, s)}
193
+ row[c] = if v == DB2CLI::Null
194
+ nil
195
+ elsif pr
196
+ pr.call(v)
197
+ else
198
+ v
199
+ end
138
200
  end
139
201
  row.delete(row_number_column) if offset
140
202
  yield row
@@ -147,30 +209,20 @@ module Sequel
147
209
 
148
210
  def get_column_info(sth)
149
211
  db = @db
150
- rc, column_count = DB2CLI.SQLNumResultCols(sth)
151
- db.check_error(rc, "Could not get number of result columns")
212
+ column_count = db.checked_error("Could not get number of result columns"){DB2CLI.SQLNumResultCols(sth)}
213
+ convert = convert_smallint_to_bool
214
+ cps = db.conversion_procs
152
215
 
153
216
  (1..column_count).map do |i|
154
- rc, name, buflen, datatype, size, digits, nullable = DB2CLI.SQLDescribeCol(sth, i, MAX_COL_SIZE)
155
- db.check_error(rc, "Could not describe column")
156
- [i, output_identifier(name), datatype, size]
217
+ name, buflen, datatype, size, digits, nullable = db.checked_error("Could not describe column"){DB2CLI.SQLDescribeCol(sth, i, MAX_COL_SIZE)}
218
+ pr = if datatype == DB2CLI::SQL_SMALLINT && convert && size <= 5 && digits <= 1
219
+ cps[:boolean]
220
+ else
221
+ cps[datatype]
222
+ end
223
+ [i, output_identifier(name), datatype, size, pr]
157
224
  end
158
225
  end
159
-
160
- def convert_type(v)
161
- case v
162
- when DB2CLI::Date
163
- Date.new(v.year, v.month, v.day)
164
- when DB2CLI::Time
165
- Sequel::SQLTime.create(v.hour, v.minute, v.second)
166
- when DB2CLI::Timestamp
167
- Sequel.database_to_application_timestamp(v.to_s)
168
- when DB2CLI::Null
169
- nil
170
- else
171
- v
172
- end
173
- end
174
226
  end
175
227
  end
176
228
  end