sequel 2.7.1 → 2.8.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 (50) hide show
  1. data/CHANGELOG +56 -0
  2. data/README +1 -0
  3. data/Rakefile +1 -1
  4. data/lib/sequel_core.rb +9 -16
  5. data/lib/sequel_core/adapters/ado.rb +6 -15
  6. data/lib/sequel_core/adapters/db2.rb +8 -10
  7. data/lib/sequel_core/adapters/dbi.rb +6 -4
  8. data/lib/sequel_core/adapters/informix.rb +21 -22
  9. data/lib/sequel_core/adapters/jdbc.rb +69 -10
  10. data/lib/sequel_core/adapters/jdbc/postgresql.rb +1 -0
  11. data/lib/sequel_core/adapters/mysql.rb +81 -13
  12. data/lib/sequel_core/adapters/odbc.rb +32 -4
  13. data/lib/sequel_core/adapters/openbase.rb +6 -5
  14. data/lib/sequel_core/adapters/oracle.rb +23 -7
  15. data/lib/sequel_core/adapters/postgres.rb +42 -32
  16. data/lib/sequel_core/adapters/shared/mssql.rb +37 -62
  17. data/lib/sequel_core/adapters/shared/mysql.rb +22 -7
  18. data/lib/sequel_core/adapters/shared/oracle.rb +27 -48
  19. data/lib/sequel_core/adapters/shared/postgres.rb +64 -43
  20. data/lib/sequel_core/adapters/shared/progress.rb +31 -0
  21. data/lib/sequel_core/adapters/shared/sqlite.rb +15 -4
  22. data/lib/sequel_core/adapters/sqlite.rb +6 -14
  23. data/lib/sequel_core/connection_pool.rb +47 -13
  24. data/lib/sequel_core/database.rb +60 -35
  25. data/lib/sequel_core/database/schema.rb +4 -4
  26. data/lib/sequel_core/dataset.rb +12 -3
  27. data/lib/sequel_core/dataset/convenience.rb +4 -13
  28. data/lib/sequel_core/dataset/prepared_statements.rb +30 -28
  29. data/lib/sequel_core/dataset/sql.rb +144 -85
  30. data/lib/sequel_core/dataset/stored_procedures.rb +75 -0
  31. data/lib/sequel_core/dataset/unsupported.rb +31 -0
  32. data/lib/sequel_core/exceptions.rb +6 -0
  33. data/lib/sequel_core/schema/generator.rb +4 -3
  34. data/lib/sequel_core/schema/sql.rb +41 -23
  35. data/lib/sequel_core/sql.rb +29 -1
  36. data/lib/sequel_model/associations.rb +1 -1
  37. data/lib/sequel_model/record.rb +31 -28
  38. data/spec/adapters/mysql_spec.rb +37 -4
  39. data/spec/adapters/oracle_spec.rb +26 -4
  40. data/spec/adapters/sqlite_spec.rb +7 -0
  41. data/spec/integration/prepared_statement_test.rb +24 -0
  42. data/spec/integration/schema_test.rb +1 -1
  43. data/spec/sequel_core/connection_pool_spec.rb +49 -2
  44. data/spec/sequel_core/core_sql_spec.rb +9 -2
  45. data/spec/sequel_core/database_spec.rb +64 -14
  46. data/spec/sequel_core/dataset_spec.rb +105 -7
  47. data/spec/sequel_core/schema_spec.rb +40 -12
  48. data/spec/sequel_core/spec_helper.rb +1 -0
  49. data/spec/sequel_model/spec_helper.rb +1 -0
  50. metadata +6 -3
@@ -13,69 +13,48 @@ module Sequel
13
13
  end
14
14
 
15
15
  module DatasetMethods
16
+ include Dataset::UnsupportedIntersectExceptAll
17
+
18
+ SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having union intersect except order limit'.freeze
19
+
16
20
  def empty?
17
21
  db[:dual].where(exists).get(1) == nil
18
22
  end
19
23
 
