sequel 5.60.1 → 5.81.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +280 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +27 -24
  5. data/doc/advanced_associations.rdoc +16 -14
  6. data/doc/association_basics.rdoc +53 -17
  7. data/doc/cheat_sheet.rdoc +3 -3
  8. data/doc/code_order.rdoc +5 -3
  9. data/doc/dataset_basics.rdoc +1 -1
  10. data/doc/mass_assignment.rdoc +1 -1
  11. data/doc/migration.rdoc +15 -0
  12. data/doc/model_hooks.rdoc +1 -1
  13. data/doc/object_model.rdoc +8 -8
  14. data/doc/opening_databases.rdoc +18 -7
  15. data/doc/postgresql.rdoc +8 -8
  16. data/doc/querying.rdoc +7 -2
  17. data/doc/release_notes/5.61.0.txt +43 -0
  18. data/doc/release_notes/5.62.0.txt +132 -0
  19. data/doc/release_notes/5.63.0.txt +33 -0
  20. data/doc/release_notes/5.64.0.txt +50 -0
  21. data/doc/release_notes/5.65.0.txt +21 -0
  22. data/doc/release_notes/5.66.0.txt +24 -0
  23. data/doc/release_notes/5.67.0.txt +32 -0
  24. data/doc/release_notes/5.68.0.txt +61 -0
  25. data/doc/release_notes/5.69.0.txt +26 -0
  26. data/doc/release_notes/5.70.0.txt +35 -0
  27. data/doc/release_notes/5.71.0.txt +21 -0
  28. data/doc/release_notes/5.72.0.txt +33 -0
  29. data/doc/release_notes/5.73.0.txt +66 -0
  30. data/doc/release_notes/5.74.0.txt +45 -0
  31. data/doc/release_notes/5.75.0.txt +35 -0
  32. data/doc/release_notes/5.76.0.txt +86 -0
  33. data/doc/release_notes/5.77.0.txt +63 -0
  34. data/doc/release_notes/5.78.0.txt +67 -0
  35. data/doc/release_notes/5.79.0.txt +28 -0
  36. data/doc/release_notes/5.80.0.txt +40 -0
  37. data/doc/release_notes/5.81.0.txt +31 -0
  38. data/doc/schema_modification.rdoc +3 -3
  39. data/doc/security.rdoc +9 -9
  40. data/doc/sharding.rdoc +3 -1
  41. data/doc/sql.rdoc +14 -14
  42. data/doc/testing.rdoc +16 -12
  43. data/doc/transactions.rdoc +6 -6
  44. data/doc/virtual_rows.rdoc +1 -1
  45. data/lib/sequel/adapters/ibmdb.rb +1 -1
  46. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  47. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  48. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  49. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  50. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  51. data/lib/sequel/adapters/jdbc.rb +10 -6
  52. data/lib/sequel/adapters/mysql.rb +19 -7
  53. data/lib/sequel/adapters/mysql2.rb +2 -2
  54. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  55. data/lib/sequel/adapters/oracle.rb +1 -0
  56. data/lib/sequel/adapters/postgres.rb +10 -5
  57. data/lib/sequel/adapters/shared/access.rb +9 -1
  58. data/lib/sequel/adapters/shared/db2.rb +12 -0
  59. data/lib/sequel/adapters/shared/mssql.rb +71 -9
  60. data/lib/sequel/adapters/shared/mysql.rb +82 -3
  61. data/lib/sequel/adapters/shared/oracle.rb +17 -7
  62. data/lib/sequel/adapters/shared/postgres.rb +469 -160
  63. data/lib/sequel/adapters/shared/sqlanywhere.rb +18 -5
  64. data/lib/sequel/adapters/shared/sqlite.rb +39 -3
  65. data/lib/sequel/adapters/sqlite.rb +42 -3
  66. data/lib/sequel/adapters/trilogy.rb +117 -0
  67. data/lib/sequel/connection_pool/sharded_threaded.rb +16 -11
  68. data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
  69. data/lib/sequel/connection_pool/threaded.rb +14 -8
  70. data/lib/sequel/connection_pool/timed_queue.rb +270 -0
  71. data/lib/sequel/connection_pool.rb +57 -31
  72. data/lib/sequel/database/connecting.rb +25 -1
  73. data/lib/sequel/database/dataset.rb +16 -6
  74. data/lib/sequel/database/misc.rb +65 -14
  75. data/lib/sequel/database/query.rb +72 -1
  76. data/lib/sequel/database/schema_generator.rb +1 -1
  77. data/lib/sequel/database/schema_methods.rb +12 -5
  78. data/lib/sequel/database/transactions.rb +6 -0
  79. data/lib/sequel/dataset/actions.rb +60 -13
  80. data/lib/sequel/dataset/dataset_module.rb +1 -1
  81. data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +42 -0
  82. data/lib/sequel/dataset/features.rb +15 -1
  83. data/lib/sequel/dataset/graph.rb +1 -0
  84. data/lib/sequel/dataset/misc.rb +12 -2
  85. data/lib/sequel/dataset/placeholder_literalizer.rb +20 -9
  86. data/lib/sequel/dataset/query.rb +119 -45
  87. data/lib/sequel/dataset/sql.rb +58 -36
  88. data/lib/sequel/dataset.rb +4 -0
  89. data/lib/sequel/exceptions.rb +5 -0
  90. data/lib/sequel/extensions/_model_pg_row.rb +0 -12
  91. data/lib/sequel/extensions/_pretty_table.rb +1 -1
  92. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  93. data/lib/sequel/extensions/async_thread_pool.rb +21 -13
  94. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  95. data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
  96. data/lib/sequel/extensions/caller_logging.rb +4 -1
  97. data/lib/sequel/extensions/connection_expiration.rb +15 -9
  98. data/lib/sequel/extensions/connection_validator.rb +16 -11
  99. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  100. data/lib/sequel/extensions/date_arithmetic.rb +1 -1
  101. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  102. data/lib/sequel/extensions/index_caching.rb +5 -1
  103. data/lib/sequel/extensions/looser_typecasting.rb +3 -0
  104. data/lib/sequel/extensions/migration.rb +65 -15
  105. data/lib/sequel/extensions/named_timezones.rb +22 -6
  106. data/lib/sequel/extensions/pg_array.rb +33 -4
  107. data/lib/sequel/extensions/pg_auto_parameterize.rb +509 -0
  108. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  109. data/lib/sequel/extensions/pg_enum.rb +1 -2
  110. data/lib/sequel/extensions/pg_extended_date_support.rb +38 -27
  111. data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
  112. data/lib/sequel/extensions/pg_hstore.rb +5 -0
  113. data/lib/sequel/extensions/pg_inet.rb +10 -11
  114. data/lib/sequel/extensions/pg_interval.rb +10 -11
  115. data/lib/sequel/extensions/pg_json.rb +10 -10
  116. data/lib/sequel/extensions/pg_json_ops.rb +3 -3
  117. data/lib/sequel/extensions/pg_multirange.rb +6 -11
  118. data/lib/sequel/extensions/pg_range.rb +9 -14
  119. data/lib/sequel/extensions/pg_row.rb +20 -19
  120. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  121. data/lib/sequel/extensions/provenance.rb +110 -0
  122. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  123. data/lib/sequel/extensions/schema_caching.rb +1 -1
  124. data/lib/sequel/extensions/schema_dumper.rb +32 -9
  125. data/lib/sequel/extensions/server_block.rb +2 -1
  126. data/lib/sequel/extensions/set_literalizer.rb +58 -0
  127. data/lib/sequel/extensions/sqlite_json_ops.rb +76 -18
  128. data/lib/sequel/extensions/temporarily_release_connection.rb +178 -0
  129. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  130. data/lib/sequel/model/associations.rb +38 -11
  131. data/lib/sequel/model/base.rb +32 -18
  132. data/lib/sequel/model/dataset_module.rb +3 -0
  133. data/lib/sequel/model/exceptions.rb +15 -3
  134. data/lib/sequel/plugins/auto_validations.rb +53 -15
  135. data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
  136. data/lib/sequel/plugins/column_encryption.rb +28 -7
  137. data/lib/sequel/plugins/composition.rb +2 -2
  138. data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
  139. data/lib/sequel/plugins/constraint_validations.rb +8 -5
  140. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  141. data/lib/sequel/plugins/dirty.rb +1 -1
  142. data/lib/sequel/plugins/finder.rb +4 -2
  143. data/lib/sequel/plugins/input_transformer.rb +1 -1
  144. data/lib/sequel/plugins/list.rb +5 -2
  145. data/lib/sequel/plugins/many_through_many.rb +1 -1
  146. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  147. data/lib/sequel/plugins/nested_attributes.rb +4 -4
  148. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  149. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  150. data/lib/sequel/plugins/paged_operations.rb +181 -0
  151. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +9 -3
  152. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  153. data/lib/sequel/plugins/prepared_statements.rb +2 -1
  154. data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
  155. data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
  156. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  157. data/lib/sequel/plugins/single_table_inheritance.rb +8 -0
  158. data/lib/sequel/plugins/sql_comments.rb +1 -1
  159. data/lib/sequel/plugins/static_cache.rb +38 -0
  160. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  161. data/lib/sequel/plugins/tactical_eager_loading.rb +14 -14
  162. data/lib/sequel/plugins/validate_associated.rb +22 -12
  163. data/lib/sequel/plugins/validation_helpers.rb +29 -2
  164. data/lib/sequel/plugins/validation_helpers_generic_type_messages.rb +73 -0
  165. data/lib/sequel/version.rb +2 -2
  166. metadata +79 -6
