sequel 4.35.0 → 4.36.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.
- checksums.yaml +4 -4
- data/CHANGELOG +32 -0
- data/doc/association_basics.rdoc +27 -4
- data/doc/migration.rdoc +24 -0
- data/doc/release_notes/4.36.0.txt +116 -0
- data/lib/sequel/adapters/jdbc/h2.rb +1 -1
- data/lib/sequel/adapters/mysql2.rb +11 -1
- data/lib/sequel/adapters/oracle.rb +3 -5
- data/lib/sequel/adapters/postgres.rb +2 -2
- data/lib/sequel/adapters/shared/access.rb +1 -1
- data/lib/sequel/adapters/shared/oracle.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +1 -1
- data/lib/sequel/adapters/shared/sqlite.rb +1 -1
- data/lib/sequel/connection_pool.rb +5 -0
- data/lib/sequel/connection_pool/sharded_single.rb +1 -1
- data/lib/sequel/connection_pool/sharded_threaded.rb +29 -14
- data/lib/sequel/connection_pool/single.rb +1 -1
- data/lib/sequel/connection_pool/threaded.rb +5 -3
- data/lib/sequel/database/schema_methods.rb +7 -1
- data/lib/sequel/dataset/sql.rb +4 -0
- data/lib/sequel/extensions/arbitrary_servers.rb +1 -1
- data/lib/sequel/extensions/connection_expiration.rb +89 -0
- data/lib/sequel/extensions/connection_validator.rb +11 -3
- data/lib/sequel/extensions/constraint_validations.rb +28 -0
- data/lib/sequel/extensions/string_agg.rb +178 -0
- data/lib/sequel/model.rb +13 -56
- data/lib/sequel/model/associations.rb +3 -1
- data/lib/sequel/model/base.rb +104 -7
- data/lib/sequel/plugins/constraint_validations.rb +17 -3
- data/lib/sequel/plugins/validation_helpers.rb +1 -1
- data/lib/sequel/sql.rb +8 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +4 -0
- data/spec/core/dataset_spec.rb +4 -0
- data/spec/core/expression_filters_spec.rb +4 -0
- data/spec/extensions/connection_expiration_spec.rb +121 -0
- data/spec/extensions/connection_validator_spec.rb +7 -0
- data/spec/extensions/constraint_validations_plugin_spec.rb +14 -0
- data/spec/extensions/constraint_validations_spec.rb +64 -0
- data/spec/extensions/string_agg_spec.rb +85 -0
- data/spec/extensions/validation_helpers_spec.rb +2 -0
- data/spec/integration/plugin_test.rb +37 -2
- data/spec/model/association_reflection_spec.rb +10 -0
- data/spec/model/model_spec.rb +49 -0
- metadata +8 -2
@@ -75,10 +75,12 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
|
|
75
75
|
# Once a connection is requested using #hold, the connection pool
|
76
76
|
# creates new connections to the database.
|
77
77
|
def disconnect(opts=OPTS)
|
78
|
+
conns = nil
|
78
79
|
sync do
|
79
|
-
@available_connections.
|
80
|
+
conns = @available_connections.dup
|
80
81
|
@available_connections.clear
|
81
82
|
end
|
83
|
+
conns.each{|conn| disconnect_connection(conn)}
|
82
84
|
end
|
83
85
|
|
84
86
|
# Chooses the first available connection, or if none are
|
@@ -106,7 +108,7 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
|
|
106
108
|
rescue Sequel::DatabaseDisconnectError
|
107
109
|
oconn = conn
|
108
110
|
conn = nil
|
109
|
-
|
111
|
+
disconnect_connection(oconn) if oconn
|
110
112
|
@allocated.delete(t)
|
111
113
|
raise
|
112
114
|
ensure
|
@@ -266,7 +268,7 @@ class Sequel::ThreadedConnectionPool < Sequel::ConnectionPool
|
|
266
268
|
conn = @allocated.delete(thread)
|
267
269
|
|
268
270
|
if @connection_handling == :disconnect
|
269
|
-
|
271
|
+
disconnect_connection(conn)
|
270
272
|
else
|
271
273
|
checkin_connection(conn)
|
272
274
|
end
|
@@ -888,7 +888,7 @@ module Sequel
|
|
888
888
|
when Class
|
889
889
|
type_literal_generic(column)
|
890
890
|
when :Bignum
|
891
|
-
|
891
|
+
type_literal_generic_bignum_symbol(column)
|
892
892
|
else
|
893
893
|
type_literal_specific(column)
|
894
894
|
end
|
@@ -912,6 +912,12 @@ module Sequel
|
|
912
912
|
|
913
913
|
# Sequel uses the bigint type by default for Bignums.
|
914
914
|
def type_literal_generic_bignum(column)
|
915
|
+
Sequel::Deprecation.deprecate("Using the Bignum class as a generic type is deprecated and will be removed in Sequel 4.41.0, as the behavior will change in ruby 2.4. Switch to using the :Bignum symbol.")
|
916
|
+
type_literal_generic_bignum_symbol(column)
|
917
|
+
end
|
918
|
+
|
919
|
+
# Sequel uses the bigint type by default for :Bignum symbol.
|
920
|
+
def type_literal_generic_bignum_symbol(column)
|
915
921
|
:bigint
|
916
922
|
end
|
917
923
|
|
data/lib/sequel/dataset/sql.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
#
|
3
|
+
# The connection_expiration extension modifies a database's
|
4
|
+
# connection pool to validate that connections checked out
|
5
|
+
# from the pool are not expired, before yielding them for
|
6
|
+
# use. If it detects an expired connection, it removes it
|
7
|
+
# from the pool and tries the next available connection,
|
8
|
+
# creating a new connection if no available connection is
|
9
|
+
# unexpired. Example of use:
|
10
|
+
#
|
11
|
+
# DB.extension(:connection_expiration)
|
12
|
+
#
|
13
|
+
# Note that this extension only affects the default threaded
|
14
|
+
# and the sharded threaded connection pool. The single
|
15
|
+
# threaded and sharded single threaded connection pools are
|
16
|
+
# not affected. As the only reason to use the single threaded
|
17
|
+
# pools is for speed, and this extension makes the connection
|
18
|
+
# pool slower, there's not much point in modifying this
|
19
|
+
# extension to work with the single threaded pools. The
|
20
|
+
# threaded pools work fine even in single threaded code, so if
|
21
|
+
# you are currently using a single threaded pool and want to
|
22
|
+
# use this extension, switch to using a threaded pool.
|
23
|
+
#
|
24
|
+
# Related module: Sequel::ConnectionExpiration
|
25
|
+
|
26
|
+
#
|
27
|
+
module Sequel
|
28
|
+
module ConnectionExpiration
|
29
|
+
class Retry < Error; end
|
30
|
+
|
31
|
+
# The number of seconds that need to pass since
|
32
|
+
# connection creation before expiring a connection.
|
33
|
+
# Defaults to 14400 seconds (4 hours).
|
34
|
+
attr_accessor :connection_expiration_timeout
|
35
|
+
|
36
|
+
# Initialize the data structures used by this extension.
|
37
|
+
def self.extended(pool)
|
38
|
+
pool.instance_eval do
|
39
|
+
sync do
|
40
|
+
@connection_expiration_timestamps ||= {}
|
41
|
+
@connection_expiration_timeout ||= 14400
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# Clean up expiration timestamps during disconnect.
|
49
|
+
def disconnect_connection(conn)
|
50
|
+
sync{@connection_expiration_timestamps.delete(conn)}
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
# Record the time the connection was created.
|
55
|
+
def make_new(*)
|
56
|
+
conn = super
|
57
|
+
@connection_expiration_timestamps[conn] = Time.now
|
58
|
+
conn
|
59
|
+
end
|
60
|
+
|
61
|
+
# When acquiring a connection, check if the connection is expired.
|
62
|
+
# If it is expired, disconnect the connection, and retry with a new
|
63
|
+
# connection.
|
64
|
+
def acquire(*a)
|
65
|
+
begin
|
66
|
+
if (conn = super) &&
|
67
|
+
(t = sync{@connection_expiration_timestamps[conn]}) &&
|
68
|
+
Time.now - t > @connection_expiration_timeout
|
69
|
+
|
70
|
+
if pool_type == :sharded_threaded
|
71
|
+
sync{allocated(a.last).delete(Thread.current)}
|
72
|
+
else
|
73
|
+
sync{@allocated.delete(Thread.current)}
|
74
|
+
end
|
75
|
+
|
76
|
+
disconnect_connection(conn)
|
77
|
+
raise Retry
|
78
|
+
end
|
79
|
+
rescue Retry
|
80
|
+
retry
|
81
|
+
end
|
82
|
+
|
83
|
+
conn
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
Database.register_extension(:connection_expiration){|db| db.pool.extend(ConnectionExpiration)}
|
88
|
+
end
|
89
|
+
|
@@ -61,8 +61,10 @@ module Sequel
|
|
61
61
|
# Initialize the data structures used by this extension.
|
62
62
|
def self.extended(pool)
|
63
63
|
pool.instance_eval do
|
64
|
-
|
65
|
-
|
64
|
+
sync do
|
65
|
+
@connection_timestamps ||= {}
|
66
|
+
@connection_validation_timeout ||= 3600
|
67
|
+
end
|
66
68
|
end
|
67
69
|
|
68
70
|
# Make sure the valid connection SQL query is precached,
|
@@ -81,6 +83,12 @@ module Sequel
|
|
81
83
|
conn
|
82
84
|
end
|
83
85
|
|
86
|
+
# Clean up timestamps during disconnect.
|
87
|
+
def disconnect_connection(conn)
|
88
|
+
sync{@connection_timestamps.delete(conn)}
|
89
|
+
super
|
90
|
+
end
|
91
|
+
|
84
92
|
# When acquiring a connection, if it has been
|
85
93
|
# idle for longer than the connection validation timeout,
|
86
94
|
# test the connection for validity. If it is not valid,
|
@@ -98,7 +106,7 @@ module Sequel
|
|
98
106
|
sync{@allocated.delete(Thread.current)}
|
99
107
|
end
|
100
108
|
|
101
|
-
|
109
|
+
disconnect_connection(conn)
|
102
110
|
raise Retry
|
103
111
|
end
|
104
112
|
rescue Retry
|
@@ -78,6 +78,10 @@
|
|
78
78
|
# includes [1, 2] :: CHECK column IN (1, 2)
|
79
79
|
# includes 3..5 :: CHECK column >= 3 AND column <= 5
|
80
80
|
# includes 3...5 :: CHECK column >= 3 AND column < 5
|
81
|
+
# operator :>, 1 :: CHECK column > 1
|
82
|
+
# operator :>=, 2 :: CHECK column >= 2
|
83
|
+
# operator :<, "M" :: CHECK column < 'M'
|
84
|
+
# operator :<=, 'K' :: CHECK column <= 'K'
|
81
85
|
# unique :: UNIQUE (column)
|
82
86
|
#
|
83
87
|
# There are some additional API differences:
|
@@ -94,6 +98,8 @@
|
|
94
98
|
# patters are very simple, so many regexp patterns cannot be expressed by
|
95
99
|
# them, but only a couple databases (PostgreSQL and MySQL) support regexp
|
96
100
|
# patterns.
|
101
|
+
# * The operator validation only supports >, >=, <, and <= operators, and the
|
102
|
+
# argument must be a string or an integer.
|
97
103
|
# * When using the unique validation, column names cannot have embedded commas.
|
98
104
|
# For similar reasons, when using an includes validation with an array of
|
99
105
|
# strings, none of the strings in the array can have embedded commas.
|
@@ -131,6 +137,9 @@ module Sequel
|
|
131
137
|
module ConstraintValidations
|
132
138
|
# The default table name used for the validation metadata.
|
133
139
|
DEFAULT_CONSTRAINT_VALIDATIONS_TABLE = :sequel_constraint_validations
|
140
|
+
OPERATORS = {:< => :lt, :<= => :lte, :> => :gt, :>= => :gte}.freeze
|
141
|
+
REVERSE_OPERATOR_MAP = {:str_lt => :<, :str_lte => :<=, :str_gt => :>, :str_gte => :>=,
|
142
|
+
:int_lt => :<, :int_lte => :<=, :int_gt => :>, :int_gte => :>=}.freeze
|
134
143
|
|
135
144
|
# Set the default validation metadata table name if it has not already
|
136
145
|
# been set.
|
@@ -164,6 +173,23 @@ module Sequel
|
|
164
173
|
END
|
165
174
|
end
|
166
175
|
|
176
|
+
# Create operator validation. The op should be either +:>+, +:>=+, +:<+, or +:<=+, and
|
177
|
+
# the arg should be either a string or an integer.
|
178
|
+
def operator(op, arg, columns, opts=OPTS)
|
179
|
+
raise Error, "invalid operator (#{op}) used when creating operator validation" unless suffix = OPERATORS[op]
|
180
|
+
|
181
|
+
prefix = case arg
|
182
|
+
when String
|
183
|
+
"str"
|
184
|
+
when Integer
|
185
|
+
"int"
|
186
|
+
else
|
187
|
+
raise Error, "invalid argument (#{arg.inspect}) used when creating operator validation"
|
188
|
+
end
|
189
|
+
|
190
|
+
@generator.validation({:type=>:"#{prefix}_#{suffix}", :columns=>Array(columns), :arg=>arg}.merge!(opts))
|
191
|
+
end
|
192
|
+
|
167
193
|
# Given the name of a constraint, drop that constraint from the database,
|
168
194
|
# and remove the related validation metadata.
|
169
195
|
def drop(constraint)
|
@@ -349,6 +375,8 @@ module Sequel
|
|
349
375
|
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) >= arg}))
|
350
376
|
when :max_length
|
351
377
|
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) <= arg}))
|
378
|
+
when *REVERSE_OPERATOR_MAP.keys
|
379
|
+
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.identifier(c).send(REVERSE_OPERATOR_MAP[validation_type], arg)}))
|
352
380
|
when :length_range
|
353
381
|
op = arg.exclude_end? ? :< : :<=
|
354
382
|
generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| (Sequel.char_length(c) >= arg.begin) & Sequel.char_length(c).send(op, arg.end)}))
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
#
|
3
|
+
# The string_agg extension adds the ability to perform database-independent
|
4
|
+
# aggregate string concatentation. For example, with a table like:
|
5
|
+
#
|
6
|
+
# c1 | c2
|
7
|
+
# ---+---
|
8
|
+
# a | 1
|
9
|
+
# a | 2
|
10
|
+
# a | 3
|
11
|
+
# b | 4
|
12
|
+
#
|
13
|
+
# You can return a result set like:
|
14
|
+
#
|
15
|
+
# c1 | c2s
|
16
|
+
# ---+---
|
17
|
+
# a | 1,2,3
|
18
|
+
# b | 4
|
19
|
+
#
|
20
|
+
# First, you need to load the extension into the database:
|
21
|
+
#
|
22
|
+
# DB.extension :string_agg
|
23
|
+
#
|
24
|
+
# Then you can use the Sequel.string_agg method to return a Sequel
|
25
|
+
# expression:
|
26
|
+
#
|
27
|
+
# sa = Sequel.string_agg(:column_name)
|
28
|
+
# # or:
|
29
|
+
# sa = Sequel.string_agg(:column_name, '-') # custom separator
|
30
|
+
#
|
31
|
+
# You can specify the order in which the concatention happens by
|
32
|
+
# calling +order+ on the expression:
|
33
|
+
#
|
34
|
+
# sa = Sequel.string_agg(:column_name).order(:other_column)
|
35
|
+
#
|
36
|
+
# Additionally, if you want to have the concatenation only operate
|
37
|
+
# on distinct values, you can call distinct:
|
38
|
+
#
|
39
|
+
# sa = Sequel.string_agg(:column_name).order(:other_column).distinct
|
40
|
+
#
|
41
|
+
# These expressions can be used in your datasets, or anywhere else that
|
42
|
+
# Sequel expressions are allowed:
|
43
|
+
#
|
44
|
+
# DB[:table].
|
45
|
+
# select_group(:c1).
|
46
|
+
# select_append(Sequel.string_agg(:c2))
|
47
|
+
#
|
48
|
+
# This extension currenly supports the following databases:
|
49
|
+
#
|
50
|
+
# * PostgreSQL 9+
|
51
|
+
# * SQLAnywhere 12+
|
52
|
+
# * Oracle 11g+ (except distinct)
|
53
|
+
# * DB2 9.7+ (except distinct)
|
54
|
+
# * MySQL
|
55
|
+
# * HSQLDB
|
56
|
+
# * CUBRID
|
57
|
+
# * H2
|
58
|
+
#
|
59
|
+
# Related module: Sequel::SQL::StringAgg
|
60
|
+
|
61
|
+
#
|
62
|
+
module Sequel
|
63
|
+
module SQL
|
64
|
+
module Builders
|
65
|
+
# Return a StringAgg expression for an aggregate string concatentation.
|
66
|
+
def string_agg(*a)
|
67
|
+
StringAgg.new(*a)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# The StringAgg class represents an aggregate string concatentation.
|
72
|
+
class StringAgg < GenericExpression
|
73
|
+
include StringMethods
|
74
|
+
include StringConcatenationMethods
|
75
|
+
include InequalityMethods
|
76
|
+
include AliasMethods
|
77
|
+
include CastMethods
|
78
|
+
include OrderMethods
|
79
|
+
include PatternMatchMethods
|
80
|
+
include SubscriptMethods
|
81
|
+
|
82
|
+
# These methods are added to datasets using the string_agg
|
83
|
+
# extension, for the purposes of correctly literalizing StringAgg
|
84
|
+
# expressions for the appropriate database type.
|
85
|
+
module DatasetMethods
|
86
|
+
# Append the SQL fragment for the StringAgg expression to the SQL query.
|
87
|
+
def string_agg_sql_append(sql, sa)
|
88
|
+
if defined?(super)
|
89
|
+
return super
|
90
|
+
end
|
91
|
+
|
92
|
+
expr = sa.expr
|
93
|
+
separator = sa.separator || ","
|
94
|
+
order = sa.order_expr
|
95
|
+
distinct = sa.is_distinct?
|
96
|
+
|
97
|
+
case db_type = db.database_type
|
98
|
+
when :postgres, :sqlanywhere
|
99
|
+
f = Function.new(db_type == :postgres ? :string_agg : :list, expr, separator)
|
100
|
+
if order
|
101
|
+
f = f.order(*order)
|
102
|
+
end
|
103
|
+
if distinct
|
104
|
+
f = f.distinct
|
105
|
+
end
|
106
|
+
literal_append(sql, f)
|
107
|
+
when :mysql, :hsqldb, :cubrid, :h2
|
108
|
+
sql << "GROUP_CONCAT("
|
109
|
+
if distinct
|
110
|
+
sql << "DISTINCT "
|
111
|
+
end
|
112
|
+
literal_append(sql, expr)
|
113
|
+
if order
|
114
|
+
sql << " ORDER BY "
|
115
|
+
expression_list_append(sql, order)
|
116
|
+
end
|
117
|
+
sql << " SEPARATOR "
|
118
|
+
literal_append(sql, separator)
|
119
|
+
sql << ")"
|
120
|
+
when :oracle, :db2
|
121
|
+
if distinct
|
122
|
+
raise Error, "string_agg with distinct is not implemented on #{db.database_type}"
|
123
|
+
end
|
124
|
+
literal_append(sql, Function.new(:listagg, expr, separator))
|
125
|
+
if order
|
126
|
+
sql << " WITHIN GROUP (ORDER BY "
|
127
|
+
expression_list_append(sql, order)
|
128
|
+
sql << ")"
|
129
|
+
else
|
130
|
+
sql << " WITHIN GROUP (ORDER BY 1)"
|
131
|
+
end
|
132
|
+
else
|
133
|
+
raise Error, "string_agg is not implemented on #{db.database_type}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# The string expression for each row that will concatenated to the output.
|
139
|
+
attr_reader :expr
|
140
|
+
|
141
|
+
# The separator between each string expression.
|
142
|
+
attr_reader :separator
|
143
|
+
|
144
|
+
# The expression that the aggregation is ordered by.
|
145
|
+
attr_reader :order_expr
|
146
|
+
|
147
|
+
# Set the expression and separator
|
148
|
+
def initialize(expr, separator=nil)
|
149
|
+
@expr = expr
|
150
|
+
@separator = separator
|
151
|
+
end
|
152
|
+
|
153
|
+
# Whether the current expression uses distinct expressions
|
154
|
+
def is_distinct?
|
155
|
+
@distinct == true
|
156
|
+
end
|
157
|
+
|
158
|
+
# Return a modified StringAgg that uses distinct expressions
|
159
|
+
def distinct
|
160
|
+
sa = dup
|
161
|
+
sa.instance_variable_set(:@distinct, true)
|
162
|
+
sa
|
163
|
+
end
|
164
|
+
|
165
|
+
# Return a modified StringAgg with the given order
|
166
|
+
def order(*o)
|
167
|
+
sa = dup
|
168
|
+
sa.instance_variable_set(:@order_expr, o.empty? ? nil : o)
|
169
|
+
sa
|
170
|
+
end
|
171
|
+
|
172
|
+
to_s_method :string_agg_sql
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
Dataset.register_extension(:string_agg, SQL::StringAgg::DatasetMethods)
|
177
|
+
end
|
178
|
+
|