sequel 5.68.0 → 5.77.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +134 -0
  3. data/README.rdoc +3 -3
  4. data/doc/mass_assignment.rdoc +1 -1
  5. data/doc/migration.rdoc +15 -0
  6. data/doc/opening_databases.rdoc +12 -3
  7. data/doc/release_notes/5.69.0.txt +26 -0
  8. data/doc/release_notes/5.70.0.txt +35 -0
  9. data/doc/release_notes/5.71.0.txt +21 -0
  10. data/doc/release_notes/5.72.0.txt +33 -0
  11. data/doc/release_notes/5.73.0.txt +66 -0
  12. data/doc/release_notes/5.74.0.txt +45 -0
  13. data/doc/release_notes/5.75.0.txt +35 -0
  14. data/doc/release_notes/5.76.0.txt +86 -0
  15. data/doc/release_notes/5.77.0.txt +63 -0
  16. data/doc/sharding.rdoc +3 -1
  17. data/doc/testing.rdoc +4 -2
  18. data/lib/sequel/adapters/ibmdb.rb +1 -1
  19. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  20. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  21. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  22. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  23. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  24. data/lib/sequel/adapters/jdbc.rb +10 -6
  25. data/lib/sequel/adapters/mysql.rb +19 -7
  26. data/lib/sequel/adapters/mysql2.rb +2 -2
  27. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  28. data/lib/sequel/adapters/postgres.rb +6 -5
  29. data/lib/sequel/adapters/shared/db2.rb +12 -0
  30. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  31. data/lib/sequel/adapters/shared/mysql.rb +31 -1
  32. data/lib/sequel/adapters/shared/oracle.rb +4 -6
  33. data/lib/sequel/adapters/shared/postgres.rb +79 -4
  34. data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
  35. data/lib/sequel/adapters/shared/sqlite.rb +20 -3
  36. data/lib/sequel/adapters/sqlite.rb +42 -3
  37. data/lib/sequel/adapters/trilogy.rb +117 -0
  38. data/lib/sequel/connection_pool/sharded_threaded.rb +11 -10
  39. data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
  40. data/lib/sequel/connection_pool/threaded.rb +6 -0
  41. data/lib/sequel/connection_pool/timed_queue.rb +16 -3
  42. data/lib/sequel/connection_pool.rb +10 -1
  43. data/lib/sequel/database/connecting.rb +1 -1
  44. data/lib/sequel/database/misc.rb +2 -2
  45. data/lib/sequel/database/schema_methods.rb +9 -2
  46. data/lib/sequel/database/transactions.rb +6 -0
  47. data/lib/sequel/dataset/actions.rb +8 -6
  48. data/lib/sequel/dataset/features.rb +10 -1
  49. data/lib/sequel/dataset/sql.rb +47 -34
  50. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  51. data/lib/sequel/extensions/async_thread_pool.rb +3 -2
  52. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  53. data/lib/sequel/extensions/connection_expiration.rb +15 -9
  54. data/lib/sequel/extensions/connection_validator.rb +15 -10
  55. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  56. data/lib/sequel/extensions/index_caching.rb +5 -1
  57. data/lib/sequel/extensions/migration.rb +52 -13
  58. data/lib/sequel/extensions/named_timezones.rb +1 -1
  59. data/lib/sequel/extensions/pg_array.rb +10 -0
  60. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  61. data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
  62. data/lib/sequel/extensions/pg_json_ops.rb +52 -0
  63. data/lib/sequel/extensions/pg_range.rb +2 -2
  64. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  65. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  66. data/lib/sequel/extensions/schema_caching.rb +1 -1
  67. data/lib/sequel/extensions/server_block.rb +2 -1
  68. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  69. data/lib/sequel/model/associations.rb +9 -2
  70. data/lib/sequel/model/base.rb +25 -12
  71. data/lib/sequel/model/dataset_module.rb +3 -0
  72. data/lib/sequel/model/exceptions.rb +15 -3
  73. data/lib/sequel/plugins/column_encryption.rb +27 -6
  74. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  75. data/lib/sequel/plugins/list.rb +5 -2
  76. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  77. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  78. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  79. data/lib/sequel/plugins/paged_operations.rb +181 -0
  80. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +5 -1
  81. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  82. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  83. data/lib/sequel/plugins/static_cache.rb +38 -0
  84. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  85. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  86. data/lib/sequel/version.rb +1 -1
  87. metadata +43 -3
