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
@@ -14,9 +14,6 @@ module Sequel
14
14
  def boolean(s) s.to_i != 0 end
15
15
  def integer(s) s.to_i end
16
16
  def float(s) s.to_f end
17
- def date(s) ::Sequel::MySQL.convert_date_time(:string_to_date, s) end
18
- def time(s) ::Sequel::MySQL.convert_date_time(:string_to_time, s) end
19
- def timestamp(s) ::Sequel::MySQL.convert_date_time(:database_to_application_timestamp, s) end
20
17
  end.new
21
18
 
22
19
  # Hash with integer keys and callable values for converting MySQL types.
@@ -30,52 +27,12 @@ module Sequel
30
27
  k.each{|n| MYSQL_TYPES[n] = v}
31
28
  end
32
29
 
33
- # Modify the type translator used for the tinyint type based
34
- # on the value given.
35
- def self.convert_tinyint_to_bool=(v)
36
- MYSQL_TYPES[1] = TYPE_TRANSLATOR.method(v ? :boolean : :integer)
37
- @convert_tinyint_to_bool = v
38
- end
39
- self.convert_tinyint_to_bool = convert_tinyint_to_bool
40
-
41
30
  class << self
42
- # By default, Sequel raises an exception if in invalid date or time is used.
43
- # However, if this is set to nil or :nil, the adapter treats dates
44
- # like 0000-00-00 and times like 838:00:00 as nil values. If set to :string,
45
- # it returns the strings as is.
46
- attr_reader :convert_invalid_date_time
47
- end
48
-
49
- # Modify the type translators for the date, time, and timestamp types
50
- # depending on the value given.
51
- def self.convert_invalid_date_time=(v)
52
- MYSQL_TYPES[11] = (v != false) ? TYPE_TRANSLATOR.method(:time) : ::Sequel.method(:string_to_time)
53
- m = (v != false) ? TYPE_TRANSLATOR.method(:date) : ::Sequel.method(:string_to_date)
54
- [10, 14].each{|i| MYSQL_TYPES[i] = m}
55
- m = (v != false) ? TYPE_TRANSLATOR.method(:timestamp) : ::Sequel.method(:database_to_application_timestamp)
56
- [7, 12].each{|i| MYSQL_TYPES[i] = m}
57
- @convert_invalid_date_time = v
31
+ # Whether to convert invalid date time values by default.
32
+ attr_accessor :convert_invalid_date_time
58
33
  end
59
34
  self.convert_invalid_date_time = false
60
35
 
61
- # If convert_invalid_date_time is nil, :nil, or :string and
62
- # the conversion raises an InvalidValue exception, return v
63
- # if :string and nil otherwise.
64
- def self.convert_date_time(meth, v)
65
- begin
66
- Sequel.send(meth, v)
67
- rescue InvalidValue
68
- case @convert_invalid_date_time
69
- when nil, :nil
70
- nil
71
- when :string
72
- v
73
- else
74
- raise
75
- end
76
- end
77
- end
78
-
79
36
  # Database class for MySQL databases used with Sequel.
80
37
  class Database < Sequel::Database
81
38
  include Sequel::MySQL::DatabaseMethods
@@ -85,7 +42,26 @@ module Sequel
85
42
  MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away|Lost connection to MySQL server during query)/
86
43
 
87
44
  set_adapter_scheme :mysql
45
+
46
+ # Hash of conversion procs for the current database
47
+ attr_reader :conversion_procs
48
+ #
49
+ # Whether to convert tinyint columns to bool for the current database
50
+ attr_reader :convert_tinyint_to_bool
51
+
52
+ # By default, Sequel raises an exception if in invalid date or time is used.
53
+ # However, if this is set to nil or :nil, the adapter treats dates
54
+ # like 0000-00-00 and times like 838:00:00 as nil values. If set to :string,
55
+ # it returns the strings as is.
56
+ attr_reader :convert_invalid_date_time
88
57
 
58
+ def initialize(opts={})
59
+ super
60
+ @conversion_procs = MYSQL_TYPES.dup
61
+ self.convert_tinyint_to_bool = Sequel::MySQL.convert_tinyint_to_bool
62
+ self.convert_invalid_date_time = Sequel::MySQL.convert_invalid_date_time
63
+ end
64
+
89
65
  # Connect to the database. In addition to the usual database options,