@@ -0,0 +1,109 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # This plugin implements optimistic locking mechanism on PostgreSQL based
6
+ # on the xmin of the row. The xmin system column is automatically set to
7
+ # the current transaction id whenever the row is inserted or updated:
8
+ #
9
+ # class Person < Sequel::Model
10
+ # plugin :pg_xmin_optimistic_locking
11
+ # end
12
+ # p1 = Person[1]
13
+ # p2 = Person[1]
14
+ # p1.update(name: 'Jim') # works
15
+ # p2.update(name: 'Bob') # raises Sequel::NoExistingObject
16
+ #
17
+ # The advantage of pg_xmin_optimistic_locking plugin compared to the
18
+ # regular optimistic_locking plugin as that it does not require any
19
+ # additional columns setup on the model. This allows it to be loaded
20
+ # in the base model and have all subclasses automatically use
21
+ # optimistic locking. The disadvantage is that testing can be
22
+ # more difficult if you are modifying the underlying row between
23
+ # when a model is retrieved and when it is saved.
24
+ #
25
+ # This plugin may not work with the class_table_inheritance plugin.
26
+ #
27
+ # This plugin relies on the instance_filters plugin.
28
+ module PgXminOptimisticLocking
29
+ WILDCARD = LiteralString.new('*').freeze
30
+
31
+ # Define the xmin column accessor
32
+ def self.apply(model)
33
+ model.instance_exec do
34
+ plugin(:optimistic_locking_base)
35
+ @lock_column = :xmin
36
+ def_column_accessor(:xmin)
37
+ end
38
+ end
39
+
40
+ # Update the dataset to append the xmin column if it is usable
41
+ # and there is a dataset for the model.
42
+ def self.configure(model)
43
+ model.instance_exec do
44
+ set_dataset(@dataset) if @dataset
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ private
50
+
51
+ # Ensure the dataset selects the xmin column if doing so
52
+ def convert_input_dataset(ds)
53
+ append_xmin_column_if_usable(super)
54
+ end
55
+
56
+ # If the xmin column is not already selected, and selecting it does not
57
+ # raise an error, append it to the selections.
58
+ def append_xmin_column_if_usable(ds)
59
+ select = ds.opts[:select]
60
+
61
+ unless select && select.include?(:xmin)
62
+ xmin_ds = ds.select_append(:xmin)
63
+ begin
64
+ columns = xmin_ds.columns!
65
+ rescue Sequel::DatabaseConnectionError, Sequel::DatabaseDisconnectError
66
+ raise
67
+ rescue Sequel::DatabaseError
68
+ # ignore, could be view, subquery, table returning function, etc.
69
+ else
70
+ ds = xmin_ds if columns.include?(:xmin)
71
+ end
72
+ end
73
+
74
+ ds
75
+ end
76
+ end
77
+
78
+ module InstanceMethods
79
+ private
80
+
81
+ # Only set the lock column instance filter if there is an xmin value.
82
+ def lock_column_instance_filter
83
+ super if @values[:xmin]
84
+ end
85
+
86
+ # Include xmin value when inserting initial row
87
+ def _insert_dataset
88
+ super.returning(WILDCARD, :xmin)
89
+ end
90
+
91
+ # Remove the xmin from the columns to update.
92
+ # PostgreSQL automatically updates the xmin value, and it cannot be assigned.
93
+ def _save_update_all_columns_hash
94
+ v = super
95
+ v.delete(:xmin)
96
+ v
97
+ end
98
+
99
+ # Add an RETURNING clause to fetch the updated xmin when updating the row.
100
+ def _update_without_checking(columns)
101
+ ds = _update_dataset
102
+ rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.returning(:xmin).update_sql(columns))).all
103
+ values[:xmin] = rows.first[:xmin] unless rows.empty?
104
+ rows.length
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -3,7 +3,8 @@
3
3
  module Sequel