@@ -159,6 +159,19 @@ module Sequel
159
159
  migration.up = block
160
160
  migration.down = MigrationReverser.new.reverse(&block)
161
161
  end
162
+
163
+ # Creates a revert migration. This is the same as creating
164
+ # the same block with +down+, but it also calls the block and attempts
165
+ # to create a +up+ block that will reverse the changes made by
166
+ # the block. This is designed to revert the changes in the
167
+ # provided block.
168
+ #
169
+ # There are no guarantees that this will work perfectly
170
+ # in all cases, but it works for some simple cases.
171
+ def revert(&block)
172
+ migration.down = block
173
+ migration.up = MigrationReverser.new.reverse(&block)
174
+ end
162
175
  end
163
176
 
164
177
  # Handles the reversing of reversible migrations. Basically records
@@ -270,6 +283,10 @@ module Sequel
270
283
  def rename_column(name, new_name)
271
284
  @actions << [:rename_column, new_name, name]
272
285
  end
286
+
287
+ def set_column_allow_null(name, allow_null=true)
288
+ @actions << [:set_column_allow_null, name, !allow_null]
289
+ end
273
290
  end
274
291
 
275
292
  # The preferred method for writing Sequel migrations, using a DSL:
@@ -478,11 +495,7 @@ module Sequel
478
495
  @use_transactions
479
496
  end
480
497
 
481
- if use_trans
482
- db.transaction(&block)
483
- else
484
- yield
485
- end
498
+ db.transaction(:skip_transaction=>use_trans == false, &block)
486
499
  end
487
500
 
488
501
  # Load the migration file, raising an exception if the file does not define
@@ -680,6 +693,13 @@ module Sequel
680
693
  @migration_tuples = get_migration_tuples
681
694
  end
682
695
 
696
+ # Apply the migration in the given file path. See Migrator.run for the
697
+ # available options. Additionally, this method supports the :direction
698
+ # option for whether to run the migration up (default) or down.
699
+ def self.run_single(db, path, opts=OPTS)
700
+ new(db, File.dirname(path), opts).run_single(path, opts[:direction] || :up)
701
+ end
702
+
683
703
  # The timestamp migrator is current if there are no migrations to apply
684
704
  # in either direction.
685
705
  def is_current?
@@ -689,20 +709,39 @@ module Sequel
689
709
  # Apply all migration tuples on the database
690
710
  def run
691
711
  migration_tuples.each do |m, f, direction|
692
- t = Time.now
693
- db.log_info("Begin applying migration #{f}, direction: #{direction}")
694
- checked_transaction(m) do
695
- m.apply(db, direction)
696
- fi = f.downcase
697
- direction == :up ? ds.insert(column=>fi) : ds.where(column=>fi).delete
698
- end
699
- db.log_info("Finished applying migration #{f}, direction: #{direction}, took #{sprintf('%0.6f', Time.now - t)} seconds")
712
+ apply_migration(m, f, direction)
700
713
  end
701
714
  nil
702
715
  end
703
716
 
717
+ # Apply single migration tuple at the given path with the given direction
718
+ # on the database.
719
+ def run_single(path, direction)
720
+ migration = load_migration_file(path)
721
+ file_name = File.basename(path)
722
+ already_applied = applied_migrations.include?(file_name.downcase)
723
+
724
+ return if direction == :up ? already_applied : !already_applied
725
+
726
+ apply_migration(migration, file_name, direction)
727
+ nil
728
+ end
729
+
704
730
  private
