sequel 5.45.0 → 5.77.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +434 -0
- data/MIT-LICENSE +1 -1
- data/README.rdoc +59 -27
- data/bin/sequel +11 -3
- data/doc/advanced_associations.rdoc +16 -14
- data/doc/association_basics.rdoc +119 -24
- data/doc/cheat_sheet.rdoc +11 -3
- data/doc/mass_assignment.rdoc +1 -1
- data/doc/migration.rdoc +27 -6
- data/doc/model_hooks.rdoc +1 -1
- data/doc/object_model.rdoc +8 -8
- data/doc/opening_databases.rdoc +28 -12
- data/doc/postgresql.rdoc +16 -8
- data/doc/querying.rdoc +5 -3
- data/doc/release_notes/5.46.0.txt +87 -0
- data/doc/release_notes/5.47.0.txt +59 -0
- data/doc/release_notes/5.48.0.txt +14 -0
- data/doc/release_notes/5.49.0.txt +59 -0
- data/doc/release_notes/5.50.0.txt +78 -0
- data/doc/release_notes/5.51.0.txt +47 -0
- data/doc/release_notes/5.52.0.txt +87 -0
- data/doc/release_notes/5.53.0.txt +23 -0
- data/doc/release_notes/5.54.0.txt +27 -0
- data/doc/release_notes/5.55.0.txt +21 -0
- data/doc/release_notes/5.56.0.txt +51 -0
- data/doc/release_notes/5.57.0.txt +23 -0
- data/doc/release_notes/5.58.0.txt +31 -0
- data/doc/release_notes/5.59.0.txt +73 -0
- data/doc/release_notes/5.60.0.txt +22 -0
- data/doc/release_notes/5.61.0.txt +43 -0
- data/doc/release_notes/5.62.0.txt +132 -0
- data/doc/release_notes/5.63.0.txt +33 -0
- data/doc/release_notes/5.64.0.txt +50 -0
- data/doc/release_notes/5.65.0.txt +21 -0
- data/doc/release_notes/5.66.0.txt +24 -0
- data/doc/release_notes/5.67.0.txt +32 -0
- data/doc/release_notes/5.68.0.txt +61 -0
- data/doc/release_notes/5.69.0.txt +26 -0
- data/doc/release_notes/5.70.0.txt +35 -0
- data/doc/release_notes/5.71.0.txt +21 -0
- data/doc/release_notes/5.72.0.txt +33 -0
- data/doc/release_notes/5.73.0.txt +66 -0
- data/doc/release_notes/5.74.0.txt +45 -0
- data/doc/release_notes/5.75.0.txt +35 -0
- data/doc/release_notes/5.76.0.txt +86 -0
- data/doc/release_notes/5.77.0.txt +63 -0
- data/doc/schema_modification.rdoc +1 -1
- data/doc/security.rdoc +9 -9
- data/doc/sharding.rdoc +3 -1
- data/doc/sql.rdoc +27 -15
- data/doc/testing.rdoc +23 -13
- data/doc/transactions.rdoc +6 -6
- data/doc/virtual_rows.rdoc +1 -1
- data/lib/sequel/adapters/ado/access.rb +1 -1
- data/lib/sequel/adapters/ado.rb +1 -1
- data/lib/sequel/adapters/amalgalite.rb +3 -5
- data/lib/sequel/adapters/ibmdb.rb +3 -3
- data/lib/sequel/adapters/jdbc/derby.rb +8 -0
- data/lib/sequel/adapters/jdbc/h2.rb +63 -10
- data/lib/sequel/adapters/jdbc/hsqldb.rb +8 -0
- data/lib/sequel/adapters/jdbc/postgresql.rb +7 -4
- data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
- data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
- data/lib/sequel/adapters/jdbc.rb +24 -22
- data/lib/sequel/adapters/mysql.rb +92 -67
- data/lib/sequel/adapters/mysql2.rb +56 -51
- data/lib/sequel/adapters/odbc/mssql.rb +1 -1
- data/lib/sequel/adapters/odbc.rb +1 -1
- data/lib/sequel/adapters/oracle.rb +4 -3
- data/lib/sequel/adapters/postgres.rb +89 -45
- data/lib/sequel/adapters/shared/access.rb +11 -1
- data/lib/sequel/adapters/shared/db2.rb +42 -0
- data/lib/sequel/adapters/shared/mssql.rb +91 -10
- data/lib/sequel/adapters/shared/mysql.rb +78 -3
- data/lib/sequel/adapters/shared/oracle.rb +86 -7
- data/lib/sequel/adapters/shared/postgres.rb +576 -171
- data/lib/sequel/adapters/shared/sqlanywhere.rb +21 -5
- data/lib/sequel/adapters/shared/sqlite.rb +92 -8
- data/lib/sequel/adapters/sqlanywhere.rb +1 -1
- data/lib/sequel/adapters/sqlite.rb +99 -18
- data/lib/sequel/adapters/tinytds.rb +1 -1
- data/lib/sequel/adapters/trilogy.rb +117 -0
- data/lib/sequel/adapters/utils/columns_limit_1.rb +22 -0
- data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
- data/lib/sequel/ast_transformer.rb +6 -0
- data/lib/sequel/connection_pool/sharded_single.rb +5 -7
- data/lib/sequel/connection_pool/sharded_threaded.rb +16 -11
- data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
- data/lib/sequel/connection_pool/single.rb +6 -8
- data/lib/sequel/connection_pool/threaded.rb +14 -8
- data/lib/sequel/connection_pool/timed_queue.rb +270 -0
- data/lib/sequel/connection_pool.rb +57 -31
- data/lib/sequel/core.rb +17 -18
- data/lib/sequel/database/connecting.rb +27 -3
- data/lib/sequel/database/dataset.rb +16 -6
- data/lib/sequel/database/misc.rb +70 -14
- data/lib/sequel/database/query.rb +73 -2
- data/lib/sequel/database/schema_generator.rb +11 -6
- data/lib/sequel/database/schema_methods.rb +23 -4
- data/lib/sequel/database/transactions.rb +6 -0
- data/lib/sequel/dataset/actions.rb +111 -15
- data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +42 -0
- data/lib/sequel/dataset/features.rb +20 -1
- data/lib/sequel/dataset/misc.rb +12 -2
- data/lib/sequel/dataset/placeholder_literalizer.rb +20 -9
- data/lib/sequel/dataset/query.rb +170 -41
- data/lib/sequel/dataset/sql.rb +190 -71
- data/lib/sequel/dataset.rb +4 -0
- data/lib/sequel/extensions/_model_pg_row.rb +0 -12
- data/lib/sequel/extensions/_pretty_table.rb +1 -1
- data/lib/sequel/extensions/any_not_empty.rb +2 -2
- data/lib/sequel/extensions/async_thread_pool.rb +14 -13
- data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
- data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
- data/lib/sequel/extensions/connection_expiration.rb +15 -9
- data/lib/sequel/extensions/connection_validator.rb +16 -11
- data/lib/sequel/extensions/constraint_validations.rb +1 -1
- data/lib/sequel/extensions/core_refinements.rb +36 -11
- data/lib/sequel/extensions/date_arithmetic.rb +36 -8
- data/lib/sequel/extensions/date_parse_input_handler.rb +67 -0
- data/lib/sequel/extensions/datetime_parse_to_time.rb +5 -1
- data/lib/sequel/extensions/duplicate_columns_handler.rb +11 -10
- data/lib/sequel/extensions/index_caching.rb +5 -1
- data/lib/sequel/extensions/inflector.rb +1 -1
- data/lib/sequel/extensions/is_distinct_from.rb +141 -0
- data/lib/sequel/extensions/looser_typecasting.rb +3 -0
- data/lib/sequel/extensions/migration.rb +57 -15
- data/lib/sequel/extensions/named_timezones.rb +22 -6
- data/lib/sequel/extensions/pagination.rb +1 -1
- data/lib/sequel/extensions/pg_array.rb +33 -4
- data/lib/sequel/extensions/pg_array_ops.rb +2 -2
- data/lib/sequel/extensions/pg_auto_parameterize.rb +509 -0
- data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
- data/lib/sequel/extensions/pg_enum.rb +1 -2
- data/lib/sequel/extensions/pg_extended_date_support.rb +39 -28
- data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
- data/lib/sequel/extensions/pg_hstore.rb +6 -1
- data/lib/sequel/extensions/pg_hstore_ops.rb +53 -3
- data/lib/sequel/extensions/pg_inet.rb +10 -11
- data/lib/sequel/extensions/pg_inet_ops.rb +1 -1
- data/lib/sequel/extensions/pg_interval.rb +11 -11
- data/lib/sequel/extensions/pg_json.rb +13 -15
- data/lib/sequel/extensions/pg_json_ops.rb +125 -2
- data/lib/sequel/extensions/pg_multirange.rb +367 -0
- data/lib/sequel/extensions/pg_range.rb +13 -26
- data/lib/sequel/extensions/pg_range_ops.rb +37 -9
- data/lib/sequel/extensions/pg_row.rb +20 -19
- data/lib/sequel/extensions/pg_row_ops.rb +1 -1
- data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
- data/lib/sequel/extensions/round_timestamps.rb +1 -1
- data/lib/sequel/extensions/s.rb +2 -1
- data/lib/sequel/extensions/schema_caching.rb +1 -1
- data/lib/sequel/extensions/schema_dumper.rb +45 -11
- data/lib/sequel/extensions/server_block.rb +10 -13
- data/lib/sequel/extensions/set_literalizer.rb +58 -0
- data/lib/sequel/extensions/sql_comments.rb +110 -3
- data/lib/sequel/extensions/sql_log_normalizer.rb +108 -0
- data/lib/sequel/extensions/sqlite_json_ops.rb +255 -0
- data/lib/sequel/extensions/string_agg.rb +1 -1
- data/lib/sequel/extensions/string_date_time.rb +19 -23
- data/lib/sequel/extensions/symbol_aref.rb +2 -0
- data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
- data/lib/sequel/model/associations.rb +286 -92
- data/lib/sequel/model/base.rb +53 -33
- data/lib/sequel/model/dataset_module.rb +3 -0
- data/lib/sequel/model/errors.rb +10 -1
- data/lib/sequel/model/exceptions.rb +15 -3
- data/lib/sequel/model/inflections.rb +1 -1
- data/lib/sequel/plugins/auto_restrict_eager_graph.rb +62 -0
- data/lib/sequel/plugins/auto_validations.rb +74 -16
- data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
- data/lib/sequel/plugins/column_encryption.rb +29 -8
- data/lib/sequel/plugins/composition.rb +3 -2
- data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
- data/lib/sequel/plugins/constraint_validations.rb +8 -5
- data/lib/sequel/plugins/defaults_setter.rb +16 -0
- data/lib/sequel/plugins/dirty.rb +1 -1
- data/lib/sequel/plugins/enum.rb +124 -0
- data/lib/sequel/plugins/finder.rb +4 -2
- data/lib/sequel/plugins/insert_conflict.rb +4 -0
- data/lib/sequel/plugins/instance_specific_default.rb +1 -1
- data/lib/sequel/plugins/json_serializer.rb +2 -2
- data/lib/sequel/plugins/lazy_attributes.rb +3 -0
- data/lib/sequel/plugins/list.rb +8 -3
- data/lib/sequel/plugins/many_through_many.rb +109 -10
- data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
- data/lib/sequel/plugins/nested_attributes.rb +4 -4
- data/lib/sequel/plugins/optimistic_locking.rb +9 -42
- data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
- data/lib/sequel/plugins/paged_operations.rb +181 -0
- data/lib/sequel/plugins/pg_array_associations.rb +46 -34
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +9 -3
- data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
- data/lib/sequel/plugins/prepared_statements.rb +12 -2
- data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
- data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
- data/lib/sequel/plugins/rcte_tree.rb +7 -4
- data/lib/sequel/plugins/require_valid_schema.rb +67 -0
- data/lib/sequel/plugins/serialization.rb +1 -0
- data/lib/sequel/plugins/serialization_modification_detection.rb +1 -0
- data/lib/sequel/plugins/single_table_inheritance.rb +8 -0
- data/lib/sequel/plugins/sql_comments.rb +189 -0
- data/lib/sequel/plugins/static_cache.rb +39 -1
- data/lib/sequel/plugins/static_cache_cache.rb +5 -1
- data/lib/sequel/plugins/subclasses.rb +28 -11
- data/lib/sequel/plugins/tactical_eager_loading.rb +23 -10
- data/lib/sequel/plugins/timestamps.rb +1 -1
- data/lib/sequel/plugins/unused_associations.rb +521 -0
- data/lib/sequel/plugins/update_or_create.rb +1 -1
- data/lib/sequel/plugins/validate_associated.rb +22 -12
- data/lib/sequel/plugins/validation_helpers.rb +41 -11
- data/lib/sequel/plugins/validation_helpers_generic_type_messages.rb +73 -0
- data/lib/sequel/plugins/xml_serializer.rb +1 -1
- data/lib/sequel/sql.rb +1 -1
- data/lib/sequel/timezones.rb +12 -14
- data/lib/sequel/version.rb +1 -1
- metadata +109 -19
@@ -68,6 +68,10 @@ module Sequel
|
|
68
68
|
private
|
69
69
|
|
70
70
|
if RUBY_VERSION >= '2.6'
|
71
|
+
# Whether Time.at with :nsec and :in is broken. True on JRuby < 9.3.9.0.
|
72
|
+
BROKEN_TIME_AT_WITH_NSEC = defined?(JRUBY_VERSION) && (JRUBY_VERSION < '9.3' || (JRUBY_VERSION < '9.4' && JRUBY_VERSION.split('.')[2].to_i < 9))
|
73
|
+
private_constant :BROKEN_TIME_AT_WITH_NSEC
|
74
|
+
|
71
75
|
# Convert the given input Time (which must be in UTC) to the given input timezone,
|
72
76
|
# which should be a TZInfo::Timezone instance.
|
73
77
|
def convert_input_time_other(v, input_timezone)
|
@@ -76,33 +80,45 @@ module Sequel
|
|
76
80
|
raise unless disamb = tzinfo_disambiguator_for(v)
|
77
81
|
period = input_timezone.period_for_local(v, &disamb)
|
78
82
|
offset = period.utc_total_offset
|
79
|
-
|
83
|
+
# :nocov:
|
84
|
+
if BROKEN_TIME_AT_WITH_NSEC
|
85
|
+
Time.at(v.to_i - offset, :in => input_timezone) + v.nsec/1000000000.0
|
86
|
+
# :nocov:
|
87
|
+
else
|
88
|
+
Time.at(v.to_i - offset, v.nsec, :nsec, :in => input_timezone)
|
89
|
+
end
|
80
90
|
end
|
81
91
|
|
82
92
|
# Convert the given input Time to the given output timezone,
|
83
93
|
# which should be a TZInfo::Timezone instance.
|
84
94
|
def convert_output_time_other(v, output_timezone)
|
85
|
-
|
95
|
+
# :nocov:
|
96
|
+
if BROKEN_TIME_AT_WITH_NSEC
|
97
|
+
Time.at(v.to_i, :in => output_timezone) + v.nsec/1000000000.0
|
98
|
+
# :nocov:
|
99
|
+
else
|
100
|
+
Time.at(v.to_i, v.nsec, :nsec, :in => output_timezone)
|
101
|
+
end
|
86
102
|
end
|
87
103
|
# :nodoc:
|
88
104
|
# :nocov:
|
89
105
|
else
|
90
106
|
def convert_input_time_other(v, input_timezone)
|
91
107
|
local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
|
92
|
-
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
|
108
|
+
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0
|
93
109
|
end
|
94
110
|
|
95
111
|
if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2'
|
96
112
|
def convert_output_time_other(v, output_timezone)
|
97
113
|
v = output_timezone.utc_to_local(v.getutc)
|
98
114
|
local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
|
99
|
-
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + local_offset
|
115
|
+
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 + local_offset
|
100
116
|
end
|
101
117
|
else
|
102
118
|
def convert_output_time_other(v, output_timezone)
|
103
119
|
v = output_timezone.utc_to_local(v.getutc)
|
104
120
|
local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
|
105
|
-
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
|
121
|
+
Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0
|
106
122
|
end
|
107
123
|
end
|
108
124
|
# :nodoc:
|
@@ -120,7 +136,7 @@ module Sequel
|
|
120
136
|
v = output_timezone.utc_to_local(v.new_offset(0))
|
121
137
|
|
122
138
|
# Force DateTime output instead of TZInfo::DateTimeWithOffset
|
123
|
-
DateTime.
|
139
|
+
DateTime.civil(v.year, v.month, v.day, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
|
124
140
|
end
|
125
141
|
# :nodoc:
|
126
142
|
# :nocov:
|
@@ -54,7 +54,7 @@ module Sequel
|
|
54
54
|
# an enumerator if no block is given.
|
55
55
|
def each_page(page_size)
|
56
56
|
raise(Error, "You cannot paginate a dataset that already has a limit") if @opts[:limit]
|
57
|
-
return to_enum(:each_page, page_size) unless
|
57
|
+
return to_enum(:each_page, page_size) unless defined?(yield)
|
58
58
|
record_count = count
|
59
59
|
total_pages = (record_count / page_size.to_f).ceil
|
60
60
|
(1..total_pages).each{|page_no| yield paginate(page_no, page_size, record_count)}
|
@@ -228,16 +228,37 @@ module Sequel
|
|
228
228
|
when Array
|
229
229
|
"{#{a.map{|i| bound_variable_array(i)}.join(',')}}"
|
230
230
|
when Sequel::SQL::Blob
|
231
|
-
|
231
|
+
bound_variable_array_string(literal(a)[BLOB_RANGE].gsub("''", "'"))
|
232
232
|
when Sequel::LiteralString
|
233
233
|
a
|
234
234
|
when String
|
235
|
-
|
235
|
+
bound_variable_array_string(a)
|
236
|
+
when Float
|
237
|
+
if a.infinite?
|
238
|
+
a > 0 ? '"Infinity"' : '"-Infinity"'
|
239
|
+
elsif a.nan?
|
240
|
+
'"NaN"'
|
241
|
+
else
|
242
|
+
literal(a)
|
243
|
+
end
|
244
|
+
when Time, Date
|
245
|
+
@default_dataset.literal_date_or_time(a)
|
236
246
|
else
|
237
|
-
|
247
|
+
if (s = bound_variable_arg(a, nil)).is_a?(String)
|
248
|
+
bound_variable_array_string(s)
|
249
|
+
else
|
250
|
+
literal(a)
|
251
|
+
end
|
238
252
|
end
|
239
253
|
end
|
240
254
|
|
255
|
+
# Escape strings used as array members in bound variables. Most complex
|
256
|
+
# will create a regular string with bound_variable_arg, and then use this
|
257
|
+
# escaping to format it as an array member.
|
258
|
+
def bound_variable_array_string(s)
|
259
|
+
"\"#{s.gsub(/("|\\)/, '\\\\\1')}\""
|
260
|
+
end
|
261
|
+
|
241
262
|
# Look into both the current database's array schema types and the global
|
242
263
|
# array schema types to get the type symbol for the given database type
|
243
264
|
# string.
|
@@ -246,7 +267,7 @@ module Sequel
|
|
246
267
|
end
|
247
268
|
|
248
269
|
# Make the column type detection handle registered array types.
|
249
|
-
def
|
270
|
+
def schema_array_type(db_type)
|
250
271
|
if (db_type =~ /\A([^(]+)(?:\([^(]+\))?\[\]\z/io) && (type = pg_array_schema_type($1))
|
251
272
|
type
|
252
273
|
else
|
@@ -457,6 +478,14 @@ module Sequel
|
|
457
478
|
end
|
458
479
|
end
|
459
480
|
|
481
|
+
# Allow automatic parameterization of the receiver if all elements can be
|
482
|
+
# can be automatically parameterized.
|
483
|
+
def sequel_auto_param_type(ds)
|
484
|
+
if array_type && all?{|x| nil == x || ds.send(:auto_param_type, x)}
|
485
|
+
"::#{array_type}[]"
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
460
489
|
private
|
461
490
|
|
462
491
|
# Recursive method that handles multi-dimensional
|
@@ -108,7 +108,7 @@ module Sequel
|
|
108
108
|
|
109
109
|
# Call the ANY function:
|
110
110
|
#
|
111
|
-
# array_op.
|
111
|
+
# array_op.any # ANY(array)
|
112
112
|
#
|
113
113
|
# Usually used like:
|
114
114
|
#
|
@@ -329,7 +329,7 @@ end
|
|
329
329
|
if defined?(Sequel::CoreRefinements)
|
330
330
|
module Sequel::CoreRefinements
|
331
331
|
refine Symbol do
|
332
|
-
|
332
|
+
send INCLUDE_METH, Sequel::Postgres::ArrayOpMethods
|
333
333
|
end
|
334
334
|
end
|
335
335
|
end
|
@@ -0,0 +1,509 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
#
|
3
|
+
# This extension changes Sequel's postgres adapter to automatically
|
4
|
+
# parameterize queries by default. Sequel's default behavior has always
|
5
|
+
# been to literalize all arguments unless specifically using
|
6
|
+
# parameters (via :$arg placeholders and the Dataset#prepare/call methods).
|
7
|
+
# This extension makes Sequel use string, numeric, blob, date, and
|
8
|
+
# time types as parameters. Example:
|
9
|
+
#
|
10
|
+
# # Default
|
11
|
+
# DB[:test].where(:a=>1)
|
12
|
+
# # SQL: SELECT * FROM test WHERE a = 1
|
13
|
+
#
|
14
|
+
# DB.extension :pg_auto_parameterize
|
15
|
+
# DB[:test].where(:a=>1)
|
16
|
+
# # SQL: SELECT * FROM test WHERE a = $1 (args: [1])
|
17
|
+
#
|
18
|
+
# Other pg_* extensions that ship with Sequel and add support for
|
19
|
+
# PostgreSQL-specific types support automatically parameterizing those
|
20
|
+
# types when used with this extension.
|
21
|
+
#
|
22
|
+
# This extension is not generally faster than the default behavior.
|
23
|
+
# In some cases it is faster, such as when using large strings.
|
24
|
+
# However, the use of parameters avoids potential security issues,
|
25
|
+
# in case Sequel does not correctly literalize one of the arguments
|
26
|
+
# that this extension would automatically parameterize.
|
27
|
+
#
|
28
|
+
# There are some known issues with automatic parameterization:
|
29
|
+
#
|
30
|
+
# 1. In order to avoid most type errors, the extension attempts to guess
|
31
|
+
# the appropriate type and automatically casts most placeholders,
|
32
|
+
# except plain Ruby strings (which PostgreSQL treats as an unknown
|
33
|
+
# type).
|
34
|
+
#
|
35
|
+
# Unfortunately, if the type guess is incorrect, or a plain Ruby
|
36
|
+
# string is used and PostgreSQL cannot determine the data type for it,
|
37
|
+
# the query may result in a DatabaseError. To fix both issues, you can
|
38
|
+
# explicitly cast values using <tt>Sequel.cast(value, type)</tt>, and
|
39
|
+
# Sequel will cast to that type.
|
40
|
+
#
|
41
|
+
# 2. PostgreSQL supports a maximum of 65535 parameters per query.
|
42
|
+
# Attempts to use a query with more than this number of parameters
|
43
|
+
# will result in a Sequel::DatabaseError being raised. Sequel tries
|
44
|
+
# to mitigate this issue by turning <tt>column IN (int, ...)</tt>
|
45
|
+
# queries into <tt>column = ANY(CAST($ AS int8[]))</tt> using an
|
46
|
+
# array parameter, to reduce the number of parameters. It also limits
|
47
|
+
# inserting multiple rows at once to a maximum of 40 rows per query by
|
48
|
+
# default. While these mitigations handle the most common cases
|
49
|
+
# where a large number of parameters would be used, there are other
|
50
|
+
# cases.
|
51
|
+
#
|
52
|
+
# 3. Automatic parameterization will consider the same objects as
|
53
|
+
# equivalent when building SQL. However, for performance, it does
|
54
|
+
# not perform equality checks. So code such as:
|
55
|
+
#
|
56
|
+
# DB[:t].select{foo('a').as(:f)}.group{foo('a')}
|
57
|
+
# # SELECT foo('a') AS "f" FROM "t" GROUP BY foo('a')
|
58
|
+
#
|
59
|
+
# Will get auto paramterized as:
|
60
|
+
#
|
61
|
+
# # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($2)
|
62
|
+
#
|
63
|
+
# Which will result in a DatabaseError, since that is not valid SQL.
|
64
|
+
#
|
65
|
+
# If you use the same expression, it will use the same parameter:
|
66
|
+
#
|
67
|
+
# foo = Sequel.function(:foo, 'a')
|
68
|
+
# DB[:t].select(foo.as(:f)).group(foo)
|
69
|
+
# # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($1)
|
70
|
+
#
|
71
|
+
# Note that Dataset#select_group and similar methods that take arguments
|
72
|
+
# used in multiple places in the SQL will generally handle this
|
73
|
+
# automatically, since they will use the same objects:
|
74
|
+
#
|
75
|
+
# DB[:t].select_group{foo('a').as(:f)}
|
76
|
+
# # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($1)
|
77
|
+
#
|
78
|
+
# You can work around any issues that come up by disabling automatic
|
79
|
+
# parameterization by calling the +no_auto_parameterize+ method on the
|
80
|
+
# dataset (which returns a clone of the dataset). You can avoid
|
81
|
+
# parameterization for specific values in the query by wrapping them
|
82
|
+
# with +Sequel.skip_pg_auto_param+.
|
83
|
+
#
|
84
|
+
# It is likely there are corner cases not mentioned above
|
85
|
+
# when using this extension. Users are encouraged to provide feedback
|
86
|
+
# when using this extension if they come across such corner cases.
|
87
|
+
#
|
88
|
+
# This extension is only compatible when using the pg driver, not
|
89
|
+
# when using the sequel-postgres-pr, jeremyevans-postgres-pr, or
|
90
|
+
# postgres-pr drivers, as those do not support bound variables.
|
91
|
+
#
|
92
|
+
# Related module: Sequel::Postgres::AutoParameterize
|
93
|
+
|
94
|
+
module Sequel
|
95
|
+
module Postgres
|
96
|
+
# Enable automatically parameterizing queries.
|
97
|
+
module AutoParameterize
|
98
|
+
# SQL query string that also holds an array of parameters
|
99
|
+
class QueryString < ::String
|
100
|
+
# The array of parameters used by this query.
|
101
|
+
attr_reader :args
|
102
|
+
|
103
|
+
# Add a new parameter to this query, which adds
|
104
|
+
# the parameter to the array of parameters, and an
|
105
|
+
# SQL placeholder to the query itself.
|
106
|
+
def add_arg(s)
|
107
|
+
unless defined?(@args)
|
108
|
+
@args = []
|
109
|
+
@arg_map = {}
|
110
|
+
@arg_map.compare_by_identity
|
111
|
+
end
|
112
|
+
|
113
|
+
unless pos = @arg_map[s]
|
114
|
+
@args << s
|
115
|
+
pos = @arg_map[s] = @args.length.to_s
|
116
|
+
end
|
117
|
+
self << '$' << pos
|
118
|
+
end
|
119
|
+
|
120
|
+
# Return a new QueryString with the given string appended
|
121
|
+
# to the receiver, and the same arguments.
|
122
|
+
def +(other)
|
123
|
+
v = self.class.new(super)
|
124
|
+
v.instance_variable_set(:@args, @args) if @args
|
125
|
+
v
|
126
|
+
end
|
127
|
+
|
128
|
+
# Whether this query string currently supports
|
129
|
+
# automatic parameterization. Automatic parameterization
|
130
|
+
# is disabled at certain points during query building where
|
131
|
+
# PostgreSQL does not support it.
|
132
|
+
def auto_param?
|
133
|
+
!@skip_auto_param
|
134
|
+
end
|
135
|
+
|
136
|
+
# Skip automatic parameterization inside the passed block.
|
137
|
+
# This is used during query generation to disable
|
138
|
+
# automatic parameterization for clauses not supporting it.
|
139
|
+
def skip_auto_param
|
140
|
+
skip_auto_param = @skip_auto_param
|
141
|
+
begin
|
142
|
+
@skip_auto_param = true
|
143
|
+
yield
|
144
|
+
ensure
|
145
|
+
@skip_auto_param = skip_auto_param
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Freeze the stored arguments when freezing the query string.
|
150
|
+
def freeze
|
151
|
+
if @args
|
152
|
+
@args.freeze
|
153
|
+
@arg_map.freeze
|
154
|
+
end
|
155
|
+
super
|
156
|
+
end
|
157
|
+
|
158
|
+
# Show args when the query string is inspected
|
159
|
+
def inspect
|
160
|
+
@args ? "#{self}; #{@args.inspect}".inspect : super
|
161
|
+
end
|
162
|
+
|
163
|
+
def initialize_copy(other)
|
164
|
+
super
|
165
|
+
if args = other.instance_variable_get(:@args)
|
166
|
+
@args = args.dup
|
167
|
+
@arg_map = other.instance_variable_get(:@arg_map).dup
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Wrapper class that skips auto parameterization for the wrapped object.
|
173
|
+
class SkipAutoParam < SQL::Wrapper
|
174
|
+
def to_s_append(ds, sql)
|
175
|
+
if sql.is_a?(QueryString)
|
176
|
+
sql.skip_auto_param{super}
|
177
|
+
else
|
178
|
+
super
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# PlacholderLiteralizer subclass with support for stored auto parameters.
|
184
|
+
class PlaceholderLiteralizer < ::Sequel::Dataset::PlaceholderLiteralizer
|
185
|
+
def initialize(dataset, fragments, final_sql, arity)
|
186
|
+
s = dataset.sql.dup
|
187
|
+
s.clear
|
188
|
+
@sql_origin = s.freeze
|
189
|
+
super
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
def sql_origin
|
195
|
+
@sql_origin.dup
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
module DatabaseMethods
|
200
|
+
def self.extended(db)
|
201
|
+
unless (db.adapter_scheme == :postgres && USES_PG) || (db.adapter_scheme == :mock && db.database_type == :postgres)
|
202
|
+
raise Error, "pg_auto_parameterize is only supported when using the postgres adapter with the pg driver"
|
203
|
+
end
|
204
|
+
db.extend_datasets(DatasetMethods)
|
205
|
+
end
|
206
|
+
|
207
|
+
# If the sql string has an embedded parameter array,
|
208
|
+
# extract the parameter values from that.
|
209
|
+
def execute(sql, opts={})
|
210
|
+
if sql.is_a?(QueryString) && (args = sql.args)
|
211
|
+
opts = opts.merge(:arguments=>args)
|
212
|
+
end
|
213
|
+
super
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
# Disable auto_parameterization during COPY TABLE.
|
219
|
+
def copy_table_sql(table, opts=OPTS)
|
220
|
+
table = _no_auto_parameterize(table)
|
221
|
+
super
|
222
|
+
end
|
223
|
+
|
224
|
+
# Disable auto_parameterization during CREATE TABLE AS.
|
225
|
+
def create_table_as(name, sql, options)
|
226
|
+
sql = _no_auto_parameterize(sql)
|
227
|
+
super
|
228
|
+
end
|
229
|
+
|
230
|
+
# Disable auto_parameterization during CREATE VIEW.
|
231
|
+
def create_view_sql(name, source, options)
|
232
|
+
source = _no_auto_parameterize(source)
|
233
|
+
super
|
234
|
+
end
|
235
|
+
|
236
|
+
# Disable automatic parameterization for the given table if supported.
|
237
|
+
def _no_auto_parameterize(table)
|
238
|
+
if table.is_a?(DatasetMethods)
|
239
|
+
table.no_auto_parameterize
|
240
|
+
else
|
241
|
+
table
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
module DatasetMethods
|
247
|
+
# Return a clone of the dataset that will not do
|
248
|
+
# automatic parameterization.
|
249
|
+
def no_auto_parameterize
|
250
|
+
cached_dataset(:_no_auto_parameterize_ds) do
|
251
|
+
@opts[:no_auto_parameterize] ? self : clone(:no_auto_parameterize=>true)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Do not add implicit typecasts for directly typecasted values,
|
256
|
+
# since the user is presumably doing so to set the type, not convert
|
257
|
+
# from the implicitly typecasted type.
|
258
|
+
def cast_sql_append(sql, expr, type)
|
259
|
+
if auto_param?(sql) && auto_param_type(expr)
|
260
|
+
sql << 'CAST('
|
261
|
+
sql.add_arg(expr)
|
262
|
+
sql << ' AS ' << db.cast_type_literal(type).to_s << ')'
|
263
|
+
else
|
264
|
+
super
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Transform column IN (int, ...) expressions into column = ANY($)
|
269
|
+
# and column NOT IN (int, ...) expressions into column != ALL($)
|
270
|
+
# using an integer array bound variable for the ANY/ALL argument.
|
271
|
+
# This is the same optimization PostgreSQL performs internally,
|
272
|
+
# but this reduces the number of bound variables.
|
273
|
+
def complex_expression_sql_append(sql, op, args)
|
274
|
+
case op
|
275
|
+
when :IN, :"NOT IN"
|
276
|
+
l, r = args
|
277
|
+
if auto_param?(sql) && !l.is_a?(Array) && _integer_array?(r) && r.size > 1
|
278
|
+
if op == :IN
|
279
|
+
op = :"="
|
280
|
+
func = :ANY
|
281
|
+
else
|
282
|
+
op = :!=
|
283
|
+
func = :ALL
|
284
|
+
end
|
285
|
+
args = [l, Sequel.function(func, Sequel.cast(_integer_array_auto_param(r), 'int8[]'))]
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
super
|
290
|
+
end
|
291
|
+
|
292
|
+
# Parameterize insertion of multiple values
|
293
|
+
def multi_insert_sql(columns, values)
|
294
|
+
if @opts[:no_auto_parameterize]
|
295
|
+
super
|
296
|
+
else
|
297
|
+
[clone(:multi_insert_values=>values.map{|r| Array(r)}).insert_sql(columns, LiteralString.new('VALUES '))]
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# For strings, numeric arguments, and date/time arguments, add
|
302
|
+
# them as parameters to the query instead of literalizing them
|
303
|
+
# into the SQL.
|
304
|
+
def literal_append(sql, v)
|
305
|
+
if auto_param?(sql) && (type = auto_param_type(v))
|
306
|
+
sql.add_arg(v) << type
|
307
|
+
else
|
308
|
+
super
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# The class to use for placeholder literalizers.
|
313
|
+
def placeholder_literalizer_class
|
314
|
+
if @opts[:no_auto_parameterize]
|
315
|
+
super
|
316
|
+
else
|
317
|
+
PlaceholderLiteralizer
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Disable automatic parameterization when using a cursor.
|
322
|
+
def use_cursor(*)
|
323
|
+
super.no_auto_parameterize
|
324
|
+
end
|
325
|
+
|
326
|
+
# Store receiving dataset and args when with_sql is used with a method name symbol, so sql
|
327
|
+
# can be parameterized correctly if used as a subselect.
|
328
|
+
def with_sql(*a)
|
329
|
+
ds = super
|
330
|
+
if Symbol === a[0]
|
331
|
+
ds = ds.clone(:with_sql_dataset=>self, :with_sql_args=>a.freeze)
|
332
|
+
end
|
333
|
+
ds
|
334
|
+
end
|
335
|
+
|
336
|
+
protected
|
337
|
+
|
338
|
+
# Disable automatic parameterization for prepared statements,
|
339
|
+
# since they will use manual parameterization.
|
340
|
+
def to_prepared_statement(*a)
|
341
|
+
@opts[:no_auto_parameterize] ? super : no_auto_parameterize.to_prepared_statement(*a)
|
342
|
+
end
|
343
|
+
|
344
|
+
private
|
345
|
+
|
346
|
+
# If auto parameterization is supported for the value, return a string
|
347
|
+
# for the implicit typecast to use. Return false/nil if the value should not be
|
348
|
+
# automatically parameterized.
|
349
|
+
def auto_param_type(v)
|
350
|
+
case v
|
351
|
+
when String
|
352
|
+
case v
|
353
|
+
when LiteralString
|
354
|
+
false
|
355
|
+
when Sequel::SQL::Blob
|
356
|
+
"::bytea"
|
357
|
+
else
|
358
|
+
""
|
359
|
+
end
|
360
|
+
when Integer
|
361
|
+
((v > 2147483647 || v < -2147483648) ? "::int8" : "::int4")
|
362
|
+
when Float
|
363
|
+
# PostgreSQL treats literal floats as numeric, not double precision
|
364
|
+
# But older versions of PostgreSQL don't handle Infinity/NaN in numeric
|
365
|
+
v.finite? ? "::numeric" : "::double precision"
|
366
|
+
when BigDecimal
|
367
|
+
"::numeric"
|
368
|
+
when Sequel::SQLTime
|
369
|
+
"::time"
|
370
|
+
when Time
|
371
|
+
"::#{@db.cast_type_literal(Time)}"
|
372
|
+
when DateTime
|
373
|
+
"::#{@db.cast_type_literal(DateTime)}"
|
374
|
+
when Date
|
375
|
+
"::date"
|
376
|
+
else
|
377
|
+
v.respond_to?(:sequel_auto_param_type) ? v.sequel_auto_param_type(self) : auto_param_type_fallback(v)
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
# Allow other extensions to support auto parameterization in ways that do not
|
382
|
+
# require adding the sequel_auto_param_type method.
|
383
|
+
def auto_param_type_fallback(v)
|
384
|
+
super if defined?(super)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Whether the given query string currently supports automatic parameterization.
|
388
|
+
def auto_param?(sql)
|
389
|
+
sql.is_a?(QueryString) && sql.auto_param?
|
390
|
+
end
|
391
|
+
|
392
|
+
# Default the import slice to 40, since PostgreSQL supports a maximum of 1600
|
393
|
+
# columns per table, and it supports a maximum of 65k parameters. Technically,
|
394
|
+
# there can be more than one parameter per column, so this doesn't prevent going
|
395
|
+
# over the limit, though it does make it less likely.
|
396
|
+
def default_import_slice
|
397
|
+
40
|
398
|
+
end
|
399
|
+
|
400
|
+
# Handle parameterization of multi_insert_sql
|
401
|
+
def _insert_values_sql(sql, values)
|
402
|
+
super
|
403
|
+
|
404
|
+
if values = @opts[:multi_insert_values]
|
405
|
+
expression_list_append(sql, values.map{|r| Array(r)})
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Whether the given argument is an array of integers or NULL values, recursively.
|
410
|
+
def _integer_array?(v)
|
411
|
+
Array === v && v.all?{|x| nil == x || Integer === x}
|
412
|
+
end
|
413
|
+
|
414
|
+
# Create the bound variable string that will be used for the IN (int, ...) to = ANY($)
|
415
|
+
# optimization for integer arrays.
|
416
|
+
def _integer_array_auto_param(v)
|
417
|
+
buf = String.new
|
418
|
+
buf << '{'
|
419
|
+
comma = false
|
420
|
+
v.each do |x|
|
421
|
+
if comma
|
422
|
+
buf << ","
|
423
|
+
else
|
424
|
+
comma = true
|
425
|
+
end
|
426
|
+
|
427
|
+
buf << (x ? x.to_s : 'NULL')
|
428
|
+
end
|
429
|
+
buf << '}'
|
430
|
+
end
|
431
|
+
|
432
|
+
# Skip auto parameterization in LIMIT and OFFSET clauses
|
433
|
+
def select_limit_sql(sql)
|
434
|
+
if auto_param?(sql) && (@opts[:limit] || @opts[:offset])
|
435
|
+
sql.skip_auto_param{super}
|
436
|
+
else
|
437
|
+
super
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# Skip auto parameterization in ORDER clause if used with
|
442
|
+
# integer values indicating ordering by the nth column.
|
443
|
+
def select_order_sql(sql)
|
444
|
+
if auto_param?(sql) && (order = @opts[:order]) && order.any?{|o| Integer === o || (SQL::OrderedExpression === o && Integer === o.expression)}
|
445
|
+
sql.skip_auto_param{super}
|
446
|
+
else
|
447
|
+
super
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
# Skip auto parameterization in CTE CYCLE clause
|
452
|
+
def select_with_sql_cte_search_cycle(sql,cte)
|
453
|
+
if auto_param?(sql) && cte[:cycle]
|
454
|
+
sql.skip_auto_param{super}
|
455
|
+
else
|
456
|
+
super
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Unless auto parameterization is disabled, use a string that
|
461
|
+
# can store the parameterized arguments.
|
462
|
+
def sql_string_origin
|
463
|
+
@opts[:no_auto_parameterize] ? super : QueryString.new
|
464
|
+
end
|
465
|
+
|
466
|
+
# If subquery uses with_sql with a method name symbol, get the dataset
|
467
|
+
# with_sql was called on, and use that as the subquery, recording the
|
468
|
+
# arguments to with_sql that will be used to calculate the sql.
|
469
|
+
def subselect_sql_dataset(sql, ds)
|
470
|
+
if ws_ds = ds.opts[:with_sql_dataset]
|
471
|
+
super(sql, ws_ds).clone(:subselect_sql_args=>ds.opts[:with_sql_args])
|
472
|
+
else
|
473
|
+
super
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
# If subquery used with_sql with a method name symbol, use the arguments to
|
478
|
+
# with_sql to determine the sql, so that the subselect can be parameterized.
|
479
|
+
def subselect_sql_append_sql(sql, ds)
|
480
|
+
if args = ds.opts[:subselect_sql_args]
|
481
|
+
ds.send(*args)
|
482
|
+
else
|
483
|
+
super
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# Use auto parameterization for datasets with static SQL using placeholders.
|
488
|
+
def static_sql(sql)
|
489
|
+
if @opts[:append_sql] || @opts[:no_auto_parameterize] || String === sql
|
490
|
+
super
|
491
|
+
else
|
492
|
+
query_string = QueryString.new
|
493
|
+
literal_append(query_string, sql)
|
494
|
+
query_string
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
module SQL::Builders
|
502
|
+
# Skip auto parameterization for the given object when building queries.
|
503
|
+
def skip_pg_auto_param(v)
|
504
|
+
Postgres::AutoParameterize::SkipAutoParam.new(v)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
Database.register_extension(:pg_auto_parameterize, Postgres::AutoParameterize::DatabaseMethods)
|
509
|
+
end
|