4
4
  module Plugins
5
5
  # The prepared_statements plugin modifies the model to use prepared statements for
6
- # instance level inserts and updates.
6
+ # instance level inserts and updates. This plugin exists for backwards compatibility
7
+ # and is not recommended for general use.
7
8
  #
8
9
  # Note that this plugin is unsafe in some circumstances, as it can allow up to
9
10
  # 2^N prepared statements to be created for each type of insert and update query, where
@@ -5,7 +5,8 @@ module Sequel
5
5
  # The prepared_statements_safe plugin modifies the model to reduce the number of
6
6
  # prepared statements that can be created, by setting as many columns as possible
7
7
  # before creating, and by changing +save_changes+ to save all columns instead of
8
- # just the changed ones.
8
+ # just the changed ones. This plugin exists for backwards compatibility
9
+ # and is not recommended for general use.
9
10
  #
10
11
  # This plugin depends on the +prepared_statements+ plugin.
11
12
  #
@@ -0,0 +1,154 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The primary_key_lookup_check_values plugin typecasts given primary key
6
+ # values before performing a lookup by primary key. If the given primary
7
+ # key value cannot be typecasted correctly, the lookup returns nil
8
+ # without issuing a query. If the schema for the primary key column
9
+ # includes minimum and maximum values, this also checks the given value
10
+ # is not outside the range. If the given value is outside the allowed
11
+ # range, the lookup returns nil without issuing a query.
12
+ #
13
+ # This affects the following Model methods:
14
+ #
15
+ # * Model.[] (when called with non-Hash)
16
+ # * Model.with_pk
17
+ # * Model.with_pk!
18
+ #
19
+ # It also affects the following Model dataset methods:
20
+ #
21
+ # * Dataset#[] (when called with Integer)
22
+ # * Dataset#with_pk
23
+ # * dataset#with_pk!
24
+ #
25
+ # Note that this can break working code. The above methods accept
26
+ # any filter condition by default, not just primary key values. The
27
+ # plugin will handle Symbol, Sequel::SQL::Expression, and
28
+ # Sequel::LiteralString objects, but code such as the following will break:
29
+ #
30
+ # # Return first Album where primary key is one of the given values
31
+ # Album.dataset.with_pk([1, 2, 3])
32
+ #
33
+ # Usage:
34
+ #
35
+ # # Make all model subclasses support checking primary key values before
36
+ # # lookup # (called before loading subclasses)
37
+ # Sequel::Model.plugin :primary_key_lookup_check_values
38
+ #
39
+ # # Make the Album class support checking primary key values before lookup
40
+ # Album.plugin :primary_key_lookup_check_values
41
+ module PrimaryKeyLookupCheckValues
42
+ def self.configure(model)
43
+ model.instance_exec do
44
+ setup_primary_key_lookup_check_values if @dataset
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ Plugins.after_set_dataset(self, :setup_primary_key_lookup_check_values)
50
+
51
+ Plugins.inherited_instance_variables(self,
52
+ :@primary_key_type=>nil,
53
+ :@primary_key_value_range=>nil)
54
+
55
+ private
56
+
57
+ # Check the given primary key value. Typecast it to the appropriate
58
+ # database type if the database type is known. If it cannot be
59
+ # typecasted, or the typecasted value is outside the range of column
60
+ # values, return nil.
61
+ def _check_pk_lookup_value(pk)
62
+ return if nil == pk
63
+ case pk
64
+ when SQL::Expression, LiteralString, Symbol
65
+ return pk
66
+ end
67
+ return pk unless pk_type = @primary_key_type
68
+
69
+ if pk_type.is_a?(Array)
70
+ return unless pk.is_a?(Array)
71
+ return unless pk.size == pk_type.size
72
+ return if pk.any?(&:nil?)
73
+
74
+ pk_value_range = @primary_key_value_range
75
+ i = 0
76
+ pk.map do |v|
77
+ if type = pk_type[i]
78
+ v = _typecast_pk_lookup_value(v, type)
79
+ return if nil == v
80
+ if pk_value_range
81
+ min, max = pk_value_range[i]
82
+ return if min && v < min
83
+ return if max && v > max
84
+ end
85
+ end
86
+ i += 1
87
+ v
88
+ end
89
+ elsif pk.is_a?(Array)
90
+ return
91
+ elsif nil != (pk = _typecast_pk_lookup_value(pk, pk_type))
92
+ min, max = @primary_key_value_range
93
+ return if min && pk < min
94
+ return if max && pk > max
95
+ pk
96
+ end
97
+ end
98
+
99
+ # Typecast the value to the appropriate type,
100
+ # returning nil if it cannot be typecasted.
101
+ def _typecast_pk_lookup_value(value, type)
102
+ db.typecast_value(type, value)
103
+ rescue InvalidValue
104
+ nil
105
+ end
106
+
107
+ # Skip the primary key lookup if the typecasted and checked
108
+ # primary key value is nil.
109
+ def primary_key_lookup(pk)
110
+ unless nil == (pk = _check_pk_lookup_value(pk))
111
+ super
112
+ end
113
+ end
114
+
115
+ # Setup the primary key type and value range used for checking
116
+ # primary key values during lookup.
117
+ def setup_primary_key_lookup_check_values
118
+ if primary_key.is_a?(Array)
119
+ types = []
120
+ value_ranges = []
121
+ primary_key.each do |pk|
122
+ type, min, max = _type_min_max_values_for_column(pk)
123
+ types << type
124
+ value_ranges << ([min, max].freeze if min || max)
125
+ end
126
+ @primary_key_type = (types.freeze if types.any?)
127
+ @primary_key_value_range = (value_ranges.freeze if @primary_key_type && value_ranges.any?)
128
+ else
129
+ @primary_key_type, min, max = _type_min_max_values_for_column(primary_key)
130
+ @primary_key_value_range = ([min, max].freeze if @primary_key_type && (min || max))
131
+ end
132
+ end
133
+
134
+ # Return the type, min_value, and max_value schema entries
135
+ # for the column, if they exist.
136
+ def _type_min_max_values_for_column(column)
137
+ if schema = db_schema[column]
138
+ schema.values_at(:type, :min_value, :max_value)
139
+ end
140
+ end
141
+ end
142
+
143
+ module DatasetMethods
144
+ # Skip the primary key lookup if the typecasted and checked
145
+ # primary key value is nil.
146
+ def with_pk(pk)
147
+ unless nil == (pk = model.send(:_check_pk_lookup_value, pk))
148
+ super
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -71,6 +71,8 @@ module Sequel
71
71
  # (default: :t)
