sequel 3.16.0 → 3.17.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.
data/CHANGELOG CHANGED
@@ -1,3 +1,31 @@
1
+ === 3.17.0 (2010-11-05)
2
+
3
+ * Ensure that the optimistic locking plugin increments the lock column when using Model#modified! (jfirebaugh)
4
+
5
+ * Correctly handle nil values in the xml_serializer plugin, instead of converting them to empty strings (george.haff) (#313)
6
+
7
+ * Use a default wait_timeout that's allowed on Windows for the mysql and mysql2 adapters (jeremyevans) (#314)
8
+
9
+ * Add support for connecting to MySQL over SSL using the :sslca, :sslkey, and related options (jeremyevans)
10
+
11
+ * Fix Database#each_server when used with jdbc or do connection strings without separate :adapter option (jeremyevans) (#312)
12
+
13
+ * Much better support in the AS400 JDBC subadapter (bhauff)
14
+
15
+ * Allow cloning of many_through_many associations (gucki, jeremyevans)
16
+
17
+ * In the nested_attributes plugin, don't make unnecessary update calls to modify associated objects that are about to be deleted (jeremyevans, gucki)
18
+
19
+ * Allow Dataset#(add|set)_graph_aliases to accept as hash values symbols and arrays with a single element (jeremyevans)
20
+
21
+ * Add Databse#views and #view_exists? to the Oracle adapter (gpheruson)
22
+
23
+ * Add Database#sql_log_level for changing the level at which SQL queries are logged (jeremyevans)
24
+
25
+ * Remove unintended use of prepared statements in swift adapter (jeremyevans)
26
+
27
+ * Fix logging in the swift PostgreSQL subadapter (jeremyevans)
28
+
1
29
  === 3.16.0 (2010-10-01)
2
30
 
3
31
  * Support composite foreign keys for associations in the identity_map plugin (harukizaemon, jeremyevans) (#310)
@@ -0,0 +1,58 @@
1
+ = New Features
2
+
3
+ * You can now change the level at which Sequel logs SQL statements,
4
+ by calling Database#sql_log_level= with the method name symbol.
5
+ The default is still :info for backwards compatibility. Previously,
6
+ you had to use a proxy logger to get similar capability.
7
+
8
+ * You can now specify graph aliases where the alias would be the same
9
+ as the table column name using just the table name symbol, instead
10
+ having to repeat the alias as the second element of an array. More
11
+ clearly:
12
+
13
+ # < 3.17.0:
14
+ DB[:a].graph(:b, :a_id=>:id).
15
+ set_graph_aliases(:c=>[:a, :c], :d=>[:b, :d])
16
+ # >= 3.17.0:
17
+ DB[:a].graph(:b, :a_id=>:id).set_graph_aliases(:c=>:a, :d=>:b)
18
+
19
+ Both of these now yield the SQL:
20
+
21
+ SELECT a.c, b.d FROM a LEFT OUTER JOIN b ON (b.a_id = a.id)
22
+
23
+ * You should now be able to connect to MySQL over SSL in the native
24
+ MySQL adapter using the :sslca, :sslkey, and related options.
25
+
26
+ * Database#views and Database#view_exists? methods were added to the
27
+ Oracle adapter, allowing you to get a an array of view name symbols
28
+ and to check whether a given view exists.
29
+
30
+ = Other Improvements
31
+
32
+ * The nested_attributes plugin now avoids unnecessary update calls
33
+ when deleting associated objects, resulting in better performance.
34
+
35
+ * The optimistic_locking plugin now increments the lock column if no
36
+ other columns were modified but the Model#modified! was called. This
37
+ means it now works correctly with the nested_attributes plugin when
38
+ no changes to the main model object are made.
39
+
40
+ * The xml_serializer plugin can now round-trip nil values correctly.
41
+ Previously, nil values would be converted into empty strings. This
42
+ is accomplished by including a nil attribute in the xml tag.
43
+
44
+ * Database#each_server now works correctly when using the jdbc and do
45
+ adapters and a connection string without a separate :adapter option.
46
+
47
+ * You can now clone many_through_many associations.
48
+
49
+ * The default wait_timeout used by the mysql and mysql2 adapters was
50
+ decreased slightly so that it works correctly with MySQL database
51
+ servers that run on Windows.
52
+
53
+ * Many improvements were made to the AS400 jdbc subadapter.
54
+
55
+ * Many improvements were made to the swift adapter and subadapters.
56
+
57
+ * Dataset#ungraphed now removes any cached graph aliases set with
58
+ set_graph_aliases or add_graph_aliases.
@@ -4,6 +4,10 @@ module Sequel
4
4
  module AS400
5
5
  # Instance methods for AS400 Database objects accessed via JDBC.
6
6
  module DatabaseMethods
7
+ TRANSACTION_BEGIN = 'Transaction.begin'.freeze
8
+ TRANSACTION_COMMIT = 'Transaction.commit'.freeze
9
+ TRANSACTION_ROLLBACK = 'Transaction.rollback'.freeze
10
+
7
11
  # AS400 uses the :as400 database type.
8
12
  def database_type
9
13
  :as400
@@ -18,28 +22,67 @@ module Sequel
18
22
  def last_insert_id(conn, opts={})
19
23
  nil
20
24
  end
25
+
26
+ # AS400 supports transaction isolation levels
27
+ def supports_transaction_isolation_levels?
28
+ true
29
+ end
30
+
31
+ private
32
+
33
+ # Use JDBC connection's setAutoCommit to false to start transactions
34
+ def begin_transaction(conn, opts={})
35
+ set_transaction_isolation(conn, opts)
36
+ log_yield(TRANSACTION_BEGIN){conn.setAutoCommit(false)}
37
+ conn
38
+ end
39
+
40
+ # Use JDBC connection's commit method to commit transactions
41
+ def commit_transaction(conn, opts={})
42
+ log_yield(TRANSACTION_COMMIT){conn.commit}
43
+ end
44
+
45
+ # Use JDBC connection's setAutoCommit to true to enable default
46
+ # auto-commit mode
47
+ def remove_transaction(conn)
48
+ conn.setAutoCommit(true) if conn
49
+ @transactions.delete(Thread.current)
50
+ end
51
+
52
+ # Use JDBC connection's rollback method to rollback transactions
53
+ def rollback_transaction(conn, opts={})
54
+ log_yield(TRANSACTION_ROLLBACK){conn.rollback}
55
+ end
21
56
  end
22
57
 
23
58
  # Dataset class for AS400 datasets accessed via JDBC.
24
59
  class Dataset < JDBC::Dataset
25
60
  WILDCARD = Sequel::LiteralString.new('*').freeze
26
61
 
27
- # AS400 needs to use a couple of subselects for all limits and offsets.
62
+ # AS400 needs to use a couple of subselects for queries with offsets.
28
63
  def select_sql
29
- return super unless l = @opts[:limit]
30
- o = @opts[:offset] || 0
64
+ return super unless o = @opts[:offset]
65
+ l = @opts[:limit]
31
66
  order = @opts[:order]
32
67
  dsa1 = dataset_alias(1)
33
68
  dsa2 = dataset_alias(2)
34
69
  rn = row_number_column
35
70
  irn = Sequel::SQL::Identifier.new(rn).qualify(dsa2)
36
71
  subselect_sql(unlimited.
37
- from_self(:alias=>dsa1).
38
- select_more(Sequel::SQL::QualifiedIdentifier.new(dsa1, WILDCARD),
72
+ from_self(:alias=>dsa1).
73
+ select_more(Sequel::SQL::QualifiedIdentifier.new(dsa1, WILDCARD),
39
74
  Sequel::SQL::WindowFunction.new(SQL::Function.new(:ROW_NUMBER), Sequel::SQL::Window.new(:order=>order)).as(rn)).
40
- from_self(:alias=>dsa2).
41
- select(Sequel::SQL::QualifiedIdentifier.new(dsa2, WILDCARD)).
42
- where((irn > o) & (irn <= l + o)))
75
+ from_self(:alias=>dsa2).
76
+ select(Sequel::SQL::QualifiedIdentifier.new(dsa2, WILDCARD)).
77
+ where(l ? ((irn > o) & (irn <= l + o)) : (irn > o))) # Leave off limit in case of limit(nil, offset)
78
+ end
79
+
80
+ # Modify the sql to limit the number of rows returned
81
+ def select_limit_sql(sql)
82
+ if @opts[:limit]
83
+ sql << " FETCH FIRST ROW ONLY" if @opts[:limit] == 1
84
+ sql << " FETCH FIRST #{@opts[:limit]} ROWS ONLY" if @opts[:limit] > 1
85
+ end
43
86
  end
44
87
 
45
88
  def supports_window_functions?
@@ -93,6 +93,7 @@ module Sequel
93
93
  conn = Mysql.init
94
94
  conn.options(Mysql::READ_DEFAULT_GROUP, opts[:config_default_group] || "client")
95
95
  conn.options(Mysql::OPT_LOCAL_INFILE, opts[:config_local_infile]) if opts.has_key?(:config_local_infile)
96
+ conn.ssl_set(opts[:sslkey], opts[:sslcert], opts[:sslca], opts[:sslcapath], opts[:sslcipher]) if opts[:sslca] || opts[:sslkey]
96
97
  if encoding = opts[:encoding] || opts[:charset]
97
98
  # Set encoding before connecting so that the mysql driver knows what
98
99
  # encoding we want to use, but this can be overridden by READ_DEFAULT_GROUP.
@@ -116,8 +117,9 @@ module Sequel
116
117
  # that feature.
117
118
  sqls << "SET NAMES #{literal(encoding.to_s)}" if encoding
118
119
 
119
- # increase timeout so mysql server doesn't disconnect us
120
- sqls << "SET @@wait_timeout = #{opts[:timeout] || 2592000}"
120
+ # Increase timeout so mysql server doesn't disconnect us
121
+ # Value used by default is maximum allowed value on Windows.
122
+ sqls << "SET @@wait_timeout = #{opts[:timeout] || 2147483}"
121
123
 
122
124
  # By default, MySQL 'where id is null' selects the last inserted id
123
125
  sqls << "SET SQL_AUTO_IS_NULL=0" unless opts[:auto_is_null]
@@ -46,8 +46,9 @@ module Sequel
46
46
  sqls << "SET NAMES #{conn.escape(encoding.to_s)}"
47
47
  end
48
48
 
49
- # increase timeout so mysql server doesn't disconnect us
50
- sqls << "SET @@wait_timeout = #{opts[:timeout] || 2592000}"
49
+ # Increase timeout so mysql server doesn't disconnect us.
50
+ # Value used by default is maximum allowed value on Windows.
51
+ sqls << "SET @@wait_timeout = #{opts[:timeout] || 2147483}"
51
52
 
52
53
  # By default, MySQL 'where id is null' selects the last inserted id
53
54
  sqls << "SET SQL_AUTO_IS_NULL=0" unless opts[:auto_is_null]
@@ -30,6 +30,15 @@ module Sequel
30
30
  from(:tab).filter(:tname =>dataset.send(:input_identifier, name), :tabtype => 'TABLE').count > 0
31
31
  end
32
32
 
33
+ def views(opts={})
34
+ ds = from(:tab).server(opts[:server]).select(:tname).filter(:tabtype => 'VIEW')
35
+ ds.map{|r| ds.send(:output_identifier, r[:tname])}
36
+ end
37
+
38
+ def view_exists?(name)
39
+ from(:tab).filter(:tname =>dataset.send(:input_identifier, name), :tabtype => 'VIEW').count > 0
40
+ end
41
+
33
42
  private
34
43
 
35
44
  def auto_increment_sql
@@ -64,7 +64,7 @@ module Sequel
64
64
  synchronize(opts[:server]) do |conn|
65
65
  begin
66
66
  res = nil
67
- log_yield(sql){res = conn.prepare(sql).execute}
67
+ log_yield(sql){conn.execute(sql); res = conn.results}
68
68
  yield res if block_given?
69
69
  nil
70
70
  rescue SwiftError => e
@@ -92,9 +92,12 @@ module Sequel
92
92
  def execute_insert(sql, opts={})
93
93
  synchronize(opts[:server]) do |conn|
94
94
  begin
95
- log_yield(sql){conn.prepare(sql).execute.insert_id}
95
+ res = nil
96
+ log_yield(sql){conn.execute(sql); (res = conn.results).insert_id}
96
97
  rescue SwiftError => e
97
98
  raise_error(e)
99
+ ensure
100
+ res.finish if res
98
101
  end
99
102
  end
100
103
  end
@@ -46,6 +46,21 @@ module Sequel
46
46
  Sequel::Swift::Postgres::Dataset.new(self, opts)
47
47
  end
48
48
 
49
+ # Run the SELECT SQL on the database and yield the rows
50
+ def execute(sql, opts={})
51
+ synchronize(opts[:server]) do |conn|
52
+ begin
53
+ conn.execute(sql)
54
+ res = conn.results
55
+ yield res if block_given?
56
+ rescue SwiftError => e
57
+ raise_error(e)
58
+ ensure
59
+ res.finish if res
60
+ end
61
+ end
62
+ end
63
+
49
64
  # Run the DELETE/UPDATE SQL on the database and return the number
50
65
  # of matched rows.
51
66
  def execute_dui(sql, opts={})
@@ -62,8 +77,12 @@ module Sequel
62
77
  # for the record.
63
78
  def execute_insert(sql, opts={})
64
79
  synchronize(opts[:server]) do |conn|
65
- conn.execute(sql)
66
- insert_result(conn, opts[:table], opts[:values])
80
+ begin
81
+ conn.execute(sql)
82
+ insert_result(conn, opts[:table], opts[:values])
83
+ rescue SwiftError => e
84
+ raise_error(e)
85
+ end
67
86
  end
68
87
  end
69
88
 
@@ -15,6 +15,8 @@ module Sequel
15
15
  # Raises Sequel::AdapterNotFound if the adapter
16
16
  # could not be loaded.
17
17
  def self.adapter_class(scheme)
18
+ return scheme if scheme.is_a?(Class)
19
+
18
20
  scheme = scheme.to_s.gsub('-', '_').to_sym
19
21
 
20
22
  unless klass = ADAPTER_MAP[scheme]
@@ -58,7 +60,7 @@ module Sequel
58
60
  end
59
61
  when Hash
60
62
  opts = conn_string.merge(opts)
61
- c = adapter_class(opts[:adapter] || opts['adapter'])
63
+ c = adapter_class(opts[:adapter_class] || opts[:adapter] || opts['adapter'])
62
64
  else
63
65
  raise Error, "Sequel::Database.connect takes either a Hash or a String, given: #{conn_string.inspect}"
64
66
  end
@@ -12,6 +12,11 @@ module Sequel
12
12
  # Array of SQL loggers to use for this database.
13
13
  attr_accessor :loggers
14
14
 
15
+ # Log level at which to log SQL queries. This is actually the method
16
+ # sent to the logger, so it should be the method name symbol. The default
17
+ # is :info, it can be set to :debug to log at DEBUG level.
18
+ attr_accessor :sql_log_level
19
+
15
20
  # Log a message at level info to all loggers.
16
21
  def log_info(message, args=nil)
17
22
  log_each(:info, args ? "#{message}; #{args.inspect}" : message)
@@ -51,7 +56,7 @@ module Sequel
51
56
  # Log message with message prefixed by duration at info level, or
52
57
  # warn level if duration is greater than log_warn_duration.
53
58
  def log_duration(duration, message)
54
- log_each((lwd = log_warn_duration and duration >= lwd) ? :warn : :info, "(#{sprintf('%0.6fs', duration)}) #{message}")
59
+ log_each((lwd = log_warn_duration and duration >= lwd) ? :warn : sql_log_level, "(#{sprintf('%0.6fs', duration)}) #{message}")
55
60
  end
56
61
 
57
62
  # Log message at level (which should be :error, :warn, or :info)
@@ -32,6 +32,7 @@ module Sequel
32
32
  # :quote_identifiers :: Whether to quote identifiers
33
33
  # :servers :: A hash specifying a server/shard specific options, keyed by shard symbol
34
34
  # :single_threaded :: Whether to use a single-threaded connection pool
35
+ # :sql_log_level :: Method to use to log SQL to a logger, :info by default.
35
36
  #
36
37
  # All options given are also passed to the connection pool. If a block
37
38
  # is given, it is used as the connection_proc for the ConnectionPool.
@@ -43,6 +44,7 @@ module Sequel
43
44
  @opts[:disconnection_proc] ||= proc{|conn| disconnect_connection(conn)}
44
45
  block ||= proc{|server| connect(server)}
45
46
  @opts[:servers] = {} if @opts[:servers].is_a?(String)
47
+ @opts[:adapter_class] = self.class
46
48
 
47
49
  @opts[:single_threaded] = @single_threaded = typecast_value_boolean(@opts.fetch(:single_threaded, @@single_threaded))
48
50
  @schemas = {}
@@ -52,6 +54,7 @@ module Sequel
52
54
  @identifier_input_method = nil
53
55
  @identifier_output_method = nil
54
56
  @quote_identifiers = nil
57
+ self.sql_log_level = @opts[:sql_log_level] ? @opts[:sql_log_level].to_sym : :info
55
58
  @pool = ConnectionPool.get_pool(@opts, &block)
56
59
 
57
60
  ::Sequel::DATABASES.push(self)
@@ -15,7 +15,8 @@ module Sequel
15
15
  # # SELECT ..., table.column AS some_alias
16
16
  # # => {:table=>{:column=>some_alias_value, ...}, ...}
17
17
  def add_graph_aliases(graph_aliases)
18
- ds = select_more(*graph_alias_columns(graph_aliases))
18
+ columns, graph_aliases = graph_alias_columns(graph_aliases)
19
+ ds = select_more(*columns)
19
20
  ds.opts[:graph_aliases] = (ds.opts[:graph_aliases] || (ds.opts[:graph][:column_aliases] rescue {}) || {}).merge(graph_aliases)
20
21
  ds
21
22
  end
@@ -183,20 +184,25 @@ module Sequel
183
184
  # graphing is used.
184
185
  #
185
186
  # graph_aliases :: Should be a hash with keys being symbols of
186
- # column aliases, and values being arrays with two or three elements.
187
- # The first element of the array should be the table alias symbol,
188
- # and the second should be the actual column name symbol. If the array
187
+ # column aliases, and values being either symbols or arrays with one to three elements.
188
+ # If the value is a symbol, it is assumed to be the same as a one element
189
+ # array containing that symbol.
190
+ # The first element of the array should be the table alias symbol.
191
+ # The second should be the actual column name symbol. If the array only
192
+ # has a single element the column name symbol will be assumed to be the
193
+ # same as the corresponding hash key. If the array
189
194
  # has a third element, it is used as the value returned, instead of
190
195
  # table_alias.column_name.
191
196
  #
192
197
  # DB[:artists].graph(:albums, :artist_id=>:id).
193
- # set_graph_aliases(:artist_name=>[:artists, :name],
198
+ # set_graph_aliases(:name=>:artists,
194
199
  # :album_name=>[:albums, :name],
195
200
  # :forty_two=>[:albums, :fourtwo, 42]).first
196
- # # SELECT artists.name AS artist_name, albums.name AS album_name, 42 AS forty_two FROM table
201
+ # # SELECT artists.name, albums.name AS album_name, 42 AS forty_two ...
197
202
  # # => {:artists=>{:name=>artists.name}, :albums=>{:name=>albums.name, :fourtwo=>42}}
198
203
  def set_graph_aliases(graph_aliases)
199
- ds = select(*graph_alias_columns(graph_aliases))
204
+ columns, graph_aliases = graph_alias_columns(graph_aliases)
205
+ ds = select(*columns)
200
206
  ds.opts[:graph_aliases] = graph_aliases
201
207
  ds
202
208
  end
@@ -204,18 +210,25 @@ module Sequel
204
210
  # Remove the splitting of results into subhashes, and all metadata
205
211
  # related to the current graph (if any).
206
212
  def ungraphed
207
- clone(:graph=>nil)
213
+ clone(:graph=>nil, :graph_aliases=>nil)
208
214
  end
209
215
 
210
216
  private
211
217
 
212
- # Transform the hash of graph aliases to an array of columns
218
+ # Transform the hash of graph aliases and return a two element array
219
+ # where the first element is an array of identifiers suitable to pass to
220
+ # a select method, and the second is a new hash of preprocessed graph aliases.
213
221
  def graph_alias_columns(graph_aliases)
214
- graph_aliases.collect do |col_alias, tc|
215
- identifier = tc[2] || SQL::QualifiedIdentifier.new(tc[0], tc[1])
216
- identifier = SQL::AliasedExpression.new(identifier, col_alias) if tc[2] or tc[1] != col_alias
222
+ gas = {}
223
+ identifiers = graph_aliases.collect do |col_alias, tc|
224
+ table, column, value = Array(tc)
225
+ column ||= col_alias
226
+ gas[col_alias] = [table, column]
227
+ identifier = value || SQL::QualifiedIdentifier.new(table, column)
228
+ identifier = SQL::AliasedExpression.new(identifier, col_alias) if value or column != col_alias
217
229
  identifier
218
230
  end
231
+ [identifiers, gas]
219
232
  end
220
233
 
221
234
  # Fetch the rows, split them into component table parts,
@@ -97,7 +97,7 @@ module Sequel
97
97
  def need_associated_primary_key?
98
98
  false
99
99
  end
100
-
100
+
101
101
  # Returns the reciprocal association variable, if one exists. The reciprocal
102
102
  # association is the association in the associated class that is the opposite
103
103
  # of the current association. For example, Album.many_to_one :artist and
@@ -128,6 +128,12 @@ module Sequel
128
128
  :"remove_all_#{self[:name]}"
129
129
  end
130
130
 
131
+ # Whether associated objects need to be removed from the association before
132
+ # being destroyed in order to preserve referential integrity.
133
+ def remove_before_destroy?
134
+ true
135
+ end
136
+
131
137
  # Name symbol for the remove_ association method
132
138
  def remove_method
133
139
  :"remove_#{singularize(self[:name])}"
@@ -144,7 +150,7 @@ module Sequel
144
150
  true
145
151
  end
146
152
 
147
- # The columns to select when loading the association, nil by default.
153
+ # The columns to select when loading the association.
148
154
  def select
149
155
  self[:select]
150
156
  end
@@ -259,22 +265,27 @@ module Sequel
259
265
  self[:primary_key] ||= self[:model].primary_key
260
266
  end
261
267
 
262
- # One to many associations set the reciprocal to self when loading associated records.
263
- def set_reciprocal_to_self?
264
- true
265
- end
266
-
267
268
  # Whether the reciprocal of this association returns an array of objects instead of a single object,
268
269
  # false for a one_to_many association.
269
270
  def reciprocal_array?
270
271
  false
271
272
  end
272
273
 
274
+ # Destroying one_to_many associated objects automatically deletes the foreign key.
275
+ def remove_before_destroy?
276
+ false
277
+ end
278
+
273
279
  # The one_to_many association needs to check that an object to be removed already is associated.
274
280
  def remove_should_check_existing?
275
281
  true
276
282
  end
277
283
 
284
+ # One to many associations set the reciprocal to self when loading associated records.
285
+ def set_reciprocal_to_self?
286
+ true
287
+ end
288
+
278
289
  private
279
290
 
280
291
  # The reciprocal type of a one_to_many association is a many_to_one association.
@@ -1236,7 +1236,7 @@ module Sequel
1236
1236
  @columns_updated = @values.reject{|k, v| !columns.include?(k)}
1237
1237
  changed_columns.reject!{|c| columns.include?(c)}
1238
1238
  end
1239
- _update(@columns_updated) unless @columns_updated.empty?
1239
+ _update_columns(@columns_updated)
1240
1240
  @this = nil
1241
1241
  after_update
1242
1242
  after_save
@@ -1264,7 +1264,14 @@ module Sequel
1264
1264
  Array(primary_key).each{|x| v.delete(x) unless changed_columns.include?(x)}
1265
1265
  v
1266
1266
  end
1267
-
1267
+
1268
+ # Call _update with the given columns, if any are present.
1269
+ # Plugins can override this method in order to update with
1270
+ # additional columns, even when the column hash is initially empty.
1271
+ def _update_columns(columns)
1272
+ _update(columns) unless columns.empty?
1273
+ end
1274
+
1268
1275
  # Update this instance's dataset with the supplied column hash.
1269
1276
  def _update(columns)
1270
1277
  n = _update_dataset.update(columns)
@@ -24,7 +24,8 @@ module Sequel
24
24
  #
25
25
  # The identity_map plugin is not compatible with the standard eager loading of
26
26
  # many_to_many and many_through_many associations. If you want to use the identity_map plugin,
27
- # you should use +eager_graph+ instead of +eager+ for those associations.
27
+ # you should use +eager_graph+ instead of +eager+ for those associations. It is also
28
+ # not compatible with the eager loading in the +rcte_tree+ plugin.
28
29
  #
29
30
  # Usage:
30
31
  #
@@ -141,8 +141,8 @@ module Sequel
141
141
  # array of symbols for a composite key association.
142
142
  # * :uniq - Adds a after_load callback that makes the array of objects unique.
143
143
  def many_through_many(name, through, opts={}, &block)
144
- associate(:many_through_many, name, opts.merge(:through=>through), &block)
145
- end
144
+ associate(:many_through_many, name, opts.merge(through.is_a?(Hash) ? through : {:through=>through}), &block)
145
+ end
146
146
 
147
147
  private
148
148
 
@@ -123,15 +123,18 @@ module Sequel
123
123
  end
124
124
 
125
125
  # Remove the matching associated object from the current object.
126
- # If the :destroy option is given, destroy the object after disassociating it.
126
+ # If the :destroy option is given, destroy the object after disassociating it
127
+ # (unless destroying the object would automatically disassociate it).
127
128
  # Returns the object removed, if it exists.
128
129
  def nested_attributes_remove(reflection, pk, opts={})
129
130
  if obj = nested_attributes_find(reflection, pk)
130
- before_save_hook do
131
- if reflection.returns_array?
132
- send(reflection.remove_method, obj)
133
- else
134
- send(reflection.setter_method, nil)
131
+ if !opts[:destroy] || reflection.remove_before_destroy?
132
+ before_save_hook do
133
+ if reflection.returns_array?
134
+ send(reflection.remove_method, obj)
135
+ else
136
+ send(reflection.setter_method, nil)
137
+ end
135
138
  end
136
139
  end
137
140
  after_save_hook{obj.destroy} if opts[:destroy]
@@ -68,7 +68,7 @@ module Sequel
68
68
 
69
69
  # Only update the row if it has the same lock version, and increment the
70
70
  # lock version.
71
- def _update(columns)
71
+ def _update_columns(columns)
72
72
  lc = model.lock_column
73
73
  lcv = send(lc)
74
74
  columns[lc] = lcv + 1
@@ -205,9 +205,9 @@ module Sequel
205
205
  klass.from_xml_node(node)
206
206
  end
207
207
  elsif cols.include?(k)
208
- self[k.to_sym] = node.children.first.to_s
208
+ self[k.to_sym] = node[:nil] ? nil : node.children.first.to_s
209
209
  elsif meths.include?("#{k}=")
210
- send("#{k}=", node.children.first.to_s)
210
+ send("#{k}=", node[:nil] ? nil : node.children.first.to_s)
211
211
  else
212
212
  raise Error, "Entry in XML not an association or column and no setter method exists: #{k}"
213
213
  end
@@ -268,7 +268,15 @@ module Sequel
268
268
  x = model.xml_builder(opts)
269
269
  x.send(name_proc[opts.fetch(:root_name, model.send(:underscore, model.name)).to_s]) do |x1|
270
270
  cols.each do |c|
271
- x1.send(name_proc[c.to_s], vals[c], types ? {:type=>db_schema.fetch(c, {})[:type]} : {})
271
+ attrs = {}
272
+ if types
273
+ attrs[:type] = db_schema.fetch(c, {})[:type]
274
+ end
275
+ v = vals[c]
276
+ if v.nil?
277
+ attrs[:nil] = ''
278
+ end
279
+ x1.send(name_proc[c.to_s], v, attrs)
272
280
  end
273
281
  if inc.is_a?(Hash)
274
282
  inc.each{|k, v| to_xml_include(x1, k, v)}
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 16
6
+ MINOR = 17
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -23,6 +23,11 @@ context "A new Database" do
23
23
  Sequel::Database.new(1 => 2, :logger => [4], :loggers => [3]).loggers.should == [4,3]
24
24
  end
25
25
 
26
+ specify "should set the sql_log_level from opts[:sql_log_level]" do
27
+ db = Sequel::Database.new(1 => 2, :sql_log_level=>:debug).sql_log_level.should == :debug
28
+ db = Sequel::Database.new(1 => 2, :sql_log_level=>'debug').sql_log_level.should == :debug
29
+ end
30
+
26
31
  specify "should create a connection pool" do
27
32
  @db.pool.should be_a_kind_of(Sequel::ConnectionPool)
28
33
  @db.pool.max_size.should == 4
@@ -262,6 +267,15 @@ context "Database#log_yield" do
262
267
  @o.logs.first.last.should =~ /\A\(\d\.\d{6}s\) blah\z/
263
268
  end
264
269
 
270
+ specify "should respect sql_log_level setting" do
271
+ @db.sql_log_level = :debug
272
+ @db.log_yield('blah'){}
273
+ @o.logs.length.should == 1
274
+ @o.logs.first.length.should == 2
275
+ @o.logs.first.first.should == :debug
276
+ @o.logs.first.last.should =~ /\A\(\d\.\d{6}s\) blah\z/
277
+ end
278
+
265
279
  specify "should log message with duration at warn level if duration greater than log_warn_duration" do
266
280
  @db.log_warn_duration = 0
267
281
  @db.log_yield('blah'){}
@@ -954,19 +968,19 @@ context "A Database adapter with a scheme" do
954
968
  c = Sequel.ccc('mydb')
955
969
  p = proc{c.opts.delete_if{|k,v| k == :disconnection_proc || k == :single_threaded}}
956
970
  c.should be_a_kind_of(CCC)
957
- p.call.should == {:adapter=>:ccc, :database => 'mydb'}
971
+ p.call.should == {:adapter=>:ccc, :database => 'mydb', :adapter_class=>CCC}
958
972
 
959
973
  c = Sequel.ccc('mydb', :host => 'localhost')
960
974
  c.should be_a_kind_of(CCC)
961
- p.call.should == {:adapter=>:ccc, :database => 'mydb', :host => 'localhost'}
975
+ p.call.should == {:adapter=>:ccc, :database => 'mydb', :host => 'localhost', :adapter_class=>CCC}
962
976
 
963
977
  c = Sequel.ccc
964
978
  c.should be_a_kind_of(CCC)
965
- p.call.should == {:adapter=>:ccc}
979
+ p.call.should == {:adapter=>:ccc, :adapter_class=>CCC}
966
980
 
967
981
  c = Sequel.ccc(:database => 'mydb', :host => 'localhost')
968
982
  c.should be_a_kind_of(CCC)
969
- p.call.should == {:adapter=>:ccc, :database => 'mydb', :host => 'localhost'}
983
+ p.call.should == {:adapter=>:ccc, :database => 'mydb', :host => 'localhost', :adapter_class=>CCC}
970
984
  end
971
985
 
972
986
  specify "should be accessible through Sequel.connect with options" do
@@ -1484,6 +1498,31 @@ context "Database#remove_servers" do
1484
1498
  end
1485
1499
  end
1486
1500
 
1501
+ context "Database#each_server with do/jdbc adapter connection string without :adapter option" do
1502
+ before do
1503
+ klass = Class.new(Sequel::Database)
1504
+ klass.should_receive(:adapter_class).once.with(:jdbc).and_return(MockDatabase)
1505
+ @db = klass.connect('jdbc:blah:', :host=>1, :database=>2, :servers=>{:server1=>{:host=>3}})
1506
+ def @db.connect(server)
1507
+ server_opts(server)
1508
+ end
1509
+ def @db.disconnect_connection(c)
1510
+ end
1511
+ end
1512
+
1513
+ specify "should yield a separate database object for each server" do
1514
+ hosts = []
1515
+ @db.each_server do |db|
1516
+ db.should be_a_kind_of(Sequel::Database)
1517
+ db.should_not == @db
1518
+ db.opts[:adapter_class].should == MockDatabase
1519
+ db.opts[:database].should == 2
1520
+ hosts << db.opts[:host]
1521
+ end
1522
+ hosts.sort.should == [1, 3]
1523
+ end
1524
+ end
1525
+
1487
1526
  context "Database#each_server" do
1488
1527
  before do
1489
1528
  @db = Sequel.connect(:adapter=>:mock, :host=>1, :database=>2, :servers=>{:server1=>{:host=>3}, :server2=>{:host=>4}})
@@ -160,6 +160,20 @@ describe Sequel::Dataset, " graphing" do
160
160
  ].should(include(ds.sql))
161
161
  end
162
162
 
163
+ it "#set_graph_aliases should allow a single array entry to specify a table, assuming the same column as the key" do
164
+ ds = @ds1.graph(:lines, :x=>:id).set_graph_aliases(:x=>[:points], :y=>[:lines])
165
+ ['SELECT points.x, lines.y FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)',
166
+ 'SELECT lines.y, points.x FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
167
+ ].should(include(ds.sql))
168
+ end
169
+
170
+ it "#set_graph_aliases should allow hash values to be symbols specifying table, assuming the same column as the key" do
171
+ ds = @ds1.graph(:lines, :x=>:id).set_graph_aliases(:x=>:points, :y=>:lines)
172
+ ['SELECT points.x, lines.y FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)',
173
+ 'SELECT lines.y, points.x FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
174
+ ].should(include(ds.sql))
175
+ end
176
+
163
177
  it "#set_graph_aliases should only alias columns if necessary" do
164
178
  ds = @ds1.set_graph_aliases(:x=>[:points, :x], :y=>[:lines, :y])
165
179
  ['SELECT points.x, lines.y FROM points',
@@ -282,6 +296,26 @@ describe Sequel::Dataset, " graphing" do
282
296
  results.first.should == {:points=>{:z1=>2}, :lines=>{:z2=>3}}
283
297
  end
284
298
 
299
+ it "#graph_each should correctly map values when #set_graph_aliases is used with a single argument for each entry" do
300
+ ds = @ds1.graph(:lines, :x=>:id).set_graph_aliases(:x=>[:points], :y=>[:lines])
301
+ def ds.fetch_rows(sql, &block)
302
+ yield({:x=>2,:y=>3})
303
+ end
304
+ results = ds.all
305
+ results.length.should == 1
306
+ results.first.should == {:points=>{:x=>2}, :lines=>{:y=>3}}
307
+ end
308
+
309
+ it "#graph_each should correctly map values when #set_graph_aliases is used with a symbol for each entry" do
310
+ ds = @ds1.graph(:lines, :x=>:id).set_graph_aliases(:x=>:points, :y=>:lines)
311
+ def ds.fetch_rows(sql, &block)
312
+ yield({:x=>2,:y=>3})
313
+ end
314
+ results = ds.all
315
+ results.length.should == 1
316
+ results.first.should == {:points=>{:x=>2}, :lines=>{:y=>3}}
317
+ end
318
+
285
319
  it "#graph_each should run the row_proc for graphed datasets" do
286
320
  @ds1.row_proc = proc{|h| h.keys.each{|k| h[k] *= 2}; h}
287
321
  @ds2.row_proc = proc{|h| h.keys.each{|k| h[k] *= 3}; h}
@@ -1,5 +1,8 @@
1
1
  require File.join(File.dirname(File.expand_path(__FILE__)), 'spec_helper')
2
2
 
3
+ if defined?(ActiveSupport::Inflector)
4
+ skip_warn "inflector extension: active_support inflector loaded"
5
+ else
3
6
  describe String do
4
7
  it "#camelize and #camelcase should transform the word to CamelCase" do
5
8
  "egg_and_hams".camelize.should == "EggAndHams"
@@ -179,3 +182,4 @@ describe 'Default inflections' do
179
182
  end
180
183
  end
181
184
  end
185
+ end
@@ -42,6 +42,25 @@ describe Sequel::Model, "many_through_many" do
42
42
  proc{@c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], :album_tags]}.should raise_error(Sequel::Error)