90
66
  # the following options have effect:
91
67
  #
@@ -155,11 +131,27 @@ module Sequel
155
131
  conn
156
132
  end
157
133
 
158
- # Returns instance of Sequel::MySQL::Dataset with the given options.
159
- def dataset(opts = nil)
160
- MySQL::Dataset.new(self, opts)
134
+ # Modify the type translators for the date, time, and timestamp types
135
+ # depending on the value given.
136
+ def convert_invalid_date_time=(v)
137
+ m0 = ::Sequel.method(:string_to_time)
138
+ @conversion_procs[11] = (v != false) ? lambda{|v| convert_date_time(v, &m0)} : m0
139
+ m1 = ::Sequel.method(:string_to_date)
140
+ m = (v != false) ? lambda{|v| convert_date_time(v, &m1)} : m1
141
+ [10, 14].each{|i| @conversion_procs[i] = m}
142
+ m2 = method(:to_application_timestamp)
143
+ m = (v != false) ? lambda{|v| convert_date_time(v, &m2)} : m2
144
+ [7, 12].each{|i| @conversion_procs[i] = m}
145
+ @convert_invalid_date_time = v
161
146
  end
162
-
147
+
148
+ # Modify the type translator used for the tinyint type based
149
+ # on the value given.
150
+ def convert_tinyint_to_bool=(v)
151
+ @conversion_procs[1] = TYPE_TRANSLATOR.method(v ? :boolean : :integer)
152
+ @convert_tinyint_to_bool = v
153
+ end
154
+
163
155
  # Return the version of the MySQL server two which we are connecting.
164
156
  def server_version(server=nil)
165
157
  @server_version ||= (synchronize(server){|conn| conn.server_version if conn.respond_to?(:server_version)} || super)
@@ -219,6 +211,24 @@ module Sequel
219
211
  :query
220
212
  end
221
213
 
214
+ # If convert_invalid_date_time is nil, :nil, or :string and
215
+ # the conversion raises an InvalidValue exception, return v
216
+ # if :string and nil otherwise.
217
+ def convert_date_time(v)
218
+ begin
219
+ yield v
220
+ rescue InvalidValue
221
+ case @convert_invalid_date_time
222
+ when nil, :nil
223
+ nil
224
+ when :string
225
+ v
226
+ else
227
+ raise
228
+ end
229
+ end
230
+ end
231
+
222
232
  # The MySQL adapter main error class is Mysql::Error
223
233
  def database_error_classes
224
234
  [Mysql::Error]
@@ -245,7 +255,7 @@ module Sequel
245
255
 
246
256
  # Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
247
257
  def schema_column_type(db_type)
248
- Sequel::MySQL.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
258
+ convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
249
259
  end
250
260
  end
251
261
 
@@ -253,6 +263,8 @@ module Sequel
253
263
  class Dataset < Sequel::Dataset
254
264
  include Sequel::MySQL::DatasetMethods
255
265
  include Sequel::MySQL::PreparedStatements::DatasetMethods
266
+
267
+ Database::DatasetClass = self
256
268
 
257
269
  # Delete rows matching this dataset
258
270
  def delete
@@ -265,11 +277,12 @@ module Sequel
265
277
  def fetch_rows(sql, &block)
266
278
  execute(sql) do |r|
267
279
  i = -1
280
+ cps = db.conversion_procs
268
281
  cols = r.fetch_fields.map do |f|
269
282
  # Pretend tinyint is another integer type if its length is not 1, to
270
283
  # avoid casting to boolean if Sequel::MySQL.convert_tinyint_to_bool
271
284
  # is set.
272
- type_proc = f.type == 1 && f.length != 1 ? MYSQL_TYPES[2] : MYSQL_TYPES[f.type]
285
+ type_proc = f.type == 1 && f.length != 1 ? cps[2] : cps[f.type]
273
286
  [output_identifier(f.name), type_proc, i+=1]
274
287
  end
275
288
  @columns = cols.map{|c| c.first}