72
72
  # :level_alias :: The symbol identifier to use when eagerly loading descendants
73
73
  # up to a given level (default: :x_level_x)
74
+ # :union_all :: Whether to use UNION ALL or UNION with the recursive
75
+ # common table expression (default: true)
74
76
  module RcteTree
75
77
  # Create the appropriate parent, children, ancestors, and descendants
76
78
  # associations for the model.
@@ -80,6 +82,7 @@ module Sequel
80
82
  opts = opts.dup
81
83
  opts[:class] = model
82
84
  opts[:methods_module] = Module.new
85
+ opts[:union_all] = opts[:union_all].nil? ? true : opts[:union_all]
83
86
  model.send(:include, opts[:methods_module])
84
87
 
85
88
  key = opts[:key] ||= :parent_id
@@ -142,7 +145,7 @@ module Sequel
142
145
  model.from(SQL::AliasedExpression.new(t, table_alias)).
143
146
  with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
144
147
  recursive_ds.select(*c_all),
145
- :args=>col_aliases)
148
+ :args=>col_aliases, union_all: opts[:union_all])
146
149
  end
147
150
  aal = Array(a[:after_load])
148
151
  aal << proc do |m, ancs|
@@ -191,7 +194,7 @@ module Sequel
191
194
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
192
195
  ds = model.from(SQL::AliasedExpression.new(t, table_alias)).
