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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/association_basics.rdoc +27 -4
  4. data/doc/migration.rdoc +24 -0
  5. data/doc/release_notes/4.36.0.txt +116 -0
  6. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  7. data/lib/sequel/adapters/mysql2.rb +11 -1
  8. data/lib/sequel/adapters/oracle.rb +3 -5
  9. data/lib/sequel/adapters/postgres.rb +2 -2
  10. data/lib/sequel/adapters/shared/access.rb +1 -1
  11. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  12. data/lib/sequel/adapters/shared/postgres.rb +1 -1
  13. data/lib/sequel/adapters/shared/sqlite.rb +1 -1
  14. data/lib/sequel/connection_pool.rb +5 -0
  15. data/lib/sequel/connection_pool/sharded_single.rb +1 -1
  16. data/lib/sequel/connection_pool/sharded_threaded.rb +29 -14
  17. data/lib/sequel/connection_pool/single.rb +1 -1
  18. data/lib/sequel/connection_pool/threaded.rb +5 -3
  19. data/lib/sequel/database/schema_methods.rb +7 -1
  20. data/lib/sequel/dataset/sql.rb +4 -0
  21. data/lib/sequel/extensions/arbitrary_servers.rb +1 -1
  22. data/lib/sequel/extensions/connection_expiration.rb +89 -0
  23. data/lib/sequel/extensions/connection_validator.rb +11 -3
  24. data/lib/sequel/extensions/constraint_validations.rb +28 -0
  25. data/lib/sequel/extensions/string_agg.rb +178 -0
  26. data/lib/sequel/model.rb +13 -56
  27. data/lib/sequel/model/associations.rb +3 -1
  28. data/lib/sequel/model/base.rb +104 -7
  29. data/lib/sequel/plugins/constraint_validations.rb +17 -3
  30. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  31. data/lib/sequel/sql.rb +8 -0
  32. data/lib/sequel/version.rb +1 -1
  33. data/spec/adapters/postgres_spec.rb +4 -0
  34. data/spec/core/dataset_spec.rb +4 -0
  35. data/spec/core/expression_filters_spec.rb +4 -0
  36. data/spec/extensions/connection_expiration_spec.rb +121 -0
  37. data/spec/extensions/connection_validator_spec.rb +7 -0
  38. data/spec/extensions/constraint_validations_plugin_spec.rb +14 -0
  39. data/spec/extensions/constraint_validations_spec.rb +64 -0
  40. data/spec/extensions/string_agg_spec.rb +85 -0
  41. data/spec/extensions/validation_helpers_spec.rb +2 -0
  42. data/spec/integration/plugin_test.rb +37 -2
  43. data/spec/model/association_reflection_spec.rb +10 -0
  44. data/spec/model/model_spec.rb +49 -0
  45. metadata +8 -2
@@ -12,7 +12,7 @@ class Sequel::SingleConnectionPool < Sequel::ConnectionPool
12
12
  # Disconnect the connection from the database.
13
13
  def disconnect(opts=nil)
14
14
  return unless @conn
15
- db.disconnect_connection(@conn)
15
+ disconnect_connection(@conn)
16
16
  @conn = nil
17
17
  end
18
18
 
@@ -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.each{|conn| db.disconnect_connection(conn)}
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
- db.disconnect_connection(oconn) if oconn
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
- db.disconnect_connection(conn)
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
- type_literal_generic_bignum(column)
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
 
@@ -570,6 +570,10 @@ module Sequel
570
570
  else
571
571
  sql << FUNCTION_DISTINCT if opts[:distinct]
572
572
  expression_list_append(sql, f.args)
573
+ if order = opts[:order]
574
+ sql << ORDER_BY
575
+ expression_list_append(sql, order)
576
+ end
573
577
  end
574
578
  sql << PAREN_CLOSE
575
579
 
@@ -102,7 +102,7 @@ module Sequel
102
102
  a = @allocated[thread]
103
103
  a.delete(server)
104
104
  @allocated.delete(thread) if a.empty?
105
- db.disconnect_connection(conn)
105
+ disconnect_connection(conn)
106
106
  else
107
107
  super
108
108
  end
@@ -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
- @connection_timestamps ||= {}
65
- @connection_validation_timeout = 3600
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
- db.disconnect_connection(conn)
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
+