43
43
  end
44
44
 
45
+ it "should allow only two arguments with the :through option" do
46
+ @c1.many_through_many :tags, :through=>[[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
47
+ n = @c1.load(:id => 1234)
48
+ a = n.tags_dataset
49
+ a.should be_a_kind_of(Sequel::Dataset)
50
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
51
+ n.tags.should == [@c2.load(:id=>1)]
52
+ end
53
+
54
+ it "should be clonable" do
55
+ @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
56
+ @c1.many_through_many :other_tags, :clone=>:tags
57
+ n = @c1.load(:id => 1234)
58
+ a = n.other_tags_dataset
59
+ a.should be_a_kind_of(Sequel::Dataset)
60
+ a.sql.should == 'SELECT tags.* FROM tags INNER JOIN albums_tags ON (albums_tags.tag_id = tags.id) INNER JOIN albums ON (albums.id = albums_tags.album_id) INNER JOIN albums_artists ON ((albums_artists.album_id = albums.id) AND (albums_artists.artist_id = 1234))'
61
+ n.tags.should == [@c2.load(:id=>1)]
62
+ end
63
+
45
64
  it "should use join tables given" do
46
65
  @c1.many_through_many :tags, [[:albums_artists, :artist_id, :album_id], [:albums, :id, :id], [:albums_tags, :album_id, :tag_id]]
47
66
  n = @c1.load(:id => 1234)
@@ -186,7 +186,7 @@ describe "NestedAttributes plugin" do
186
186
  ar.set(:first_album_attributes=>{:id=>10, :_delete=>'t'})
187
187
  @mods.should == []
188
188
  ar.save
189
- @mods.should == [[:u, :albums, {:artist_id=>nil}, "(artist_id = 20)"], [:u, :artists, {:name=>"Ar"}, "(id = 20)"], [:d, :albums, "(id = 10)"]]
189
+ @mods.should == [[:u, :artists, {:name=>"Ar"}, "(id = 20)"], [:d, :albums, "(id = 10)"]]
190
190
  end
191
191
 
192
192
  it "should support destroying one_to_many objects" do
@@ -196,7 +196,7 @@ describe "NestedAttributes plugin" do
196
196
  ar.set(:albums_attributes=>[{:id=>10, :_delete=>'t'}])
197
197
  @mods.should == []
198
198
  ar.save
199
- @mods.should == [[:u, :albums, {:name=>"Al", :artist_id=>nil}, '(id = 10)'], [:u, :artists, {:name=>"Ar"}, '(id = 20)'], [:d, :albums, '(id = 10)']]
199
+ @mods.should == [[:u, :artists, {:name=>"Ar"}, '(id = 20)'], [:d, :albums, '(id = 10)']]
200
200
  end
201
201
 
202
202
  it "should support destroying many_to_many objects" do
@@ -20,6 +20,14 @@ describe "optimistic_locking plugin" do
20
20
  else
21
21
  0
22
22
  end
23
+ when /UPDATE people SET #{lv} = (\d+) WHERE \(\(id = (\d+)\) AND \(#{lv} = (\d+)\)\)/
24
+ m = h[$2.to_i]
25
+ if m && m[:lock_version] == $3.to_i
26
+ m.merge!(:lock_version=>$1.to_i)
27
+ 1
28
+ else
29
+ 0
30
+ end
23
31
  else
24
32
  puts update_sql(opts)
25
33
  end
@@ -96,5 +104,22 @@ describe "optimistic_locking plugin" do
96
104
  p2 = c[1]
97
105
  p1.update(:name=>'Jim')
98
106
  proc{p2.update(:name=>'Bob')}.should raise_error(Sequel::Plugins::OptimisticLocking::Error)
99
- end
107
+ end
108
+
109
+ specify "should increment the lock column when #modified! even if no columns are changed" do
110
+ p1 = @c[1]
111
+ p1.modified!
112
+ lv = p1.lock_version
113
+ p1.save_changes
114
+ p1.lock_version.should == lv + 1
115
+ end
116
+
117
+ specify "should not increment the lock column when the update fails" do
118
+ @c.dataset.meta_def(:update) { raise Exception }
119
+ p1 = @c[1]
120
+ p1.modified!
121
+ lv = p1.lock_version
122
+ proc{p1.save_changes}.should raise_error(Exception)
123
+ p1.lock_version.should == lv
124
+ end
100
125
  end
@@ -36,6 +36,16 @@ describe "Sequel::Plugins::XmlSerializer" do
36
36
  Album.from_xml(@album.to_xml).should == @album
37
37
  end
38
38
 
39
+ it "should round trip successfully with empty strings" do
40
+ artist = Artist.load(:id=>2, :name=>'')
41
+ Artist.from_xml(artist.to_xml).should == artist
42
+ end
43
+
44
+ it "should round trip successfully with nil values" do
45
+ artist = Artist.load(:id=>2, :name=>nil)
46
+ Artist.from_xml(artist.to_xml).should == artist
47
+ end
48
+
39
49
  it "should handle the :only option" do
40
50
  Artist.from_xml(@artist.to_xml(:only=>:name)).should == Artist.load(:name=>@artist.name)
41
51
  Album.from_xml(@album.to_xml(:only=>[:id, :name])).should == Album.load(:id=>@album.id, :name=>@album.name)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- hash: 71
4
+ hash: 67
5
5
  prerelease: false
6
6
  segments:
7
7
  - 3
8
- - 16
8
+ - 17
9
9
  - 0
10
- version: 3.16.0
10
+ version: 3.17.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jeremy Evans
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-10-01 00:00:00 -07:00
18
+ date: 2010-11-05 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -80,6 +80,7 @@ extra_rdoc_files:
80
80
  - doc/release_notes/3.14.0.txt
81
81
  - doc/release_notes/3.15.0.txt
82
82
  - doc/release_notes/3.16.0.txt
83
+ - doc/release_notes/3.17.0.txt
83
84
  files:
84
85
  - COPYING
85
86
  - CHANGELOG
@@ -127,6 +128,7 @@ files:
127
128
  - doc/release_notes/3.14.0.txt
128
129
  - doc/release_notes/3.15.0.txt
129
130
  - doc/release_notes/3.16.0.txt
131
+ - doc/release_notes/3.17.0.txt
130
132
  - doc/sharding.rdoc
131
133
  - doc/sql.rdoc
132
134
  - doc/virtual_rows.rdoc