193
196
  with_recursive(t, base_case, recursive_case,
194
- :args=>((key_aliases + col_aliases) if col_aliases))
197
+ :args=>((key_aliases + col_aliases) if col_aliases), union_all: opts[:union_all])
195
198
  ds = r.apply_eager_dataset_changes(ds)
196
199
  ds = ds.select_append(ka) unless ds.opts[:select] == nil
197
200
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil)) do |obj|
@@ -240,7 +243,7 @@ module Sequel
240
243
  model.from(SQL::AliasedExpression.new(t, table_alias)).
241
244
  with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
242
245
  recursive_ds.select(*c_all),
243
- :args=>col_aliases)
246
+ :args=>col_aliases, union_all: opts[:union_all])
244
247
  end
245
248
  dal = Array(d[:after_load])
246
249
  dal << proc do |m, descs|
@@ -299,7 +302,7 @@ module Sequel
299
302
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
300
303
  ds = model.from(SQL::AliasedExpression.new(t, table_alias)).
301
304
  with_recursive(t, base_case, recursive_case,
302
- :args=>((key_aliases + col_aliases + (level ? [la] : [])) if col_aliases))
305
+ :args=>((key_aliases + col_aliases + (level ? [la] : [])) if col_aliases), union_all: opts[:union_all])
303
306
  ds = r.apply_eager_dataset_changes(ds)
304
307
  ds = ds.select_append(ka) unless ds.opts[:select] == nil
305
308
  model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil, :associations=>OPTS)) do |obj|
@@ -253,6 +253,14 @@ module Sequel
253
253
 
254
254
  private
255
255
 
256
+ # Limit tactical eager loading objects to objects that support the same association.
257
+ def _filter_tactical_eager_load_objects(opts)
258
+ objects = defined?(super) ? super : retrieved_with.dup
259
+ name = opts[:name]
260
+ objects.select!{|x| x.model.association_reflections.include?(name)}
261
+ objects
262
+ end
263
+
256
264
  # Don't allow use of prepared statements.
257
265
  def use_prepared_statements_for?(type)
258
266
  false
