sequel 3.36.1 → 3.37.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 +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
|