sequel 5.75.0 → 5.77.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +48 -0
  3. data/doc/opening_databases.rdoc +4 -2
  4. data/doc/release_notes/5.76.0.txt +86 -0
  5. data/doc/release_notes/5.77.0.txt +63 -0
  6. data/doc/testing.rdoc +3 -1
  7. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  8. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  9. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +11 -0
  10. data/lib/sequel/adapters/mysql2.rb +2 -2
  11. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  12. data/lib/sequel/adapters/postgres.rb +2 -2
  13. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +10 -0
  15. data/lib/sequel/adapters/shared/oracle.rb +4 -6
  16. data/lib/sequel/adapters/shared/postgres.rb +22 -1
  17. data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
  18. data/lib/sequel/adapters/shared/sqlite.rb +20 -2
  19. data/lib/sequel/adapters/sqlite.rb +42 -3
  20. data/lib/sequel/connection_pool.rb +4 -2
  21. data/lib/sequel/database/misc.rb +2 -2
  22. data/lib/sequel/database/schema_methods.rb +6 -0
  23. data/lib/sequel/dataset/features.rb +10 -1
  24. data/lib/sequel/dataset/sql.rb +47 -34
  25. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  26. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  27. data/lib/sequel/extensions/named_timezones.rb +1 -1
  28. data/lib/sequel/extensions/pg_array.rb +2 -0
  29. data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
  30. data/lib/sequel/extensions/pg_range.rb +2 -2
  31. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  32. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  33. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  34. data/lib/sequel/model/associations.rb +9 -2
  35. data/lib/sequel/model/base.rb +5 -2
  36. data/lib/sequel/plugins/list.rb +5 -2
  37. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  38. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  39. data/lib/sequel/version.rb +1 -1
  40. metadata +9 -3
@@ -25,11 +25,16 @@ module Sequel
25
25
  false
26
26
  end
27
27
 
28
+ # :nocov:
29
+
28
30
  # Whether the dataset requires SQL standard datetimes. False by default,
29
- # as most allow strings with ISO 8601 format.
31
+ # as most allow strings with ISO 8601 format. Only for backwards compatibility,
32
+ # no longer used internally, do not use in new code.
30
33
  def requires_sql_standard_datetimes?
34
+ # SEQUEL6: Remove
31
35
  false
32
36
  end
37
+ # :nocov:
33
38
 
34
39
  # Whether type specifiers are required for prepared statement/bound
35
40
  # variable argument placeholders (i.e. :bv__integer), false by default.
@@ -183,10 +188,14 @@ module Sequel
183
188
  true
184
189
  end
185
190
 
191
+ # :nocov:
192
+
186
193
  # Whether the dataset supports timezones in literal timestamps, false by default.
187
194
  def supports_timestamp_timezones?
195
+ # SEQUEL6: Remove
188
196
  false
189
197
  end
198
+ # :nocov:
190
199
 
191
200
  # Whether the dataset supports fractional seconds in literal timestamps, true by default.
192
201
  def supports_timestamp_usecs?
@@ -82,7 +82,7 @@ module Sequel
82
82
  when DateTime
83
83
  literal_datetime_append(sql, v)
84
84
  when Date
85
- sql << literal_date(v)
85
+ literal_date_append(sql, v)
86
86
  when Dataset
87
87
  literal_dataset_append(sql, v)
88
88
  else
@@ -115,6 +115,33 @@ module Sequel
115
115
  sql
116
116
  end
117
117
 