20
- # Formats a SELECT statement using the given options and the dataset
21
- # options.
22
- def select_sql(opts = nil)
23
- opts = opts ? @opts.merge(opts) : @opts
24
-
25
- if sql = opts[:sql]
26
- return sql
27
- end
28
-
29
- columns = opts[:select]
30
- select_columns = columns ? column_list(columns) : '*'
31
- sql = opts[:distinct] ? \
32
- "SELECT DISTINCT #{select_columns}" : \
33
- "SELECT #{select_columns}"
34
-
35
- if opts[:from]
36
- sql << " FROM #{source_list(opts[:from])}"
37
- end
38
-
39
- if join = opts[:join]
40
- join.each{|j| sql << literal(j)}
41
- end
42
-
43
- if where = opts[:where]
44
- sql << " WHERE #{literal(where)}"
45
- end
24
+ private
46
25
 
47
- if group = opts[:group]
48
- sql << " GROUP BY #{expression_list(group)}"
49
- end
26
+ # Oracle doesn't support the use of AS when aliasing a dataset. It doesn't require
27
+ # the use of AS anywhere, so this disables it in all cases.
28
+ def as_sql(expression, aliaz)
29
+ "#{expression} #{quote_identifier(aliaz)}"
30
+ end
50
31
 
51
- if having = opts[:having]
52
- sql << " HAVING #{literal(having)}"
53
- end
32
+ def select_clause_order
33
+ SELECT_CLAUSE_ORDER
34
+ end
54
35
 
55
- if union = opts[:union]
56
- sql << (opts[:union_all] ? \
57
- " UNION ALL #{union.sql}" : " UNION #{union.sql}")
58
- elsif intersect = opts[:intersect]
59
- sql << (opts[:intersect_all] ? \
60
- " INTERSECT ALL #{intersect.sql}" : " INTERSECT #{intersect.sql}")
61
- elsif except = opts[:except]
62
- sql << (opts[:except_all] ? \
63
- " EXCEPT ALL #{except.sql}" : " EXCEPT #{except.sql}")
36
+ # Oracle doesn't support DISTINCT ON
37
+ def select_distinct_sql(sql, opts)
38
+ if opts[:distinct]
39
+ raise(Error, "DISTINCT ON not supported by Oracle") unless opts[:distinct].empty?
40
+ sql << " DISTINCT"
64
41
  end
42
+ end
65
43
 
66
- if order = opts[:order]
67
- sql << " ORDER BY #{expression_list(order)}"
68
- end
44
+ # Oracle uses MINUS instead of EXCEPT, and doesn't support EXCEPT ALL
45
+ def select_except_sql(sql, opts)
46
+ sql << " MINUS #{opts[:except].sql}" if opts[:except]
47
+ end
69
48
 
49
+ # Oracle requires a subselect to do limit and offset
50
+ def select_limit_sql(sql, opts)
70
51
  if limit = opts[:limit]
71
52
  if (offset = opts[:offset]) && (offset > 0)
72
- sql = "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM(#{sql}) raw_sql_ WHERE ROWNUM <= #{limit + offset}) WHERE raw_rnum_ > #{offset}"
53
+ sql.replace("SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM(#{sql}) raw_sql_ WHERE ROWNUM <= #{limit + offset}) WHERE raw_rnum_ > #{offset}")
73
54
  else
74
- sql = "SELECT * FROM (#{sql}) WHERE ROWNUM <= #{limit}"
55
+ sql.replace("SELECT * FROM (#{sql}) WHERE ROWNUM <= #{limit}")
75
56
  end
76
57
  end
77
-
78
- sql
79
58
  end
80
59
  end
81
60
  end
@@ -4,6 +4,15 @@ module Sequel
4
4
  # uses NativeExceptions, the native adapter uses PGError.
5
5
  CONVERTED_EXCEPTIONS = []
6
6
 
7
+ @force_standard_strings = true
8
+
9
+ # By default, Sequel forces the use of standard strings, so that
10
+ # '\\' is interpreted as '\\' and not '\\'. While PostgreSQL defaults
11
+ # to interpreting plain strings as extended strings, this will change
12
+ # in a future version of PostgreSQL. Sequel assumes that SQL standard
13
+ # strings will be used.
14
+ metaattr_accessor :force_standard_strings
15
+
7
16
  # Methods shared by adapter/connection instances.