705
731
 
732
+ # Apply a single migration with the given filename in the given direction.
733
+ def apply_migration(migration, file_name, direction)
734
+ fi = file_name.downcase
735
+ t = Time.now
736
+
737
+ db.log_info("Begin applying migration #{file_name}, direction: #{direction}")
738
+ checked_transaction(migration) do
739
+ migration.apply(db, direction)
740
+ direction == :up ? ds.insert(column=>fi) : ds.where(column=>fi).delete
741
+ end
742
+ db.log_info("Finished applying migration #{file_name}, direction: #{direction}, took #{sprintf('%0.6f', Time.now - t)} seconds")
743
+ end
744
+
706
745
  # Convert the schema_info table to the new schema_migrations table format,
707
746
  # using the version of the schema_info table and the current migration files.
708
747
  def convert_from_schema_info
@@ -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:
@@ -233,6 +233,16 @@ module Sequel
233
233
  a
234
234
  when String
235
235
  bound_variable_array_string(a)
236
+ when Float
237
+ if a.infinite?
238
+ a > 0 ? '"Infinity"' : '"-Infinity"'
239
+ elsif a.nan?
240
+ '"NaN"'
241
+ else
242
+ literal(a)
243
+ end
244
+ when Time, Date
245
+ @default_dataset.literal_date_or_time(a)
236
246
  else
237
247
  if (s = bound_variable_arg(a, nil)).is_a?(String)
238
248
  bound_variable_array_string(s)