@@ -12,7 +12,7 @@ module Sequel
12
12
  # # SELECT * FROM albums WHERE (id = 1) LIMIT 1
13
13
  # # -- model:Album,method_type:class,method:[]
14
14
  #
15
- # album.update(:name=>'A')
15
+ # album.update(name: 'A')
16
16
  # # UPDATE albums SET name = 'baz' WHERE (id = 1)
17
17
  # # -- model:Album,method_type:instance,method:update
18
18
  #
@@ -64,6 +64,9 @@ module Sequel
64
64
  def self.configure(model, opts=OPTS)
65
65
  model.instance_exec do
66
66
  @static_cache_frozen = opts.fetch(:frozen, true)
67
+ if @static_cache_frozen && defined?(::Sequel::Plugins::ForbidLazyLoad::ClassMethods) && is_a?(::Sequel::Plugins::ForbidLazyLoad::ClassMethods)
68
+ extend ForbidLazyLoadClassMethods
69
+ end
67
70
  load_cache
68
71
  end
69
72
  end
@@ -246,6 +249,41 @@ module Sequel
246
249
  end
247
250
  end
248
251
 
252
+ module ForbidLazyLoadClassMethods
253
+ # Do not forbid lazy loading for single object retrieval.
254
+ def cache_get_pk(pk)
255
+ primary_key_lookup(pk)
256
+ end
257
+
258
+ # Use static cache to return first arguments.
259
+ def first(*args)
260
+ if !defined?(yield) && args.empty?
261
+ if o = @all.first
262
+ _static_cache_frozen_copy(o)
263
+ end
264
+ else
265
+ super
266
+ end
267
+ end
268
+
269
+ private
270
+
271
+ # Return a frozen copy of the object that does not have lazy loading
272
+ # forbidden.
273
+ def _static_cache_frozen_copy(o)
274
+ o = call(Hash[o.values])
275
+ o.errors.freeze
276
+ o.freeze
277
+ end
278
+
279
+ # Do not forbid lazy loading for single object retrieval.
280
+ def primary_key_lookup(pk)
281
+ if o = cache[pk]
282
+ _static_cache_frozen_copy(o)
283
+ end
284
+ end
285
+ end
286
+
249
287
  module InstanceMethods
250
288
  # Disallowing destroying the object unless the frozen: false option was used.
251
289
  def before_destroy
@@ -26,7 +26,11 @@ module Sequel
26
26
  module ClassMethods
27
27
  # Dump the in-memory cached rows to the cache file.
28
28
  def dump_static_cache_cache
29
- File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(@static_cache_cache))}
29
+ static_cache_cache = {}
30
+ @static_cache_cache.sort.each do |k, v|
31
+ static_cache_cache[k] = v
32
+ end
33
+ File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(static_cache_cache))}
30
34
  nil
31
35
  end
32
36
 
@@ -152,23 +152,23 @@ module Sequel
152
152
  name = opts[:name]
153
153
  eager_reload = dynamic_opts[:eager_reload]
154
154
  if (!associations.include?(name) || eager_reload) && opts[:allow_eager] != false && retrieved_by && !frozen? && !dynamic_opts[:callback] && !dynamic_opts[:reload]
155
- begin
156
- objects = if eager_reload
157
- retrieved_with.reject(&:frozen?)
158
- else
159
- retrieved_with.reject{|x| x.frozen? || x.associations.include?(name)}
160
- end
161
- retrieved_by.send(:eager_load, objects, name=>dynamic_opts[:eager] || OPTS)
162
- rescue Sequel::UndefinedAssociation
163
- # This can happen if class table inheritance is used and the association
164
- # is only defined in a subclass. This particular instance can use the
165
- # association, but it can't be eagerly loaded as the parent class doesn't
166
- # have access to the association, and that's the class doing the eager loading.
167
- nil
168
- end
155
+ retrieved_by.send(:eager_load, _filter_tactical_eager_load_objects(:eager_reload=>eager_reload, :name=>name), {name=>dynamic_opts[:eager] || OPTS}, model)
169
156
  end
170
157
  super
171
158
  end
159
+
160
+ # Filter the objects used when tactical eager loading.
161
+ # By default, this removes frozen objects and objects that alreayd have the association loaded
162
+ def _filter_tactical_eager_load_objects(opts)
163
+ objects = defined?(super) ? super : retrieved_with.dup
164
+ if opts[:eager_reload]
165
+ objects.reject!(&:frozen?)
166
+ else
167
+ name = opts[:name]
168
+ objects.reject!{|x| x.frozen? || x.associations.include?(name)}
169
+ end
170
+ objects
171
+ end
172
172
  end
173
173
 
174
174
  module DatasetMethods
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sequel
4
4
  module Plugins