8
17
  module AdapterMethods
9
18
  attr_writer :db
@@ -57,6 +66,20 @@ module Sequel
57
66
  # to implement multi-level transactions with savepoints.
58
67
  attr_accessor :transaction_depth
59
68
 
69
+ # Apply connection settings for this connection. Currently, turns
70
+ # standard_conforming_strings ON if Postgres.force_standard_strings
71
+ # is true.
72
+ def apply_connection_settings
73
+ if Postgres.force_standard_strings
74
+ sql = "SET standard_conforming_strings = ON"
75
+ @db.log_info(sql)
76
+ # This setting will only work on PostgreSQL 8.2 or greater
77
+ # and we don't know the server version at this point, so
78
+ # try it unconditionally and rescue any errors.
79
+ execute(sql) rescue nil
80
+ end
81
+ end
82
+
60
83
  # Get the last inserted value for the given sequence.
61
84
  def last_insert_id(sequence)
62
85
  sql = SELECT_CURRVAL % sequence
@@ -67,6 +90,15 @@ module Sequel
67
90
  end
68
91
  end
69
92
 
93
+ # Get the primary key for the given table.
94
+ def primary_key(schema, table)
95
+ sql = SELECT_PK % [schema, table]
96
+ @db.log_info(sql)
97
+ execute(sql) do |r|
98
+ return single_value(r)
99
+ end
100
+ end
101
+
70
102
  # Get the primary key and sequence for the given table.
71
103
  def sequence(schema, table)
72
104
  sql = SELECT_SERIAL_SEQUENCE % [schema, table]
@@ -82,15 +114,6 @@ module Sequel
82
114
  return single_value(r)
83
115
  end
84
116
  end
85
-
86
- # Get the primary key for the given table.
87
- def primary_key(schema, table)
88
- sql = SELECT_PK % [schema, table]
89
- @db.log_info(sql)
90
- execute(sql) do |r|
91
- return single_value(r)
92
- end
93
- end
94
117
  end
95
118
 
96
119
  # Methods shared by Database instances that connect to PostgreSQL.
@@ -110,11 +133,6 @@ module Sequel
110
133
  @default_schema ||= :public
111
134
  end
112
135
 
113
- # Set a new default schema to use.
114
- def default_schema=(schema)
115
- @default_schema = schema
116
- end
117
-
118
136
  # Remove the cached entries for primary keys and sequences when dropping a table.
119
137
  def drop_table(*names)
120
138
  names.each do |name|
@@ -161,12 +179,11 @@ module Sequel
161
179
  synchronize(server){|conn| primary_key_for_table(conn, table)}
162
180
  end
163
181
 
164
- # Support :schema__table format for table
165
- def schema(table_name=nil, opts={})
166
- schema, table_name = schema_and_table(table_name) if table_name
167
- opts[:schema] = schema if schema
168
- super(table_name, opts)
169
- end
182
+ # SQL DDL statement for renaming a table. PostgreSQL doesn't allow you to change a table's schema in
183
+ # a rename table operation, so speciying a new schema in new_name will not have an effect.
184
+ def rename_table_sql(name, new_name)
185
+ "ALTER TABLE #{quote_schema_table(name)} RENAME TO #{quote_identifier(schema_and_table(new_name).last)}"
186
+ end
170
187
 
171
188
  # PostgreSQL uses SERIAL psuedo-type instead of AUTOINCREMENT for
172
189
  # managing incrementing primary keys.
@@ -255,6 +272,11 @@ module Sequel
255
272
 
256
273
  private
257
274
 
275
+ # PostgreSQL folds unquoted identifiers to lowercase, so it shouldn't need to upcase identifiers by default.
276
+ def upcase_identifiers_default
277
+ false
278
+ end
279
+
258
280
  # The result of the insert for the given table and values. If values
259
281
  # is an array, assume the first column is the primary key and return
260
282
  # that. If values is a hash, lookup the primary key for the table. If
@@ -322,7 +344,7 @@ module Sequel
322
344
  def schema_parse_tables(opts)