@@ -10,9 +10,18 @@ module Sequel
10
10
  include Sequel::MySQL::PreparedStatements::DatabaseMethods
11
11
 
12
12
  # Mysql::Error messages that indicate the current connection should be disconnected
13
- MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away)/
13
+ MYSQL_DATABASE_DISCONNECT_ERRORS = /\A(Commands out of sync; you can't run this command now|Can't connect to local MySQL server through socket|MySQL server has gone away|This connection is still waiting for a result, try again once you have the result)/
14
14
 
15
15
  set_adapter_scheme :mysql2
16
+
17
+ # Whether to convert tinyint columns to bool for this database
18
+ attr_accessor :convert_tinyint_to_bool
19
+
20
+ # Set the convert_tinyint_to_bool setting based on the default value.
21
+ def initialize(opts={})
22
+ super
23
+ self.convert_tinyint_to_bool = Sequel::MySQL.convert_tinyint_to_bool
24
+ end
16
25
 
17
26
  # Connect to the database. In addition to the usual database options,
18
27
  # the following options have effect:
@@ -60,11 +69,6 @@ module Sequel
60
69
  conn
61
70
  end
62
71
 
63
- # Returns instance of Sequel::MySQL::Dataset with the given options.
64
- def dataset(opts = nil)
65
- Mysql2::Dataset.new(self, opts)
66
- end
67
-
68
72
  # Return the version of the MySQL server two which we are connecting.
69
73
  def server_version(server=nil)
70
74
  @server_version ||= (synchronize(server){|conn| conn.server_info[:id]} || super)
@@ -77,7 +81,7 @@ module Sequel
77
81
  # yield the connection if a block is given.
78
82
  def _execute(conn, sql, opts)
79
83
  begin
80
- r = log_yield(sql){conn.query(sql, :symbolize_keys => true, :database_timezone => Sequel.database_timezone, :application_timezone => Sequel.application_timezone)}
84
+ r = log_yield(sql){conn.query(sql, :symbolize_keys => true, :database_timezone => timezone, :application_timezone => Sequel.application_timezone)}
81
85
  if opts[:type] == :select
82
86
  yield r if r
83
87
  elsif block_given?
@@ -115,7 +119,7 @@ module Sequel
115
119
 
116
120
  # Convert tinyint(1) type to boolean if convert_tinyint_to_bool is true
117
121
  def schema_column_type(db_type)
118
- Sequel::MySQL.convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
122
+ convert_tinyint_to_bool && db_type == 'tinyint(1)' ? :boolean : super
119
123
  end
120
124
  end
121
125
 
@@ -124,6 +128,8 @@ module Sequel
124
128
  include Sequel::MySQL::DatasetMethods
125
129
  include Sequel::MySQL::PreparedStatements::DatasetMethods
126
130
 
131
+ Database::DatasetClass = self
132
+
127
133
  # Delete rows matching this dataset
128
134
  def delete
129
135
  execute_dui(delete_sql){|c| return c.affected_rows}
@@ -133,7 +139,7 @@ module Sequel
133
139
  def fetch_rows(sql, &block)
134
140
  execute(sql) do |r|
135
141
  @columns = r.fields
136
- r.each(:cast_booleans => Sequel::MySQL.convert_tinyint_to_bool, &block)
142
+ r.each(:cast_booleans => db.convert_tinyint_to_bool, &block)
137
143
  end
138
144
  self
139
145
  end
@@ -15,13 +15,16 @@ module Sequel
15
15
  when 'mssql'
16
16
  Sequel.ts_require 'adapters/odbc/mssql'
17
17
  extend Sequel::ODBC::MSSQL::DatabaseMethods
18
+ @dataset_class = Sequel::ODBC::MSSQL::Dataset
18
19
  set_mssql_unicode_strings
19
20
  when 'progress'
20
21
  Sequel.ts_require 'adapters/shared/progress'
21
22
  extend Sequel::Progress::DatabaseMethods
23
+ extend_datasets(Sequel::Progress::DatasetMethods)
22
24
  when 'db2'
23
25
  Sequel.ts_require 'adapters/odbc/db2'
24
26
  extend Sequel::ODBC::DB2::DatabaseMethods
