sequel 5.97.0 → 5.99.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/lib/sequel/adapters/amalgalite.rb +16 -1
- data/lib/sequel/adapters/postgres.rb +1 -0
- data/lib/sequel/adapters/shared/mssql.rb +1 -1
- data/lib/sequel/adapters/shared/mysql.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +18 -5
- data/lib/sequel/adapters/sqlite.rb +15 -1
- data/lib/sequel/ast_transformer.rb +2 -0
- data/lib/sequel/core.rb +15 -1
- data/lib/sequel/database/misc.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +1 -0
- data/lib/sequel/dataset/actions.rb +65 -8
- data/lib/sequel/dataset/query.rb +1 -1
- data/lib/sequel/dataset/sql.rb +10 -2
- data/lib/sequel/extensions/constraint_validations.rb +3 -3
- data/lib/sequel/extensions/empty_array_consider_nulls.rb +7 -0
- data/lib/sequel/extensions/eval_inspect.rb +2 -0
- data/lib/sequel/extensions/pg_auto_parameterize.rb +2 -2
- data/lib/sequel/extensions/pg_auto_parameterize_duplicate_query_detection.rb +191 -0
- data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +2 -1
- data/lib/sequel/extensions/pg_hstore_ops.rb +10 -5
- data/lib/sequel/extensions/pg_json_ops.rb +12 -5
- data/lib/sequel/extensions/pg_row.rb +2 -2
- data/lib/sequel/extensions/set_literalizer.rb +20 -39
- data/lib/sequel/extensions/split_array_nil.rb +12 -2
- data/lib/sequel/extensions/to_dot.rb +5 -0
- data/lib/sequel/model/associations.rb +6 -6
- data/lib/sequel/model/base.rb +31 -3
- data/lib/sequel/plugins/insert_returning_select.rb +10 -1
- data/lib/sequel/plugins/pg_array_associations.rb +2 -2
- data/lib/sequel/plugins/rcte_tree.rb +5 -5
- data/lib/sequel/plugins/split_values.rb +10 -0
- data/lib/sequel/plugins/static_cache.rb +13 -0
- data/lib/sequel/plugins/subset_static_cache.rb +15 -0
- data/lib/sequel/plugins/table_select.rb +7 -0
- data/lib/sequel/sql.rb +1 -1
- data/lib/sequel/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e5d22a32d8c5f27e15e00203d0c2ce2a2fb3f5352d7b7c25c82389f4947882c
|
|
4
|
+
data.tar.gz: 81909dc070cc8d48def396ae76e953a1f31dc478b45d3a5fa8b69646bc7df094
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c24ae48ece66511a09b2d8a5ad8b5b82e7486a4446bcb66108140b8afe449cd9c9e7645a29ca0bbe0c994d5c060a52f1ab4c41434dc068175751b1d6bd57b5dc
|
|
7
|
+
data.tar.gz: e8acd2e5cb4272bd47aed87e3077018dfc5ba9673856b871c8222eec6b2de1be0843840969e046efc459b52902753e9e0f0e35b95dcdcf31914d98b5f487d78a
|
|
@@ -62,11 +62,26 @@ module Sequel
|
|
|
62
62
|
|
|
63
63
|
# Mimic the file:// uri, by having 2 preceding slashes specify a relative
|
|
64
64
|
# path, and 3 preceding slashes specify an absolute path.
|
|
65
|
+
# Also support no preceding slashes to specify a relative path.
|
|
65
66
|
def self.uri_to_options(uri) # :nodoc:
|
|
66
|
-
|
|
67
|
+
database = if uri.host.nil?
|
|
68
|
+
case uri.path
|
|
69
|
+
when '/'
|
|
70
|
+
nil
|
|
71
|
+
when nil
|
|
72
|
+
uri.opaque
|
|
73
|
+
else
|
|
74
|
+
uri.path
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
"#{uri.host}#{uri.path}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
{ :database => database }
|
|
67
81
|
end
|
|
68
82
|
private_class_method :uri_to_options
|
|
69
83
|
|
|
84
|
+
|
|
70
85
|
# Connect to the database. Since SQLite is a file based database,
|
|
71
86
|
# the only options available are :database (to specify the database
|
|
72
87
|
# name), and :timeout, to specify how long to wait for the database to
|
|
@@ -679,7 +679,7 @@ module Sequel
|
|
|
679
679
|
|
|
680
680
|
# MSSQL uses the CONTAINS keyword for full text search
|
|
681
681
|
def full_text_search(cols, terms, opts = OPTS)
|
|
682
|
-
terms = "\"#{
|
|
682
|
+
terms = "\"#{Sequel.array_or_set_join(terms, '" OR "')}\"" if terms.is_a?(Array) || terms.is_a?(Set)
|
|
683
683
|
where(Sequel.lit("CONTAINS (?, ?)", cols, terms))
|
|
684
684
|
end
|
|
685
685
|
|
|
@@ -797,7 +797,7 @@ module Sequel
|
|
|
797
797
|
|
|
798
798
|
# MySQL specific full text search syntax.
|
|
799
799
|
def full_text_sql(cols, terms, opts = OPTS)
|
|
800
|
-
terms =
|
|
800
|
+
terms = Sequel.array_or_set_join(terms, ' ') if terms.is_a?(Array) || terms.is_a?(Set)
|
|
801
801
|
SQL::PlaceholderLiteralString.new((opts[:boolean] ? MATCH_AGAINST_BOOLEAN : MATCH_AGAINST), [Array(cols), terms])
|
|
802
802
|
end
|
|
803
803
|
|
|
@@ -669,12 +669,26 @@ module Sequel
|
|
|
669
669
|
_set_constraints(' IMMEDIATE', opts)
|
|
670
670
|
end
|
|
671
671
|
|
|
672
|
-
# Use the pg_* system tables to determine indexes on a table
|
|
672
|
+
# Use the pg_* system tables to determine indexes on a table. Options:
|
|
673
|
+
#
|
|
674
|
+
# :include_partial :: Set to true to include partial indexes
|
|
675
|
+
# :invalid :: Set to true or :only to only return invalid indexes.
|
|
676
|
+
# Set to :include to also return both valid and invalid indexes.
|
|
677
|
+
# When not set or other value given, does not return invalid indexes.
|
|
673
678
|
def indexes(table, opts=OPTS)
|
|
674
679
|
m = output_identifier_meth
|
|
675
680
|
cond = {Sequel[:tab][:oid]=>regclass_oid(table, opts)}
|
|
676
681
|
cond[:indpred] = nil unless opts[:include_partial]
|
|
677
682
|
|
|
683
|
+
case opts[:invalid]
|
|
684
|
+
when true, :only
|
|
685
|
+
cond[:indisvalid] = false
|
|
686
|
+
when :include
|
|
687
|
+
# nothing
|
|
688
|
+
else
|
|
689
|
+
cond[:indisvalid] = true
|
|
690
|
+
end
|
|
691
|
+
|
|
678
692
|
indexes = {}
|
|
679
693
|
_indexes_ds.where_each(cond) do |r|
|
|
680
694
|
i = indexes[m.call(r[:name])] ||= {:columns=>[], :unique=>r[:unique], :deferrable=>r[:deferrable]}
|
|
@@ -1049,8 +1063,7 @@ module Sequel
|
|
|
1049
1063
|
where{{
|
|
1050
1064
|
indc[:relkind]=>%w'i I',
|
|
1051
1065
|
ind[:indisprimary]=>false,
|
|
1052
|
-
:indexprs=>nil
|
|
1053
|
-
:indisvalid=>true}}.
|
|
1066
|
+
:indexprs=>nil}}.
|
|
1054
1067
|
order(*order).
|
|
1055
1068
|
select{[indc[:relname].as(:name), ind[:indisunique].as(:unique), att[:attname].as(:column), con[:condeferrable].as(:deferrable)]}
|
|
1056
1069
|
|
|
@@ -1757,7 +1770,7 @@ module Sequel
|
|
|
1757
1770
|
index_type = :gist
|
|
1758
1771
|
end
|
|
1759
1772
|
|
|
1760
|
-
"CREATE #{unique}INDEX#{' CONCURRENTLY' if index[:concurrently]}#{if_not_exists} #{quote_identifier(index_name)} ON #{quote_schema_table(table_name)} #{"USING #{index_type} " if index_type}#{expr}#{" INCLUDE #{literal(Array(index[:include]))}" if index[:include]}#{nulls_distinct}#{" TABLESPACE #{quote_identifier(index[:tablespace])}" if index[:tablespace]}#{filter}"
|
|
1773
|
+
"CREATE #{unique}INDEX#{' CONCURRENTLY' if index[:concurrently]}#{if_not_exists} #{quote_identifier(index_name)} ON#{' ONLY' if index[:only]} #{quote_schema_table(table_name)} #{"USING #{index_type} " if index_type}#{expr}#{" INCLUDE #{literal(Array(index[:include]))}" if index[:include]}#{nulls_distinct}#{" TABLESPACE #{quote_identifier(index[:tablespace])}" if index[:tablespace]}#{filter}"
|
|
1761
1774
|
end
|
|
1762
1775
|
|
|
1763
1776
|
# Setup datastructures shared by all postgres adapters.
|
|
@@ -2160,7 +2173,7 @@ module Sequel
|
|
|
2160
2173
|
end
|
|
2161
2174
|
|
|
2162
2175
|
unless opts[:tsquery]
|
|
2163
|
-
phrase_terms = terms.is_a?(Array)
|
|
2176
|
+
phrase_terms = terms.is_a?(Array) || terms.is_a?(Set) ? Sequel.array_or_set_join(terms, ' | ') : terms
|
|
2164
2177
|
|
|
2165
2178
|
query_func = case to_tsquery = opts[:to_tsquery]
|
|
2166
2179
|
when :phrase, :plain
|
|
@@ -89,8 +89,22 @@ module Sequel
|
|
|
89
89
|
|
|
90
90
|
# Mimic the file:// uri, by having 2 preceding slashes specify a relative
|
|
91
91
|
# path, and 3 preceding slashes specify an absolute path.
|
|
92
|
+
# Also support no preceding slashes to specify a relative path.
|
|
92
93
|
def self.uri_to_options(uri) # :nodoc:
|
|
93
|
-
|
|
94
|
+
database = if uri.host.nil?
|
|
95
|
+
case uri.path
|
|
96
|
+
when '/'
|
|
97
|
+
nil
|
|
98
|
+
when nil
|
|
99
|
+
uri.opaque
|
|
100
|
+
else
|
|
101
|
+
uri.path
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
"#{uri.host}#{uri.path}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
{ :database => database }
|
|
94
108
|
end
|
|
95
109
|
|
|
96
110
|
private_class_method :uri_to_options
|
data/lib/sequel/core.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
|
|
3
|
-
%w'bigdecimal date thread time uri'.each{|f| require f}
|
|
3
|
+
%w'bigdecimal date set thread time uri'.each{|f| require f}
|
|
4
4
|
|
|
5
5
|
# Top level module for Sequel
|
|
6
6
|
#
|
|
@@ -164,6 +164,20 @@ module Sequel
|
|
|
164
164
|
JSON::ParserError
|
|
165
165
|
end
|
|
166
166
|
|
|
167
|
+
if RUBY_VERSION >= '3'
|
|
168
|
+
# Join the array or set.
|
|
169
|
+
def array_or_set_join(obj, arg)
|
|
170
|
+
obj.join(arg)
|
|
171
|
+
end
|
|
172
|
+
# :nocov:
|
|
173
|
+
else
|
|
174
|
+
def array_or_set_join(obj, arg)
|
|
175
|
+
obj = obj.to_a if obj.is_a?(Set)
|
|
176
|
+
obj.join(arg)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
# :nocov:
|
|
180
|
+
|
|
167
181
|
if RUBY_VERSION >= '3.3'
|
|
168
182
|
# Create a new module using the block, and set the temporary name
|
|
169
183
|
# on it using the given a containing module and name.
|
data/lib/sequel/database/misc.rb
CHANGED
|
@@ -299,6 +299,7 @@ module Sequel
|
|
|
299
299
|
# :opclass :: Set an opclass to use for all columns (per-column opclasses require
|
|
300
300
|
# custom SQL).
|
|
301
301
|
# :tablespace :: Specify tablespace for index.
|
|
302
|
+
# :only :: Create index only for given table, not for any child tables (PostgreSQL 11+)
|
|
302
303
|
#
|
|
303
304
|
# Microsoft SQL Server specific options:
|
|
304
305
|
#
|
|
@@ -11,11 +11,12 @@ module Sequel
|
|
|
11
11
|
|
|
12
12
|
# Action methods defined by Sequel that execute code on the database.
|
|
13
13
|
ACTION_METHODS = (<<-METHS).split.map(&:to_sym).freeze
|
|
14
|
-
<< [] all as_hash avg count columns columns! delete each
|
|
14
|
+
<< [] all as_hash as_set avg count columns columns! delete each
|
|
15
15
|
empty? fetch_rows first first! get import insert last
|
|
16
|
-
map max min multi_insert paged_each select_hash select_hash_groups
|
|
17
|
-
single_record single_record!
|
|
18
|
-
|
|
16
|
+
map max min multi_insert paged_each select_hash select_hash_groups
|
|
17
|
+
select_map select_order_map select_set single_record single_record!
|
|
18
|
+
single_value single_value! sum to_hash to_hash_groups
|
|
19
|
+
truncate update where_all where_each where_single_value
|
|
19
20
|
METHS
|
|
20
21
|
|
|
21
22
|
# The clone options to use when retrieving columns for a dataset.
|
|
@@ -51,6 +52,26 @@ module Sequel
|
|
|
51
52
|
_all(block){|a| each{|r| a << r}}
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# Returns sets for column values for each record in the dataset.
|
|
56
|
+
#
|
|
57
|
+
# DB[:table].as_set(:id) # SELECT * FROM table
|
|
58
|
+
# # => Set[1, 2, 3, ...]
|
|
59
|
+
#
|
|
60
|
+
# You can also provide an array of column names, in which case the elements
|
|
61
|
+
# of the returned set are arrays (not sets):
|
|
62
|
+
#
|
|
63
|
+
# DB[:table].as_set([:id, :name]) # SELECT * FROM table
|
|
64
|
+
# # => Set[[1, 'A'], [2, 'B'], [3, 'C'], ...]
|
|
65
|
+
def as_set(column)
|
|
66
|
+
return naked.as_set(column) if row_proc
|
|
67
|
+
|
|
68
|
+
if column.is_a?(Array)
|
|
69
|
+
to_set{|r| r.values_at(*column)}
|
|
70
|
+
else
|
|
71
|
+
to_set{|r| r[column]}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
54
75
|
# Returns the average value for the given column/expression.
|
|
55
76
|
# Uses a virtual row block if no argument is given.
|
|
56
77
|
#
|
|
@@ -727,10 +748,7 @@ module Sequel
|
|
|
727
748
|
end
|
|
728
749
|
|
|
729
750
|
# Selects the column given (either as an argument or as a block), and
|
|
730
|
-
# returns an array of all values of that column in the dataset.
|
|
731
|
-
# give a block argument that returns an array with multiple entries,
|
|
732
|
-
# the contents of the resulting array are undefined. Raises an Error
|
|
733
|
-
# if called with both an argument and a block.
|
|
751
|
+
# returns an array of all values of that column in the dataset.
|
|
734
752
|
#
|
|
735
753
|
# DB[:table].select_map(:id) # SELECT id FROM table
|
|
736
754
|
# # => [3, 5, 8, 1, ...]
|
|
@@ -768,6 +786,34 @@ module Sequel
|
|
|
768
786
|
_select_map(column, true, &block)
|
|
769
787
|
end
|
|
770
788
|
|
|
789
|
+
# Selects the column given (either as an argument or as a block), and
|
|
790
|
+
# returns a set of all values of that column in the dataset.
|
|
791
|
+
#
|
|
792
|
+
# DB[:table].select_set(:id) # SELECT id FROM table
|
|
793
|
+
# # => Set[3, 5, 8, 1, ...]
|
|
794
|
+
#
|
|
795
|
+
# DB[:table].select_set{id * 2} # SELECT (id * 2) FROM table
|
|
796
|
+
# # => Set[6, 10, 16, 2, ...]
|
|
797
|
+
#
|
|
798
|
+
# You can also provide an array of column names, which returns a set
|
|
799
|
+
# with array elements (not set elements):
|
|
800
|
+
#
|
|
801
|
+
# DB[:table].select_map([:id, :name]) # SELECT id, name FROM table
|
|
802
|
+
# # => Set[[1, 'A'], [2, 'B'], [3, 'C'], ...]
|
|
803
|
+
#
|
|
804
|
+
# If you provide an array of expressions, you must be sure that each entry
|
|
805
|
+
# in the array has an alias that Sequel can determine.
|
|
806
|
+
def select_set(column=nil, &block)
|
|
807
|
+
ds = ungraphed.naked
|
|
808
|
+
columns = Array(column)
|
|
809
|
+
virtual_row_columns(columns, block)
|
|
810
|
+
if column.is_a?(Array) || (columns.length > 1)
|
|
811
|
+
ds.select(*columns)._select_set_multiple(hash_key_symbols(columns))
|
|
812
|
+
else
|
|
813
|
+
ds.select(auto_alias_expression(columns.first))._select_set_single
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
771
817
|
# Limits the dataset to one record, and returns the first record in the dataset,
|
|
772
818
|
# or nil if the dataset has no records. Users should probably use +first+ instead of
|
|
773
819
|
# this method. Example:
|
|
@@ -1092,6 +1138,17 @@ module Sequel
|
|
|
1092
1138
|
map{|r| r[k||=r.keys.first]}
|
|
1093
1139
|
end
|
|
1094
1140
|
|
|
1141
|
+
# Return a set of arrays of values given by the symbols in ret_cols.
|
|
1142
|
+
def _select_set_multiple(ret_cols)
|
|
1143
|
+
to_set{|r| r.values_at(*ret_cols)}
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Returns a set of the first value in each row.
|
|
1147
|
+
def _select_set_single
|
|
1148
|
+
k = nil
|
|
1149
|
+
to_set{|r| r[k||=r.keys.first]}
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1095
1152
|
# A dataset for returning single values from the current dataset.
|
|
1096
1153
|
def single_value_ds
|
|
1097
1154
|
clone(:limit=>1).ungraphed.naked
|
data/lib/sequel/dataset/query.rb
CHANGED
|
@@ -1496,7 +1496,7 @@ module Sequel
|
|
|
1496
1496
|
end
|
|
1497
1497
|
when LiteralString
|
|
1498
1498
|
LiteralString.new("(#{expr})")
|
|
1499
|
-
when Numeric, SQL::NumericExpression, SQL::StringExpression, Proc, String
|
|
1499
|
+
when Numeric, SQL::NumericExpression, SQL::StringExpression, Proc, String, Set
|
|
1500
1500
|
raise Error, "Invalid filter expression: #{expr.inspect}"
|
|
1501
1501
|
when TrueClass, FalseClass
|
|
1502
1502
|
if supports_where_true?
|
data/lib/sequel/dataset/sql.rb
CHANGED
|
@@ -85,6 +85,8 @@ module Sequel
|
|
|
85
85
|
literal_date_append(sql, v)
|
|
86
86
|
when Dataset
|
|
87
87
|
literal_dataset_append(sql, v)
|
|
88
|
+
when Set
|
|
89
|
+
literal_set_append(sql, v)
|
|
88
90
|
else
|
|
89
91
|
literal_other_append(sql, v)
|
|
90
92
|
end
|
|
@@ -375,9 +377,9 @@ module Sequel
|
|
|
375
377
|
cols = args[0]
|
|
376
378
|
vals = args[1]
|
|
377
379
|
col_array = true if cols.is_a?(Array)
|
|
378
|
-
if vals.is_a?(Array)
|
|
380
|
+
if vals.is_a?(Array) || vals.is_a?(Set)
|
|
379
381
|
val_array = true
|
|
380
|
-
empty_val_array = vals
|
|
382
|
+
empty_val_array = vals.empty?
|
|
381
383
|
end
|
|
382
384
|
if empty_val_array
|
|
383
385
|
literal_append(sql, empty_array_value(op, cols))
|
|
@@ -1448,6 +1450,12 @@ module Sequel
|
|
|
1448
1450
|
end
|
|
1449
1451
|
end
|
|
1450
1452
|
|
|
1453
|
+
# Append a literalization of the set to SQL string.
|
|
1454
|
+
# Treats as an expression as an SQL value list.
|
|
1455
|
+
def literal_set_append(sql, v)
|
|
1456
|
+
array_sql_append(sql, v)
|
|
1457
|
+
end
|
|
1458
|
+
|
|
1451
1459
|
# SQL fragment for Sequel::SQLTime, containing just the time part
|
|
1452
1460
|
def literal_sqltime(v)
|
|
1453
1461
|
v.strftime(default_time_format)
|
|
@@ -436,7 +436,7 @@ module Sequel
|
|
|
436
436
|
else
|
|
437
437
|
raise Error, "validates includes with a range only supports integers currently, cannot handle: #{arg.inspect}"
|
|
438
438
|
end
|
|
439
|
-
elsif arg.is_a?(Array)
|
|
439
|
+
elsif arg.is_a?(Array) || arg.is_a?(Set)
|
|
440
440
|
if arg.all?{|x| x.is_a?(Integer)}
|
|
441
441
|
validation_type = :includes_int_array
|
|
442
442
|
elsif arg.all?{|x| x.is_a?(String)}
|
|
@@ -444,9 +444,9 @@ module Sequel
|
|
|
444
444
|
else
|
|
445
445
|
raise Error, "validates includes with an array only supports strings and integers currently, cannot handle: #{arg.inspect}"
|
|
446
446
|
end
|
|
447
|
-
arg =
|
|
447
|
+
arg = Sequel.array_or_set_join(arg, ',')
|
|
448
448
|
else
|
|
449
|
-
raise Error, "validates includes only supports arrays and ranges currently, cannot handle: #{arg.inspect}"
|
|
449
|
+
raise Error, "validates includes only supports arrays, sets, and ranges currently, cannot handle: #{arg.inspect}"
|
|
450
450
|
end
|
|
451
451
|
when :like, :ilike
|
|
452
452
|
generator.constraint(constraint, constraint_validation_expression(columns, allow_nil){|c| Sequel.public_send(validation_type, c, arg)})
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
# DB[:test].exclude(name: [])
|
|
10
10
|
# # SELECT * FROM test WHERE (name = name)
|
|
11
11
|
#
|
|
12
|
+
# This works for sets in addition to arrays:
|
|
13
|
+
#
|
|
14
|
+
# DB[:test].where(name: Set[])
|
|
15
|
+
# # SELECT * FROM test WHERE (name != name)
|
|
16
|
+
# DB[:test].exclude(name: Set[])
|
|
17
|
+
# # SELECT * FROM test WHERE (name = name)
|
|
18
|
+
#
|
|
12
19
|
# The default Sequel behavior is to ignore NULLs, as the above
|
|
13
20
|
# query is not generally optimized well by databases.
|
|
14
21
|
#
|
|
@@ -34,6 +34,8 @@ module Sequel
|
|
|
34
34
|
"#{obj.class}.new(#{obj.to_a.inspect})"
|
|
35
35
|
when Array
|
|
36
36
|
"[#{obj.map{|o| eval_inspect(o)}.join(', ')}]"
|
|
37
|
+
when Set
|
|
38
|
+
"Set[#{obj.map{|o| eval_inspect(o)}.join(', ')}]"
|
|
37
39
|
when Hash
|
|
38
40
|
"{#{obj.map{|k, v| "#{eval_inspect(k)} => #{eval_inspect(v)}"}.join(', ')}}"
|
|
39
41
|
when Time
|
|
@@ -408,9 +408,9 @@ module Sequel
|
|
|
408
408
|
end
|
|
409
409
|
end
|
|
410
410
|
|
|
411
|
-
# Whether the given argument is an array of integers or NULL values, recursively.
|
|
411
|
+
# Whether the given argument is an array or set of integers or NULL values, recursively.
|
|
412
412
|
def _integer_array?(v)
|
|
413
|
-
Array === v && v.all?{|x| nil == x || Integer === x}
|
|
413
|
+
(Array === v || Set === v) && v.all?{|x| nil == x || Integer === x}
|
|
414
414
|
end
|
|
415
415
|
|
|
416
416
|
# Create the bound variable string that will be used for the IN (int, ...) to = ANY($)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
#
|
|
3
|
+
# The pg_auto_parameterize_duplicate_query_detection extension builds on the
|
|
4
|
+
# pg_auto_parameterize extension, adding support for detecting duplicate
|
|
5
|
+
# queries inside a block that occur at the same location. This is designed
|
|
6
|
+
# mostly to catch duplicate query issues (e.g. N+1 queries) during testing.
|
|
7
|
+
#
|
|
8
|
+
# To detect duplicate queries inside a block of code, wrap the code with
|
|
9
|
+
# +detect_duplicate_queries+:
|
|
10
|
+
#
|
|
11
|
+
# DB.detect_duplicate_queries{your_code}
|
|
12
|
+
#
|
|
13
|
+
# With this approach, if the test runs code where the same query is executed
|
|
14
|
+
# more than once with the same call stack, a
|
|
15
|
+
# Sequel::Postgres::AutoParameterizeDuplicateQueryDetection::DuplicateQueries
|
|
16
|
+
# exception will be raised.
|
|
17
|
+
#
|
|
18
|
+
# You can pass the +:warn+ option to +detect_duplicate_queries+ to warn
|
|
19
|
+
# instead of raising. Note that if the block passed to +detect_duplicate_queries+
|
|
20
|
+
# raises, this extension will warn, and raise the original exception.
|
|
21
|
+
#
|
|
22
|
+
# For more control, you can pass the +:handler+ option to
|
|
23
|
+
# +detect_duplicate_queries+. If the +:handler+ option is provided, this
|
|
24
|
+
# extension will call the +:handler+ option with the hash of duplicate
|
|
25
|
+
# query information, and will not raise or warn. This can be useful in
|
|
26
|
+
# production environments, to record duplicate queries for later analysis.
|
|
27
|
+
#
|
|
28
|
+
# For accuracy, the entire call stack is always used as part of the hash key
|
|
29
|
+
# to determine whether a query is duplicate. However, you can filter the
|
|
30
|
+
# displayed backtrace by using the +:backtrace_filter+ option.
|
|
31
|
+
#
|
|
32
|
+
# +detect_duplicate_queries+ is concurrency aware, it uses the same approach
|
|
33
|
+
# that Sequel's default connection pools use. So if you are running multiple
|
|
34
|
+
# threads, +detect_duplicate_queries+ will only report duplicate queries for
|
|
35
|
+
# the current thread (or fiber if the fiber_concurrency extension is used).
|
|
36
|
+
#
|
|
37
|
+
# When testing applications, it's probably best to use this to wrap the
|
|
38
|
+
# application being tested. For example, testing with rack-test, if your app
|
|
39
|
+
# is +App+, you would want to wrap it:
|
|
40
|
+
#
|
|
41
|
+
# include Rack::Test::Methods
|
|
42
|
+
#
|
|
43
|
+
# WrappedApp = lambda do |env|
|
|
44
|
+
# DB.detect_duplicate_queries{App.call(env)}
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# def app
|
|
48
|
+
# WrappedApp
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# It is possible to use this to wrap each separate spec using an around hook,
|
|
52
|
+
# but that can result in false positives when using libraries that have
|
|
53
|
+
# implicit retrying (such as Capybara), as you can have the same call stack
|
|
54
|
+
# for multiple requests.
|
|
55
|
+
#
|
|
56
|
+
# Related module: Sequel::Postgres::AutoParameterizeDuplicateQueryDetection
|
|
57
|
+
|
|
58
|
+
module Sequel
|
|
59
|
+
module Postgres
|
|
60
|
+
# Enable detecting duplicate queries inside a block
|
|
61
|
+
module AutoParameterizeDuplicateQueryDetection
|
|
62
|
+
def self.extended(db)
|
|
63
|
+
db.instance_exec do
|
|
64
|
+
@duplicate_query_detection_contexts = {}
|
|
65
|
+
@duplicate_query_detection_mutex = Mutex.new
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Exception class raised when duplicate queries are detected.
|
|
70
|
+
class DuplicateQueries < Sequel::Error
|
|
71
|
+
# A hash of queries that were duplicate. Keys are arrays
|
|
72
|
+
# with 2 entries, the first being the query SQL, and the
|
|
73
|
+
# second being the related call stack (backtrace).
|
|
74
|
+
# The values are the number of query executions.
|
|
75
|
+
attr_reader :queries
|
|
76
|
+
|
|
77
|
+
def initialize(message, queries)
|
|
78
|
+
@queries = queries
|
|
79
|
+
super(message)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Record each query executed so duplicates can be detected,
|
|
84
|
+
# if queries are being recorded.
|
|
85
|
+
def execute(sql, opts=OPTS, &block)
|
|
86
|
+
record, queries = duplicate_query_recorder_state
|
|
87
|
+
|
|
88
|
+
if record
|
|
89
|
+
queries[[sql.is_a?(Symbol) ? sql : sql.to_s, caller].freeze] += 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Ignore (do not record) queries inside given block. This can
|
|
96
|
+
# be useful in situations where you want to run your entire test suite
|
|
97
|
+
# with duplicate query detection, but you have duplicate queries in
|
|
98
|
+
# some parts of your application where it is not trivial to use a
|
|
99
|
+
# different approach. You can mark those specific sections with
|
|
100
|
+
# +ignore_duplicate_queries+, and still get duplicate query detection
|
|
101
|
+
# for the rest of the application.
|
|
102
|
+
def ignore_duplicate_queries(&block)
|
|
103
|
+
if state = duplicate_query_recorder_state
|
|
104
|
+
change_duplicate_query_recorder_state(state, false, &block)
|
|
105
|
+
else
|
|
106
|
+
# If we are not inside a detect_duplicate_queries block, there is
|
|
107
|
+
# no need to do anything, since we are not recording queries.
|
|
108
|
+
yield
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Run the duplicate query detector during the block.
|
|
113
|
+
# Options:
|
|
114
|
+
#
|
|
115
|
+
# :backtrace_filter :: Regexp used to filter the displayed backtrace.
|
|
116
|
+
# :handler :: If present, called with hash of duplicate query information,
|
|
117
|
+
# instead of raising or warning.
|
|
118
|
+
# :warn :: Always warn instead of raising for duplicate queries.
|
|
119
|
+
#
|
|
120
|
+
# Note that if you nest calls to this method, only the top
|
|
121
|
+
# level call will respect the passed options.
|
|
122
|
+
def detect_duplicate_queries(opts=OPTS, &block)
|
|
123
|
+
current = Sequel.current
|
|
124
|
+
if state = duplicate_query_recorder_state(current)
|
|
125
|
+
return change_duplicate_query_recorder_state(state, true, &block)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
@duplicate_query_detection_mutex.synchronize do
|
|
129
|
+
@duplicate_query_detection_contexts[current] = [true, Hash.new(0)]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
yield
|
|
134
|
+
rescue Exception => e
|
|
135
|
+
raise
|
|
136
|
+
ensure
|
|
137
|
+
_, queries = @duplicate_query_detection_mutex.synchronize do
|
|
138
|
+
@duplicate_query_detection_contexts.delete(current)
|
|
139
|
+
end
|
|
140
|
+
queries.delete_if{|_,v| v < 2}
|
|
141
|
+
|
|
142
|
+
unless queries.empty?
|
|
143
|
+
if handler = opts[:handler]
|
|
144
|
+
handler.call(queries)
|
|
145
|
+
else
|
|
146
|
+
backtrace_filter = opts[:backtrace_filter]
|
|
147
|
+
backtrace_filter_note = backtrace_filter ? " (filtered)" : ""
|
|
148
|
+
query_info = queries.map do |k,v|
|
|
149
|
+
backtrace = k[1]
|
|
150
|
+
backtrace = backtrace.grep(backtrace_filter) if backtrace_filter
|
|
151
|
+
"times:#{v}\nsql:#{k[0]}\nbacktrace#{backtrace_filter_note}:\n#{backtrace.join("\n")}\n"
|
|
152
|
+
end
|
|
153
|
+
message = "duplicate queries detected:\n\n#{query_info.join("\n")}"
|
|
154
|
+
|
|
155
|
+
if e || opts[:warn]
|
|
156
|
+
warn(message)
|
|
157
|
+
else
|
|
158
|
+
raise DuplicateQueries.new(message, queries)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
# Get the query record state for the given context.
|
|
168
|
+
def duplicate_query_recorder_state(current=Sequel.current)
|
|
169
|
+
@duplicate_query_detection_mutex.synchronize{@duplicate_query_detection_contexts[current]}
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Temporarily change whether to record queries for the block, resetting the
|
|
173
|
+
# previous setting after the block exits.
|
|
174
|
+
def change_duplicate_query_recorder_state(state, setting)
|
|
175
|
+
prev = state[0]
|
|
176
|
+
state[0] = setting
|
|
177
|
+
|
|
178
|
+
begin
|
|
179
|
+
yield
|
|
180
|
+
ensure
|
|
181
|
+
state[0] = prev
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
Database.register_extension(:pg_auto_parameterize_duplicate_query_detection) do |db|
|
|
188
|
+
db.extension(:pg_auto_parameterize)
|
|
189
|
+
db.extend(Postgres::AutoParameterizeDuplicateQueryDetection)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -120,7 +120,7 @@ module Sequel
|
|
|
120
120
|
# The bound variable type string to use for the bound variable array.
|
|
121
121
|
# Returns nil if a bound variable should not be used for the array.
|
|
122
122
|
def _bound_variable_type_for_array(r)
|
|
123
|
-
return unless Array === r && r.size >= pg_auto_parameterize_min_array_size
|
|
123
|
+
return unless (Array === r || Set === r) && r.size >= pg_auto_parameterize_min_array_size
|
|
124
124
|
classes = r.map(&:class)
|
|
125
125
|
classes.uniq!
|
|
126
126
|
classes.delete(NilClass)
|
|
@@ -165,6 +165,7 @@ module Sequel
|
|
|
165
165
|
|
|
166
166
|
# Convert RHS of IN/NOT IN operator to PGArray with given type.
|
|
167
167
|
def _convert_array_to_pg_array_with_type(r, type)
|
|
168
|
+
r = r.to_a if Set === r
|
|
168
169
|
Sequel.pg_array(r, type)
|
|
169
170
|
end
|
|
170
171
|
end
|
|
@@ -294,13 +294,18 @@ module Sequel
|
|
|
294
294
|
SQL::Function.new(name, self, *args)
|
|
295
295
|
end
|
|
296
296
|
|
|
297
|
-
# Wrap argument in a PGArray if it is an array
|
|
297
|
+
# Wrap argument in a PGArray if it is an array or a set
|
|
298
298
|
def wrap_input_array(obj)
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
299
|
+
if Sequel.respond_to?(:pg_array)
|
|
300
|
+
case obj
|
|
301
|
+
when Array
|
|
302
|
+
return Sequel.pg_array(obj)
|
|
303
|
+
when Set
|
|
304
|
+
return Sequel.pg_array(obj.to_a)
|
|
305
|
+
end
|
|
303
306
|
end
|
|
307
|
+
|
|
308
|
+
obj
|
|
304
309
|
end
|
|
305
310
|
|
|
306
311
|
# Wrap argument in an Hstore if it is a hash
|
|
@@ -841,13 +841,20 @@ module Sequel
|
|
|
841
841
|
Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(str, [value, other]))
|
|
842
842
|
end
|
|
843
843
|
|
|
844
|
-
# Wrap argument in a PGArray if it is an array
|
|
844
|
+
# Wrap argument in a PGArray if it is an array or a set
|
|
845
845
|
def wrap_input_array(obj)
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
obj
|
|
846
|
+
# :nocov:
|
|
847
|
+
if Sequel.respond_to?(:pg_array)
|
|
848
|
+
# :nocov:
|
|
849
|
+
case obj
|
|
850
|
+
when Array
|
|
851
|
+
return Sequel.pg_array(obj)
|
|
852
|
+
when Set
|
|
853
|
+
return Sequel.pg_array(obj.to_a)
|
|
854
|
+
end
|
|
850
855
|
end
|
|
856
|
+
|
|
857
|
+
obj
|
|
851
858
|
end
|
|
852
859
|
|
|
853
860
|
# Wrap argument in a JSONBArray or JSONBHash if it is an array or hash.
|
|
@@ -319,8 +319,8 @@ module Sequel
|
|
|
319
319
|
end
|
|
320
320
|
|
|
321
321
|
# Typecast the given object to the appropriate type using the
|
|
322
|
-
# typecaster. Note that this does
|
|
323
|
-
# of the composite type, since those
|
|
322
|
+
# typecaster. Note that this does no conversion for the members
|
|
323
|
+
# of the composite type, since those conversions expect strings and
|
|
324
324
|
# strings may not be provided.
|
|
325
325
|
def typecast(obj)
|
|
326
326
|
case obj
|
|
@@ -1,56 +1,37 @@
|
|
|
1
1
|
# frozen-string-literal: true
|
|
2
2
|
#
|
|
3
|
-
# The set_literalizer extension
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# DB.extension :set_literalizer
|
|
3
|
+
# The set_literalizer extension should no longer be used, as Sequel
|
|
4
|
+
# now supports Set values by default. For backwards compatibility
|
|
5
|
+
# the set_literalizer extension will treat a set that contains only
|
|
6
|
+
# 2 element arrays as a condition specifier (matching the behavior
|
|
7
|
+
# for arrays where all elements are 2 element arrays). This is not
|
|
8
|
+
# compatible with Sequel's current default behavior. If you are
|
|
9
|
+
# relying on this behavior, it is recommended you convert the set
|
|
10
|
+
# to an array.
|
|
12
11
|
#
|
|
13
12
|
# Related module: Sequel::Dataset::SetLiteralizer
|
|
14
13
|
|
|
15
|
-
require 'set'
|
|
16
|
-
|
|
17
14
|
module Sequel
|
|
15
|
+
# SEQUEL6: Remove
|
|
16
|
+
Sequel::Deprecation.deprecate("The set_literalizer extension", "Sequel now supports set literalization by default")
|
|
17
|
+
|
|
18
18
|
class Dataset
|
|
19
19
|
module SetLiteralizer
|
|
20
|
-
# Try to generate the same SQL for Set instances used in datasets
|
|
21
|
-
# that would be used for equivalent Array instances.
|
|
22
|
-
def complex_expression_sql_append(sql, op, args)
|
|
23
|
-
# Array instances are treated specially by
|
|
24
|
-
# Sequel::SQL::BooleanExpression.from_value_pairs. That cannot
|
|
25
|
-
# be modified by a dataset extension, so this tries to convert
|
|
26
|
-
# the complex expression values generated by default to what would
|
|
27
|
-
# be the complex expression values used for the equivalent array.
|
|
28
|
-
case op
|
|
29
|
-
when :'=', :'!='
|
|
30
|
-
if (set = args[1]).is_a?(Set)
|
|
31
|
-
op = op == :'=' ? :IN : :'NOT IN'
|
|
32
|
-
col = args[0]
|
|
33
|
-
array = set.to_a
|
|
34
|
-
if Sequel.condition_specifier?(array) && col.is_a?(Array)
|
|
35
|
-
array = Sequel.value_list(array)
|
|
36
|
-
end
|
|
37
|
-
args = [col, array]
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
super
|
|
42
|
-
end
|
|
43
|
-
|
|
44
20
|
private
|
|
45
21
|
|
|
46
|
-
#
|
|
47
|
-
def
|
|
48
|
-
if Set
|
|
49
|
-
|
|
22
|
+
# Allow using sets as condition specifiers.
|
|
23
|
+
def filter_expr(expr = nil, &block)
|
|
24
|
+
if expr.is_a?(Set)
|
|
25
|
+
expr
|
|
50
26
|
else
|
|
51
27
|
super
|
|
52
28
|
end
|
|
53
29
|
end
|
|
30
|
+
|
|
31
|
+
# Literalize Set instances by converting the set to array.
|
|
32
|
+
def literal_set_append(sql, v)
|
|
33
|
+
literal_append(sql, v.to_a)
|
|
34
|
+
end
|
|
54
35
|
end
|
|
55
36
|
|
|
56
37
|
register_extension(:set_literalizer, SetLiteralizer)
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
# # with split_array_nils extension:
|
|
26
26
|
# # SELECT * FROM table WHERE ((column NOT IN (1)) AND (column IS NOT NULL)))
|
|
27
27
|
#
|
|
28
|
+
# In addition to arrays, this splitting is also done for sets:
|
|
29
|
+
#
|
|
30
|
+
# ds = DB[:table].where(column: Set[1, nil])
|
|
31
|
+
# # SELECT * FROM table WHERE ((column IN (1)) OR (column IS NULL)))
|
|
32
|
+
#
|
|
28
33
|
# To use this extension with a single dataset:
|
|
29
34
|
#
|
|
30
35
|
# ds = ds.extension(:split_array_nil)
|
|
@@ -47,9 +52,14 @@ module Sequel
|
|
|
47
52
|
case op
|
|
48
53
|
when :IN, :"NOT IN"
|
|
49
54
|
vals = args[1]
|
|
50
|
-
if vals.is_a?(Array) && vals.any?(&:nil?)
|
|
55
|
+
if (vals.is_a?(Array) || vals.is_a?(Set)) && vals.any?(&:nil?)
|
|
51
56
|
cols = args[0]
|
|
52
|
-
|
|
57
|
+
if vals.is_a?(Set)
|
|
58
|
+
vals = vals.dup
|
|
59
|
+
vals.delete(nil)
|
|
60
|
+
else
|
|
61
|
+
vals = vals.compact
|
|
62
|
+
end
|
|
53
63
|
c = Sequel::SQL::BooleanExpression
|
|
54
64
|
if op == :IN
|
|
55
65
|
literal_append(sql, c.new(:OR, c.new(:IN, cols, vals), c.new(:IS, cols, nil)))
|
|
@@ -302,7 +302,7 @@ module Sequel
|
|
|
302
302
|
|
|
303
303
|
if strategy == :window_function
|
|
304
304
|
delete_rn = ds.row_number_column
|
|
305
|
-
objects.each{|obj| obj.
|
|
305
|
+
objects.each{|obj| obj.remove_key!(delete_rn)}
|
|
306
306
|
end
|
|
307
307
|
elsif strategy == :union
|
|
308
308
|
objects = []
|
|
@@ -1291,11 +1291,11 @@ module Sequel
|
|
|
1291
1291
|
name = self[:name]
|
|
1292
1292
|
|
|
1293
1293
|
self[:model].eager_load_results(self, eo) do |assoc_record|
|
|
1294
|
-
assoc_record.
|
|
1294
|
+
assoc_record.remove_key!(delete_rn) if delete_rn
|
|
1295
1295
|
hash_key = if uses_lcks
|
|
1296
|
-
left_key_alias.map{|k| assoc_record.
|
|
1296
|
+
left_key_alias.map{|k| assoc_record.remove_key!(k)}
|
|
1297
1297
|
else
|
|
1298
|
-
assoc_record.
|
|
1298
|
+
assoc_record.remove_key!(left_key_alias)
|
|
1299
1299
|
end
|
|
1300
1300
|
|
|
1301
1301
|
objects = h[hash_key]
|
|
@@ -2387,7 +2387,7 @@ module Sequel
|
|
|
2387
2387
|
delete_rn = opts.delete_row_number_column
|
|
2388
2388
|
|
|
2389
2389
|
eager_load_results(opts, eo) do |assoc_record|
|
|
2390
|
-
assoc_record.
|
|
2390
|
+
assoc_record.remove_key!(delete_rn) if delete_rn
|
|
2391
2391
|
hash_key = uses_cks ? km.map{|k| assoc_record.get_column_value(k)} : assoc_record.get_column_value(km)
|
|
2392
2392
|
objects = h[hash_key]
|
|
2393
2393
|
if assign_singular
|
|
@@ -3544,7 +3544,7 @@ module Sequel
|
|
|
3544
3544
|
else
|
|
3545
3545
|
vals = Array(obj).reject{|o| !meths.all?{|m| o.get_column_value(m)}}
|
|
3546
3546
|
return SQL::Constants::FALSE if vals.empty?
|
|
3547
|
-
if obj.is_a?(Array)
|
|
3547
|
+
if obj.is_a?(Array) || obj.is_a?(Set)
|
|
3548
3548
|
if keys.length == 1
|
|
3549
3549
|
meth = meths.first
|
|
3550
3550
|
{keys.first=>vals.map{|o| o.get_column_value(meth)}}
|
data/lib/sequel/model/base.rb
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Sequel
|
|
4
4
|
class Model
|
|
5
|
+
# SEQUEL6: Remove Enumerable here, and send all Enumerable methods to dataset
|
|
6
|
+
# by default, with a plugin for the current behavior.
|
|
5
7
|
extend Enumerable
|
|
6
8
|
extend Inflections
|
|
7
9
|
|
|
8
10
|
# Class methods for Sequel::Model that implement basic model functionality.
|
|
9
11
|
#
|
|
10
12
|
# * All of the following methods have class methods created that send the method
|
|
11
|
-
# to the model's dataset: all, as_hash, avg, count, cross_join, distinct, each,
|
|
13
|
+
# to the model's dataset: all, any?, as_hash, as_set, avg, count, cross_join, distinct, each,
|
|
12
14
|
# each_server, empty?, except, exclude, exclude_having, fetch_rows,
|
|
13
15
|
# filter, first, first!, for_update, from, from_self, full_join, full_outer_join,
|
|
14
16
|
# get, graph, grep, group, group_and_count, group_append, group_by, having, import,
|
|
@@ -17,7 +19,7 @@ module Sequel
|
|
|
17
19
|
# natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by,
|
|
18
20
|
# order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join,
|
|
19
21
|
# right_outer_join, select, select_all, select_append, select_group, select_hash,
|
|
20
|
-
# select_hash_groups, select_map, select_more, select_order_map, select_prepend, server,
|
|
22
|
+
# select_hash_groups, select_map, select_more, select_order_map, select_prepend, select_set, server,
|
|
21
23
|
# single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups,
|
|
22
24
|
# truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all,
|
|
23
25
|
# where_each, where_single_value, with, with_recursive, with_sql
|
|
@@ -695,7 +697,7 @@ module Sequel
|
|
|
695
697
|
end
|
|
696
698
|
|
|
697
699
|
# Add model methods that call dataset methods
|
|
698
|
-
Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases])
|
|
700
|
+
Plugins.def_dataset_methods(self, (Dataset::ACTION_METHODS + Dataset::QUERY_METHODS + [:any?, :each_server]) - [:<<, :or, :[], :columns, :columns!, :delete, :update, :set_graph_aliases, :add_graph_aliases])
|
|
699
701
|
|
|
700
702
|
private
|
|
701
703
|
|
|
@@ -1499,6 +1501,20 @@ END
|
|
|
1499
1501
|
refresh
|
|
1500
1502
|
end
|
|
1501
1503
|
|
|
1504
|
+
# Remove a key from the instances values, and return the value
|
|
1505
|
+
# of the key.
|
|
1506
|
+
#
|
|
1507
|
+
# a = Album[1]
|
|
1508
|
+
# a.values
|
|
1509
|
+
# # => {id: 1, artist_id: 2}
|
|
1510
|
+
# a.remove_key!(:artist_id)
|
|
1511
|
+
# # => 2
|
|
1512
|
+
# a.values
|
|
1513
|
+
# # => {id: 1}
|
|
1514
|
+
def remove_key!(key)
|
|
1515
|
+
@values.delete(key)
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1502
1518
|
# Creates or updates the record, after making sure the record
|
|
1503
1519
|
# is valid and before hooks execute successfully. Fails if:
|
|
1504
1520
|
#
|
|
@@ -2328,5 +2344,17 @@ END
|
|
|
2328
2344
|
# :nocov:
|
|
2329
2345
|
singleton_class.send(:undef_method, :initialize_clone, :initialize_dup)
|
|
2330
2346
|
end
|
|
2347
|
+
|
|
2348
|
+
# :nocov:
|
|
2349
|
+
if defined?(Sequel::Postgres::SEQUEL_PG_VERSION_INTEGER) && Sequel::Postgres::SEQUEL_PG_VERSION_INTEGER >= 11800
|
|
2350
|
+
# Automatically optimize model loading when sequel/core was loaded,
|
|
2351
|
+
# then sequel/adapters/postgres (with sequel_pg), then sequel/model
|
|
2352
|
+
begin
|
|
2353
|
+
require "sequel_pg/model"
|
|
2354
|
+
rescue LoadError
|
|
2355
|
+
# nothing
|
|
2356
|
+
end
|
|
2357
|
+
end
|
|
2358
|
+
# :nocov:
|
|
2331
2359
|
end
|
|
2332
2360
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Sequel
|
|
4
4
|
module Plugins
|
|
5
|
-
# If the model's dataset selects explicit columns and the
|
|
5
|
+
# If the model's dataset selects explicit columns (or table.*) and the
|
|
6
6
|
# database supports it, the insert_returning_select plugin will
|
|
7
7
|
# automatically set the RETURNING clause on the dataset used to
|
|
8
8
|
# insert rows to the columns selected, which allows the default model
|
|
@@ -45,6 +45,9 @@ module Sequel
|
|
|
45
45
|
ret
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
RETURN_ALL = [Sequel::Dataset::WILDCARD].freeze
|
|
49
|
+
private_constant :RETURN_ALL
|
|
50
|
+
|
|
48
51
|
# Determine the columns to use for the returning clause, or return nil
|
|
49
52
|
# if they can't be determined and a returning clause should not be
|
|
50
53
|
# added automatically.
|
|
@@ -52,6 +55,12 @@ module Sequel
|
|
|
52
55
|
return unless ds.supports_returning?(:insert)
|
|
53
56
|
return unless values = ds.opts[:select]
|
|
54
57
|
|
|
58
|
+
# SELECT table.* -> RETURNING *
|
|
59
|
+
if values.length == 1 && values[0].is_a?(Sequel::SQL::ColumnAll)
|
|
60
|
+
return RETURN_ALL
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# SELECT column1, table.column2, ... -> RETURNING column1, column2, ...
|
|
55
64
|
values = values.map{|v| ds.unqualified_column_for(v)}
|
|
56
65
|
if values.all?
|
|
57
66
|
values
|
|
@@ -541,7 +541,7 @@ module Sequel
|
|
|
541
541
|
if (assoc_pks = obj.get_column_value(key)) && !assoc_pks.empty?
|
|
542
542
|
Sequel[pk=>assoc_pks.to_a]
|
|
543
543
|
end
|
|
544
|
-
when Array
|
|
544
|
+
when Array, Set
|
|
545
545
|
if (assoc_pks = obj.map{|o| o.get_column_value(key)}.flatten.compact.uniq) && !assoc_pks.empty?
|
|
546
546
|
Sequel[pk=>assoc_pks]
|
|
547
547
|
end
|
|
@@ -563,7 +563,7 @@ module Sequel
|
|
|
563
563
|
if pkv = obj.get_column_value(ref.primary_key_method)
|
|
564
564
|
Sequel.pg_array_op(key).contains(Sequel.pg_array([pkv], ref.array_type))
|
|
565
565
|
end
|
|
566
|
-
when Array
|
|
566
|
+
when Array, Set
|
|
567
567
|
if (pkvs = obj.map{|o| o.get_column_value(ref.primary_key_method)}.compact) && !pkvs.empty?
|
|
568
568
|
Sequel.pg_array(key).overlaps(Sequel.pg_array(pkvs, ref.array_type))
|
|
569
569
|
end
|
|
@@ -111,7 +111,7 @@ module Sequel
|
|
|
111
111
|
ancestor_base_case_columns = prkey_array.zip(key_aliases).map{|k, ka_| SQL::AliasedExpression.new(k, ka_)} + c_all
|
|
112
112
|
descendant_base_case_columns = key_array.zip(key_aliases).map{|k, ka_| SQL::AliasedExpression.new(k, ka_)} + c_all
|
|
113
113
|
recursive_case_columns = prkey_array.zip(key_aliases).map{|k, ka_| SQL::QualifiedIdentifier.new(t, ka_)} + c_all
|
|
114
|
-
extract_key_alias = lambda{|m| key_aliases.map{|ka_| bd_conv[m.
|
|
114
|
+
extract_key_alias = lambda{|m| key_aliases.map{|ka_| bd_conv[m.remove_key!(ka_)]}}
|
|
115
115
|
else
|
|
116
116
|
key_present = key_conv = lambda{|m| m[key]}
|
|
117
117
|
prkey_conv = lambda{|m| m[prkey]}
|
|
@@ -119,7 +119,7 @@ module Sequel
|
|
|
119
119
|
ancestor_base_case_columns = [SQL::AliasedExpression.new(prkey, ka)] + c_all
|
|
120
120
|
descendant_base_case_columns = [SQL::AliasedExpression.new(key, ka)] + c_all
|
|
121
121
|
recursive_case_columns = [SQL::QualifiedIdentifier.new(t, ka)] + c_all
|
|
122
|
-
extract_key_alias = lambda{|m| bd_conv[m.
|
|
122
|
+
extract_key_alias = lambda{|m| bd_conv[m.remove_key!(ka)]}
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
parent = opts.merge(opts.fetch(:parent, OPTS)).fetch(:name, :parent)
|
|
@@ -200,7 +200,7 @@ module Sequel
|
|
|
200
200
|
model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil)) do |obj|
|
|
201
201
|
opk = prkey_conv[obj]
|
|
202
202
|
if idm_obj = parent_map[opk]
|
|
203
|
-
key_aliases.each{|ka_| idm_obj
|
|
203
|
+
key_aliases.each{|ka_| idm_obj[ka_] = obj[ka_]}
|
|
204
204
|
obj = idm_obj
|
|
205
205
|
else
|
|
206
206
|
obj.associations[parent] = nil
|
|
@@ -307,12 +307,12 @@ module Sequel
|
|
|
307
307
|
ds = ds.select_append(ka) unless ds.opts[:select] == nil
|
|
308
308
|
model.eager_load_results(r, eo.merge(:loader=>false, :initialize_rows=>false, :dataset=>ds, :id_map=>nil, :associations=>OPTS)) do |obj|
|
|
309
309
|
if level
|
|
310
|
-
no_cache = no_cache_level == obj.
|
|
310
|
+
no_cache = no_cache_level == obj.remove_key!(la)
|
|
311
311
|
end
|
|
312
312
|
|
|
313
313
|
opk = prkey_conv[obj]
|
|
314
314
|
if idm_obj = parent_map[opk]
|
|
315
|
-
key_aliases.each{|ka_| idm_obj
|
|
315
|
+
key_aliases.each{|ka_| idm_obj[ka_] = obj[ka_]}
|
|
316
316
|
obj = idm_obj
|
|
317
317
|
else
|
|
318
318
|
obj.associations[childrena] = [] unless no_cache
|
|
@@ -54,6 +54,16 @@ module Sequel
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Remove the key from noncolumn values if it is present there. If it is not
|
|
58
|
+
# present there, then use the default behavior of removing it from values.
|
|
59
|
+
def remove_key!(key)
|
|
60
|
+
if @noncolumn_values && @noncolumn_values.key?(key)
|
|
61
|
+
@noncolumn_values.delete(key)
|
|
62
|
+
else
|
|
63
|
+
super
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
57
67
|
# Check all entries in the values hash. If any of the keys are not columns,
|
|
58
68
|
# move the entry into the noncolumn_values hash.
|
|
59
69
|
def split_noncolumn_values
|
|
@@ -176,6 +176,19 @@ module Sequel
|
|
|
176
176
|
h
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
+
# Use the cache instead of a query to get the results.
|
|
180
|
+
def as_set(column)
|
|
181
|
+
set = Set.new
|
|
182
|
+
|
|
183
|
+
if column.is_a?(Array)
|
|
184
|
+
@all.each{|r| set.add(r.values.values_at(*column))}
|
|
185
|
+
else
|
|
186
|
+
@all.each{|r| set.add(r[column])}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
set
|
|
190
|
+
end
|
|
191
|
+
|
|
179
192
|
# Alias of as_hash for backwards compatibility.
|
|
180
193
|
def to_hash(*a)
|
|
181
194
|
as_hash(*a)
|
|
@@ -226,6 +226,21 @@ module Sequel
|
|
|
226
226
|
h
|
|
227
227
|
end
|
|
228
228
|
|
|
229
|
+
# Use the cache instead of a query to get the results.
|
|
230
|
+
def as_set(column)
|
|
231
|
+
return super unless all = @cache[:subset_static_cache_all]
|
|
232
|
+
|
|
233
|
+
set = Set.new
|
|
234
|
+
|
|
235
|
+
if column.is_a?(Array)
|
|
236
|
+
all.each{|r| set.add(r.values.values_at(*column))}
|
|
237
|
+
else
|
|
238
|
+
all.each{|r| set.add(r[column])}
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
set
|
|
242
|
+
end
|
|
243
|
+
|
|
229
244
|
# Alias of as_hash for backwards compatibility.
|
|
230
245
|
def to_hash(*a)
|
|
231
246
|
as_hash(*a)
|
|
@@ -9,6 +9,13 @@ module Sequel
|
|
|
9
9
|
# in the result sets (and possibly overwrite columns in the
|
|
10
10
|
# current model with the same name).
|
|
11
11
|
#
|
|
12
|
+
# Note that by default on databases that supporting RETURNING,
|
|
13
|
+
# using this plugin will cause instance creations
|
|
14
|
+
# to use two queries (insert and refresh) instead of a single
|
|
15
|
+
# query using RETURNING. You can use the insert_returning_select
|
|
16
|
+
# plugin to automatically use RETURNING for instance creations
|
|
17
|
+
# for models using this plugin.
|
|
18
|
+
#
|
|
12
19
|
# Usage:
|
|
13
20
|
#
|
|
14
21
|
# # Make all model subclasses select table.*
|
data/lib/sequel/sql.rb
CHANGED
data/lib/sequel/version.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Sequel
|
|
|
6
6
|
|
|
7
7
|
# The minor version of Sequel. Bumped for every non-patch level
|
|
8
8
|
# release, generally around once a month.
|
|
9
|
-
MINOR =
|
|
9
|
+
MINOR = 99
|
|
10
10
|
|
|
11
11
|
# The tiny version of Sequel. Usually 0, only bumped for bugfix
|
|
12
12
|
# releases that fix regressions from previous versions.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sequel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.99.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Evans
|
|
@@ -250,6 +250,7 @@ files:
|
|
|
250
250
|
- lib/sequel/extensions/pg_array.rb
|
|
251
251
|
- lib/sequel/extensions/pg_array_ops.rb
|
|
252
252
|
- lib/sequel/extensions/pg_auto_parameterize.rb
|
|
253
|
+
- lib/sequel/extensions/pg_auto_parameterize_duplicate_query_detection.rb
|
|
253
254
|
- lib/sequel/extensions/pg_auto_parameterize_in_array.rb
|
|
254
255
|
- lib/sequel/extensions/pg_enum.rb
|
|
255
256
|
- lib/sequel/extensions/pg_extended_date_support.rb
|