sequel 3.33.0 → 3.34.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.
- data/CHANGELOG +140 -0
- data/Rakefile +7 -0
- data/bin/sequel +22 -2
- data/doc/dataset_basics.rdoc +1 -1
- data/doc/mass_assignment.rdoc +3 -1
- data/doc/querying.rdoc +28 -4
- data/doc/reflection.rdoc +23 -3
- data/doc/release_notes/3.34.0.txt +671 -0
- data/doc/schema_modification.rdoc +18 -2
- data/doc/virtual_rows.rdoc +49 -0
- data/lib/sequel/adapters/do/mysql.rb +0 -5
- data/lib/sequel/adapters/ibmdb.rb +9 -4
- data/lib/sequel/adapters/jdbc.rb +9 -4
- data/lib/sequel/adapters/jdbc/h2.rb +8 -2
- data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
- data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
- data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
- data/lib/sequel/adapters/mock.rb +24 -3
- data/lib/sequel/adapters/mysql.rb +29 -50
- data/lib/sequel/adapters/mysql2.rb +13 -28
- data/lib/sequel/adapters/oracle.rb +8 -2
- data/lib/sequel/adapters/postgres.rb +115 -20
- data/lib/sequel/adapters/shared/db2.rb +1 -1
- data/lib/sequel/adapters/shared/mssql.rb +14 -3
- data/lib/sequel/adapters/shared/mysql.rb +59 -11
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
- data/lib/sequel/adapters/shared/oracle.rb +1 -1
- data/lib/sequel/adapters/shared/postgres.rb +127 -30
- data/lib/sequel/adapters/shared/sqlite.rb +55 -38
- data/lib/sequel/adapters/sqlite.rb +9 -3
- data/lib/sequel/adapters/swift.rb +2 -2
- data/lib/sequel/adapters/swift/mysql.rb +0 -5
- data/lib/sequel/adapters/swift/postgres.rb +10 -0
- data/lib/sequel/ast_transformer.rb +4 -0
- data/lib/sequel/connection_pool.rb +8 -0
- data/lib/sequel/connection_pool/sharded_single.rb +5 -0
- data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
- data/lib/sequel/connection_pool/single.rb +5 -0
- data/lib/sequel/connection_pool/threaded.rb +14 -0
- data/lib/sequel/core.rb +24 -3
- data/lib/sequel/database/connecting.rb +24 -14
- data/lib/sequel/database/dataset_defaults.rb +1 -0
- data/lib/sequel/database/misc.rb +16 -25
- data/lib/sequel/database/query.rb +20 -2
- data/lib/sequel/database/schema_generator.rb +2 -2
- data/lib/sequel/database/schema_methods.rb +120 -23
- data/lib/sequel/dataset/actions.rb +91 -18
- data/lib/sequel/dataset/features.rb +5 -0
- data/lib/sequel/dataset/prepared_statements.rb +6 -2
- data/lib/sequel/dataset/sql.rb +68 -51
- data/lib/sequel/extensions/_pretty_table.rb +79 -0
- data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
- data/lib/sequel/extensions/migration.rb +4 -0
- data/lib/sequel/extensions/null_dataset.rb +90 -0
- data/lib/sequel/extensions/pg_array.rb +460 -0
- data/lib/sequel/extensions/pg_array_ops.rb +220 -0
- data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
- data/lib/sequel/extensions/pg_hstore.rb +296 -0
- data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
- data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
- data/lib/sequel/extensions/pretty_table.rb +5 -71
- data/lib/sequel/extensions/query_literals.rb +79 -0
- data/lib/sequel/extensions/schema_caching.rb +76 -0
- data/lib/sequel/extensions/schema_dumper.rb +227 -31
- data/lib/sequel/extensions/select_remove.rb +35 -0
- data/lib/sequel/extensions/sql_expr.rb +4 -110
- data/lib/sequel/extensions/to_dot.rb +1 -1
- data/lib/sequel/model.rb +11 -2
- data/lib/sequel/model/associations.rb +35 -7
- data/lib/sequel/model/base.rb +159 -36
- data/lib/sequel/no_core_ext.rb +2 -0
- data/lib/sequel/plugins/caching.rb +25 -18
- data/lib/sequel/plugins/composition.rb +1 -1
- data/lib/sequel/plugins/hook_class_methods.rb +1 -1
- data/lib/sequel/plugins/identity_map.rb +11 -3
- data/lib/sequel/plugins/instance_filters.rb +10 -0
- data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
- data/lib/sequel/plugins/nested_attributes.rb +4 -3
- data/lib/sequel/plugins/prepared_statements.rb +3 -1
- data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
- data/lib/sequel/plugins/schema.rb +7 -2
- data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
- data/lib/sequel/plugins/static_cache.rb +99 -0
- data/lib/sequel/plugins/validation_class_methods.rb +1 -1
- data/lib/sequel/sql.rb +417 -7
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/firebird_spec.rb +1 -1
- data/spec/adapters/mssql_spec.rb +12 -15
- data/spec/adapters/mysql_spec.rb +81 -23
- data/spec/adapters/postgres_spec.rb +444 -77
- data/spec/adapters/spec_helper.rb +2 -0
- data/spec/adapters/sqlite_spec.rb +8 -8
- data/spec/core/connection_pool_spec.rb +85 -0
- data/spec/core/database_spec.rb +29 -5
- data/spec/core/dataset_spec.rb +171 -3
- data/spec/core/expression_filters_spec.rb +364 -0
- data/spec/core/mock_adapter_spec.rb +17 -3
- data/spec/core/schema_spec.rb +133 -0
- data/spec/extensions/association_dependencies_spec.rb +13 -13
- data/spec/extensions/caching_spec.rb +26 -3
- data/spec/extensions/class_table_inheritance_spec.rb +2 -2
- data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
- data/spec/extensions/force_encoding_spec.rb +4 -2
- data/spec/extensions/hook_class_methods_spec.rb +5 -2
- data/spec/extensions/identity_map_spec.rb +17 -0
- data/spec/extensions/instance_filters_spec.rb +1 -1
- data/spec/extensions/lazy_attributes_spec.rb +2 -2
- data/spec/extensions/list_spec.rb +4 -4
- data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
- data/spec/extensions/migration_spec.rb +6 -2
- data/spec/extensions/nested_attributes_spec.rb +20 -0
- data/spec/extensions/null_dataset_spec.rb +85 -0
- data/spec/extensions/optimistic_locking_spec.rb +2 -2
- data/spec/extensions/pg_array_ops_spec.rb +105 -0
- data/spec/extensions/pg_array_spec.rb +196 -0
- data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
- data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
- data/spec/extensions/pg_hstore_spec.rb +195 -0
- data/spec/extensions/pg_statement_cache_spec.rb +209 -0
- data/spec/extensions/prepared_statements_spec.rb +4 -0
- data/spec/extensions/pretty_table_spec.rb +6 -0
- data/spec/extensions/query_literals_spec.rb +168 -0
- data/spec/extensions/schema_caching_spec.rb +41 -0
- data/spec/extensions/schema_dumper_spec.rb +231 -11
- data/spec/extensions/schema_spec.rb +14 -2
- data/spec/extensions/select_remove_spec.rb +38 -0
- data/spec/extensions/sharding_spec.rb +6 -6
- data/spec/extensions/skip_create_refresh_spec.rb +1 -1
- data/spec/extensions/spec_helper.rb +2 -1
- data/spec/extensions/sql_expr_spec.rb +28 -19
- data/spec/extensions/static_cache_spec.rb +145 -0
- data/spec/extensions/touch_spec.rb +1 -1
- data/spec/extensions/typecast_on_load_spec.rb +9 -1
- data/spec/integration/associations_test.rb +6 -6
- data/spec/integration/database_test.rb +1 -1
- data/spec/integration/dataset_test.rb +89 -26
- data/spec/integration/migrator_test.rb +2 -3
- data/spec/integration/model_test.rb +3 -3
- data/spec/integration/plugin_test.rb +85 -22
- data/spec/integration/prepared_statement_test.rb +28 -8
- data/spec/integration/schema_test.rb +78 -7
- data/spec/integration/spec_helper.rb +1 -0
- data/spec/integration/timezone_test.rb +1 -1
- data/spec/integration/transaction_test.rb +4 -6
- data/spec/integration/type_test.rb +2 -2
- data/spec/model/associations_spec.rb +94 -8
- data/spec/model/base_spec.rb +4 -4
- data/spec/model/hooks_spec.rb +2 -2
- data/spec/model/model_spec.rb +19 -7
- data/spec/model/record_spec.rb +135 -58
- data/spec/model/spec_helper.rb +1 -0
- metadata +35 -7
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# The pg_array_ops extension adds support to Sequel's DSL to make
|
|
2
|
+
# it easier to call PostgreSQL array functions and operators. The
|
|
3
|
+
# most common usage is taking an object that represents an SQL
|
|
4
|
+
# identifier (such as a :symbol), and calling #pg_array on it:
|
|
5
|
+
#
|
|
6
|
+
# ia = :int_array_column.pg_array
|
|
7
|
+
#
|
|
8
|
+
# This creates a Sequel::Postgres::ArrayOp object that can be used
|
|
9
|
+
# for easier querying:
|
|
10
|
+
#
|
|
11
|
+
# ia[1] # int_array_column[1]
|
|
12
|
+
# ia[1][2] # int_array_column[1][2]
|
|
13
|
+
#
|
|
14
|
+
# ia.contains(:other_int_array_column) # @>
|
|
15
|
+
# ia.contained_by(:other_int_array_column) # <@
|
|
16
|
+
# ia.overlaps(:other_int_array_column) # &&
|
|
17
|
+
# ia.concat(:other_int_array_column) # ||
|
|
18
|
+
#
|
|
19
|
+
# ia.push(1) # int_array_column || 1
|
|
20
|
+
# ia.unshift(1) # 1 || int_array_column
|
|
21
|
+
#
|
|
22
|
+
# ia.any # ANY(int_array_column)
|
|
23
|
+
# ia.all # ALL(int_array_column)
|
|
24
|
+
# ia.dims # array_dims(int_array_column)
|
|
25
|
+
# ia.length # array_length(int_array_column, 1)
|
|
26
|
+
# ia.length(2) # array_length(int_array_column, 2)
|
|
27
|
+
# ia.lower # array_lower(int_array_column, 1)
|
|
28
|
+
# ia.lower(2) # array_lower(int_array_column, 2)
|
|
29
|
+
# ia.join # array_to_string(int_array_column, '', NULL)
|
|
30
|
+
# ia.join(':') # array_to_string(int_array_column, ':', NULL)
|
|
31
|
+
# ia.join(':', ' ') # array_to_string(int_array_column, ':', ' ')
|
|
32
|
+
# ia.unnest # unnest(int_array_column)
|
|
33
|
+
#
|
|
34
|
+
# See the PostgreSQL array function and operator documentation for more
|
|
35
|
+
# details on what these functions and operators do.
|
|
36
|
+
#
|
|
37
|
+
# If you are also using the pg_array extension, you should load it before
|
|
38
|
+
# loading this extension. Doing so will allow you to use PGArray#op to get
|
|
39
|
+
# an ArrayOp, allowing you to perform array operations on array literals.
|
|
40
|
+
module Sequel
|
|
41
|
+
module Postgres
|
|
42
|
+
# The ArrayOp class is a simple container for a single object that
|
|
43
|
+
# defines methods that yield Sequel expression objects representing
|
|
44
|
+
# PostgreSQL array operators and functions.
|
|
45
|
+
#
|
|
46
|
+
# In the method documentation examples, assume that:
|
|
47
|
+
#
|
|
48
|
+
# array_op = :array.pg_array
|
|
49
|
+
class ArrayOp < Sequel::SQL::Wrapper
|
|
50
|
+
CONCAT = ["(".freeze, " || ".freeze, ")".freeze].freeze
|
|
51
|
+
CONTAINS = ["(".freeze, " @> ".freeze, ")".freeze].freeze
|
|
52
|
+
CONTAINED_BY = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
|
|
53
|
+
OVERLAPS = ["(".freeze, " && ".freeze, ")".freeze].freeze
|
|
54
|
+
|
|
55
|
+
# Access a member of the array, returns an SQL::Subscript instance:
|
|
56
|
+
#
|
|
57
|
+
# array_op[1] # array[1]
|
|
58
|
+
def [](key)
|
|
59
|
+
Sequel::SQL::Subscript.new(self, [key])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Call the ALL function:
|
|
63
|
+
#
|
|
64
|
+
# array_op.all # ALL(array)
|
|
65
|
+
#
|
|
66
|
+
# Usually used like:
|
|
67
|
+
#
|
|
68
|
+
# dataset.where(1=>array_op.all)
|
|
69
|
+
# # WHERE (1 = ALL(array))
|
|
70
|
+
def all
|
|
71
|
+
function(:ALL)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Call the ANY function:
|
|
75
|
+
#
|
|
76
|
+
# array_op.all # ANY(array)
|
|
77
|
+
#
|
|
78
|
+
# Usually used like:
|
|
79
|
+
#
|
|
80
|
+
# dataset.where(1=>array_op.any)
|
|
81
|
+
# # WHERE (1 = ANY(array))
|
|
82
|
+
def any
|
|
83
|
+
function(:ANY)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Use the contains (@>) operator:
|
|
87
|
+
#
|
|
88
|
+
# array_op.contains(:a) # (array @> a)
|
|
89
|
+
def contains(other)
|
|
90
|
+
bool_op(CONTAINS, other)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Use the contained by (<@) operator:
|
|
94
|
+
#
|
|
95
|
+
# array_op.contained_by(:a) # (array <@ a)
|
|
96
|
+
def contained_by(other)
|
|
97
|
+
bool_op(CONTAINED_BY, other)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Call the array_dims method:
|
|
101
|
+
#
|
|
102
|
+
# array_op.dims # array_dims(array)
|
|
103
|
+
def dims
|
|
104
|
+
function(:array_dims)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Call the array_length method:
|
|
108
|
+
#
|
|
109
|
+
# array_op.length # array_length(array, 1)
|
|
110
|
+
# array_op.length(2) # array_length(array, 2)
|
|
111
|
+
def length(dimension = 1)
|
|
112
|
+
function(:array_length, dimension)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Call the array_lower method:
|
|
116
|
+
#
|
|
117
|
+
# array_op.lower # array_lower(array, 1)
|
|
118
|
+
# array_op.lower(2) # array_lower(array, 2)
|
|
119
|
+
def lower(dimension = 1)
|
|
120
|
+
function(:array_lower, dimension)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Use the overlaps (&&) operator:
|
|
124
|
+
#
|
|
125
|
+
# array_op.overlaps(:a) # (array && a)
|
|
126
|
+
def overlaps(other)
|
|
127
|
+
bool_op(OVERLAPS, other)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Use the concatentation (||) operator:
|
|
131
|
+
#
|
|
132
|
+
# array_op.push(:a) # (array || a)
|
|
133
|
+
# array_op.concat(:a) # (array || a)
|
|
134
|
+
def push(other)
|
|
135
|
+
array_op(CONCAT, [self, other])
|
|
136
|
+
end
|
|
137
|
+
alias concat push
|
|
138
|
+
|
|
139
|
+
# Return the receiver.
|
|
140
|
+
def pg_array
|
|
141
|
+
self
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Call the array_to_string method:
|
|
145
|
+
#
|
|
146
|
+
# array_op.join # array_to_string(array, '', NULL)
|
|
147
|
+
# array_op.to_string # array_to_string(array, '', NULL)
|
|
148
|
+
# array_op.join(":") # array_to_string(array, ':', NULL)
|
|
149
|
+
# array_op.join(":", "*") # array_to_string(array, ':', '*')
|
|
150
|
+
def to_string(joiner="", null=nil)
|
|
151
|
+
function(:array_to_string, joiner, null)
|
|
152
|
+
end
|
|
153
|
+
alias join to_string
|
|
154
|
+
|
|
155
|
+
# Call the unnest method:
|
|
156
|
+
#
|
|
157
|
+
# array_op.unnest # unnest(array)
|
|
158
|
+
def unnest
|
|
159
|
+
function(:unnest)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Use the concatentation (||) operator, reversing the order:
|
|
163
|
+
#
|
|
164
|
+
# array_op.unshift(:a) # (a || array)
|
|
165
|
+
def unshift(other)
|
|
166
|
+
array_op(CONCAT, [other, self])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# Return a placeholder literal with the given str and args, wrapped
|
|
172
|
+
# in an ArrayOp, used by operators that return arrays.
|
|
173
|
+
def array_op(str, args)
|
|
174
|
+
ArrayOp.new(Sequel::SQL::PlaceholderLiteralString.new(str, args))
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Return a placeholder literal with the given str and args, wrapped
|
|
178
|
+
# in a boolean expression, used by operators that return booleans.
|
|
179
|
+
def bool_op(str, other)
|
|
180
|
+
Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(str, [value, other]))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Return a function with the given name, and the receiver as the first
|
|
184
|
+
# argument, with any additional arguments given.
|
|
185
|
+
def function(name, *args)
|
|
186
|
+
SQL::Function.new(name, self, *args)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
module ArrayOpMethods
|
|
191
|
+
# Wrap the receiver in an ArrayOp so you can easily use the PostgreSQL
|
|
192
|
+
# array functions and operators with it.
|
|
193
|
+
def pg_array
|
|
194
|
+
ArrayOp.new(self)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if defined?(PGArray)
|
|
199
|
+
class PGArray
|
|
200
|
+
# Wrap the PGArray instance in an ArrayOp, allowing you to easily use
|
|
201
|
+
# the PostgreSQL array functions and operators with literal arrays.
|
|
202
|
+
def op
|
|
203
|
+
ArrayOp.new(self)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
class SQL::GenericExpression
|
|
210
|
+
include Sequel::Postgres::ArrayOpMethods
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class LiteralString
|
|
214
|
+
include Sequel::Postgres::ArrayOpMethods
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
class Symbol
|
|
219
|
+
include Sequel::Postgres::ArrayOpMethods
|
|
220
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# This extension allows Sequel's postgres adapter to automatically
|
|
2
|
+
# parameterize all common queries. Sequel's default behavior has always
|
|
3
|
+
# been to literalize all arguments unless specifically using
|
|
4
|
+
# parameters (via :$arg placeholders and the prepare/call methods).
|
|
5
|
+
# This extension makes Sequel take all string, numeric, date, and
|
|
6
|
+
# time types and automatically turn them into parameters. Example:
|
|
7
|
+
#
|
|
8
|
+
# # Default
|
|
9
|
+
# DB[:test].where(:a=>1)
|
|
10
|
+
# # SQL: SELECT * FROM test WHERE a = 1
|
|
11
|
+
#
|
|
12
|
+
# DB.extend Sequel::Postgres::AutoParameterize::DatabaseMethods
|
|
13
|
+
# DB[:test].where(:a=>1)
|
|
14
|
+
# # SQL: SELECT * FROM test WHERE a = $1 (args: [1])
|
|
15
|
+
#
|
|
16
|
+
# This extension is not necessarily faster or more safe than the
|
|
17
|
+
# default behavior. In some cases it is faster, such as when using
|
|
18
|
+
# large strings. However, there are also some known issues with
|
|
19
|
+
# this approach:
|
|
20
|
+
#
|
|
21
|
+
# 1. Because of the way it operates, it has no context to make a
|
|
22
|
+
# determination about whether to literalize an object or not.
|
|
23
|
+
# For example, if it comes across an integer, it will turn it
|
|
24
|
+
# into a parameter. That breaks code such as:
|
|
25
|
+
#
|
|
26
|
+
# DB[:table].select(:a, :b).order(2, 1)
|
|
27
|
+
#
|
|
28
|
+
# Since it will use the following SQL (which isn't valid):
|
|
29
|
+
#
|
|
30
|
+
# SELECT a, b FROM table ORDER BY $1, $2
|
|
31
|
+
#
|
|
32
|
+
# To work around this, you can either specify the columns
|
|
33
|
+
# manually or use a literal string:
|
|
34
|
+
#
|
|
35
|
+
# DB[:table].select(:a, :b).order(:b, :a)
|
|
36
|
+
# DB[:table].select(:a, :b).order('2, 1'.lit)
|
|
37
|
+
#
|
|
38
|
+
# 2. In order to avoid many type errors, it attempts to guess the
|
|
39
|
+
# appropriate type and automatically casts all placeholders.
|
|
40
|
+
# Unfortunately, if the type guess is incorrect, the query will
|
|
41
|
+
# be rejected. For example, the following works without
|
|
42
|
+
# automatic parameterization, but fails with it:
|
|
43
|
+
#
|
|
44
|
+
# DB[:table].insert(:interval=>'1 day')
|
|
45
|
+
#
|
|
46
|
+
# To work around this, you can just add the necessary casts
|
|
47
|
+
# manually:
|
|
48
|
+
#
|
|
49
|
+
# DB[:table].insert(:interval=>'1 day'.cast(:interval))
|
|
50
|
+
#
|
|
51
|
+
# You can also work around any issues that come up by disabling automatic
|
|
52
|
+
# parameterization by calling the no_auto_parameterize method on the
|
|
53
|
+
# dataset (which returns a clone of the dataset).
|
|
54
|
+
#
|
|
55
|
+
# It is likely there are other corner cases I am not yet aware of
|
|
56
|
+
# when using this extension, so use this extension with caution.
|
|
57
|
+
#
|
|
58
|
+
# This extension is only compatible when using the pg driver, not
|
|
59
|
+
# when using the old postgres driver or the postgres-pr driver.
|
|
60
|
+
|
|
61
|
+
module Sequel
|
|
62
|
+
module Postgres
|
|
63
|
+
# Enable automatically parameterizing queries by hijacking the
|
|
64
|
+
# SQL query string that Sequel builds to also hold the array
|
|
65
|
+
# of parameters.
|
|
66
|
+
module AutoParameterize
|
|
67
|
+
# String that holds an array of parameters
|
|
68
|
+
class StringWithArray < ::String
|
|
69
|
+
# The array of parameters used by this query.
|
|
70
|
+
attr_reader :args
|
|
71
|
+
|
|
72
|
+
# Add a new parameter to this query, which adds
|
|
73
|
+
# the parameter to the array of parameters, and an
|
|
74
|
+
# SQL placeholder to the query itself.
|
|
75
|
+
def add_arg(s, type)
|
|
76
|
+
@args ||= []
|
|
77
|
+
@args << s
|
|
78
|
+
self << "$#{@args.length}::#{type}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Show args when the string is inspected
|
|
82
|
+
def inspect
|
|
83
|
+
@args ? "#{self}; #{@args.inspect}".inspect : super
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
module DatabaseMethods
|
|
88
|
+
# Extend the database's datasets with the necessary code.
|
|
89
|
+
def self.extended(db)
|
|
90
|
+
db.extend_datasets(DatasetMethods)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# If the sql string has an embedded parameter array,
|
|
94
|
+
# extract the arguments from that.
|
|
95
|
+
def execute(sql, opts={})
|
|
96
|
+
if sql.is_a?(StringWithArray) && (args = sql.args)
|
|
97
|
+
opts = opts.merge(:arguments=>args)
|
|
98
|
+
end
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# If the sql string has an embedded parameter array,
|
|
103
|
+
# extract the arguments from that.
|
|
104
|
+
def execute_insert(sql, opts={})
|
|
105
|
+
if sql.is_a?(StringWithArray) && (args = sql.args)
|
|
106
|
+
opts = opts.merge(:arguments=>args)
|
|
107
|
+
end
|
|
108
|
+
super
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
module DatasetMethods
|
|
113
|
+
# Return a clone of the dataset that will not do
|
|
114
|
+
# automatic parameterization.
|
|
115
|
+
def no_auto_parameterize
|
|
116
|
+
clone(:no_auto_parameterize=>true)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# For strings, numeric arguments, and date/time arguments, add
|
|
120
|
+
# them as parameters to the query instead of literalizing them
|
|
121
|
+
# into the SQL.
|
|
122
|
+
def literal_append(sql, v)
|
|
123
|
+
if sql.is_a?(StringWithArray)
|
|
124
|
+
case v
|
|
125
|
+
when String
|
|
126
|
+
case v
|
|
127
|
+
when LiteralString
|
|
128
|
+
super
|
|
129
|
+
when Sequel::SQL::Blob
|
|
130
|
+
sql.add_arg(v, :bytea)
|
|
131
|
+
else
|
|
132
|
+
sql.add_arg(v, :text)
|
|
133
|
+
end
|
|
134
|
+
when Bignum
|
|
135
|
+
sql.add_arg(v, :int8)
|
|
136
|
+
when Fixnum
|
|
137
|
+
sql.add_arg(v, :int4)
|
|
138
|
+
when Float
|
|
139
|
+
sql.add_arg(v, :"double precision")
|
|
140
|
+
when BigDecimal
|
|
141
|
+
sql.add_arg(v, :numeric)
|
|
142
|
+
when Sequel::SQLTime
|
|
143
|
+
sql.add_arg(v, :time)
|
|
144
|
+
when Time, DateTime
|
|
145
|
+
sql.add_arg(v, :timestamp)
|
|
146
|
+
when Date
|
|
147
|
+
sql.add_arg(v, :date)
|
|
148
|
+
else
|
|
149
|
+
super
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
super
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
protected
|
|
157
|
+
|
|
158
|
+
# Disable automatic parameterization for prepared statements,
|
|
159
|
+
# since they will use manual parameterization.
|
|
160
|
+
def to_prepared_statement(*a)
|
|
161
|
+
opts[:no_auto_parameterize] ? super : no_auto_parameterize.to_prepared_statement(*a)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
# Unless auto parameterization is turned off, use a string that
|
|
167
|
+
# can store the parameterized arguments.
|
|
168
|
+
def sql_string_origin
|
|
169
|
+
opts[:no_auto_parameterize] ? super : StringWithArray.new
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# The pg_hstore extension adds support for the PostgreSQL hstore type
|
|
2
|
+
# to Sequel. hstore is an extension that ships with PostgreSQL, and
|
|
3
|
+
# the hstore type stores an arbitrary key-value table, where the keys
|
|
4
|
+
# are strings and the values are strings or NULL.
|
|
5
|
+
#
|
|
6
|
+
# This extension integrates with Sequel's native postgres adapter, so
|
|
7
|
+
# that when hstore fields are retrieved, they are parsed and returned
|
|
8
|
+
# as instances of Sequel::Postgres::HStore. HStore is
|
|
9
|
+
# a DelegateClass of Hash, so it mostly acts like a hash, but not
|
|
10
|
+
# completely (is_a?(Hash) is false). If you want the actual hash,
|
|
11
|
+
# you can call Hstore#to_hash. This is done so that Sequel does not
|
|
12
|
+
# treat a HStore like a Hash by default, which would cause issues.
|
|
13
|
+
#
|
|
14
|
+
# In addition to the parsers, this extension comes with literalizers
|
|
15
|
+
# for HStore using the standard Sequel literalization callbacks, so
|
|
16
|
+
# they work with on all adapters.
|
|
17
|
+
#
|
|
18
|
+
# To turn an existing Hash into an HStore:
|
|
19
|
+
#
|
|
20
|
+
# hash.hstore
|
|
21
|
+
#
|
|
22
|
+
# Since the hstore type only supports strings, non string keys and
|
|
23
|
+
# values are converted to strings
|
|
24
|
+
#
|
|
25
|
+
# {:foo=>1}.hstore.to_hash # {'foo'=>'1'}
|
|
26
|
+
# v = {}.hstore
|
|
27
|
+
# v[:foo] = 1
|
|
28
|
+
# v # {'foo'=>'1'}
|
|
29
|
+
#
|
|
30
|
+
# However, to make life easier, lookups by key are converted to
|
|
31
|
+
# strings (even when accessing the underlying hash directly):
|
|
32
|
+
#
|
|
33
|
+
# {'foo'=>'bar'}.hstore[:foo] # 'bar'
|
|
34
|
+
# {'foo'=>'bar'}.hstore.to_hash[:foo] # 'bar'
|
|
35
|
+
#
|
|
36
|
+
# HStore instances mostly just delegate to the underlying hash
|
|
37
|
+
# instance, so Hash methods that modify the receiver or returned
|
|
38
|
+
# modified copies of the receiver may not do string conversion.
|
|
39
|
+
# The following methods will handle string conversion, and more
|
|
40
|
+
# can be added later if desired:
|
|
41
|
+
#
|
|
42
|
+
# * \[\]
|
|
43
|
+
# * \[\]=
|
|
44
|
+
# * assoc (ruby 1.9 only)
|
|
45
|
+
# * delete
|
|
46
|
+
# * fetch
|
|
47
|
+
# * has_key?
|
|
48
|
+
# * has_value?
|
|
49
|
+
# * include?
|
|
50
|
+
# * key (ruby 1.9 only)
|
|
51
|
+
# * key?
|
|
52
|
+
# * member?
|
|
53
|
+
# * merge
|
|
54
|
+
# * merge!
|
|
55
|
+
# * rassoc (ruby 1.9 only)
|
|
56
|
+
# * replace
|
|
57
|
+
# * store
|
|
58
|
+
# * update
|
|
59
|
+
# * value?
|
|
60
|
+
#
|
|
61
|
+
# If you want to insert a hash into an hstore database column:
|
|
62
|
+
#
|
|
63
|
+
# DB[:table].insert(:column=>{'foo'=>'bar'}.hstore)
|
|
64
|
+
#
|
|
65
|
+
# If you would like to use hstore columns in your model objects, you
|
|
66
|
+
# probably want to modify the schema parsing/typecasting so that it
|
|
67
|
+
# recognizes and correctly handles the hstore columns, which you can
|
|
68
|
+
# do by:
|
|
69
|
+
#
|
|
70
|
+
# DB.extend Sequel::Postgres::HStore::DatabaseMethods
|
|
71
|
+
#
|
|
72
|
+
# If you are not using the native postgres adapter, you probably
|
|
73
|
+
# also want to use the typecast_on_load plugin in the model, and
|
|
74
|
+
# set it to typecast the hstore column(s) on load.
|
|
75
|
+
#
|
|
76
|
+
# This extension requires the delegate and strscan libraries.
|
|
77
|
+
|
|
78
|
+
require 'delegate'
|
|
79
|
+
require 'strscan'
|
|
80
|
+
|
|
81
|
+
module Sequel
|
|
82
|
+
module Postgres
|
|
83
|
+
class HStore < DelegateClass(Hash)
|
|
84
|
+
# Parser for PostgreSQL hstore output format.
|
|
85
|
+
class Parser < StringScanner
|
|
86
|
+
QUOTE_RE = /"/.freeze
|
|
87
|
+
KV_SEP_RE = /"\s*=>\s*/.freeze
|
|
88
|
+
NULL_RE = /NULL/.freeze
|
|
89
|
+
SEP_RE = /,\s*/.freeze
|
|
90
|
+
QUOTED_RE = /(\\"|[^"])*/.freeze
|
|
91
|
+
REPLACE_RE = /\\(.)/.freeze
|
|
92
|
+
REPLACE_WITH = '\1'.freeze
|
|
93
|
+
|
|
94
|
+
# Parse the output format that PostgreSQL uses for hstore
|
|
95
|
+
# columns. Note that this does not attempt to parse all
|
|
96
|
+
# input formats that PostgreSQL will accept. For instance,
|
|
97
|
+
# it expects all keys and non-NULL values to be quoted.
|
|
98
|
+
#
|
|
99
|
+
# Return the resulting hash of objects. This can be called
|
|
100
|
+
# multiple times, it will cache the parsed hash on the first
|
|
101
|
+
# call and use it for subsequent calls.
|
|
102
|
+
def parse
|
|
103
|
+
return @result if @result
|
|
104
|
+
hash = {}
|
|
105
|
+
while !eos?
|
|
106
|
+
skip(QUOTE_RE)
|
|
107
|
+
k = parse_quoted
|
|
108
|
+
skip(KV_SEP_RE)
|
|
109
|
+
if skip(QUOTE_RE)
|
|
110
|
+
v = parse_quoted
|
|
111
|
+
skip(QUOTE_RE)
|
|
112
|
+
else
|
|
113
|
+
scan(NULL_RE)
|
|
114
|
+
v = nil
|
|
115
|
+
end
|
|
116
|
+
skip(SEP_RE)
|
|
117
|
+
hash[k] = v
|
|
118
|
+
end
|
|
119
|
+
@result = hash
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Parse and unescape a quoted key/value.
|
|
125
|
+
def parse_quoted
|
|
126
|
+
scan(QUOTED_RE).gsub(REPLACE_RE, REPLACE_WITH)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
module DatabaseMethods
|
|
131
|
+
# Reset the conversion procs if using the native postgres adapter.
|
|
132
|
+
def self.extended(db)
|
|
133
|
+
db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Handle hstores in bound variables
|
|
137
|
+
def bound_variable_arg(arg, conn)
|
|
138
|
+
case arg
|
|
139
|
+
when HStore
|
|
140
|
+
arg.unquoted_literal
|
|
141
|
+
when Hash
|
|
142
|
+
HStore.new(arg).unquoted_literal
|
|
143
|
+
else
|
|
144
|
+
super
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
# Recognize the hstore database type.
|
|
151
|
+
def schema_column_type(db_type)
|
|
152
|
+
db_type == 'hstore' ? :hstore : super
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Typecast value correctly to HStore. If already an
|
|
156
|
+
# HStore instance, return as is. If a hash, return
|
|
157
|
+
# an HStore version of it. If a string, assume it is
|
|
158
|
+
# in PostgreSQL output format and parse it using the
|
|
159
|
+
# parser.
|
|
160
|
+
def typecast_value_hstore(value)
|
|
161
|
+
case value
|
|
162
|
+
when HStore
|
|
163
|
+
value
|
|
164
|
+
when Hash
|
|
165
|
+
HStore.new(value)
|
|
166
|
+
when String
|
|
167
|
+
HStore.parse(value)
|
|
168
|
+
else
|
|
169
|
+
raise Sequel::InvalidValue, "invalid value for hstore: #{value.inspect}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Default proc used for all underlying HStore hashes, so that even
|
|
175
|
+
# if you grab the underlying hash, it will still convert non-string
|
|
176
|
+
# keys to strings during lookup.
|
|
177
|
+
DEFAULT_PROC = lambda{|h, k| h[k.to_s] unless k.is_a?(String)}
|
|
178
|
+
|
|
179
|
+
QUOTE = '"'.freeze
|
|
180
|
+
COMMA = ",".freeze
|
|
181
|
+
KV_SEP = "=>".freeze
|
|
182
|
+
NULL = "NULL".freeze
|
|
183
|
+
ESCAPE_RE = /("|\\)/.freeze
|
|
184
|
+
ESCAPE_REPLACE = '\\\\\1'.freeze
|
|
185
|
+
HSTORE_CAST = '::hstore'.freeze
|
|
186
|
+
|
|
187
|
+
# Parse the given string into an HStore, assuming the str is in PostgreSQL
|
|
188
|
+
# hstore output format.
|
|
189
|
+
def self.parse(str)
|
|
190
|
+
new(Parser.new(str).parse)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Override methods that accept key argument to convert to string.
|
|
194
|
+
(%w'[] delete has_key? include? key? member?' + Array((%w'assoc' if RUBY_VERSION >= '1.9.0'))).each do |m|
|
|
195
|
+
class_eval("def #{m}(k) super(k.to_s) end", __FILE__, __LINE__)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Override methods that accept value argument to convert to string unless nil.
|
|
199
|
+
(%w'has_value? value?' + Array((%w'key rassoc' if RUBY_VERSION >= '1.9.0'))).each do |m|
|
|
200
|
+
class_eval("def #{m}(v) super(convert_value(v)) end", __FILE__, __LINE__)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Override methods that accept key and value arguments to convert to string appropriately.
|
|
204
|
+
%w'[]= store'.each do |m|
|
|
205
|
+
class_eval("def #{m}(k, v) super(k.to_s, convert_value(v)) end", __FILE__, __LINE__)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Override methods that take hashes to convert the hashes to using strings for keys and
|
|
209
|
+
# values before using them.
|
|
210
|
+
%w'initialize merge! update replace'.each do |m|
|
|
211
|
+
class_eval("def #{m}(h, &block) super(convert_hash(h), &block) end", __FILE__, __LINE__)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Override to force the key argument to a string.
|
|
215
|
+
def fetch(key, *args, &block)
|
|
216
|
+
super(key.to_s, *args, &block)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Convert the input hash to string keys and values before merging,
|
|
220
|
+
# and return a new HStore instance with the merged hash.
|
|
221
|
+
def merge(hash, &block)
|
|
222
|
+
self.class.new(super(convert_hash(hash), &block))
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Return the underlying hash used by this HStore instance.
|
|
226
|
+
alias to_hash __getobj__
|
|
227
|
+
|
|
228
|
+
# Append a literalize version of the hstore to the sql.
|
|
229
|
+
def sql_literal_append(ds, sql)
|
|
230
|
+
ds.literal_append(sql, unquoted_literal)
|
|
231
|
+
sql << HSTORE_CAST
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Return a string containing the unquoted, unstring-escaped
|
|
235
|
+
# literal version of the hstore. Separated out for use by
|
|
236
|
+
# the bound argument code.
|
|
237
|
+
def unquoted_literal
|
|
238
|
+
str = ''
|
|
239
|
+
comma = false
|
|
240
|
+
commas = COMMA
|
|
241
|
+
quote = QUOTE
|
|
242
|
+
kv_sep = KV_SEP
|
|
243
|
+
null = NULL
|
|
244
|
+
each do |k, v|
|
|
245
|
+
str << commas if comma
|
|
246
|
+
str << quote << escape_value(k) << quote
|
|
247
|
+
str << kv_sep
|
|
248
|
+
if v.nil?
|
|
249
|
+
str << null
|
|
250
|
+
else
|
|
251
|
+
str << quote << escape_value(v) << quote
|
|
252
|
+
end
|
|
253
|
+
comma = true
|
|
254
|
+
end
|
|
255
|
+
str
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
private
|
|
259
|
+
|
|
260
|
+
# Return a new hash based on the input hash with string
|
|
261
|
+
# keys and string or nil values.
|
|
262
|
+
def convert_hash(h)
|
|
263
|
+
hash = Hash.new(&DEFAULT_PROC)
|
|
264
|
+
h.each{|k,v| hash[k.to_s] = convert_value(v)}
|
|
265
|
+
hash
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Return value v as a string unless it is already nil.
|
|
269
|
+
def convert_value(v)
|
|
270
|
+
v.to_s unless v.nil?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Escape key/value strings when literalizing to
|
|
274
|
+
# correctly handle backslash and quote characters.
|
|
275
|
+
def escape_value(k)
|
|
276
|
+
k.to_s.gsub(ESCAPE_RE, ESCAPE_REPLACE)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
PG_NAMED_TYPES = {} unless defined?(PG_NAMED_TYPES)
|
|
281
|
+
# Associate the named types by default.
|
|
282
|
+
PG_NAMED_TYPES[:hstore] = HStore.method(:parse)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
class Hash
|
|
287
|
+
# Create a new HStore using the receiver as the input
|
|
288
|
+
# hash. Note that the HStore created will not use the
|
|
289
|
+
# receiver as the backing store, since it has to
|
|
290
|
+
# modify the hash. To get the new backing store, use:
|
|
291
|
+
#
|
|
292
|
+
# hash.hstore.to_hash
|
|
293
|
+
def hstore
|
|
294
|
+
Sequel::Postgres::HStore.new(self)
|
|
295
|
+
end
|
|
296
|
+
end
|