27
+ @dataset_class = Sequel::ODBC::DB2::Dataset
25
28
  end
26
29
  end
27
30
 
@@ -45,10 +48,6 @@ module Sequel
45
48
  conn
46
49
  end
47
50
 
48
- def dataset(opts = nil)
49
- ODBC::Dataset.new(self, opts)
50
- end
51
-
52
51
  def execute(sql, opts={})
53
52
  synchronize(opts[:server]) do |conn|
54
53
  begin
@@ -99,15 +98,23 @@ module Sequel
99
98
  ODBC_DATE_FORMAT = "{d '%Y-%m-%d'}".freeze
100
99
  TIMESTAMP_FORMAT="{ts '%Y-%m-%d %H:%M:%S'}".freeze
101
100
 
101
+ Database::DatasetClass = self
102
+
102
103
  def fetch_rows(sql)
103
104
  execute(sql) do |s|
104
105
  i = -1
105
106
  cols = s.columns(true).map{|c| [output_identifier(c.name), i+=1]}
106
- @columns = cols.map{|c| c.at(0)}
107
+ columns = cols.map{|c| c.at(0)}
108
+ if opts[:offset] && offset_returns_row_number_column?
109
+ rn = row_number_column
110
+ columns.delete(rn)
111
+ end
112
+ @columns = columns
107
113
  if rows = s.fetch_all
108
114
  rows.each do |row|
109
115
  hash = {}
110
116
  cols.each{|n,i| hash[n] = convert_odbc_value(row[i])}
117
+ hash.delete(rn) if rn
111
118
  yield hash
112
119
  end
113
120
  end
@@ -126,7 +133,7 @@ module Sequel
126
133
  # ODBCColumn#mapSqlTypeToGenericType and Column#klass.
127
134
  case v
128
135
  when ::ODBC::TimeStamp
129
- Sequel.database_to_application_timestamp([v.year, v.month, v.day, v.hour, v.minute, v.second])
136
+ db.to_application_timestamp([v.year, v.month, v.day, v.hour, v.minute, v.second])
130
137
  when ::ODBC::Time
131
138
  Sequel::SQLTime.create(v.hour, v.minute, v.second)
132
139
  when ::ODBC::Date
@@ -7,10 +7,6 @@ module Sequel
7
7
  module DB2
8
8
  module DatabaseMethods
9
9
  include ::Sequel::DB2::DatabaseMethods
10
-
11
- def dataset(opts=nil)
12
- Sequel::ODBC::DB2::Dataset.new(self, opts)
13
- end
14
10
  end
15
11
 
16
12
  class Dataset < ODBC::Dataset
@@ -9,11 +9,6 @@ module Sequel
9
9
  include Sequel::MSSQL::DatabaseMethods
10
10
  LAST_INSERT_ID_SQL='SELECT SCOPE_IDENTITY()'
11
11
 
12
- # Return an instance of Sequel::ODBC::MSSQL::Dataset with the given opts.
13
- def dataset(opts=nil)
14
- Sequel::ODBC::MSSQL::Dataset.new(self, opts)
15
- end
16
-
17
12
  # Return the last inserted identity value.
18
13
  def execute_insert(sql, opts={})
19
14
  synchronize(opts[:server]) do |conn|
@@ -15,10 +15,6 @@ module Sequel
15
15
  )
16
16
  end
17
17
 
18
- def dataset(opts = nil)
19
- OpenBase::Dataset.new(self, opts)
20
- end
21
-
22
18
  def execute(sql, opts={})
23
19
  synchronize(opts[:server]) do |conn|
24
20
  r = log_yield(sql){conn.execute(sql)}
@@ -37,6 +33,8 @@ module Sequel
37
33
 
38
34
  class Dataset < Sequel::Dataset
39
35
  SELECT_CLAUSE_METHODS = clause_methods(:select, %w'distinct columns from join where group having compounds order limit')
36
+
37
+ Database::DatasetClass = self
40
38
 
41
39
  def fetch_rows(sql)
42
40
  execute(sql) do |result|
@@ -13,6 +13,18 @@ module Sequel
13
13
  # ORA-03114: not connected to ORACLE
14
14
  CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
15
15
 