323
345
  schemas = {}
324
346
  schema_parser_dataset(nil, opts).each do |row|
325
- (schemas[row.delete(:table).to_sym] ||= []) << row
347
+ (schemas[quote_schema_table(SQL::QualifiedIdentifier.new(row.delete(:schema), row.delete(:table)))] ||= []) << row
326
348
  end
327
349
  schemas.each do |table, rows|
328
350
  schemas[table] = schema_parse_rows(rows)
@@ -348,9 +370,9 @@ module Sequel
348
370
  if table_name
349
371
  ds.filter!(:pg_class__relname=>table_name.to_s)
350
372
  else
351
- ds.select_more!(:pg_class__relname___table)
373
+ ds.select_more!(:pg_class__relname___table, :pg_namespace__nspname___schema)
374
+ ds.join!(:pg_namespace, :oid=>:pg_class__relnamespace, :nspname=>(opts[:schema] || default_schema).to_s)
352
375
  end
353
- ds.join!(:pg_namespace, :oid=>:pg_class__relnamespace, :nspname=>(opts[:schema] || default_schema).to_s) if opts[:schema] || !opts.include?(:schema)
354
376
  ds
355
377
  end
356
378
  end
@@ -372,6 +394,7 @@ module Sequel
372
394
  QUERY_PLAN = 'QUERY PLAN'.to_sym
373
395
  ROW_EXCLUSIVE = 'ROW EXCLUSIVE'.freeze
374
396
  ROW_SHARE = 'ROW SHARE'.freeze
397
+ SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having intersect union except order limit lock'.freeze
375
398
  SHARE = 'SHARE'.freeze
376
399
  SHARE_ROW_EXCLUSIVE = 'SHARE ROW EXCLUSIVE'.freeze
377
400
  SHARE_UPDATE_EXCLUSIVE = 'SHARE UPDATE EXCLUSIVE'.freeze
@@ -452,8 +475,10 @@ module Sequel
452
475
  case v
453
476
  when LiteralString
454
477
  v
478
+ when SQL::Blob
479
+ db.synchronize{|c| "E'#{c.escape_bytea(v)}'"}
455
480
  when String
456
- db.synchronize{|c| "'#{SQL::Blob === v ? c.escape_bytea(v) : c.escape_string(v)}'"}
481
+ db.synchronize{|c| "'#{c.escape_string(v)}'"}
457
482
  when Time
458
483
  "#{v.strftime(PG_TIMESTAMP_FORMAT)}.#{sprintf("%06d",v.usec)}'"
459
484
  when DateTime
@@ -489,25 +514,6 @@ module Sequel
489
514
  ["INSERT INTO #{source_list(@opts[:from])} (#{identifier_list(columns)}) VALUES #{values}"]
490
515
  end
491
516
 
492
- # PostgreSQL assumes unquoted identifiers are lower case by default,
493
- # so do not upcase the identifier when quoting it.
494
- def quoted_identifier(c)
495
- "\"#{c}\""
496
- end
497
-
498
- # Support lock mode, allowing FOR SHARE and FOR UPDATE queries.
499
- def select_sql(opts = nil)
500
- row_lock_mode = opts ? opts[:lock] : @opts[:lock]
501
- sql = super
502
- case row_lock_mode
503
- when :update
504
- sql << FOR_UPDATE
505
- when :share
506
- sql << FOR_SHARE
507
- end
508
- sql
509
- end
510
-
511
517
  private
512
518
 
513
519
  # Call execute_insert on the database object with the given values.
@@ -521,6 +527,21 @@ module Sequel
521
527
  insert_returning_sql(pk ? Sequel::SQL::Identifier.new(pk) : 'NULL'.lit, *values)
522
528
  end
523
529
 
530
+ # The order of clauses in the SELECT SQL statement
531
+ def select_clause_order
532
+ SELECT_CLAUSE_ORDER
533
+ end
534
+
535
+ # Support lock mode, allowing FOR SHARE and FOR UPDATE queries.
536
+ def select_lock_sql(sql, opts)
537
+ case opts[:lock]
538
+ when :update
539
+ sql << FOR_UPDATE
540
+ when :share
541
+ sql << FOR_SHARE
542
+ end
543
+ end
544
+
524
545
  # The version of the database server
