sequel 3.16.0 → 3.17.0

Sign up to get free protection for your applications and to get access to all the features.
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