5
- # The validates_associated plugin allows you to validate associated
5
+ # The validate_associated plugin allows you to validate associated
6
6
  # objects. It also offers the ability to delay the validation of
7
7
  # associated objects until the current object is validated.
8
8
  # If the associated object is invalid, validation error messages
@@ -54,22 +54,32 @@ module Sequel
54
54
  return if reflection[:validate] == false
55
55
  association = reflection[:name]
56
56
  if (reflection[:type] == :one_to_many || reflection[:type] == :one_to_one) && (key = reflection[:key]).is_a?(Symbol) && !(pk_val = obj.values[key])
57
- # There could be a presence validation on the foreign key in the associated model,
58
- # which will fail if we validate before saving the current object. If there is
59
- # no value for the foreign key, set it to the current primary key value, or a dummy
60
- # value of 0 if we haven't saved the current object.
61
57
  p_key = pk unless pk.is_a?(Array)
62
- obj.values[key] = p_key || 0
63
- key = nil if p_key
58
+ if p_key
59
+ obj.values[key] = p_key
60
+ else
61
+ ignore_key_errors = true
62
+ end
64
63
  end
65
- obj.errors.full_messages.each{|m| errors.add(association, m)} unless obj.valid?
66
- if key && !pk_val
67
- # If we used a dummy value of 0, remove it so it doesn't accidently remain.
68
- obj.values.delete(key)
64
+
65
+ unless obj.valid?
66
+ if ignore_key_errors
67
+ # Ignore errors on the key column in the associated object. This column
68
+ # will be set when saving to a presumably valid value using a column
69
+ # in the current object (which may not be available until after the current
70
+ # object is saved).
71
+ obj.errors.delete(key)
72
+ obj.errors.delete_if{|k,| Array === k && k.include?(key)}
73
+ end
74
+
75
+ obj.errors.full_messages.each do |m|
76
+ errors.add(association, m)
77
+ end
69
78
  end
79
+
80
+ nil
70
81
  end
71
82
  end
72
83
  end
73
84
  end
74
85
  end
75
-
@@ -75,6 +75,10 @@ module Sequel
75
75
  # "#{Array(attribute).join(I18n.t('errors.joiner'))} #{error_msg}"
76
76
  # end
77
77
  # end
78
+ #
79
+ # It is recommended that users of this plugin that use validates_schema_types also use
80
+ # the validation_helpers_generic_type_messages plugin for more useful type validation
81
+ # failure messages.
78
82
  module ValidationHelpers