525
546
  def server_version
526
547
  db.server_version(@opts[:server])
@@ -0,0 +1,31 @@
1
+ module Sequel
2
+ module Progress
3
+ module DatabaseMethods
4
+
5
+ def dataset(opts = nil)
6
+ ds = super
7
+ ds.extend(DatasetMethods)
8
+ ds
9
+ end
10
+ end
11
+
12
+ module DatasetMethods
13
+ include Dataset::UnsupportedIntersectExcept
14
+
15
+ SELECT_CLAUSE_ORDER = %w'limit distinct columns from join where group order having union'.freeze
16
+
17
+ private
18
+
19
+ def select_clause_order
20
+ SELECT_CLAUSE_ORDER
21
+ end
22
+
23
+ # Progress uses TOP for limit, but it is only supported in Progress 10.
24
+ # The Progress adapter targets Progress 9, so it silently ignores the option.
25
+ def select_limit_sql(sql, opts)
26
+ raise(Error, "OFFSET not supported") if opts[:offset]
27
+ #sql << " TOP #{opts[:limit]}" if opts[:limit]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -6,6 +6,12 @@ module Sequel
6
6
  TABLES_FILTER = "type = 'table' AND NOT name = 'sqlite_sequence'"
7
7
  TEMP_STORE = {'0' => :default, '1' => :file, '2' => :memory}.freeze
8
8
 
9
+ # Run all alter_table commands in a transaction. This is technically only
10
+ # needed for drop column.
11
+ def alter_table(name, generator=nil, &block)
12
+ transaction{super}
13
+ end
14
+
9
15
  # SQLite supports limited table modification. You can add a column
10
16
  # or an index. Dropping columns is supported by copying the table into
11
17
  # a temporary table, dropping the table, and creating a new table without
@@ -18,14 +24,12 @@ module Sequel
18
24
  index_definition_sql(table, op)
19
25
  when :drop_column
20
26
  columns_str = (schema_parse_table(table, {}).map{|c| c[0]} - Array(op[:name])).join(",")
21
- ["BEGIN TRANSACTION",
22
- "CREATE TEMPORARY TABLE #{table}_backup(#{columns_str})",
27
+ ["CREATE TEMPORARY TABLE #{table}_backup(#{columns_str})",
23
28
  "INSERT INTO #{table}_backup SELECT #{columns_str} FROM #{table}",
24
29
  "DROP TABLE #{table}",
25
30
  "CREATE TABLE #{table}(#{columns_str})",
26
31
  "INSERT INTO #{table} SELECT #{columns_str} FROM #{table}_backup",
27
- "DROP TABLE #{table}_backup",
28
- "COMMIT"]
32
+ "DROP TABLE #{table}_backup"]
29
33
  else
30
34
  raise Error, "Unsupported ALTER TABLE operation"
31
35
  end
@@ -82,6 +86,11 @@ module Sequel
82
86
 
83
87
  private
84
88
 
89
+ # SQLite folds unquoted identifiers to lowercase, so it shouldn't need to upcase identifiers by default.
90
+ def upcase_identifiers_default
91
+ false
92
+ end
93
+
85
94
  # SQLite supports schema parsing using the table_info PRAGMA, so
86
95
  # parse the output of that into the format Sequel expects.
87
96
  def schema_parse_table(table_name, opts)
@@ -100,6 +109,8 @@ module Sequel
100
109
 
101
110
  # Instance methods for datasets that connect to an SQLite database
102
111
  module DatasetMethods
112
+ include Dataset::UnsupportedIntersectExceptAll
113
+
103
114
  # SQLite does not support pattern matching via regular expressions.
104
115
  # SQLite is case insensitive (depending on pragma), so use LIKE for
105
116
  # ILIKE.
@@ -46,11 +46,6 @@ module Sequel
46
46
  SQLite::Dataset.new(self, opts)
47
47
  end
48
48
 