16
+ ORACLE_TYPES = {:blob=>lambda{|b| Sequel::SQL::Blob.new(b.read)}}
17
+
18
+ # Hash of conversion procs for this database.
19
+ attr_reader :conversion_procs
20
+
21
+ def initialize(opts={})
22
+ super
23
+ @autosequence = opts[:autosequence]
24
+ @primary_key_sequences = {}
25
+ @conversion_procs = ORACLE_TYPES.dup
26
+ end
27
+
16
28
  def connect(server)
17
29
  opts = server_opts(server)
18
30
  if opts[:database]
@@ -22,6 +34,7 @@ module Sequel
22
34
  dbname = opts[:host]
23
35
  end
24
36
  conn = OCI8.new(opts[:user], opts[:password], dbname, opts[:privilege])
37
+ conn.prefetch_rows = typecast_value_integer(opts[:prefetch_rows]) if opts[:prefetch_rows]
25
38
  conn.autocommit = true
26
39
  conn.non_blocking = true
27
40
 
@@ -36,59 +49,129 @@ module Sequel
36
49
  conn.exec("ALTER SESSION SET TIME_ZONE='-00:00'")
37
50
  end
38
51
 
52
+ class << conn
53
+ attr_reader :prepared_statements
54
+ end
55
+ conn.instance_variable_set(:@prepared_statements, {})
56
+
39
57
  conn
40
58
  end
41
-
42
- def dataset(opts = nil)
43
- Oracle::Dataset.new(self, opts)
59
+
60
+ def execute(sql, opts={}, &block)
61
+ _execute(nil, sql, opts, &block)
44
62
  end
63
+ alias do execute
45
64
 
46
- def schema_parse_table(table, opts={})
47
- ds = dataset
48
- ds.identifier_output_method = :downcase
49
- schema_and_table = "#{"#{quote_identifier(opts[:schema])}." if opts[:schema]}#{quote_identifier(table)}"
50
- table_schema = []
51
- metadata = transaction(opts){|conn| conn.describe_table(schema_and_table)}
52
- metadata.columns.each do |column|
53
- table_schema << [
54
- column.name.downcase.to_sym,
55
- {
56
- :type => column.data_type,
57
- :db_type => column.type_string.split(' ')[0],
58
- :type_string => column.type_string,
59
- :charset_form => column.charset_form,
60
- :char_used => column.char_used?,
61
- :char_size => column.char_size,
62
- :data_size => column.data_size,
63
- :precision => column.precision,
64
- :scale => column.scale,
65
- :fsprecision => column.fsprecision,
66
- :lfprecision => column.lfprecision,
67
- :allow_null => column.nullable?
68
- }
69
- ]
70
- end
71
- table_schema
65
+ def execute_insert(sql, opts={})
66
+ _execute(:insert, sql, opts)
72
67
  end
73
68
 
74
- def execute(sql, opts={})
69
+ private
70
+
71
+ def _execute(type, sql, opts={}, &block)
75
72
  synchronize(opts[:server]) do |conn|
76
73
  begin
77
- r = log_yield(sql){conn.exec(sql)}
78
- yield(r) if block_given?
79
- r
80
- rescue OCIException => e
74
+ return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
75
+ if args = opts[:arguments]
76
+ r = conn.parse(sql)
77
+ args = cursor_bind_params(conn, r, args)
78
+ nr = log_yield(sql, args){r.exec}
79
+ r = nr unless block_given?
80
+ else
81
+ r = log_yield(sql){conn.exec(sql)}
82
+ end
83
+ if block_given?
84
+ begin
85
+ yield(r)
86
+ ensure
87
+ r.close
88
+ end
89
+ elsif type == :insert
90
+ last_insert_id(conn, opts)
91
+ else
92
+ r
93
+ end
94
+ rescue OCIException, RuntimeError => e
95
+ # ruby-oci8 is naughty and raises strings in some places
81
96
  raise_error(e)
82
97
  end
83
98
  end
84
99
  end
85
- alias_method :do, :execute
86
100
 