79
83
  DEFAULT_OPTIONS = {
80
84
  :exact_length=>{:message=>lambda{|exact| "is not #{exact} characters"}},
@@ -83,7 +87,9 @@ module Sequel
83
87
  :integer=>{:message=>lambda{"is not a number"}},
84
88
  :length_range=>{:message=>lambda{|range| "is too short or too long"}},
85
89
  :max_length=>{:message=>lambda{|max| "is longer than #{max} characters"}, :nil_message=>lambda{"is not present"}},
90
+ :max_value=>{:message=>lambda{|max| "is greater than maximum allowed value"}},
86
91
  :min_length=>{:message=>lambda{|min| "is shorter than #{min} characters"}},
92
+ :min_value=>{:message=>lambda{|min| "is less than minimum allowed value"}},
87
93
  :not_null=>{:message=>lambda{"is not present"}},
88
94
  :no_null_byte=>{:message=>lambda{"contains a null byte"}},
89
95
  :numeric=>{:message=>lambda{"is not a number"}},
@@ -141,11 +147,29 @@ module Sequel
141
147
  end
142
148
  end
143
149
 
150
+ # Check that the attribute values are not greater that the given maximum value.
151
+ # Does not perform validation if attribute value is nil.
152
+ # You should only call this if you have checked the attribute value has the expected type.
153
+ def validates_max_value(max, atts, opts=OPTS)
154
+ validatable_attributes_for_type(:max_value, atts, opts) do |a,v,m|
155
+ validation_error_message(m, max) if !v.nil? && v > max
156
+ end
157
+ end
158
+
144
159
  # Check that the attribute values are not shorter than the given min length.
145
160
  def validates_min_length(min, atts, opts=OPTS)
146
161
  validatable_attributes_for_type(:min_length, atts, opts){|a,v,m| validation_error_message(m, min) if v.nil? || v.length < min}
147
162
  end
148
163
 
164
+ # Check that the attribute values are not less that the given minimum value.
165
+ # Does not perform validation if attribute value is nil.
166
+ # You should only call this if you have checked the attribute value has the expected type.
167
+ def validates_min_value(min, atts, opts=OPTS)
168
+ validatable_attributes_for_type(:min_value, atts, opts) do |a,v,m|
169
+ validation_error_message(m, min) if !v.nil? && v < min
170
+ end
171
+ end
172
+
149
173
  # Check attribute value(s) are not NULL/nil.
150
174
  def validates_not_null(atts, opts=OPTS)
151
175
  validatable_attributes_for_type(:not_null, atts, opts){|a,v,m| validation_error_message(m) if v.nil?}
@@ -191,7 +215,7 @@ module Sequel
191
215
  klass = klass.to_s.constantize if klass.is_a?(String) || klass.is_a?(Symbol)
192
216
  validatable_attributes_for_type(:type, atts, opts) do |a,v,m|
193
217
  if klass.is_a?(Array) ? !klass.any?{|kls| v.is_a?(kls)} : !v.is_a?(klass)
194
- validation_error_message(m, klass)
218
+ validates_type_error_message(m, klass)
195
219
  end
196
220
  end
197
221
  end
@@ -271,7 +295,7 @@ module Sequel
271
295
  h = ds.joined_dataset? ? qualified_pk_hash : pk_hash
272
296
  ds = ds.exclude(h)
273
297
  end
274
- errors.add(a, message) unless ds.count == 0
298
+ errors.add(a, message) unless ds.empty?
275
299
  end
276
300
  end
277
301
 
@@ -318,6 +342,9 @@ module Sequel
318
342
  def validation_error_message(message, *args)
319
343
  message.is_a?(Proc) ? message.call(*args) : message
320
344
  end
345
+
346
+ # The validation error message for type validations, for the given class.
347
+ alias validates_type_error_message validation_error_message
321
348
  end
322
349
  end
323
350
  end
@@ -0,0 +1,73 @@
1
+ # frozen-string-literal: true
2
+
3
+ require_relative 'validation_helpers'
4
+
5
+ module Sequel
6
+ module Plugins
7
+ # The validation_helpers_generic_type_messages plugin overrides the default
8
+ # type validation failure messages in the validation_helpers plugin to be
9
+ # more generic and understandable by the average user, instead of always
10
+ # be based on the names of the allowed classes for the type. For example:
11
+ #
12
+ # # :blob type
13
+ # # validation_helpers default: "value is not a valid sequel::sql::blob"
14
+ # # with this plugin: "value is not a blob"
15
+ #
16
+ # # :boolean type
17
+ # # validation_helpers default: "value is not a valid trueclass or falseclass"
18
+ # # with this plugin: "value is not true or false"
19
+ #
20
+ # # :datetime type
21
+ # # validation_helpers default: "value is not a valid time or datetime"
22
+ # # with this plugin: "value is not a valid timestamp"
23
+ #
24
+ # # custom/database-specific types
25
+ # # validation_helpers default: "value is not a valid sequel::class_name"
26
+ # # with this plugin: "value is not the expected type"
27
+ #
28
+ # It is expected that this plugin will become the default behavior of
29
+ # validation_helpers in Sequel 6.
30
+ #
31
+ # To enable this the use of generic type messages for all models, load this
32
+ # plugin into Sequel::Model.
33
+ #
34
+ # Sequel::Model.plugin :validation_helpers_generic_type_messages
35
+ module ValidationHelpersGenericTypeMessages
36
+ OVERRIDE_PROC = ValidationHelpers::DEFAULT_OPTIONS[:type][:message]
37
+ private_constant :OVERRIDE_PROC
38
+
39
+ TYPE_ERROR_STRINGS = {
40
+ String => 'is not a string'.freeze,
41
+ Integer => 'is not an integer'.freeze,
42
+ Date => 'is not a valid date'.freeze,
43
+ [Time, DateTime].freeze => 'is not a valid timestamp'.freeze,
44
+ Sequel::SQLTime => 'is not a valid time'.freeze,
45
+ [TrueClass, FalseClass].freeze => 'is not true or false'.freeze,
46
+ Float => 'is not a number'.freeze,
47
+ BigDecimal => 'is not a number'.freeze,
48
+ Sequel::SQL::Blob => 'is not a blob'.freeze,
49
+ }
50
+ TYPE_ERROR_STRINGS.default = "is not the expected type".freeze
51
+ TYPE_ERROR_STRINGS.freeze
52
+ private_constant :TYPE_ERROR_STRINGS
53
+
54
+ def self.apply(mod)
55
+ mod.plugin :validation_helpers
56
+ end
57
+
58
+ module InstanceMethods
59
+ private
60
+
61
+ # Use a generic error message for type validations.
62
+ def validates_type_error_message(m, klass)
63
+ # SEQUEL6: Make this the default behavior in validation_helpers
64
+ if OVERRIDE_PROC.equal?(m)
65
+ TYPE_ERROR_STRINGS[klass]
66
+ else
67
+ super
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end