49
- # Disconnect all connections from the database.
50
- def disconnect
51
- @pool.disconnect {|c| c.close}
52
- end
53
-
54
49
  # Run the given SQL with the given arguments and return the number of changed rows.
55
50
  def execute_dui(sql, opts={})
56
51
  _execute(sql, opts){|conn| conn.execute_batch(sql, opts[:arguments]); conn.changes}
@@ -110,6 +105,11 @@ module Sequel
110
105
  o[:max_connections] = 1 if @opts[:database] == ':memory:' || @opts[:database].blank?
111
106
  o
112
107
  end
108
+
109
+ # Disconnect given connections from the database.
110
+ def disconnect_connection(c)
111
+ c.close
112
+ end
113
113
  end
114
114
 
115
115
  # Dataset class for SQLite datasets that use the ruby-sqlite3 driver.
@@ -135,14 +135,6 @@ module Sequel
135
135
 
136
136
  private
137
137
 
138
- # Work around for the default prepared statement and argument
139
- # mapper code, which wants a hash that maps. SQLite doesn't
140
- # need to do this, but still requires a value for the argument
141
- # in order for the substitution to work correctly.
142
- def prepared_args_hash
143
- true
144
- end
145
-
146
138
  # SQLite uses a : before the name of the argument for named
147
139
  # arguments.
148
140
  def prepared_arg(k)
@@ -221,7 +213,7 @@ module Sequel
221
213
  # Prepare the given type of query with the given name and store
222
214
  # it in the database. Note that a new native prepared statement is
223
215
  # created on each call to this prepared statement.
224
- def prepare(type, name, values=nil)
216
+ def prepare(type, name=nil, values=nil)
225
217
  ps = to_prepared_statement(type, values)
226
218
  ps.extend(PreparedStatementMethods)
227
219
  db.prepared_statements[name] = ps if name
@@ -5,6 +5,9 @@ class Sequel::ConnectionPool
5
5
  # The proc used to create a new database connection.
6
6
  attr_accessor :connection_proc
7
7
 
8
+ # The proc used to disconnect a database connection.
9
+ attr_accessor :disconnection_proc
10
+
8
11
  # The maximum number of connections.
9
12
  attr_reader :max_size
10
13
 
@@ -40,6 +43,7 @@ class Sequel::ConnectionPool
40
43
  @max_size = opts[:max_connections] || 4
41
44
  @mutex = Mutex.new
42
45
  @connection_proc = block
46
+ @disconnection_proc = opts[:disconnection_proc]
43
47
  @servers = [:default]
44
48
  @servers += opts[:servers].keys - @servers if opts[:servers]
45
49
  @available_connections = Hash.new{|h,k| h[:default]}
@@ -103,8 +107,11 @@ class Sequel::ConnectionPool
103
107
  end
104
108
  begin
105
109
  yield conn
110
+ rescue Sequel::DatabaseDisconnectError => dde
111
+ remove(t, conn, server)
112
+ raise
106
113
  ensure
107
- release(t, conn, server)
114
+ release(t, conn, server) unless dde
108
115
  end
109
116
  rescue Exception => e
110
117
  raise(@convert_exceptions && !e.is_a?(StandardError) ? RuntimeError.new(e.message) : e)
@@ -116,24 +123,19 @@ class Sequel::ConnectionPool
116
123
  # disconnecting from the database, assuming that no connections are currently
117
124
  # being used. Once a connection is requested using #hold, the connection pool
118
125
  # creates new connections to the database.
119
- def disconnect
126
+ def disconnect(&block)
127
+ block ||= @disconnection_proc
120
128
  @mutex.synchronize do
121
129
  @available_connections.each do |server, conns|
122
- conns.each{|c| yield(c)} if block_given?
130
+ conns.each{|c| block.call(c)} if block
123
131
  conns.clear
124
- @created_count[server] = allocated(server).length
132
+ set_created_count(server, allocated(server).length)
125
133
  end
126
134
  end
127
135
  end
128
136
 
129
137
  private
130
138
 