118
+ # Literalize a date or time value, as a SQL string value with no
119
+ # typecasting. If +raw+ is true, remove the surrounding single
120
+ # quotes. This is designed for usage by bound argument code that
121
+ # can work even if the auto_cast_date_and_time extension is
122
+ # used (either manually or implicitly in the related adapter).
123
+ def literal_date_or_time(dt, raw=false)
124
+ value = case dt
125
+ when SQLTime
126
+ literal_sqltime(dt)
127
+ when Time
128
+ literal_time(dt)
129
+ when DateTime
130
+ literal_datetime(dt)
131
+ when Date
132
+ literal_date(dt)
133
+ else
134
+ raise TypeError, "unsupported type: #{dt.inspect}"
135
+ end
136
+
137
+ if raw
138
+ value.sub!(/\A'/, '')
139
+ value.sub!(/'\z/, '')
140
+ end
141
+
142
+ value
143
+ end
144
+
118
145
  # Returns an array of insert statements for inserting multiple records.
119
146
  # This method is used by +multi_insert+ to format insert statements and
120
147
  # expects a keys array and and an array of value arrays.
@@ -1104,9 +1131,14 @@ module Sequel
1104
1131
  :"t#{number}"
1105
1132
  end
1106
1133
 
1107
- # The strftime format to use when literalizing the time.
1134
+ # The strftime format to use when literalizing time (Sequel::SQLTime) values.
1135
+ def default_time_format
1136
+ "'%H:%M:%S.%6N'"
1137
+ end
1138
+
1139
+ # The strftime format to use when literalizing timestamp (Time/DateTime) values.
1108
1140
  def default_timestamp_format
1109
- requires_sql_standard_datetimes? ? "TIMESTAMP '%Y-%m-%d %H:%M:%S%N%z'" : "'%Y-%m-%d %H:%M:%S%N%z'"
1141
+ "'%Y-%m-%d %H:%M:%S.%6N'"
1110
1142
  end
1111
1143
 
1112
1144
  def delete_delete_sql(sql)
@@ -1169,43 +1201,23 @@ module Sequel
1169
1201
  {1 => ((op == :IN) ? 0 : 1)}
1170
1202
  end
1171
1203
 
1172
- # Format the timestamp based on the default_timestamp_format, with a couple
1173
- # of modifiers. First, allow %N to be used for fractions seconds (if the
1174
- # database supports them), and override %z to always use a numeric offset
1175
- # of hours and minutes.
1204
+ # Format the timestamp based on the default_timestamp_format.
1176
1205
  def format_timestamp(v)
1177
- v2 = db.from_application_timestamp(v)
1178
- fmt = default_timestamp_format.gsub(/%[Nz]/) do |m|
1179
- if m == '%N'
1180
- # Ruby 1.9 supports %N in timestamp formats, but Sequel has supported %N
1181
- # for longer in a different way, where the . is already appended and only 6
1182
- # decimal places are used by default.
1183
- format_timestamp_usec(v.is_a?(DateTime) ? v.sec_fraction*(1000000) : v.usec) if supports_timestamp_usecs?
1184
- else
1185
- if supports_timestamp_timezones?
1186
- # Would like to just use %z format, but it doesn't appear to work on Windows
1187
- # Instead, the offset fragment is constructed manually
1188
- minutes = (v2.is_a?(DateTime) ? v2.offset * 1440 : v2.utc_offset/60).to_i
1189
- format_timestamp_offset(*minutes.divmod(60))
1190
- end
1191
- end
1192
- end
1193
- v2.strftime(fmt)
1206
+ db.from_application_timestamp(v).strftime(default_timestamp_format)
1194
1207
  end
1195
1208
 
1196
- # Return the SQL timestamp fragment to use for the timezone offset.
1197
- def format_timestamp_offset(hour, minute)
1198
- sprintf("%+03i%02i", hour, minute)
1199
- end
1209
+ # :nocov:
1200
1210
 
1201
1211
  # Return the SQL timestamp fragment to use for the fractional time part.
1202
1212
  # Should start with the decimal point. Uses 6 decimal places by default.
1203
1213
  def format_timestamp_usec(usec, ts=timestamp_precision)
1214
+ # SEQUEL6: Remove
1204
1215
  unless ts == 6
1205
1216
  usec = usec/(10 ** (6 - ts))
1206
1217
  end
1207
1218
  sprintf(".%0#{ts}d", usec)
1208
1219
  end
1220
+ # :nocov:
1209
1221
 
1210
1222
  # Append literalization of identifier to SQL string, considering regular strings
1211
1223
  # as SQL identifiers instead of SQL strings.
@@ -1347,11 +1359,12 @@ module Sequel
1347
1359
 
1348
1360
  # SQL fragment for Date, using the ISO8601 format.
1349
1361
  def literal_date(v)
1350
- if requires_sql_standard_datetimes?
1351
- v.strftime("DATE '%Y-%m-%d'")
1352
- else
1353
- v.strftime("'%Y-%m-%d'")
1354
- end
1362
+ v.strftime("'%Y-%m-%d'")
1363
+ end
1364
+
1365
+ # Append literalization of date to SQL string.
1366
+ def literal_date_append(sql, v)
1367
+ sql << literal_date(v)
1355
1368
  end
1356
1369
 
1357
1370
  # SQL fragment for DateTime
@@ -1414,7 +1427,7 @@ module Sequel
1414
1427
 
1415
1428
  # SQL fragment for Sequel::SQLTime, containing just the time part
1416
1429
  def literal_sqltime(v)
1417
- v.strftime("'%H:%M:%S#{format_timestamp_usec(v.usec, sqltime_precision) if supports_timestamp_usecs?}'")
1430
+ v.strftime(default_time_format)
1418
1431
  end
1419
1432
 
1420
1433
  # Append literalization of Sequel::SQLTime to SQL string.
@@ -0,0 +1,94 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The auto_cast_date_and_time extension uses SQL standard type casting
4
+ # when literalizing date, time, and timestamp values:
5
+ #
6
+ # DB.literal(Time.now)
7
+ # # => "TIMESTAMP '...'"
8
+ #
9
+ # DB.literal(Date.today)
10
+ # # => "DATE '...'"
11
+ #
12
+ # DB.literal(Sequel::SQLTime.create(10, 20, 30))
13
+ # # => "TIME '10:20:30.000000'"
14
+ #
15
+ # The default behavior of Sequel on adapters that do not require the
16
+ # SQL standard behavior is to format the date or time value without:
17
+ # casting
18
+ #
19
+ # DB.literal(Sequel::SQLTime.create(10, 20, 30))
20
+ # # => "'10:20:30.000000'"
21
+ #
22
+ # However, then the database cannot determine the type of the string,
23
+ # and must perform some implicit casting. If implicit casting cannot
24
+ # be used, it will probably treat the value as a string:
25
+ #
26
+ # DB.get(Time.now).class
27
+ # # Without auto_cast_date_and_time: String
28
+ # # With auto_cast_date_and_time: Time
29
+ #
30
+ # Note that not all databases support this extension. PostgreSQL and
31
+ # MySQL support it, but SQLite and Microsoft SQL Server do not.
32
+ #
33
+ # You can load this extension into specific datasets:
34
+ #
35
+ # ds = DB[:table]
36
+ # ds = ds.extension(:auto_cast_date_and_time)
37
+ #
38
+ # Or you can load it into all of a database's datasets, which
39
+ # is probably the desired behavior if you are using this extension:
40
+ #
41
+ # DB.extension(:auto_cast_date_and_time)
42
+ #
43
+ # Related module: Sequel::AutoCastDateAndTime
44
+
45
+ #
46
+ module Sequel
47
+ module AutoCastDateAndTime
48
+ # :nocov:
49
+
50
+ # Mark the datasets as requiring sql standard date times. This is only needed
51
+ # for backwards compatibility.
52
+ def requires_sql_standard_datetimes?
53
+ # SEQUEL6: Remove
54
+ true
55
+ end
56
+ # :nocov:
57
+
58
+ private
59
+
60
+ # Explicitly cast SQLTime objects to TIME.
61
+ def literal_sqltime_append(sql, v)
62
+ sql << "TIME "
63
+ super
64
+ end
65
+
66
+ # Explicitly cast Time objects to TIMESTAMP.
67
+ def literal_time_append(sql, v)
68
+ sql << literal_datetime_timestamp_cast
69
+ super
70
+ end
71
+
72
+ # Explicitly cast DateTime objects to TIMESTAMP.
73
+ def literal_datetime_append(sql, v)
74
+ sql << literal_datetime_timestamp_cast
75
+ super
76
+ end
77
+
78
+ # Explicitly cast Date objects to DATE.
79
+ def literal_date_append(sql, v)
80
+ sql << "DATE "
81
+ super
82
+ end
83
+
84
+ # The default cast string to use for Time/DateTime objects.
85
+ # Respects existing method if already defined.
86
+ def literal_datetime_timestamp_cast
87
+ return super if defined?(super)
88
+ 'TIMESTAMP '
89
+ end
90
+ end
91
+
92
+ Dataset.register_extension(:auto_cast_date_and_time, AutoCastDateAndTime)
93
+ end
94
+
@@ -14,12 +14,12 @@
14
14
  #
15
15
  # ds = DB[:items].extension(:duplicate_columns_handler)
16
16
  #
17
- # A database option is introduced: :on_duplicate_columns. It accepts a Symbol
18
- # or any object that responds to :call.
17
+ # If the Database option :on_duplicate_columns is set, it configures how this
18
+ # extension works. The value should be # or any object that responds to :call.
19
19
  #
20
- # on_duplicate_columns: :raise
21
- # on_duplicate_columns: :warn
22
- # on_duplicate_columns: :ignore
20
+ # on_duplicate_columns: :raise # or 'raise'
21
+ # on_duplicate_columns: :warn # or 'warn'
22
+ # on_duplicate_columns: :ignore # or anything unrecognized
23
23
  # on_duplicate_columns: lambda{|columns| arbitrary_condition? ? :raise : :warn}
24
24
  #
25
25
  # You may also configure duplicate columns handling for a specific dataset:
@@ -30,9 +30,10 @@
30
30
  # ds.on_duplicate_columns{|columns| arbitrary_condition? ? :raise : :warn}
31
31
  # ds.on_duplicate_columns(lambda{|columns| arbitrary_condition? ? :raise : :warn})
32
32
  #
33
- # If :raise is specified, a Sequel::DuplicateColumnError is raised.
34
- # If :warn is specified, you will receive a warning via +warn+.
33
+ # If :raise or 'raise' is specified, a Sequel::DuplicateColumnError is raised.
34
+ # If :warn or 'warn' is specified, you will receive a warning via +warn+.
35
35
  # If a callable is specified, it will be called.
36
+ # For other values, duplicate columns are ignored (Sequel's default behavior)
36
37
  # If no on_duplicate_columns is specified, the default is :warn.
37
38
  #
38
39
  # Related module: Sequel::DuplicateColumnsHandler
@@ -64,9 +65,9 @@ module Sequel
64
65
  message = "#{caller(*CALLER_ARGS).first}: One or more duplicate columns present in #{cols.inspect}"
65
66
 
66
67
  case duplicate_columns_handler_type(cols)
67
- when :raise
68
+ when :raise, 'raise'
68
69
  raise DuplicateColumnError, message
69
- when :warn
70
+ when :warn, 'warn'
70
71
  warn message
71
72
  end
72
73
  end
@@ -136,7 +136,7 @@ module Sequel
136
136
  v = output_timezone.utc_to_local(v.new_offset(0))
137
137
 
138
138
  # Force DateTime output instead of TZInfo::DateTimeWithOffset
139
- DateTime.jd(v.jd, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
139
+ DateTime.civil(v.year, v.month, v.day, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
140
140
  end
141
141
  # :nodoc:
142
142
  # :nocov:
@@ -241,6 +241,8 @@ module Sequel
241
241
  else
242
242
  literal(a)
243
243
  end
244
+ when Time, Date
245
+ @default_dataset.literal_date_or_time(a)
244
246
  else
245
247
  if (s = bound_variable_arg(a, nil)).is_a?(String)
246
248
  bound_variable_array_string(s)
@@ -53,8 +53,8 @@ module Sequel
53
53
  # on jdbc.
54
54
  def bound_variable_arg(arg, conn)
55
55
  case arg
56
- when Date, Time
57
- literal(arg)
56
+ when Time, Date
57
+ @default_dataset.literal_date_or_time(arg)
58
58
  else
59
59
  super
60
60
  end
@@ -203,7 +203,7 @@ module Sequel
203
203
  date <<= ((date.year) * 24 - 12)
204
204
  date = db.from_application_timestamp(date)
205
205
  minutes = (date.offset * 1440).to_i
206
- date.strftime("'%Y-%m-%d %H:%M:%S.%N#{format_timestamp_offset(*minutes.divmod(60))} BC'")
206
+ date.strftime("'%Y-%m-%d %H:%M:%S.%6N#{sprintf("%+03i%02i", *minutes.divmod(60))} BC'")
207
207
  else
208
208
  super
209
209
  end
@@ -247,7 +247,7 @@ module Sequel
247
247
  def literal_time(time)
248
248
  if time < TIME_YEAR_1
249
249
  time = db.from_application_timestamp(time)
250
- time.strftime("'#{sprintf('%04i', time.year.abs+1)}-%m-%d %H:%M:%S.%N#{format_timestamp_offset(*(time.utc_offset/RATIONAL_60).divmod(60))} BC'")
250
+ time.strftime("'#{sprintf('%04i', time.year.abs+1)}-%m-%d %H:%M:%S.%6N#{sprintf("%+03i%02i", *(time.utc_offset/RATIONAL_60).divmod(60))} BC'")
251
251
  else
252
252
  super
253
253
  end
@@ -494,8 +494,8 @@ module Sequel
494
494
  case k
495
495
  when nil
496
496
  ''
497
- when Date, Time
498
- ds.literal(k)[1...-1]
497
+ when Time, Date
498
+ ds.literal_date_or_time(k, true)
499
499
  when Integer, Float
500
500
  k.to_s
501
501
  when BigDecimal
@@ -1,20 +1,35 @@
1
1
  # frozen-string-literal: true
2
2
  #
3
3
  # The pg_timestamptz extension changes the default timestamp
4
- # type for the database to be +timestamptz+ (+timestamp with time zone+)
5
- # instead of +timestamp+ (+timestamp without time zone+). This is
4
+ # type for the database to be +timestamptz+ (<tt>timestamp with time zone</tt>)
5
+ # instead of +timestamp+ (<tt>timestamp without time zone</tt>). This is
6
6
  # recommended if you are dealing with multiple timezones in your application.
7
+ #
8
+ # If you are using the auto_cast_date_and_time extension, the pg_timestamptz
9
+ # extension will automatically cast Time and DateTime values to
10
+ # <tt>TIMESTAMP WITH TIME ZONE</tt> instead of +TIMESTAMP+.
7
11
  #
8
12
  # To load the extension into the database:
9
13
  #
10
14
  # DB.extension :pg_timestamptz
11
15
  #
12
- # Related module: Sequel::Postgres::Timestamptz
16
+ # To load the extension into individual datasets:
17
+ #
18
+ # ds = ds.extension(:pg_timestamptz)
19
+ #
20
+ # Note that the loading into individual datasets only affects the integration
21
+ # with the auto_cast_date_and_time extension.
22
+ #
23
+ # Related modules: Sequel::Postgres::Timestamptz, Sequel::Postgres::TimestamptzDatasetMethods
13
24
 
14
25
  #
15
26
  module Sequel
16
27
  module Postgres
17
28
  module Timestamptz
29
+ def self.extended(db)
30
+ db.extend_datasets(TimestamptzDatasetMethods)
31
+ end
32
+
18
33
  private
19
34
 
20
35
  # Use timestamptz by default for generic timestamp value.
@@ -22,7 +37,16 @@ module Sequel
22
37
  :timestamptz
23
38
  end
24
39
  end
40
+
41
+ module TimestamptzDatasetMethods
42
+ private
43
+
44
+ def literal_datetime_timestamp_cast
45
+ 'TIMESTAMP WITH TIME ZONE '
46
+ end
47
+ end
25
48
  end
26
49
 
50
+ Dataset.register_extension(:pg_timestamptz, Postgres::TimestamptzDatasetMethods)
27
51
  Database.register_extension(:pg_timestamptz, Postgres::Timestamptz)
28
52
  end
@@ -35,7 +35,7 @@ module Sequel
35
35
 
36
36
  # Round Sequel::SQLTime values before literalizing
37
37
  def literal_sqltime(v)
38
- super(v.round(timestamp_precision))
38
+ super(v.round(sqltime_precision))
39
39
  end
40
40
 
41
41
  # Round Time values before literalizing
@@ -0,0 +1,78 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The transaction_connection_validator extension automatically
4
+ # retries a transaction on a connection if an disconnect error
5
+ # is raised when sending the statement to begin a new
6
+ # transaction, as long as the user has not already checked out
7
+ # a connection. This is safe to do because no other queries
8
+ # have been issued on the connection, and no user-level code
9
+ # is run before retrying.
10
+ #
11
+ # This approach to connection validation can be significantly
12
+ # lower overhead than the connection_validator extension,
13
+ # though it does not handle all cases handled by the
14
+ # connection_validator extension. However, it performs the
15
+ # validation checks on every new transaction, so it will
16
+ # automatically handle disconnected connections in some cases
17
+ # where the connection_validator extension will not by default
18
+ # (as the connection_validator extension only checks
19
+ # connections if they have not been used in the last hour by
20
+ # default).
21
+ #
22
+ # Related module: Sequel::TransactionConnectionValidator
23
+
24
+ #
25
+ module Sequel
26
+ module TransactionConnectionValidator
27
+ class DisconnectRetry < DatabaseDisconnectError
28
+ # The connection that raised the disconnect error
29
+ attr_accessor :connection
30
+
31
+ # The underlying disconnect error, in case it needs to be reraised.
32
+ attr_accessor :database_error
33
+ end
34
+
35
+ # Rescue disconnect errors raised when beginning a new transaction. If there
36
+ # is a disconnnect error, it should be safe to retry the transaction using a
37
+ # new connection, as we haven't yielded control to the user yet.
38
+ def transaction(opts=OPTS)
39
+ super
40
+ rescue DisconnectRetry => e
41
+ if synchronize(opts[:server]){|conn| conn.equal?(e.connection)}
42
+ # If retrying would use the same connection, that means the
43
+ # connection was not removed from the pool, which means the caller has
44
+ # already checked out the connection, and retrying will not be successful.
45
+ # In this case, we can only reraise the exception.
46
+ raise e.database_error
47
+ end
48
+
49
+ num_retries ||= 0
50
+ num_retries += 1
51
+ retry if num_retries < 5
52
+
53
+ raise e.database_error
54
+ end
55
+
56
+ private
57
+
58
+ # Reraise disconnect errors as DisconnectRetry so they can be retried.
59
+ def begin_new_transaction(conn, opts)
60
+ super
61
+ rescue Sequel::DatabaseDisconnectError, *database_error_classes => e
62
+ if e.is_a?(Sequel::DatabaseDisconnectError) || disconnect_error?(e, OPTS)
63
+ exception = DisconnectRetry.new(e.message)
64
+ exception.set_backtrace([])
65
+ exception.connection = conn
66
+ unless e.is_a?(Sequel::DatabaseError)
67
+ e = Sequel.convert_exception_class(e, database_error_class(e, OPTS))
68
+ end
69
+ exception.database_error = e
70
+ raise exception
71
+ end
72
+
73
+ raise
74
+ end
75
+ end
76
+
77
+ Database.register_extension(:transaction_connection_validator, TransactionConnectionValidator)
78
+ end
@@ -3387,8 +3387,15 @@ module Sequel
3387
3387
  local_opts = ds.opts[:eager_graph][:local]
3388
3388
  limit_strategy = r.eager_graph_limit_strategy(local_opts[:limit_strategy])
3389
3389
 
3390
- if r[:conditions] && !Sequel.condition_specifier?(r[:conditions]) && !r[:orig_opts].has_key?(:graph_conditions) && !r[:orig_opts].has_key?(:graph_only_conditions) && !r.has_key?(:graph_block)
3391
- raise Error, "Cannot eager_graph association when :conditions specified and not a hash or an array of pairs. Specify :graph_conditions, :graph_only_conditions, or :graph_block for the association. Model: #{r[:model]}, association: #{r[:name]}"
3390
+ # SEQUEL6: remove and integrate the auto_restrict_eager_graph plugin
3391
+ if !r[:orig_opts].has_key?(:graph_conditions) && !r[:orig_opts].has_key?(:graph_only_conditions) && !r.has_key?(:graph_block) && !r[:allow_eager_graph]
3392
+ if r[:conditions] && !Sequel.condition_specifier?(r[:conditions])
3393
+ raise Error, "Cannot eager_graph association when :conditions specified and not a hash or an array of pairs. Specify :graph_conditions, :graph_only_conditions, or :graph_block for the association. Model: #{r[:model]}, association: #{r[:name]}"
3394
+ end
3395
+
3396
+ if r[:block] && !r[:graph_use_association_block]
3397
+ warn "eager_graph used for association when association given a block without graph options. The block is ignored in this case. This will result in an exception starting in Sequel 6. Model: #{r[:model]}, association: #{r[:name]}"
3398
+ end
3392
3399
  end
3393
3400
 
3394
3401
  ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>(ta == ds.opts[:eager_graph][:master]) ? first_source : qualifier_from_alias_symbol(ta, first_source), :callback=>callback, :join_type=>join_type || local_opts[:join_type], :join_only=>local_opts[:join_only], :limit_strategy=>limit_strategy, :from_self_alias=>ds.opts[:eager_graph][:master])
@@ -1244,18 +1244,21 @@ module Sequel
1244
1244
  @errors ||= errors_class.new
1245
1245
  end
1246
1246
 
1247
+ EXISTS_SELECT_ = SQL::AliasedExpression.new(1, :one)
1248
+ private_constant :EXISTS_SELECT_
1249
+
1247
1250
  # Returns true when current instance exists, false otherwise.
1248
1251
  # Generally an object that isn't new will exist unless it has
1249
1252
  # been deleted. Uses a database query to check for existence,
1250
1253
  # unless the model object is new, in which case this is always
1251
1254
  # false.
1252
1255
  #
1253
- # Artist[1].exists? # SELECT 1 FROM artists WHERE (id = 1)
1256
+ # Artist[1].exists? # SELECT 1 AS one FROM artists WHERE (id = 1)
1254
1257
  # # => true
1255
1258
  # Artist.new.exists?
1256
1259
  # # => false
1257
1260
  def exists?
1258
- new? ? false : !this.get(SQL::AliasedExpression.new(1, :one)).nil?
1261
+ new? ? false : !this.get(EXISTS_SELECT_).nil?
1259
1262
  end
1260
1263
 
1261
1264
  # Ignore the model's setter method cache when this instances extends a module, as the
@@ -185,10 +185,13 @@ module Sequel
185
185
  end
186
186
 
187
187
  # Set the value of the position_field to the maximum value plus 1 unless the
188
- # position field already has a value.
188
+ # position field already has a value. If the list is empty, the position will
189
+ # be set to the model's +top_of_list+ value.
189
190
  def before_validation
190
191
  unless get_column_value(position_field)
191
- set_column_value("#{position_field}=", list_dataset.max(position_field).to_i+1)
192
+ current_max = list_dataset.max(position_field)
193
+ value = current_max.nil? ? model.top_of_list : current_max.to_i + 1
194
+ set_column_value("#{position_field}=", value)
192
195
  end
193
196
  super
194
197
  end
@@ -71,6 +71,8 @@ module Sequel
71
71
  # (default: :t)
72
72
  # :level_alias :: The symbol identifier to use when eagerly loading descendants
73
73
  # up to a given level (default: :x_level_x)
74
+ # :union_all :: Whether to use UNION ALL or UNION with the recursive
75
+ # common table expression (default: true)
74
76
  module RcteTree
75
77
  # Create the appropriate parent, children, ancestors, and descendants
76
78
  # associations for the model.
@@ -80,6 +82,7 @@ module Sequel
80
82
  opts = opts.dup
81
83
  opts[:class] = model
82
84
  opts[:methods_module] = Module.new
85
+ opts[:union_all] = opts[:union_all].nil? ? true : opts[:union_all]
83
86
  model.send(:include, opts[:methods_module])
84
87
 
85
88
  key = opts[:key] ||= :parent_id
@@ -142,7 +145,7 @@ module Sequel
142
145
  model.from(SQL::AliasedExpression.new(t, table_alias)).
143
146
  with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
144
147
  recursive_ds.select(*c_all),
145
- :args=>col_aliases)
148
+ :args=>col_aliases, union_all: opts[:union_all])
146
149
  end
147
150
  aal = Array(a[:after_load])
148
151
  aal << proc do |m, ancs|
@@ -191,7 +194,7 @@ module Sequel
191
194
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
192
195
  ds = model.from(SQL::AliasedExpression.new(t, table_alias)).
193
196
  with_recursive(t, base_case, recursive_case,
194
- :args=>((key_aliases + col_aliases) if col_aliases))
197
+ :args=>((key_aliases + col_aliases) if col_aliases), union_all: opts[:union_all])
195
198
  ds = r.apply_eager_dataset_changes(ds)