87
- private
88
-
101
+ PS_TYPES = {'string'.freeze=>String, 'integer'.freeze=>Integer, 'float'.freeze=>Float,
102
+ 'decimal'.freeze=>Float, 'date'.freeze=>Time, 'datetime'.freeze=>Time,
103
+ 'time'.freeze=>Time, 'boolean'.freeze=>String, 'blob'.freeze=>OCI8::BLOB}
104
+ def cursor_bind_params(conn, cursor, args)
105
+ cursor
106
+ i = 0
107
+ args.map do |arg, type|
108
+ i += 1
109
+ case arg
110
+ when true
111
+ arg = 'Y'
112
+ when false
113
+ arg = 'N'
114
+ when BigDecimal
115
+ arg = arg.to_f
116
+ when ::Sequel::SQL::Blob
117
+ raise Error, "Sequel's oracle adapter does not currently support using a blob in a bound variable"
118
+ end
119
+ if t = PS_TYPES[type]
120
+ cursor.bind_param(i, arg, t)
121
+ else
122
+ cursor.bind_param(i, arg, arg.class)
123
+ end
124
+ arg
125
+ end
126
+ end
127
+
128
+ def execute_prepared_statement(conn, type, name, opts)
129
+ ps = prepared_statements[name]
130
+ sql = ps.prepared_sql
131
+ if cursora = conn.prepared_statements[name]
132
+ cursor, cursor_sql = cursora
133
+ if cursor_sql != sql
134
+ cursor.close
135
+ cursor = nil
136
+ end
137
+ end
138
+ unless cursor
139
+ cursor = log_yield("Preparing #{name}: #{sql}"){conn.parse(sql)}
140
+ conn.prepared_statements[name] = [cursor, sql]
141
+ end
142
+ args = cursor_bind_params(conn, cursor, opts[:arguments])
143
+ r = log_yield("Executing #{name}", args){cursor.exec}
144
+ if block_given?
145
+ yield(cursor)
146
+ elsif type == :insert
147
+ last_insert_id(conn, opts)
148
+ else
149
+ r
150
+ end
151
+ end
152
+
153
+ def last_insert_id(conn, opts)
154
+ unless sequence = opts[:sequence]
155
+ if t = opts[:table]
156
+ sequence = sequence_for_table(t)
157
+ end
158
+ end
159
+ if sequence
160
+ sql = "SELECT #{literal(sequence)}.currval FROM dual"
161
+ begin
162
+ cursor = log_yield(sql){conn.exec(sql)}
163
+ row = cursor.fetch
164
+ row.each{|v| return (v.to_i if v)}
165
+ rescue OCIError
166
+ nil
167
+ ensure
168
+ cursor.close if cursor
169
+ end
170
+ end
171
+ end
172
+
89
173
  def begin_transaction(conn, opts={})
90
174
  log_yield(TRANSACTION_BEGIN){conn.autocommit = false}
91
- conn
92
175
  end
93
176
 
94
177
  def commit_transaction(conn, opts={})
@@ -102,44 +185,239 @@ module Sequel
102
185
  end
103
186
 
104
187
  def disconnect_error?(e, opts)
105
- super || (e.is_a?(::OCIException) && CONNECTION_ERROR_CODES.include?(e.code))
188
+ super || (e.is_a?(::OCIError) && CONNECTION_ERROR_CODES.include?(e.code))
106
189
  end
107
190
 
108
- def remove_transaction(conn)
109
- conn.autocommit = true if conn
191
+ def oracle_column_type(h)
192
+ case h[:oci8_type]
193
+ when :number
194
+ case h[:scale]
195
+ when 0
196
+ :integer
197
+ when -127
198
+ :float
199
+ else
200
+ :decimal
201
+ end
202
+ when :date
203
+ :datetime
204
+ else
205
+ schema_column_type(h[:db_type])
206
+ end
207
+ end
208
+
209
+ def remove_transaction(conn, committed)
210
+ conn.autocommit = true
211
+ ensure
110
212
  super
111
213
  end
112
214
 
113
215
  def rollback_transaction(conn, opts={})
114
216
  log_yield(TRANSACTION_ROLLBACK){conn.rollback}
115
217
  end