131
- # Returns the connection owned by the supplied thread for the given server,
132
- # if any.
133
- def owned_connection(thread, server)
134
- @mutex.synchronize{@allocated[server][thread]}
135
- end
136
-
137
139
  # Assigns a connection to the supplied thread for the given server, if one
138
140
  # is available.
139
141
  def acquire(thread, server)
@@ -154,12 +156,18 @@ class Sequel::ConnectionPool
154
156
  # the server is less than the maximum size of the pool.
155
157
  def make_new(server)
156
158
  if @created_count[server] < @max_size
157
- @created_count[server] += 1
159
+ set_created_count(server, @created_count[server] + 1)
158
160
  @connection_proc ? @connection_proc.call(server) : \
159
161
  (raise Error, "No connection proc specified")
160
162
  end
161
163
  end
162
164
 
165
+ # Returns the connection owned by the supplied thread for the given server,
166
+ # if any.
167
+ def owned_connection(thread, server)
168
+ @mutex.synchronize{@allocated[server][thread]}
169
+ end
170
+
163
171
  # Releases the connection assigned to the supplied thread and server.
164
172
  def release(thread, conn, server)
165
173
  @mutex.synchronize do
@@ -167,6 +175,21 @@ class Sequel::ConnectionPool
167
175
  available_connections(server) << conn
168
176
  end
169
177
  end
178
+
179
+ # Removes the currently allocated connection from the connection pool.
180
+ def remove(thread, conn, server)
181
+ @mutex.synchronize do
182
+ allocated(server).delete(thread)
183
+ set_created_count(server, @created_count[server] - 1)
184
+ @disconnection_proc.call(conn) if @disconnection_proc
185
+ end
186
+ end
187
+
188
+ # Set the created count for the given server type
189
+ def set_created_count(server, value)
190
+ server = :default unless @created_count.include?(server)
191
+ @created_count[server] = value
192
+ end
170
193
  end
171
194
 
172
195
  # A SingleThreadedPool acts as a replacement for a ConnectionPool for use
@@ -179,6 +202,9 @@ class Sequel::SingleThreadedPool
179
202
  # The proc used to create a new database connection
180
203
  attr_writer :connection_proc
181
204
 
205
+ # The proc used to disconnect a database connection.
206
+ attr_accessor :disconnection_proc
207
+
182
208
  # Initializes the instance with the supplied block as the connection_proc.
183
209
  #
184
210
  # The single threaded pool takes the following options:
@@ -187,6 +213,7 @@ class Sequel::SingleThreadedPool
187
213
  # to RuntimeError exceptions (default true)
188
214
  def initialize(opts={}, &block)
189
215
  @connection_proc = block
216
+ @disconnection_proc = opts[:disconnection_proc]
190
217
  @conns = {}
191
218
  @convert_exceptions = opts.include?(:pool_convert_exceptions) ? opts[:pool_convert_exceptions] : true
192
219
  end
@@ -200,7 +227,13 @@ class Sequel::SingleThreadedPool
200
227
  # This method simulates the ConnectionPool#hold API.
201
228
  def hold(server=:default)
202
229
  begin
203
- yield(@conns[server] ||= @connection_proc.call(server))
230
+ begin
231
+ yield(c = (@conns[server] ||= @connection_proc.call(server)))
232
+ rescue Sequel::DatabaseDisconnectError => dde
233
+ @conns.delete(server)
234
+ @disconnection_proc.call(c) if @disconnection_proc
235
+ raise
236
+ end
204
237
  rescue Exception => e
205
238
  # if the error is not a StandardError it is converted into RuntimeError.
206
239
  raise(@convert_exceptions && !e.is_a?(StandardError) ? RuntimeError.new(e.message) : e)
@@ -210,7 +243,8 @@ class Sequel::SingleThreadedPool
210
243
  # Disconnects from the database. Once a connection is requested using
211
244
  # #hold, the connection is reestablished.
212
245
  def disconnect(&block)
213
- @conns.values.each{|conn| yield(conn) if block_given?}
246
+ block ||= @disconnection_proc
247
+ @conns.values.each{|conn| block.call(conn) if block}
214
248
  @conns = {}
215
249
  end
216
250
  end