sequel 3.36.1 → 3.37.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +84 -0
- data/Rakefile +13 -0
- data/bin/sequel +12 -16
- data/doc/advanced_associations.rdoc +36 -67
- data/doc/association_basics.rdoc +11 -16
- data/doc/release_notes/3.37.0.txt +338 -0
- data/doc/schema_modification.rdoc +4 -0
- data/lib/sequel/adapters/jdbc/h2.rb +1 -1
- data/lib/sequel/adapters/jdbc/postgresql.rb +26 -8
- data/lib/sequel/adapters/mysql2.rb +4 -3
- data/lib/sequel/adapters/odbc/mssql.rb +2 -2
- data/lib/sequel/adapters/postgres.rb +4 -60
- data/lib/sequel/adapters/shared/mssql.rb +2 -1
- data/lib/sequel/adapters/shared/mysql.rb +0 -5
- data/lib/sequel/adapters/shared/postgres.rb +68 -2
- data/lib/sequel/adapters/shared/sqlite.rb +17 -1
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +12 -1
- data/lib/sequel/adapters/utils/pg_types.rb +76 -0
- data/lib/sequel/core.rb +13 -0
- data/lib/sequel/database/misc.rb +41 -1
- data/lib/sequel/database/schema_generator.rb +23 -10
- data/lib/sequel/database/schema_methods.rb +26 -4
- data/lib/sequel/dataset/graph.rb +2 -1
- data/lib/sequel/dataset/query.rb +62 -2
- data/lib/sequel/extensions/_pretty_table.rb +7 -3
- data/lib/sequel/extensions/arbitrary_servers.rb +5 -4
- data/lib/sequel/extensions/blank.rb +4 -0
- data/lib/sequel/extensions/columns_introspection.rb +13 -2
- data/lib/sequel/extensions/core_extensions.rb +6 -0
- data/lib/sequel/extensions/eval_inspect.rb +158 -0
- data/lib/sequel/extensions/inflector.rb +4 -0
- data/lib/sequel/extensions/looser_typecasting.rb +5 -4
- data/lib/sequel/extensions/migration.rb +4 -1
- data/lib/sequel/extensions/named_timezones.rb +4 -0
- data/lib/sequel/extensions/null_dataset.rb +4 -0
- data/lib/sequel/extensions/pagination.rb +4 -0
- data/lib/sequel/extensions/pg_array.rb +219 -168
- data/lib/sequel/extensions/pg_array_ops.rb +7 -2
- data/lib/sequel/extensions/pg_auto_parameterize.rb +10 -4
- data/lib/sequel/extensions/pg_hstore.rb +3 -1
- data/lib/sequel/extensions/pg_hstore_ops.rb +7 -2
- data/lib/sequel/extensions/pg_inet.rb +28 -3
- data/lib/sequel/extensions/pg_interval.rb +192 -0
- data/lib/sequel/extensions/pg_json.rb +21 -9
- data/lib/sequel/extensions/pg_range.rb +487 -0
- data/lib/sequel/extensions/pg_range_ops.rb +122 -0
- data/lib/sequel/extensions/pg_statement_cache.rb +3 -2
- data/lib/sequel/extensions/pretty_table.rb +12 -1
- data/lib/sequel/extensions/query.rb +4 -0
- data/lib/sequel/extensions/query_literals.rb +6 -6
- data/lib/sequel/extensions/schema_dumper.rb +39 -38
- data/lib/sequel/extensions/select_remove.rb +4 -0
- data/lib/sequel/extensions/server_block.rb +3 -2
- data/lib/sequel/extensions/split_array_nil.rb +65 -0
- data/lib/sequel/extensions/sql_expr.rb +4 -0
- data/lib/sequel/extensions/string_date_time.rb +4 -0
- data/lib/sequel/extensions/thread_local_timezones.rb +9 -3
- data/lib/sequel/extensions/to_dot.rb +4 -0
- data/lib/sequel/model/associations.rb +150 -91
- data/lib/sequel/plugins/identity_map.rb +2 -2
- data/lib/sequel/plugins/list.rb +1 -0
- data/lib/sequel/plugins/many_through_many.rb +33 -32
- data/lib/sequel/plugins/nested_attributes.rb +11 -3
- data/lib/sequel/plugins/rcte_tree.rb +2 -2
- data/lib/sequel/plugins/schema.rb +1 -1
- data/lib/sequel/sql.rb +14 -14
- data/lib/sequel/version.rb +2 -2
- data/spec/adapters/mysql_spec.rb +25 -0
- data/spec/adapters/postgres_spec.rb +572 -28
- data/spec/adapters/sqlite_spec.rb +16 -1
- data/spec/core/database_spec.rb +61 -2
- data/spec/core/dataset_spec.rb +92 -0
- data/spec/core/expression_filters_spec.rb +12 -0
- data/spec/extensions/arbitrary_servers_spec.rb +1 -1
- data/spec/extensions/boolean_readers_spec.rb +25 -25
- data/spec/extensions/eval_inspect_spec.rb +58 -0
- data/spec/extensions/json_serializer_spec.rb +0 -6
- data/spec/extensions/list_spec.rb +1 -1
- data/spec/extensions/looser_typecasting_spec.rb +7 -7
- data/spec/extensions/many_through_many_spec.rb +81 -0
- data/spec/extensions/nested_attributes_spec.rb +21 -4
- data/spec/extensions/pg_array_ops_spec.rb +1 -11
- data/spec/extensions/pg_array_spec.rb +181 -90
- data/spec/extensions/pg_auto_parameterize_spec.rb +3 -3
- data/spec/extensions/pg_hstore_spec.rb +1 -3
- data/spec/extensions/pg_inet_spec.rb +6 -1
- data/spec/extensions/pg_interval_spec.rb +73 -0
- data/spec/extensions/pg_json_spec.rb +5 -9
- data/spec/extensions/pg_range_ops_spec.rb +49 -0
- data/spec/extensions/pg_range_spec.rb +372 -0
- data/spec/extensions/pg_statement_cache_spec.rb +1 -2
- data/spec/extensions/query_literals_spec.rb +1 -2
- data/spec/extensions/schema_dumper_spec.rb +48 -89
- data/spec/extensions/serialization_spec.rb +1 -5
- data/spec/extensions/server_block_spec.rb +2 -2
- data/spec/extensions/spec_helper.rb +12 -2
- data/spec/extensions/split_array_nil_spec.rb +24 -0
- data/spec/integration/associations_test.rb +4 -4
- data/spec/integration/database_test.rb +2 -2
- data/spec/integration/dataset_test.rb +4 -4
- data/spec/integration/eager_loader_test.rb +6 -6
- data/spec/integration/plugin_test.rb +2 -2
- data/spec/integration/spec_helper.rb +2 -2
- data/spec/model/association_reflection_spec.rb +5 -0
- data/spec/model/associations_spec.rb +156 -49
- data/spec/model/eager_loading_spec.rb +137 -2
- data/spec/model/model_spec.rb +10 -10
- metadata +15 -2
@@ -2,6 +2,12 @@
|
|
2
2
|
# They make using Sequel's DSL easier by adding methods to Array,
|
3
3
|
# Hash, String, and Symbol to add methods that return Sequel
|
4
4
|
# expression objects.
|
5
|
+
#
|
6
|
+
# This extension is currently loaded by default, but that will no
|
7
|
+
# longer be true in a future version. In a future version, you will
|
8
|
+
# need to load it manually via:
|
9
|
+
#
|
10
|
+
# Sequel.extension :core_extensions
|
5
11
|
|
6
12
|
# This extension loads the core extensions.
|
7
13
|
def Sequel.core_extensions?
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# The eval_inspect extension changes #inspect for Sequel::SQL::Expression
|
2
|
+
# subclasses to return a string suitable for ruby's eval, such that
|
3
|
+
#
|
4
|
+
# eval(obj.inspect) == obj
|
5
|
+
#
|
6
|
+
# is true. The above code is true for most of ruby's simple classes such
|
7
|
+
# as String, Integer, Float, and Symbol, but it's not true for classes such
|
8
|
+
# as Time, Date, and BigDecimal. Sequel attempts to handle situations where
|
9
|
+
# instances of these classes are a component of a Sequel expression.
|
10
|
+
#
|
11
|
+
# To load the extension:
|
12
|
+
#
|
13
|
+
# Sequel.extension :eval_inspect
|
14
|
+
|
15
|
+
module Sequel
|
16
|
+
module EvalInspect
|
17
|
+
# Special case objects where inspect does not generally produce input
|
18
|
+
# suitable for eval. Used by Sequel::SQL::Expression#inspect so that
|
19
|
+
# it can produce a string suitable for eval even if components of the
|
20
|
+
# expression have inspect methods that do not produce strings suitable
|
21
|
+
# for eval.
|
22
|
+
def eval_inspect(obj)
|
23
|
+
case obj
|
24
|
+
when Sequel::SQL::Blob, Sequel::LiteralString, Sequel::SQL::ValueList
|
25
|
+
"#{obj.class}.new(#{obj.inspect})"
|
26
|
+
when Array
|
27
|
+
"[#{obj.map{|o| eval_inspect(o)}.join(', ')}]"
|
28
|
+
when Hash
|
29
|
+
"{#{obj.map{|k, v| "#{eval_inspect(k)} => #{eval_inspect(v)}"}.join(', ')}}"
|
30
|
+
when Time
|
31
|
+
if RUBY_VERSION < '1.9'
|
32
|
+
# Time on 1.8 doesn't handle %N (or %z on Windows), manually set the usec value in the string
|
33
|
+
hours, mins = obj.utc_offset.divmod(3600)
|
34
|
+
mins /= 60
|
35
|
+
"#{obj.class}.parse(#{obj.strftime("%Y-%m-%dT%H:%M:%S.#{sprintf('%06i%+03i%02i', obj.usec, hours, mins)}").inspect})#{'.utc' if obj.utc?}"
|
36
|
+
else
|
37
|
+
"#{obj.class}.parse(#{obj.strftime('%FT%T.%N%z').inspect})#{'.utc' if obj.utc?}"
|
38
|
+
end
|
39
|
+
when DateTime
|
40
|
+
# Ignore date of calendar reform
|
41
|
+
"DateTime.parse(#{obj.strftime('%FT%T.%N%z').inspect})"
|
42
|
+
when Date
|
43
|
+
# Ignore offset and date of calendar reform
|
44
|
+
"Date.new(#{obj.year}, #{obj.month}, #{obj.day})"
|
45
|
+
when BigDecimal
|
46
|
+
"BigDecimal.new(#{obj.to_s.inspect})"
|
47
|
+
else
|
48
|
+
obj.inspect
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
extend EvalInspect
|
54
|
+
|
55
|
+
module SQL
|
56
|
+
class Expression
|
57
|
+
# Attempt to produce a string suitable for eval, such that:
|
58
|
+
#
|
59
|
+
# eval(obj.inspect) == obj
|
60
|
+
def inspect
|
61
|
+
# Assume by default that the object can be recreated by calling
|
62
|
+
# self.class.new with any attr_reader values defined on the class,
|
63
|
+
# in the order they were defined.
|
64
|
+
klass = self.class
|
65
|
+
args = inspect_args.map do |arg|
|
66
|
+
if arg.is_a?(String) && arg =~ /\A\*/
|
67
|
+
# Special case string arguments starting with *, indicating that
|
68
|
+
# they should return an array to be splatted as the remaining arguments
|
69
|
+
send(arg.sub('*', '')).map{|a| Sequel.eval_inspect(a)}.join(', ')
|
70
|
+
else
|
71
|
+
Sequel.eval_inspect(send(arg))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
"#{klass}.new(#{args.join(', ')})"
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Which attribute values to use in the inspect string.
|
80
|
+
def inspect_args
|
81
|
+
self.class.comparison_attrs
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class ComplexExpression
|
86
|
+
private
|
87
|
+
|
88
|
+
# ComplexExpression's initializer uses a splat for the operator arguments.
|
89
|
+
def inspect_args
|
90
|
+
[:op, "*args"]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class CaseExpression
|
95
|
+
private
|
96
|
+
|
97
|
+
# CaseExpression's initializer checks whether an argument was
|
98
|
+
# provided, to differentiate CASE WHEN from CASE NULL WHEN, so
|
99
|
+
# check if an expression was provided, and only include the
|
100
|
+
# expression in the inspect output if so.
|
101
|
+
def inspect_args
|
102
|
+
if expression?
|
103
|
+
[:conditions, :default, :expression]
|
104
|
+
else
|
105
|
+
[:conditions, :default]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class Function
|
111
|
+
private
|
112
|
+
|
113
|
+
# Function's initializer uses a splat for the function arguments.
|
114
|
+
def inspect_args
|
115
|
+
[:f, "*args"]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class JoinOnClause
|
120
|
+
private
|
121
|
+
|
122
|
+
# JoinOnClause's initializer takes the on argument as the first argument
|
123
|
+
# instead of the last.
|
124
|
+
def inspect_args
|
125
|
+
[:on, :join_type, :table, :table_alias]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class JoinUsingClause
|
130
|
+
private
|
131
|
+
|
132
|
+
# JoinOnClause's initializer takes the using argument as the first argument
|
133
|
+
# instead of the last.
|
134
|
+
def inspect_args
|
135
|
+
[:using, :join_type, :table, :table_alias]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
class OrderedExpression
|
140
|
+
private
|
141
|
+
|
142
|
+
# OrderedExpression's initializer takes the :nulls information inside a hash,
|
143
|
+
# so if a NULL order was given, include a hash with that information.
|
144
|
+
def inspect_args
|
145
|
+
if nulls
|
146
|
+
[:expression, :descending, :opts_hash]
|
147
|
+
else
|
148
|
+
[:expression, :descending]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# A hash of null information suitable for passing to the initializer.
|
153
|
+
def opts_hash
|
154
|
+
{:nulls=>nulls}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -2,6 +2,10 @@
|
|
2
2
|
# words from singular to plural, class names to table names, modularized class
|
3
3
|
# names to ones without, and class names to foreign keys. It exists for
|
4
4
|
# backwards compatibility to legacy Sequel code.
|
5
|
+
#
|
6
|
+
# To load the extension:
|
7
|
+
#
|
8
|
+
# Sequel.extension :inflector
|
5
9
|
|
6
10
|
class String
|
7
11
|
# This module acts as a singleton returned/yielded by String.inflections,
|
@@ -1,10 +1,8 @@
|
|
1
1
|
# The LooserTypecasting extension changes the float and integer typecasting to
|
2
2
|
# use the looser .to_f and .to_i instead of the more strict Kernel.Float and
|
3
|
-
# Kernel.Integer. To
|
4
|
-
# Sequel::LooserTypecasting module after loading the extension:
|
3
|
+
# Kernel.Integer. To load the extension into the database:
|
5
4
|
#
|
6
|
-
#
|
7
|
-
# DB.extend(Sequel::LooserTypecasting)
|
5
|
+
# DB.extension :looser_typecasting
|
8
6
|
|
9
7
|
module Sequel
|
10
8
|
module LooserTypecasting
|
@@ -18,4 +16,7 @@ module Sequel
|
|
18
16
|
value.to_i
|
19
17
|
end
|
20
18
|
end
|
19
|
+
|
20
|
+
Database.register_extension(:looser_typecasting, LooserTypecasting)
|
21
21
|
end
|
22
|
+
|
@@ -1,8 +1,11 @@
|
|
1
1
|
# Adds the Sequel::Migration and Sequel::Migrator classes, which allow
|
2
2
|
# the user to easily group schema changes and migrate the database
|
3
3
|
# to a newer version or revert to a previous version.
|
4
|
-
|
5
4
|
#
|
5
|
+
# To load the extension:
|
6
|
+
#
|
7
|
+
# Sequel.extension :migration
|
8
|
+
|
6
9
|
module Sequel
|
7
10
|
# Sequel's older migration class, available for backward compatibility.
|
8
11
|
# Uses subclasses with up and down instance methods for each migration:
|
@@ -8,6 +8,10 @@
|
|
8
8
|
# typecast_timezone=. If a string is passed, it is converted to a
|
9
9
|
# TZInfo::Timezone using TZInfo::Timezone.get.
|
10
10
|
#
|
11
|
+
# To load the extension:
|
12
|
+
#
|
13
|
+
# Sequel.extension :named_timezones
|
14
|
+
#
|
11
15
|
# Let's say you have the database server in New York and the
|
12
16
|
# application server in Los Angeles. For historical reasons, data
|
13
17
|
# is stored in local New York time, but the application server only
|
@@ -2,6 +2,10 @@
|
|
2
2
|
# returns a cloned dataset that will never issue a query to the
|
3
3
|
# database. It implements the null object pattern for datasets.
|
4
4
|
#
|
5
|
+
# To load the extension:
|
6
|
+
#
|
7
|
+
# Sequel.extension :null_dataset
|
8
|
+
#
|
5
9
|
# The most common usage is probably in a method that must return
|
6
10
|
# a dataset, where the method knows the dataset shouldn't return
|
7
11
|
# anything. With standard Sequel, you'd probably just add a
|
@@ -1,6 +1,10 @@
|
|
1
1
|
# The pagination extension adds the Sequel::Dataset#paginate and #each_page methods,
|
2
2
|
# which return paginated (limited and offset) datasets with some helpful methods
|
3
3
|
# that make creating a paginated display easier.
|
4
|
+
#
|
5
|
+
# To load the extension:
|
6
|
+
#
|
7
|
+
# Sequel.extension :pagination
|
4
8
|
|
5
9
|
module Sequel
|
6
10
|
class Dataset
|
@@ -1,14 +1,9 @@
|
|
1
1
|
# The pg_array extension adds support for Sequel to handle
|
2
|
-
# PostgreSQL's
|
3
|
-
# single and multi-dimensional arrays. For integer and
|
4
|
-
# float arrays, it uses a JSON-based parser which is usually written in C
|
5
|
-
# and should be fairly fast. For string and decimal arrays, it uses
|
6
|
-
# a hand coded parser written in ruby that is unoptimized and probably
|
7
|
-
# slow.
|
2
|
+
# PostgreSQL's array types.
|
8
3
|
#
|
9
4
|
# This extension integrates with Sequel's native postgres adapter, so
|
10
5
|
# that when array fields are retrieved, they are parsed and returned
|
11
|
-
# as instances of Sequel::Postgres::PGArray
|
6
|
+
# as instances of Sequel::Postgres::PGArray. PGArray is
|
12
7
|
# a DelegateClass of Array, so it mostly acts like an array, but not
|
13
8
|
# completely (is_a?(Array) is false). If you want the actual array,
|
14
9
|
# you can call PGArray#to_a. This is done so that Sequel does not
|
@@ -24,9 +19,9 @@
|
|
24
19
|
#
|
25
20
|
# You can also provide a type, though it many cases it isn't necessary:
|
26
21
|
#
|
27
|
-
# array.pg_array(:varchar) # or :
|
22
|
+
# array.pg_array(:varchar) # or :integer, :"double precision", etc.
|
28
23
|
#
|
29
|
-
# So if you want to insert an array into an
|
24
|
+
# So if you want to insert an array into an integer[] database column:
|
30
25
|
#
|
31
26
|
# DB[:table].insert(:column=>[1, 2, 3].pg_array)
|
32
27
|
#
|
@@ -34,12 +29,31 @@
|
|
34
29
|
# probably want to modify the schema parsing/typecasting so that it
|
35
30
|
# recognizes and correctly handles the arrays, which you can do by:
|
36
31
|
#
|
37
|
-
# DB.
|
32
|
+
# DB.extension :pg_array
|
38
33
|
#
|
39
34
|
# If you are not using the native postgres adapter, you probably
|
40
35
|
# also want to use the typecast_on_load plugin in the model, and
|
41
36
|
# set it to typecast the array column(s) on load.
|
42
37
|
#
|
38
|
+
# This extension by default includes handlers for array types for
|
39
|
+
# all scalar types that the native postgres adapter handles. It
|
40
|
+
# also makes it easy to add support for other array types. In
|
41
|
+
# general, you just need to make sure that the scalar type is
|
42
|
+
# handled and has the appropriate converter installed in
|
43
|
+
# Sequel::Postgres::PG_TYPES under the appropriate type OID.
|
44
|
+
# Then you can call Sequel::Postgres::PGArray.register with
|
45
|
+
# the appropriate arguments to automatically set up a handler
|
46
|
+
# for the array type.
|
47
|
+
#
|
48
|
+
# For example, if you add support for a scalar custom type named
|
49
|
+
# foo which uses OID 1234, and you want to add support for the
|
50
|
+
# foo[] type, which uses type OID 4321, you need to do:
|
51
|
+
#
|
52
|
+
# Sequel::Postgres::PGArray.register('foo', :oid=>4321, :scalar_oid=>1234)
|
53
|
+
#
|
54
|
+
# Sequel::Postgres::PGArray.register has many additional options
|
55
|
+
# and should be able to handle most PostgreSQL array types.
|
56
|
+
#
|
43
57
|
# If you want an easy way to call PostgreSQL array functions and
|
44
58
|
# operators, look into the pg_array_ops extension.
|
45
59
|
#
|
@@ -73,12 +87,11 @@
|
|
73
87
|
|
74
88
|
require 'delegate'
|
75
89
|
require 'json'
|
90
|
+
Sequel.require 'adapters/utils/pg_types'
|
76
91
|
|
77
92
|
module Sequel
|
78
93
|
module Postgres
|
79
|
-
#
|
80
|
-
# just deal with parsing, so instances manually created from arrays
|
81
|
-
# can use this class correctly.
|
94
|
+
# Represents a PostgreSQL array column value.
|
82
95
|
class PGArray < DelegateClass(Array)
|
83
96
|
ARRAY = "ARRAY".freeze
|
84
97
|
DOUBLE_COLON = '::'.freeze
|
@@ -93,9 +106,80 @@ module Sequel
|
|
93
106
|
NULL = 'NULL'.freeze
|
94
107
|
QUOTE = '"'.freeze
|
95
108
|
|
109
|
+
# Hash of database array type name strings to symbols (e.g. 'double precision' => :float),
|
110
|
+
# used by the schema parsing.
|
111
|
+
ARRAY_TYPES = {}
|
112
|
+
|
113
|
+
# Registers an array type that the extension should handle. Makes a Database instance that
|
114
|
+
# has been extended with DatabaseMethods recognize the array type given and set up the
|
115
|
+
# appropriate typecasting. Also sets up automatic typecasting for the native postgres
|
116
|
+
# adapter, so that on retrieval, the values are automatically converted to PGArray instances.
|
117
|
+
# The db_type argument should be the exact database type used (as returned by the PostgreSQL
|
118
|
+
# format_type database function). Accepts the following options:
|
119
|
+
#
|
120
|
+
# :array_type :: The type to automatically cast the array to when literalizing the array.
|
121
|
+
# Usually the same as db_type.
|
122
|
+
# :converter :: A callable object (e.g. Proc), that is called with each element of the array
|
123
|
+
# (usually a string), and should return the appropriate typecasted object.
|
124
|
+
# :oid :: The PostgreSQL OID for the array type. This is used by the Sequel postgres adapter
|
125
|
+
# to set up automatic type conversion on retrieval from the database.
|
126
|
+
# :parser :: Can be set to :json to use the faster JSON-based parser. Note that the JSON-based
|
127
|
+
# parser can only correctly handle integers values correctly. It doesn't handle
|
128
|
+
# full precision for numeric types, and doesn't handle NaN/Infinity values for
|
129
|
+
# floating point types.
|
130
|
+
# :scalar_oid :: Should be the PostgreSQL OID for the scalar version of this array type. If given,
|
131
|
+
# automatically sets the :converter option by looking for scalar conversion
|
132
|
+
# proc.
|
133
|
+
# :scalar_typecast :: Should be a symbol indicating the typecast method that should be called on
|
134
|
+
# each element of the array, when a plain array is passed into a database
|
135
|
+
# typecast method. For example, for an array of integers, this could be set to
|
136
|
+
# :integer, so that the typecast_value_integer method is called on all of the
|
137
|
+
# array elements. Defaults to :type_symbol option.
|
138
|
+
# :type_symbol :: The base of the schema type symbol for this type. For example, if you provide
|
139
|
+
# :integer, Sequel will recognize this type as :integer_array during schema parsing.
|
140
|
+
# Defaults to the db_type argument.
|
141
|
+
# :typecast_method :: If given, specifies the :type_symbol option, but additionally causes no
|
142
|
+
# typecasting method to be created in the database. This should only be used
|
143
|
+
# to alias existing array types. For example, if there is an array type that can be
|
144
|
+
# treated just like an integer array, you can do :typecast_method=>:integer.
|
145
|
+
#
|
146
|
+
# If a block is given, it is treated as the :converter option.
|
147
|
+
def self.register(db_type, opts={}, &block)
|
148
|
+
db_type = db_type.to_s
|
149
|
+
typecast_method = opts[:typecast_method]
|
150
|
+
type = (typecast_method || opts[:type_symbol] || db_type).to_sym
|
151
|
+
|
152
|
+
if converter = opts[:converter]
|
153
|
+
raise Error, "can't provide both a block and :converter option to register" if block
|
154
|
+
else
|
155
|
+
converter = block
|
156
|
+
end
|
157
|
+
|
158
|
+
if soid = opts[:scalar_oid]
|
159
|
+
raise Error, "can't provide both a converter and :scalar_oid option to register" if converter
|
160
|
+
raise Error, "no conversion proc for :scalar_oid=>#{soid.inspect} in PG_TYPES" unless converter = PG_TYPES[soid]
|
161
|
+
end
|
162
|
+
|
163
|
+
array_type = (opts[:array_type] || db_type).to_s.dup.freeze
|
164
|
+
creator = (opts[:parser] == :json ? JSONCreator : Creator).new(array_type, converter)
|
165
|
+
|
166
|
+
ARRAY_TYPES[db_type] = :"#{type}_array"
|
167
|
+
|
168
|
+
DatabaseMethods.define_array_typecast_method(type, creator, opts.fetch(:scalar_typecast, type)) unless typecast_method
|
169
|
+
|
170
|
+
if oid = opts[:oid]
|
171
|
+
Sequel::Postgres::PG_TYPES[oid] = creator
|
172
|
+
end
|
173
|
+
|
174
|
+
nil
|
175
|
+
end
|
176
|
+
|
96
177
|
module DatabaseMethods
|
178
|
+
APOS = "'".freeze
|
179
|
+
DOUBLE_APOS = "''".freeze
|
97
180
|
ESCAPE_RE = /("|\\)/.freeze
|
98
181
|
ESCAPE_REPLACEMENT = '\\\\\1'.freeze
|
182
|
+
BLOB_RANGE = 1...-1
|
99
183
|
|
100
184
|
# Reset the conversion procs when extending the Database object, so
|
101
185
|
# it will pick up the array convertors. This is only done for the native
|
@@ -104,6 +188,15 @@ module Sequel
|
|
104
188
|
db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
|
105
189
|
end
|
106
190
|
|
191
|
+
# Define a private array typecasting method for the given type that uses
|
192
|
+
# the creator argument to do the type conversion.
|
193
|
+
def self.define_array_typecast_method(type, creator, scalar_typecast)
|
194
|
+
meth = :"typecast_value_#{type}_array"
|
195
|
+
scalar_typecast_method = :"typecast_value_#{scalar_typecast}"
|
196
|
+
define_method(meth){|v| typecast_value_pg_array(v, creator, scalar_typecast_method)}
|
197
|
+
private meth
|
198
|
+
end
|
199
|
+
|
107
200
|
# Handle arrays in bound variables
|
108
201
|
def bound_variable_arg(arg, conn)
|
109
202
|
case arg
|
@@ -116,17 +209,10 @@ module Sequel
|
|
116
209
|
end
|
117
210
|
end
|
118
211
|
|
119
|
-
# Make the column type detection
|
212
|
+
# Make the column type detection handle registered array types.
|
120
213
|
def schema_column_type(db_type)
|
121
|
-
|
122
|
-
|
123
|
-
:string_array
|
124
|
-
when /\A(integer|bigint|smallint)\[\]\z/io
|
125
|
-
:integer_array
|
126
|
-
when /\A(real|double precision)\[\]\z/io
|
127
|
-
:float_array
|
128
|
-
when /\Anumeric.*\[\]\z/io
|
129
|
-
:decimal_array
|
214
|
+
if (db_type =~ /\A([^(]+)(?:\([^(]+\))?\[\]\z/io) && (type = ARRAY_TYPES[$1])
|
215
|
+
type
|
130
216
|
else
|
131
217
|
super
|
132
218
|
end
|
@@ -139,6 +225,10 @@ module Sequel
|
|
139
225
|
case a
|
140
226
|
when Array
|
141
227
|
"{#{a.map{|i| bound_variable_array(i)}.join(COMMA)}}"
|
228
|
+
when Sequel::SQL::Blob
|
229
|
+
"\"#{literal(a)[BLOB_RANGE].gsub(DOUBLE_APOS, APOS).gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)}\""
|
230
|
+
when Sequel::LiteralString
|
231
|
+
a
|
142
232
|
when String
|
143
233
|
"\"#{a.gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)}\""
|
144
234
|
else
|
@@ -146,38 +236,55 @@ module Sequel
|
|
146
236
|
end
|
147
237
|
end
|
148
238
|
|
239
|
+
# Manually override the typecasting for timestamp array types so that
|
240
|
+
# they use the database's timezone instead of the global Sequel
|
241
|
+
# timezone.
|
242
|
+
def get_conversion_procs(conn)
|
243
|
+
procs = super
|
244
|
+
|
245
|
+
converter = method(:to_application_timestamp)
|
246
|
+
procs[1115] = Creator.new("timestamp without time zone", converter)
|
247
|
+
procs[1185] = Creator.new("timestamp with time zone", converter)
|
248
|
+
|
249
|
+
procs
|
250
|
+
end
|
251
|
+
|
149
252
|
# Given a value to typecast and the type of PGArray subclass:
|
150
|
-
# * If given a PGArray
|
151
|
-
# * If given
|
253
|
+
# * If given a PGArray with a matching array_type, use it directly.
|
254
|
+
# * If given a PGArray with a different array_type, return a PGArray
|
255
|
+
# with the creator's type.
|
256
|
+
# * If given an Array, create a new PGArray instance for it. This does not
|
257
|
+
# typecast all members of the array in ruby for performance reasons, but
|
258
|
+
# it will cast the array the appropriate database type when the array is
|
259
|
+
# literalized.
|
152
260
|
# * If given a String, call the parser for the subclass with it.
|
153
|
-
def typecast_value_pg_array(value,
|
261
|
+
def typecast_value_pg_array(value, creator, scalar_typecast_method=nil)
|
154
262
|
case value
|
155
263
|
when PGArray
|
156
|
-
value
|
264
|
+
if value.array_type != creator.type
|
265
|
+
PGArray.new(value.to_a, creator.type)
|
266
|
+
else
|
267
|
+
value
|
268
|
+
end
|
157
269
|
when Array
|
158
|
-
|
270
|
+
if scalar_typecast_method && respond_to?(scalar_typecast_method, true)
|
271
|
+
value = Sequel.recursive_map(value, method(scalar_typecast_method))
|
272
|
+
end
|
273
|
+
PGArray.new(value, creator.type)
|
159
274
|
when String
|
160
|
-
|
275
|
+
creator.call(value)
|
161
276
|
else
|
162
|
-
raise Sequel::InvalidValue, "invalid value for
|
277
|
+
raise Sequel::InvalidValue, "invalid value for array type: #{value.inspect}"
|
163
278
|
end
|
164
279
|
end
|
165
|
-
|
166
|
-
# Create typecast methods for the supported array types that
|
167
|
-
# delegate to typecast_value_pg_array with the related class.
|
168
|
-
%w'string integer float decimal'.each do |t|
|
169
|
-
class_eval("def typecast_value_#{t}_array(v) typecast_value_pg_array(v, PG#{t.capitalize}Array) end", __FILE__, __LINE__)
|
170
|
-
end
|
171
280
|
end
|
172
281
|
|
173
|
-
# PostgreSQL array parser that handles
|
174
|
-
# input. Because PostgreSQL arrays can contain objects that
|
175
|
-
# can be literalized in any number of ways, it is not possible
|
176
|
-
# to make a fully generic parser.
|
282
|
+
# PostgreSQL array parser that handles all types of input.
|
177
283
|
#
|
178
284
|
# This parser is very simple and unoptimized, but should still
|
179
285
|
# be O(n) where n is the length of the input string.
|
180
286
|
class Parser
|
287
|
+
# Current position in the input string.
|
181
288
|
attr_reader :pos
|
182
289
|
|
183
290
|
# Set the source for the input, and any converter callable
|
@@ -269,19 +376,58 @@ module Sequel
|
|
269
376
|
raise Sequel::Error, "array dimensions not balanced" unless @dimension == 0
|
270
377
|
@entries
|
271
378
|
end
|
272
|
-
end
|
379
|
+
end unless Sequel::Postgres.respond_to?(:parse_pg_array)
|
380
|
+
|
381
|
+
# Callable object that takes the input string and parses it using Parser.
|
382
|
+
class Creator
|
383
|
+
# The converter callable that is called on each member of the array
|
384
|
+
# to convert it to the correct type.
|
385
|
+
attr_reader :converter
|
273
386
|
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
387
|
+
# The database type to set on the PGArray instances returned.
|
388
|
+
attr_reader :type
|
389
|
+
|
390
|
+
# Set the type and optional converter callable that will be used.
|
391
|
+
def initialize(type, converter=nil)
|
392
|
+
@type = type
|
393
|
+
@converter = converter
|
394
|
+
end
|
395
|
+
|
396
|
+
if Sequel::Postgres.respond_to?(:parse_pg_array)
|
397
|
+
# Use sequel_pg's C-based parser if it has already been defined.
|
398
|
+
def call(string)
|
399
|
+
PGArray.new(Sequel::Postgres.parse_pg_array(string, @converter), @type)
|
400
|
+
end
|
401
|
+
else
|
402
|
+
# Parse the string using Parser with the appropriate
|
403
|
+
# converter, and return a PGArray with the appropriate database
|
404
|
+
# type.
|
405
|
+
def call(string)
|
406
|
+
PGArray.new(Parser.new(string, @converter).parse, @type)
|
407
|
+
end
|
408
|
+
end
|
278
409
|
end
|
279
410
|
|
280
|
-
#
|
281
|
-
|
282
|
-
|
411
|
+
# Callable object that takes the input string and parses it using.
|
412
|
+
# a JSON parser. This should be faster than the standard Creator,
|
413
|
+
# but only handles integer types correctly.
|
414
|
+
class JSONCreator < Creator
|
415
|
+
# Character conversion map mapping input strings to JSON replacements
|
416
|
+
SUBST = {'{'.freeze=>'['.freeze, '}'.freeze=>']'.freeze, 'NULL'.freeze=>'null'.freeze}
|
417
|
+
|
418
|
+
# Regular expression matching input strings to convert
|
419
|
+
SUBST_RE = %r[\{|\}|NULL].freeze
|
420
|
+
|
421
|
+
# Parse the input string by using a gsub to convert non-JSON characters to
|
422
|
+
# JSON, running it through a regular JSON parser. If a converter is used, a
|
423
|
+
# recursive map of the output is done to make sure that the entires in the
|
424
|
+
# correct type.
|
425
|
+
def call(string)
|
426
|
+
array = JSON.parse(string.gsub(SUBST_RE){|m| SUBST[m]})
|
427
|
+
array = Sequel.recursive_map(array, @converter) if @converter
|
428
|
+
PGArray.new(array, @type)
|
429
|
+
end
|
283
430
|
end
|
284
|
-
private_class_method :convert_item
|
285
431
|
|
286
432
|
# The type of this array. May be nil if no type was given. If a type
|
287
433
|
# is provided, the array is automatically casted to this type when
|
@@ -292,12 +438,9 @@ module Sequel
|
|
292
438
|
# Set the array to delegate to, and a database type.
|
293
439
|
def initialize(array, type=nil)
|
294
440
|
super(array)
|
295
|
-
|
441
|
+
@array_type = type
|
296
442
|
end
|
297
443
|
|
298
|
-
# The delegated object is always an array.
|
299
|
-
alias to_a __getobj__
|
300
|
-
|
301
444
|
# Append the array SQL to the given sql string.
|
302
445
|
# If the receiver has a type, add a cast to the
|
303
446
|
# database array type.
|
@@ -329,124 +472,32 @@ module Sequel
|
|
329
472
|
end
|
330
473
|
sql << CLOSE_BRACKET
|
331
474
|
end
|
332
|
-
end
|
333
|
-
|
334
|
-
# PGArray subclass handling integer and float types, using a fast JSON
|
335
|
-
# parser. Does not handle numeric/decimal types, since JSON does deal
|
336
|
-
# with arbitrary precision (see PGDecimalArray for that).
|
337
|
-
class PGNumericArray < PGArray
|
338
|
-
# Character conversion map mapping input strings to JSON replacements
|
339
|
-
SUBST = {'{'.freeze=>'['.freeze, '}'.freeze=>']'.freeze, 'NULL'.freeze=>'null'.freeze}
|
340
|
-
|
341
|
-
# Regular expression matching input strings to convert
|
342
|
-
SUBST_RE = %r[\{|\}|NULL].freeze
|
343
|
-
|
344
|
-
# Parse the input string by using a gsub to convert non-JSON characters to
|
345
|
-
# JSON, running it through a regular JSON parser, and the doing a recursive
|
346
|
-
# map over the output to make sure the entries are in the correct type (mostly,
|
347
|
-
# to make sure real/double precision database types are returned as float and
|
348
|
-
# not integer).
|
349
|
-
def self.parse(string, type=nil)
|
350
|
-
new(recursive_map(JSON.parse(string.gsub(SUBST_RE){|m| SUBST[m]})), type)
|
351
|
-
end
|
352
|
-
|
353
|
-
# Convert each item in the array to the correct type, handling multi-dimensional
|
354
|
-
# arrays.
|
355
|
-
def self.recursive_map(array)
|
356
|
-
array.map do |i|
|
357
|
-
if i.is_a?(Array)
|
358
|
-
recursive_map(i)
|
359
|
-
elsif i
|
360
|
-
convert_item(i)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
end
|
364
|
-
private_class_method :recursive_map
|
365
|
-
end
|
366
|
-
|
367
|
-
# PGArray subclass for decimal/numeric types. Uses the general
|
368
|
-
# parser as the JSON parser cannot handle arbitrary precision numbers.
|
369
|
-
class PGDecimalArray < PGArray
|
370
|
-
# Convert the item to a BigDecimal.
|
371
|
-
def self.convert_item(s)
|
372
|
-
BigDecimal.new(s.to_s)
|
373
|
-
end
|
374
|
-
private_class_method :convert_item
|
375
|
-
|
376
|
-
ARRAY_TYPE = 'decimal'.freeze
|
377
|
-
|
378
|
-
# Use the decimal type by default.
|
379
|
-
def array_type
|
380
|
-
super || ARRAY_TYPE
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
# PGArray subclass for handling real/double precision arrays.
|
385
|
-
class PGFloatArray < PGNumericArray
|
386
|
-
# Convert the item to a float.
|
387
|
-
def self.convert_item(s)
|
388
|
-
s.to_f
|
389
|
-
end
|
390
|
-
private_class_method :convert_item
|
391
|
-
|
392
|
-
ARRAY_TYPE = 'double precision'.freeze
|
393
|
-
|
394
|
-
# Use the double precision type by default.
|
395
|
-
def array_type
|
396
|
-
super || ARRAY_TYPE
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
# PGArray subclass for handling int2/int4/int8 arrays.
|
401
|
-
class PGIntegerArray < PGNumericArray
|
402
|
-
ARRAY_TYPE = 'int4'.freeze
|
403
|
-
|
404
|
-
# Use the int4 type by default.
|
405
|
-
def array_type
|
406
|
-
super || ARRAY_TYPE
|
407
|
-
end
|
408
|
-
end
|
409
475
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
PG_TYPES = {} unless defined?(PG_TYPES)
|
432
|
-
|
433
|
-
# Automatically convert the built-in numeric and text array
|
434
|
-
# types. to PGArray instances on retrieval if the native
|
435
|
-
# postgres adapter is used.
|
436
|
-
[ [1005, PGIntegerArray, 'int2'.freeze],
|
437
|
-
[1007, PGIntegerArray, 'int4'.freeze],
|
438
|
-
[1016, PGIntegerArray, 'int8'.freeze],
|
439
|
-
[1021, PGFloatArray, 'real'.freeze],
|
440
|
-
[1022, PGFloatArray, 'double precision'.freeze],
|
441
|
-
[1231, PGDecimalArray, 'numeric'.freeze],
|
442
|
-
[1009, PGStringArray, 'text'.freeze],
|
443
|
-
[1014, PGStringArray, 'char'.freeze],
|
444
|
-
[1015, PGStringArray, 'varchar'.freeze]
|
445
|
-
].each do |ftype, klass, type|
|
446
|
-
meth = klass.method(:parse)
|
447
|
-
PG_TYPES[ftype] = lambda{|s| meth.call(s, type)}
|
476
|
+
# Register all array types that this extension handles by default.
|
477
|
+
|
478
|
+
register('text', :oid=>1009, :type_symbol=>:string)
|
479
|
+
register('integer', :oid=>1007, :parser=>:json)
|
480
|
+
register('bigint', :oid=>1016, :parser=>:json, :scalar_typecast=>:integer)
|
481
|
+
register('numeric', :oid=>1231, :scalar_oid=>1700, :type_symbol=>:decimal)
|
482
|
+
register('double precision', :oid=>1022, :scalar_oid=>701, :type_symbol=>:float)
|
483
|
+
|
484
|
+
register('boolean', :oid=>1000, :scalar_oid=>16)
|
485
|
+
register('bytea', :oid=>1001, :scalar_oid=>17, :type_symbol=>:blob)
|
486
|
+
register('date', :oid=>1182, :scalar_oid=>1082)
|
487
|
+
register('time without time zone', :oid=>1183, :scalar_oid=>1083, :type_symbol=>:time)
|
488
|
+
register('timestamp without time zone', :oid=>1115, :scalar_oid=>1114, :type_symbol=>:datetime)
|
489
|
+
register('time with time zone', :oid=>1270, :scalar_oid=>1083, :type_symbol=>:time_timezone, :scalar_typecast=>:time)
|
490
|
+
register('timestamp with time zone', :oid=>1185, :scalar_oid=>1184, :type_symbol=>:datetime_timezone, :scalar_typecast=>:datetime)
|
491
|
+
|
492
|
+
register('smallint', :oid=>1005, :parser=>:json, :typecast_method=>:integer)
|
493
|
+
register('oid', :oid=>1028, :parser=>:json, :typecast_method=>:integer)
|
494
|
+
register('real', :oid=>1021, :scalar_oid=>701, :typecast_method=>:float)
|
495
|
+
register('character', :oid=>1014, :array_type=>:text, :typecast_method=>:string)
|
496
|
+
register('character varying', :oid=>1015, :typecast_method=>:string)
|
448
497
|
end
|
449
498
|
end
|
499
|
+
|
500
|
+
Database.register_extension(:pg_array, Postgres::PGArray::DatabaseMethods)
|
450
501
|
end
|
451
502
|
|
452
503
|
class Array
|