218
+
219
+ def schema_parse_table(table, opts={})
220
+ schema, table = schema_and_table(table)
221
+ schema ||= opts[:schema]
222
+ schema_and_table = if ds = opts[:dataset]
223
+ ds.literal(schema ? SQL::QualifiedIdentifier.new(schema, table) : SQL::Identifier.new(table))
224
+ else
225
+ "#{"#{quote_identifier(schema)}." if schema}#{quote_identifier(table)}"
226
+ end
227
+ table_schema = []
228
+ m = output_identifier_meth(ds)
229
+ im = input_identifier_meth(ds)
230
+
231
+ # Primary Keys
232
+ ds = metadata_dataset.from(:all_constraints___cons, :all_cons_columns___cols).
233
+ where(:cols__table_name=>im.call(table), :cons__constraint_type=>'P',
234
+ :cons__constraint_name=>:cols__constraint_name, :cons__owner=>:cols__owner)
235
+ ds = ds.where(:cons__owner=>im.call(schema)) if schema
236
+ pks = ds.select_map(:cols__column_name)
237
+
238
+ # Default values
239
+ defaults = metadata_dataset.from(:dba_tab_cols).
240
+ where(:table_name=>im.call(table)).
241
+ to_hash(:column_name, :data_default)
242
+
243
+ metadata = synchronize(opts[:server]) do |conn|
244
+ begin
245
+ log_yield("Connection.describe_table"){conn.describe_table(schema_and_table)}
246
+ rescue OCIError => e
247
+ raise_error(e)
248
+ end
249
+ end
250
+ metadata.columns.each do |column|
251
+ h = {
252
+ :primary_key => pks.include?(column.name),
253
+ :default => defaults[column.name],
254
+ :oci8_type => column.data_type,
255
+ :db_type => column.type_string.split(' ')[0],
256
+ :type_string => column.type_string,
257
+ :charset_form => column.charset_form,
258
+ :char_used => column.char_used?,
259
+ :char_size => column.char_size,
260
+ :data_size => column.data_size,
261
+ :precision => column.precision,
262
+ :scale => column.scale,
263
+ :fsprecision => column.fsprecision,
264
+ :lfprecision => column.lfprecision,
265
+ :allow_null => column.nullable?
266
+ }
267
+ h[:type] = oracle_column_type(h)
268
+ table_schema << [m.call(column.name), h]
269
+ end
270
+ table_schema
271
+ end
116
272
  end
117
273
 
118
274
  class Dataset < Sequel::Dataset
119
275
  include DatasetMethods
120
276
 
277
+ Database::DatasetClass = self
278
+
279
+ PREPARED_ARG_PLACEHOLDER = ':'.freeze
280
+
281
+ # Oracle already supports named bind arguments, so use directly.
282
+ module ArgumentMapper
283
+ include Sequel::Dataset::ArgumentMapper
284
+
285
+ protected
286
+
287
+ # Return a hash with the same values as the given hash,
288
+ # but with the keys converted to strings.
289
+ def map_to_prepared_args(bind_vars)
290
+ prepared_args.map{|v, t| [bind_vars[v], t]}
291
+ end
292
+
293
+ private
294
+
295
+ # Oracle uses a : before the name of the argument for named
296
+ # arguments.
297
+ def prepared_arg(k)
298
+ y, type = k.to_s.split("__", 2)
299
+ prepared_args << [y.to_sym, type]
300
+ i = prepared_args.length
301
+ LiteralString.new(":#{i}")
302
+ end
303
+
304
+ # Always assume a prepared argument.
305
+ def prepared_arg?(k)
306
+ true
307
+ end
308
+ end
309
+
310
+ # Oracle prepared statement uses a new prepared statement each time
311
+ # it is called, but it does use the bind arguments.
312
+ module BindArgumentMethods
313
+ include ArgumentMapper
314
+
315
+ private
316
+
317
+ # Run execute_select on the database with the given SQL and the stored
318
+ # bind arguments.
319
+ def execute(sql, opts={}, &block)
320
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
321
+ end
322
+
323
+ # Same as execute, explicit due to intricacies of alias and super.
324
+ def execute_dui(sql, opts={}, &block)
325
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
326
+ end
327
+
328
+ # Same as execute, explicit due to intricacies of alias and super.
329
+ def execute_insert(sql, opts={}, &block)
330
+ super(sql, {:arguments=>bind_arguments}.merge(opts), &block)
331
+ end
332
+ end
333
+
334
+ module PreparedStatementMethods
335
+ include BindArgumentMethods
336
+
337
+ private
338
+
339
+ # Execute the stored prepared statement name and the stored bind
340
+ # arguments instead of the SQL given.
341
+ def execute(sql, opts={}, &block)
342
+ super(prepared_statement_name, opts, &block)
343
+ end
344
+
345
+ # Same as execute, explicit due to intricacies of alias and super.
346
+ def execute_dui(sql, opts={}, &block)
347
+ super(prepared_statement_name, opts, &block)
348
+ end
349
+
350
+ # Same as execute, explicit due to intricacies of alias and super.
351
+ def execute_insert(sql, opts={}, &block)
352
+ super(prepared_statement_name, opts, &block)
353
+ end
354
+ end
355
+
356
+ # Execute the given type of statement with the hash of values.
357
+ def call(type, bind_vars={}, *values, &block)
358
+ ps = to_prepared_statement(type, values)
359
+ ps.extend(BindArgumentMethods)
360
+ ps.call(bind_vars, &block)
361
+ end
362
+
363
+ # Prepare the given type of query with the given name and store
364
+ # it in the database. Note that a new native prepared statement is
365
+ # created on each call to this prepared statement.
366
+ def prepare(type, name=nil, *values)
367
+ ps = to_prepared_statement(type, values)
368
+ ps.extend(PreparedStatementMethods)
369
+ if name
370
+ ps.prepared_statement_name = name
371
+ db.prepared_statements[name] = ps
372
+ end
373
+ ps
374
+ end
375
+
121
376
  def fetch_rows(sql)
