sequel 5.68.0 → 5.77.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +134 -0
- data/README.rdoc +3 -3
- data/doc/mass_assignment.rdoc +1 -1
- data/doc/migration.rdoc +15 -0
- data/doc/opening_databases.rdoc +12 -3
- data/doc/release_notes/5.69.0.txt +26 -0
- data/doc/release_notes/5.70.0.txt +35 -0
- data/doc/release_notes/5.71.0.txt +21 -0
- data/doc/release_notes/5.72.0.txt +33 -0
- data/doc/release_notes/5.73.0.txt +66 -0
- data/doc/release_notes/5.74.0.txt +45 -0
- data/doc/release_notes/5.75.0.txt +35 -0
- data/doc/release_notes/5.76.0.txt +86 -0
- data/doc/release_notes/5.77.0.txt +63 -0
- data/doc/sharding.rdoc +3 -1
- data/doc/testing.rdoc +4 -2
- data/lib/sequel/adapters/ibmdb.rb +1 -1
- data/lib/sequel/adapters/jdbc/h2.rb +3 -0
- data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
- data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +10 -6
- data/lib/sequel/adapters/mysql.rb +19 -7
- data/lib/sequel/adapters/mysql2.rb +2 -2
- data/lib/sequel/adapters/odbc/mssql.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +6 -5
- data/lib/sequel/adapters/shared/db2.rb +12 -0
- data/lib/sequel/adapters/shared/mssql.rb +1 -1
- data/lib/sequel/adapters/shared/mysql.rb +31 -1
- data/lib/sequel/adapters/shared/oracle.rb +4 -6
- data/lib/sequel/adapters/shared/postgres.rb +79 -4
- data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
- data/lib/sequel/adapters/shared/sqlite.rb +20 -3
- data/lib/sequel/adapters/sqlite.rb +42 -3
- data/lib/sequel/adapters/trilogy.rb +117 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +11 -10
- data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
- data/lib/sequel/connection_pool/threaded.rb +6 -0
- data/lib/sequel/connection_pool/timed_queue.rb +16 -3
- data/lib/sequel/connection_pool.rb +10 -1
- data/lib/sequel/database/connecting.rb +1 -1
- data/lib/sequel/database/misc.rb +2 -2
- data/lib/sequel/database/schema_methods.rb +9 -2
- data/lib/sequel/database/transactions.rb +6 -0
- data/lib/sequel/dataset/actions.rb +8 -6
- data/lib/sequel/dataset/features.rb +10 -1
- data/lib/sequel/dataset/sql.rb +47 -34
- data/lib/sequel/extensions/any_not_empty.rb +2 -2
- data/lib/sequel/extensions/async_thread_pool.rb +3 -2
- data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
- data/lib/sequel/extensions/connection_expiration.rb +15 -9
- data/lib/sequel/extensions/connection_validator.rb +15 -10
- data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
- data/lib/sequel/extensions/index_caching.rb +5 -1
- data/lib/sequel/extensions/migration.rb +52 -13
- data/lib/sequel/extensions/named_timezones.rb +1 -1
- data/lib/sequel/extensions/pg_array.rb +10 -0
- data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
- data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
- data/lib/sequel/extensions/pg_json_ops.rb +52 -0
- data/lib/sequel/extensions/pg_range.rb +2 -2
- data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
- data/lib/sequel/extensions/round_timestamps.rb +1 -1
- data/lib/sequel/extensions/schema_caching.rb +1 -1
- data/lib/sequel/extensions/server_block.rb +2 -1
- data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
- data/lib/sequel/model/associations.rb +9 -2
- data/lib/sequel/model/base.rb +25 -12
- data/lib/sequel/model/dataset_module.rb +3 -0
- data/lib/sequel/model/exceptions.rb +15 -3
- data/lib/sequel/plugins/column_encryption.rb +27 -6
- data/lib/sequel/plugins/defaults_setter.rb +16 -0
- data/lib/sequel/plugins/list.rb +5 -2
- data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
- data/lib/sequel/plugins/optimistic_locking.rb +9 -42
- data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
- data/lib/sequel/plugins/paged_operations.rb +181 -0
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +5 -1
- data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
- data/lib/sequel/plugins/rcte_tree.rb +7 -4
- data/lib/sequel/plugins/static_cache.rb +38 -0
- data/lib/sequel/plugins/static_cache_cache.rb +5 -1
- data/lib/sequel/plugins/validation_helpers.rb +1 -1
- data/lib/sequel/version.rb +1 -1
- 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
|
-
|
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
|
-
|
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.
|
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
|
57
|
-
|
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.%
|
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.%
|
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)
|
@@ -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+ (
|
5
|
-
# instead of +timestamp+ (
|
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
|
-
#
|
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
|
@@ -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
|
-
|
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
|
-
|
3391
|
-
|
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])
|
data/lib/sequel/model/base.rb
CHANGED
@@ -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(
|
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
|
-
|
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
|
2041
|
-
raise MassAssignmentRestriction
|
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
|
2049
|
+
raise MassAssignmentRestriction.create("#{k} is a restricted column", self, k)
|
2044
2050
|
end
|
2045
2051
|
else
|
2046
|
-
raise MassAssignmentRestriction
|
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
|
-
|
2151
|
-
|
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 &&
|
2235
|
-
|
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
|