@@ -0,0 +1,110 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_auto_parameterize_in_array extension builds on the pg_auto_parameterize
4
+ # extension, adding support for handling additional types when converting from
5
+ # IN to = ANY and NOT IN to != ALL:
6
+ #
7
+ # DB[:table].where(column: [1.0, 2.0, ...])
8
+ # # Without extension: column IN ($1::numeric, $2:numeric, ...) # bound variables: 1.0, 2.0, ...
9
+ # # With extension: column = ANY($1::numeric[]) # bound variables: [1.0, 2.0, ...]
10
+ #
11
+ # This prevents the use of an unbounded number of bound variables based on the
12
+ # size of the array, as well as using different SQL for different array sizes.
13
+ #
14
+ # The following types are supported when doing the conversions, with the database
15
+ # type used:
16
+ #
17
+ # Float :: if any are infinite or NaN, double precision, otherwise numeric
18
+ # BigDecimal :: numeric
19
+ # Date :: date
20
+ # Time :: timestamp (or timestamptz if pg_timestamptz extension is used)
21
+ # DateTime :: timestamp (or timestamptz if pg_timestamptz extension is used)
22
+ # Sequel::SQLTime :: time
23
+ # Sequel::SQL::Blob :: bytea
24
+ #
25
+ # String values are also supported using the +text+ type, but only if the
26
+ # +:treat_string_list_as_text_array+ Database option is used. This is because
27
+ # treating strings as text can break programs, since the type for
28
+ # literal strings in PostgreSQL is +unknown+, not +text+.
29
+ #
30
+ # The conversion is only done for single dimensional arrays that have more
31
+ # than two elements, where all elements are of the same class (other than
32
+ # nil values).
33
+ #
34
+ # Related module: Sequel::Postgres::AutoParameterizeInArray
35
+
36
+ module Sequel
37
+ module Postgres
38
+ # Enable automatically parameterizing queries.
39
+ module AutoParameterizeInArray
40
+ # Transform column IN (...) expressions into column = ANY($)
41
+ # and column NOT IN (...) expressions into column != ALL($)
42
+ # using an array bound variable for the ANY/ALL argument,
43
+ # if all values inside the predicate are of the same type and
44
+ # the type is handled by the extension.
45
+ # This is the same optimization PostgreSQL performs internally,
46
+ # but this reduces the number of bound variables.
47
+ def complex_expression_sql_append(sql, op, args)
48
+ case op
49
+ when :IN, :"NOT IN"
50
+ l, r = args
51
+ if auto_param?(sql) && (type = _bound_variable_type_for_array(r))
52
+ if op == :IN
53
+ op = :"="
54
+ func = :ANY
55
+ else
56
+ op = :!=
57
+ func = :ALL
58
+ end
59
+ args = [l, Sequel.function(func, Sequel.pg_array(r, type))]
60
+ end
61
+ end
62
+
63
+ super
64
+ end
65
+
66
+ private
67
+
68
+ # The bound variable type string to use for the bound variable array.
69
+ # Returns nil if a bound variable should not be used for the array.
70
+ def _bound_variable_type_for_array(r)
71
+ return unless Array === r && r.size > 1
72
+ classes = r.map(&:class)
73
+ classes.uniq!
74
+ classes.delete(NilClass)
75
+ return unless classes.size == 1
76
+
77
+ klass = classes[0]
78
+ if klass == Integer
79
+ # This branch is not taken on Ruby <2.4, because of the Fixnum/Bignum split.
80
+ # However, that causes no problems as pg_auto_parameterize handles integer
81
+ # arrays natively (though the SQL used is different)
82
+ "int8"
83
+ elsif klass == String
84
+ "text" if db.typecast_value(:boolean, db.opts[:treat_string_list_as_text_array])
85
+ elsif klass == BigDecimal
86
+ "numeric"
87
+ elsif klass == Date
88
+ "date"
89
+ elsif klass == Time
90
+ @db.cast_type_literal(Time)
91
+ elsif klass == Float
92
+ # PostgreSQL treats literal floats as numeric, not double precision
93
+ # But older versions of PostgreSQL don't handle Infinity/NaN in numeric
94
+ r.all?{|v| v.nil? || v.finite?} ? "numeric" : "double precision"
95
+ elsif klass == Sequel::SQLTime
96
+ "time"
97
+ elsif klass == DateTime
98
+ @db.cast_type_literal(DateTime)
99
+ elsif klass == Sequel::SQL::Blob
100
+ "bytea"
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ Database.register_extension(:pg_auto_parameterize_in_array) do |db|
107
+ db.extension(:pg_array, :pg_auto_parameterize)
108
+ db.extend_datasets(Postgres::AutoParameterizeInArray)
109
+ end
110
+ end
@@ -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
@@ -123,6 +123,15 @@
123
123
  # c = Sequel.pg_jsonb_op(:c)
124
124
  # DB[:t].update(c['key1'] => 1.to_json, c['key2'] => "a".to_json)
125
125
  #
126
+ # On PostgreSQL 16+, the <tt>IS [NOT] JSON</tt> operator is supported:
127
+ #
128
+ # j.is_json # j IS JSON
129
+ # j.is_json(type: :object) # j IS JSON OBJECT
130
+ # j.is_json(type: :object, unique: true) # j IS JSON OBJECT WITH UNIQUE
131
+ # j.is_not_json # j IS NOT JSON
132
+ # j.is_not_json(type: :array) # j IS NOT JSON ARRAY
133
+ # j.is_not_json(unique: true) # j IS NOT JSON WITH UNIQUE
134
+ #
126
135
  # If you are also using the pg_json extension, you should load it before
127
136
  # loading this extension. Doing so will allow you to use the #op method on
128
137
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -151,6 +160,18 @@ module Sequel
151
160
  GET_PATH = ["(".freeze, " #> ".freeze, ")".freeze].freeze
152
161
  GET_PATH_TEXT = ["(".freeze, " #>> ".freeze, ")".freeze].freeze
153
162
 