196
199
  ds = ds.select_append(ka) unless ds.opts[:select] == nil
197
200
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil)) do |obj|
@@ -240,7 +243,7 @@ module Sequel
240
243
  model.from(SQL::AliasedExpression.new(t, table_alias)).
241
244
  with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
242
245
  recursive_ds.select(*c_all),
243
- :args=>col_aliases)
246
+ :args=>col_aliases, union_all: opts[:union_all])
244
247
  end
245
248
  dal = Array(d[:after_load])
246
249
  dal << proc do |m, descs|
@@ -299,7 +302,7 @@ module Sequel
299
302
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
300
303
  ds = model.from(SQL::AliasedExpression.new(t, table_alias)).
301
304
  with_recursive(t, base_case, recursive_case,
302
- :args=>((key_aliases + col_aliases + (level ? [la] : [])) if col_aliases))
305
+ :args=>((key_aliases + col_aliases + (level ? [la] : [])) if col_aliases), union_all: opts[:union_all])
303
306
  ds = r.apply_eager_dataset_changes(ds)
304
307
  ds = ds.select_append(ka) unless ds.opts[:select] == nil
305
308
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil, :associations=>OPTS)) do |obj|
@@ -295,7 +295,7 @@ module Sequel
295
295
  h = ds.joined_dataset? ? qualified_pk_hash : pk_hash
296
296
  ds = ds.exclude(h)
297
297
  end
298
- errors.add(a, message) unless ds.count == 0
298
+ errors.add(a, message) unless ds.empty?
299
299
  end
300
300
  end
301
301
 
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 75
9
+ MINOR = 77
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.