122
377
  execute(sql) do |cursor|
123
- begin
124
- @columns = cursor.get_col_names.map{|c| output_identifier(c)}
125
- while r = cursor.fetch
126
- row = {}
127
- r.each_with_index {|v, i| row[@columns[i]] = v unless @columns[i] == :raw_rnum_}
128
- yield row
129
- end
130
- ensure
131
- cursor.close
378
+ offset = @opts[:offset]
379
+ rn = row_number_column
380
+ cps = db.conversion_procs
381
+ cols = columns = cursor.get_col_names.map{|c| output_identifier(c)}
382
+ metadata = cursor.column_metadata
383
+ cm = cols.zip(metadata).map{|c, m| [c, cps[m.data_type]]}
384
+ columns = cols.reject{|x| x == rn} if offset
385
+ @columns = columns
386
+ while r = cursor.fetch
387
+ row = {}
388
+ r.zip(cm).each{|v, (c, cp)| row[c] = ((v && cp) ? cp.call(v) : v)}
389
+ row.delete(rn) if offset
390
+ yield row
132
391
  end
133
392
  end
134
393
  self
135
394
  end
136
395
 
396
+ # Create a named prepared statement that is stored in the
397
+ # database (and connection) for reuse.
398
+ def prepare(type, name=nil, *values)
399
+ ps = to_prepared_statement(type, values)
400
+ ps.extend(PreparedStatementMethods)
401
+ if name
402
+ ps.prepared_statement_name = name
403
+ db.prepared_statements[name] = ps
404
+ end
405
+ ps
406
+ end
407
+
408
+ # Oracle requires type specifiers for placeholders, at least
409
+ # if you ever want to use a nil/NULL value as the value for
410
+ # the placeholder.
411
+ def requires_placeholder_type_specifiers?
412
+ true
413
+ end
414
+
137
415
  private
138
416
 
139
417
  def literal_other(v)
140
418
  case v
141
419
  when OraDate
142
- literal(Sequel.database_to_application_timestamp(v))
420
+ literal(db.to_application_timestamp(v))
143
421
  when OCI8::CLOB
144
422
  v.rewind
145
423
  literal(v.read)
@@ -147,6 +425,10 @@ module Sequel
147
425
  super
148
426
  end
149
427
  end
428
+
429
+ def prepared_arg_placeholder
430
+ PREPARED_ARG_PLACEHOLDER
431
+ end
150
432
  end
151
433
  end
152
434
  end