163
+ IS_JSON = ["(".freeze, " IS JSON".freeze, "".freeze, ")".freeze].freeze
164
+ IS_NOT_JSON = ["(".freeze, " IS NOT JSON".freeze, "".freeze, ")".freeze].freeze
165
+ EMPTY_STRING = Sequel::LiteralString.new('').freeze
166
+ WITH_UNIQUE = Sequel::LiteralString.new(' WITH UNIQUE').freeze
167
+ IS_JSON_MAP = {
168
+ nil => EMPTY_STRING,
169
+ :value => Sequel::LiteralString.new(' VALUE').freeze,
170
+ :scalar => Sequel::LiteralString.new(' SCALAR').freeze,
171
+ :object => Sequel::LiteralString.new(' OBJECT').freeze,
172
+ :array => Sequel::LiteralString.new(' ARRAY').freeze
173
+ }.freeze
174
+
154
175
  # Get JSON array element or object field as json. If an array is given,
155
176
  # gets the object at the specified path.
156
177
  #
@@ -233,6 +254,30 @@ module Sequel
233
254
  end
234
255
  end
235
256
 
257
+ # Return whether the json object can be parsed as JSON.
258
+ #
259
+ # Options:
260
+ # :type :: Check whether the json object can be parsed as a specific type
261
+ # of JSON (:value, :scalar, :object, :array).
262
+ # :unique :: Check JSON objects for unique keys.
263
+ #
264
+ # json_op.is_json # json IS JSON
265
+ # json_op.is_json(type: :object) # json IS JSON OBJECT
266
+ # json_op.is_json(unique: true) # json IS JSON WITH UNIQUE
267
+ def is_json(opts=OPTS)
268
+ _is_json(IS_JSON, opts)
269
+ end
270
+
271
+ # Return whether the json object cannot be parsed as JSON. The opposite
272
+ # of #is_json. See #is_json for options.
273
+ #
274
+ # json_op.is_not_json # json IS NOT JSON
275
+ # json_op.is_not_json(type: :object) # json IS NOT JSON OBJECT
276
+ # json_op.is_not_json(unique: true) # json IS NOT JSON WITH UNIQUE
277
+ def is_not_json(opts=OPTS)
278
+ _is_json(IS_NOT_JSON, opts)
279
+ end
280
+
236
281
  # Returns a set of keys AS text in the json object.
237
282
  #
238
283
  # json_op.keys # json_object_keys(json)
@@ -286,6 +331,13 @@ module Sequel
286
331
 
287
332
  private
288
333
 
334
+ # Internals of IS [NOT] JSON support
335
+ def _is_json(lit_array, opts)
336
+ raise Error, "invalid is_json :type option: #{opts[:type].inspect}" unless type = IS_JSON_MAP[opts[:type]]
337
+ unique = opts[:unique] ? WITH_UNIQUE : EMPTY_STRING
338
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(lit_array, [self, type, unique]))
339
+ end
340
+
289
341
  # Return a placeholder literal with the given str and args, wrapped
290
342
  # in an JSONOp or JSONBOp, used by operators that return json or jsonb.
291
343
  def json_op(str, args)
@@ -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
@@ -52,7 +52,7 @@ module Sequel
52
52
  # Dump the cached schema to the filename given in Marshal format.
53
53
  def dump_schema_cache(file)
54
54
  sch = {}
55
- @schemas.each do |k,v|
55
+ @schemas.sort.each do |k,v|
56
56
  sch[k] = v.map do |c, h|
57
57
  h = Hash[h]
58
58
  h.delete(:callable_default)
@@ -69,7 +69,8 @@ module Sequel
69
69
  # Also defines the with_server method on the receiver for easy use.
70
70
  def self.extended(db)
71
71
  pool = db.pool
72
- if defined?(ShardedThreadedConnectionPool) && pool.is_a?(ShardedThreadedConnectionPool)
72
+ case pool.pool_type
73
+ when :sharded_threaded, :sharded_timed_queue
73
74
  pool.extend(ThreadedServerBlock)
74
75
  pool.instance_variable_set(:@default_servers, {})
75
76
  else
@@ -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
@@ -1945,8 +1948,10 @@ module Sequel
1945
1948
  end
1946
1949
 
1947
1950
  # If transactions should be used, wrap the yield in a transaction block.
1948
- def checked_transaction(opts=OPTS)
1949
- use_transaction?(opts) ? db.transaction({:server=>this_server}.merge!(opts)){yield} : yield
1951
+ def checked_transaction(opts=OPTS, &block)
1952
+ h = {:server=>this_server}.merge!(opts)
1953
+ h[:skip_transaction] = true unless use_transaction?(opts)
1954
+ db.transaction(h, &block)
1950
1955
  end
1951
1956
 
1952
1957
  # Change the value of the column to given value, recording the change.
@@ -2031,19 +2036,20 @@ module Sequel
2031
2036
  meths = setter_methods(type)
2032
2037
  strict = strict_param_setting
2033
2038
  hash.each do |k,v|
2039
+ k = k.to_s
2034
2040
  m = "#{k}="
2035
2041
  if meths.include?(m)
2036
2042
  set_column_value(m, v)
2037
2043
  elsif strict
2038
2044
  # Avoid using respond_to? or creating symbols from user input
2039
2045
  if public_methods.map(&:to_s).include?(m)
2040
- if Array(model.primary_key).map(&:to_s).member?(k.to_s) && model.restrict_primary_key?
2041
- raise MassAssignmentRestriction, "#{k} is a restricted primary key"
2046
+ if Array(model.primary_key).map(&:to_s).member?(k) && model.restrict_primary_key?
2047
+ raise MassAssignmentRestriction.create("#{k} is a restricted primary key", self, k)
2042
2048
  else
2043
- raise MassAssignmentRestriction, "#{k} is a restricted column"
2049
+ raise MassAssignmentRestriction.create("#{k} is a restricted column", self, k)
2044
2050
  end
2045
2051
  else
2046
- raise MassAssignmentRestriction, "method #{m} doesn't exist"
2052
+ raise MassAssignmentRestriction.create("method #{m} doesn't exist", self, k)
2047
2053
  end
2048
2054
  end
2049
2055
  end
@@ -2147,8 +2153,9 @@ module Sequel
2147
2153
  # # DELETE FROM artists WHERE (id = 2)
2148
2154
  # # ...
2149
2155
  def destroy
2150
- pr = proc{all(&:destroy).length}
2151
- model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call
2156
+ @db.transaction(:server=>opts[:server], :skip_transaction=>model.use_transactions == false) do
2157
+ all(&:destroy).length
2158
+ end
2152
2159
  end
2153
2160
 
2154
2161
  # If there is no order already defined on this dataset, order it by
@@ -2228,11 +2235,17 @@ module Sequel
2228
2235
 
2229
2236
  private
2230
2237
 
2238
+ # Return the dataset ordered by the model's primary key. This should not
2239
+ # be used if the model does not have a primary key.
2240
+ def _force_primary_key_order
2241
+ cached_dataset(:_pk_order_ds){order(*model.primary_key)}
2242
+ end
2243
+
2231
2244
  # If the dataset is not already ordered, and the model has a primary key,
2232
2245
  # return a clone ordered by the primary key.
2233
2246
  def _primary_key_order
2234
- if @opts[:order].nil? && model && (pk = model.primary_key)
2235
- cached_dataset(:_pk_order_ds){order(*pk)}
2247
+ if @opts[:order].nil? && model && model.primary_key
2248
+ _force_primary_key_order
2236
2249
  end
2237
2250
  end
2238
2251
 
@@ -8,6 +8,9 @@ module Sequel
8
8
  # automatically creates class methods for public dataset
9
9
  # methods.
10
10
  class DatasetModule < Dataset::DatasetModule
11
+ # The model class related to this dataset module.
12
+ attr_reader :model
13
+
11
14
  # Store the model related to this dataset module.
12
15
  def initialize(model)
13
16
  @model = model