sequel 3.33.0 → 3.34.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,79 @@
|
|
1
|
+
# The pretty_table extension adds Sequel::Dataset#print and the
|
2
|
+
# Sequel::PrettyTable class for creating nice-looking plain-text
|
3
|
+
# tables.
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
module PrettyTable
|
7
|
+
# Prints nice-looking plain-text tables via puts
|
8
|
+
#
|
9
|
+
# +--+-------+
|
10
|
+
# |id|name |
|
11
|
+
# |--+-------|
|
12
|
+
# |1 |fasdfas|
|
13
|
+
# |2 |test |
|
14
|
+
# +--+-------+
|
15
|
+
def self.print(records, columns=nil)
|
16
|
+
puts string(records, columns)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Return the string that #print will print via puts.
|
20
|
+
def self.string(records, columns = nil) # records is an array of hashes
|
21
|
+
columns ||= records.first.keys.sort_by{|x|x.to_s}
|
22
|
+
sizes = column_sizes(records, columns)
|
23
|
+
sep_line = separator_line(columns, sizes)
|
24
|
+
|
25
|
+
array = [sep_line, header_line(columns, sizes), sep_line]
|
26
|
+
records.each {|r| array << data_line(columns, sizes, r)}
|
27
|
+
array << sep_line
|
28
|
+
array.join("\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
### Private Module Methods ###
|
32
|
+
|
33
|
+
# Hash of the maximum size of the value for each column
|
34
|
+
def self.column_sizes(records, columns) # :nodoc:
|
35
|
+
sizes = Hash.new {0}
|
36
|
+
columns.each do |c|
|
37
|
+
s = c.to_s.size
|
38
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
39
|
+
end
|
40
|
+
records.each do |r|
|
41
|
+
columns.each do |c|
|
42
|
+
s = r[c].to_s.size
|
43
|
+
sizes[c.to_sym] = s if s > sizes[c.to_sym]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
sizes
|
47
|
+
end
|
48
|
+
|
49
|
+
# String for each data line
|
50
|
+
def self.data_line(columns, sizes, record) # :nodoc:
|
51
|
+
'|' << columns.map {|c| format_cell(sizes[c], record[c])}.join('|') << '|'
|
52
|
+
end
|
53
|
+
|
54
|
+
# Format the value so it takes up exactly size characters
|
55
|
+
def self.format_cell(size, v) # :nodoc:
|
56
|
+
case v
|
57
|
+
when Bignum, Fixnum
|
58
|
+
"%#{size}d" % v
|
59
|
+
when Float
|
60
|
+
"%#{size}g" % v
|
61
|
+
else
|
62
|
+
"%-#{size}s" % v.to_s
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# String for header line
|
67
|
+
def self.header_line(columns, sizes) # :nodoc:
|
68
|
+
'|' << columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') << '|'
|
69
|
+
end
|
70
|
+
|
71
|
+
# String for separtor line
|
72
|
+
def self.separator_line(columns, sizes) # :nodoc:
|
73
|
+
'+' << columns.map {|c| '-' * sizes[c]}.join('+') << '+'
|
74
|
+
end
|
75
|
+
|
76
|
+
private_class_method :column_sizes, :data_line, :format_cell, :header_line, :separator_line
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
@@ -1,3 +1,13 @@
|
|
1
|
+
# These are extensions to core classes that Sequel enables by default.
|
2
|
+
# They make using Sequel's DSL easier by adding methods to Array,
|
3
|
+
# Hash, String, and Symbol to add methods that return Sequel
|
4
|
+
# expression objects.
|
5
|
+
|
6
|
+
# This extension loads the core extensions.
|
7
|
+
def Sequel.core_extensions?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
1
11
|
# Sequel extends +Array+ to add methods to implement the SQL DSL.
|
2
12
|
# Most of these methods require that the array not be empty and that it
|
3
13
|
# must consist solely of other arrays that have exactly two elements.
|
@@ -8,7 +18,7 @@ class Array
|
|
8
18
|
# ~[[:a, true]] # SQL: a IS NOT TRUE
|
9
19
|
# ~[[:a, 1], [:b, [2, 3]]] # SQL: a != 1 OR b NOT IN (2, 3)
|
10
20
|
def ~
|
11
|
-
|
21
|
+
Sequel.~(self)
|
12
22
|
end
|
13
23
|
|
14
24
|
# +true+ if the array is not empty and all of its elements are
|
@@ -21,6 +31,7 @@ class Array
|
|
21
31
|
# [[:b]].to_a.all_two_pairs? # => false
|
22
32
|
# [[:a, 1]].to_a.all_two_pairs? # => true
|
23
33
|
def all_two_pairs?
|
34
|
+
warn('Array#all_two_pairs? is deprecated and will be removed in Sequel 3.35.0')
|
24
35
|
!empty? && all?{|i| (Array === i) && (i.length == 2)}
|
25
36
|
end
|
26
37
|
|
@@ -59,7 +70,7 @@ class Array
|
|
59
70
|
# [[:a, true]].sql_expr # SQL: a IS TRUE
|
60
71
|
# [[:a, 1], [:b, [2, 3]]].sql_expr # SQL: a = 1 AND b IN (2, 3)
|
61
72
|
def sql_expr
|
62
|
-
|
73
|
+
Sequel.expr(self)
|
63
74
|
end
|
64
75
|
|
65
76
|
# Return a <tt>Sequel::SQL::BooleanExpression</tt> created from this array, matching none
|
@@ -68,7 +79,7 @@ class Array
|
|
68
79
|
# [[:a, true]].sql_negate # SQL: a IS NOT TRUE
|
69
80
|
# [[:a, 1], [:b, [2, 3]]].sql_negate # SQL: a != 1 AND b NOT IN (2, 3)
|
70
81
|
def sql_negate
|
71
|
-
|
82
|
+
Sequel.negate(self)
|
72
83
|
end
|
73
84
|
|
74
85
|
# Return a <tt>Sequel::SQL::BooleanExpression</tt> created from this array, matching any of the
|
@@ -77,10 +88,10 @@ class Array
|
|
77
88
|
# [[:a, true]].sql_or # SQL: a IS TRUE
|
78
89
|
# [[:a, 1], [:b, [2, 3]]].sql_or # SQL: a = 1 OR b IN (2, 3)
|
79
90
|
def sql_or
|
80
|
-
|
91
|
+
Sequel.or(self)
|
81
92
|
end
|
82
93
|
|
83
|
-
# Return a <tt>Sequel::SQL::
|
94
|
+
# Return a <tt>Sequel::SQL::StringExpression</tt> representing an SQL string made up of the
|
84
95
|
# concatenation of this array's elements. If an argument is passed
|
85
96
|
# it is used in between each element of the array in the SQL
|
86
97
|
# concatenation.
|
@@ -90,20 +101,14 @@ class Array
|
|
90
101
|
# [:a, 'b'].sql_string_join # SQL: a || 'b'
|
91
102
|
# ['a', :b].sql_string_join(' ') # SQL: 'a' || ' ' || b
|
92
103
|
def sql_string_join(joiner=nil)
|
93
|
-
|
94
|
-
args = zip([joiner]*length).flatten
|
95
|
-
args.pop
|
96
|
-
else
|
97
|
-
args = self
|
98
|
-
end
|
99
|
-
args = args.collect{|a| [Symbol, ::Sequel::SQL::Expression, ::Sequel::LiteralString, TrueClass, FalseClass, NilClass].any?{|c| a.is_a?(c)} ? a : a.to_s}
|
100
|
-
::Sequel::SQL::StringExpression.new(:'||', *args)
|
104
|
+
Sequel.join(self, joiner)
|
101
105
|
end
|
102
106
|
|
103
107
|
private
|
104
108
|
|
105
109
|
# Raise an error if this array is not made up all two element arrays, otherwise create a <tt>Sequel::SQL::BooleanExpression</tt> from this array.
|
106
110
|
def sql_expr_if_all_two_pairs(*args)
|
111
|
+
warn('Array#sql_expr_if_all_two_pairs? is deprecated and will be removed in Sequel 3.35.0')
|
107
112
|
raise(Sequel::Error, 'Not all elements of the array are arrays of size 2, so it cannot be converted to an SQL expression') unless all_two_pairs?
|
108
113
|
::Sequel::SQL::BooleanExpression.from_value_pairs(self, *args)
|
109
114
|
end
|
@@ -184,6 +184,10 @@ module Sequel
|
|
184
184
|
@actions << [:alter_table, table, MigrationAlterTableReverser.new.reverse(&block)]
|
185
185
|
end
|
186
186
|
|
187
|
+
def create_join_table(*args)
|
188
|
+
@actions << [:drop_join_table, *args]
|
189
|
+
end
|
190
|
+
|
187
191
|
def create_table(*args)
|
188
192
|
@actions << [:drop_table, args.first]
|
189
193
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# The null_dataset extension adds the Dataset#nullify method, which
|
2
|
+
# returns a cloned dataset that will never issue a query to the
|
3
|
+
# database. It implements the null object pattern for datasets.
|
4
|
+
#
|
5
|
+
# The most common usage is probably in a method that must return
|
6
|
+
# a dataset, where the method knows the dataset shouldn't return
|
7
|
+
# anything. With standard Sequel, you'd probably just add a
|
8
|
+
# WHERE condition that is always false, but that still results
|
9
|
+
# in a query being sent to the database, and can be overridden
|
10
|
+
# using #unfiltered, the OR operator, or a UNION.
|
11
|
+
#
|
12
|
+
# Usage:
|
13
|
+
#
|
14
|
+
# ds = DB[:items].nullify.where(:a=>:b).select(:c)
|
15
|
+
# ds.sql # => "SELECT c FROM items WHERE (a = b)"
|
16
|
+
# ds.all # => [] # no query sent to the database
|
17
|
+
#
|
18
|
+
# Note that there is one case where a null dataset will sent
|
19
|
+
# a query to the database. If you call #columns on a nulled
|
20
|
+
# dataset and the dataset doesn't have an already cached
|
21
|
+
# version of the columns, it will create a new dataset with
|
22
|
+
# the same options to get the columns.
|
23
|
+
|
24
|
+
module Sequel
|
25
|
+
class Dataset
|
26
|
+
module NullDataset
|
27
|
+
# Create a new dataset from the dataset (which won't
|
28
|
+
# be nulled) to get the columns if they aren't already cached.
|
29
|
+
def columns
|
30
|
+
@columns ||= db.dataset(@opts).columns
|
31
|
+
end
|
32
|
+
|
33
|
+
# Return 0 without sending a database query.
|
34
|
+
def delete
|
35
|
+
0
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return self without sending a database query, never yielding.
|
39
|
+
def each
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return nil without sending a database query, never yielding.
|
44
|
+
def fetch_rows(sql)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return nil without sending a database query.
|
49
|
+
def insert(*)
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return nil without sending a database query.
|
54
|
+
def truncate
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return 0 without sending a database query.
|
59
|
+
def update(v={})
|
60
|
+
0
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
# Return nil without sending a database query.
|
66
|
+
def _import(columns, values, opts)
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Just in case these are called directly by some internal code,
|
73
|
+
# make them noops. There's nothing we can do if the db
|
74
|
+
# is accessed directly to make a change, though.
|
75
|
+
(%w'_ddl _dui _insert' << '').each do |m|
|
76
|
+
class_eval("private; def execute#{m}(sql, opts={}) end", __FILE__, __LINE__)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return a cloned nullified dataset.
|
81
|
+
def nullify
|
82
|
+
clone.nullify!
|
83
|
+
end
|
84
|
+
|
85
|
+
# Nullify the current dataset
|
86
|
+
def nullify!
|
87
|
+
extend NullDataset
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,460 @@
|
|
1
|
+
# The pg_array extension adds support for Sequel to handle
|
2
|
+
# PostgreSQL's string and numeric array types. It supports both
|
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.
|
8
|
+
#
|
9
|
+
# This extension integrates with Sequel's native postgres adapter, so
|
10
|
+
# that when array fields are retrieved, they are parsed and returned
|
11
|
+
# as instances of Sequel::Postgres::PGArray subclasses. PGArray is
|
12
|
+
# a DelegateClass of Array, so it mostly acts like an array, but not
|
13
|
+
# completely (is_a?(Array) is false). If you want the actual array,
|
14
|
+
# you can call PGArray#to_a. This is done so that Sequel does not
|
15
|
+
# treat a PGArray like an Array by default, which would cause issues.
|
16
|
+
#
|
17
|
+
# In addition to the parsers, this extension comes with literalizers
|
18
|
+
# for PGArray using the standard Sequel literalization callbacks, so
|
19
|
+
# they work with on all adapters.
|
20
|
+
#
|
21
|
+
# To turn an existing Array into a PGArray:
|
22
|
+
#
|
23
|
+
# array.pg_array
|
24
|
+
#
|
25
|
+
# You can also provide a type, though it many cases it isn't necessary:
|
26
|
+
#
|
27
|
+
# array.pg_array(:varchar) # or :int4, :"double precision", etc.
|
28
|
+
#
|
29
|
+
# So if you want to insert an array into an int4[] database column:
|
30
|
+
#
|
31
|
+
# DB[:table].insert(:column=>[1, 2, 3].pg_array)
|
32
|
+
#
|
33
|
+
# If you would like to use PostgreSQL arrays in your model objects, you
|
34
|
+
# probably want to modify the schema parsing/typecasting so that it
|
35
|
+
# recognizes and correctly handles the arrays, which you can do by:
|
36
|
+
#
|
37
|
+
# DB.extend Sequel::Postgres::PGArray::DatabaseMethods
|
38
|
+
#
|
39
|
+
# If you are not using the native postgres adapter, you probably
|
40
|
+
# also want to use the typecast_on_load plugin in the model, and
|
41
|
+
# set it to typecast the array column(s) on load.
|
42
|
+
#
|
43
|
+
# If you want an easy way to call PostgreSQL array functions and
|
44
|
+
# operators, look into the pg_array_ops extension.
|
45
|
+
#
|
46
|
+
# This extension requires both the json and delegate libraries.
|
47
|
+
#
|
48
|
+
# == Additional License
|
49
|
+
#
|
50
|
+
# PGArray::Parser code was translated from Javascript code in the
|
51
|
+
# node-postgres project and has the following additional license:
|
52
|
+
#
|
53
|
+
# Copyright (c) 2010 Brian Carlson (brian.m.carlson@gmail.com)
|
54
|
+
#
|
55
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
56
|
+
# a copy of this software and associated documentation files (the
|
57
|
+
# "Software"), to deal in the Software without restriction, including
|
58
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
59
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
60
|
+
# permit persons to whom the Software is furnished to do so, subject
|
61
|
+
# to the following conditions:
|
62
|
+
#
|
63
|
+
# The above copyright notice and this permission notice shall be included
|
64
|
+
# in all copies or substantial portions of the Software.
|
65
|
+
#
|
66
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
67
|
+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
68
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
69
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
70
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
71
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
72
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
73
|
+
|
74
|
+
require 'delegate'
|
75
|
+
require 'json'
|
76
|
+
|
77
|
+
module Sequel
|
78
|
+
module Postgres
|
79
|
+
# Base class for the PostgreSQL array types. Subclasses generally
|
80
|
+
# just deal with parsing, so instances manually created from arrays
|
81
|
+
# can use this class correctly.
|
82
|
+
class PGArray < DelegateClass(Array)
|
83
|
+
ARRAY = "ARRAY".freeze
|
84
|
+
DOUBLE_COLON = '::'.freeze
|
85
|
+
EMPTY_BRACKET = '[]'.freeze
|
86
|
+
OPEN_BRACKET = '['.freeze
|
87
|
+
CLOSE_BRACKET = ']'.freeze
|
88
|
+
COMMA = ','.freeze
|
89
|
+
BACKSLASH = '\\'.freeze
|
90
|
+
EMPTY_STRING = ''.freeze
|
91
|
+
OPEN_BRACE = '{'.freeze
|
92
|
+
CLOSE_BRACE = '}'.freeze
|
93
|
+
NULL = 'NULL'.freeze
|
94
|
+
QUOTE = '"'.freeze
|
95
|
+
|
96
|
+
module DatabaseMethods
|
97
|
+
ESCAPE_RE = /("|\\)/.freeze
|
98
|
+
ESCAPE_REPLACEMENT = '\\\\\1'.freeze
|
99
|
+
|
100
|
+
# Reset the conversion procs when extending the Database object, so
|
101
|
+
# it will pick up the array convertors. This is only done for the native
|
102
|
+
# postgres adapter.
|
103
|
+
def self.extended(db)
|
104
|
+
db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Handle arrays in bound variables
|
108
|
+
def bound_variable_arg(arg, conn)
|
109
|
+
case arg
|
110
|
+
when PGArray
|
111
|
+
bound_variable_array(arg.to_a)
|
112
|
+
when Array
|
113
|
+
bound_variable_array(arg)
|
114
|
+
else
|
115
|
+
super
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Make the column type detection deal with string and numeric array types.
|
120
|
+
def schema_column_type(db_type)
|
121
|
+
case db_type
|
122
|
+
when /\A(character( varying)?|text).*\[\]\z/io
|
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
|
130
|
+
else
|
131
|
+
super
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
# Format arrays used in bound variables.
|
138
|
+
def bound_variable_array(a)
|
139
|
+
case a
|
140
|
+
when Array
|
141
|
+
"{#{a.map{|i| bound_variable_array(i)}.join(COMMA)}}"
|
142
|
+
when String
|
143
|
+
"\"#{a.gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)}\""
|
144
|
+
else
|
145
|
+
literal(a)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Given a value to typecast and the type of PGArray subclass:
|
150
|
+
# * If given a PGArray, just return the value (even if different subclass)
|
151
|
+
# * If given an Array, create a new instance of the subclass
|
152
|
+
# * If given a String, call the parser for the subclass with it.
|
153
|
+
def typecast_value_pg_array(value, klass)
|
154
|
+
case value
|
155
|
+
when PGArray
|
156
|
+
value
|
157
|
+
when Array
|
158
|
+
klass.new(value)
|
159
|
+
when String
|
160
|
+
klass.parse(value)
|
161
|
+
else
|
162
|
+
raise Sequel::InvalidValue, "invalid value for #{klass}: #{value.inspect}"
|
163
|
+
end
|
164
|
+
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
|
+
end
|
172
|
+
|
173
|
+
# PostgreSQL array parser that handles both text and numeric
|
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.
|
177
|
+
#
|
178
|
+
# This parser is very simple and unoptimized, but should still
|
179
|
+
# be O(n) where n is the length of the input string.
|
180
|
+
class Parser
|
181
|
+
attr_reader :pos
|
182
|
+
|
183
|
+
# Set the source for the input, and any converter callable
|
184
|
+
# to call with objects to be created. For nested parsers
|
185
|
+
# the source may contain text after the end current parse,
|
186
|
+
# which will be ignored.
|
187
|
+
def initialize(source, converter=nil)
|
188
|
+
@source = source
|
189
|
+
@source_length = source.length
|
190
|
+
@converter = converter
|
191
|
+
@pos = -1
|
192
|
+
@entries = []
|
193
|
+
@recorded = ""
|
194
|
+
@dimension = 0
|
195
|
+
end
|
196
|
+
|
197
|
+
# Return 2 objects, whether the next character in the input
|
198
|
+
# was escaped with a backslash, and what the next character is.
|
199
|
+
def next_char
|
200
|
+
@pos += 1
|
201
|
+
if (c = @source[@pos..@pos]) == BACKSLASH
|
202
|
+
@pos += 1
|
203
|
+
[true, @source[@pos..@pos]]
|
204
|
+
else
|
205
|
+
[false, c]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Add a new character to the buffer of recorded characters.
|
210
|
+
def record(c)
|
211
|
+
@recorded << c
|
212
|
+
end
|
213
|
+
|
214
|
+
# Take the buffer of recorded characters and add it to the array
|
215
|
+
# of entries, and use a new buffer for recorded characters.
|
216
|
+
def new_entry(include_empty=false)
|
217
|
+
if !@recorded.empty? || include_empty
|
218
|
+
entry = @recorded
|
219
|
+
if entry == NULL && !include_empty
|
220
|
+
entry = nil
|
221
|
+
elsif @converter
|
222
|
+
entry = @converter.call(entry)
|
223
|
+
end
|
224
|
+
@entries.push(entry)
|
225
|
+
@recorded = ""
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Parse the input character by character, returning an array
|
230
|
+
# of parsed (and potentially converted) objects.
|
231
|
+
def parse(nested=false)
|
232
|
+
# quote sets whether we are inside of a quoted string.
|
233
|
+
quote = false
|
234
|
+
until @pos >= @source_length
|
235
|
+
escaped, char = next_char
|
236
|
+
if char == OPEN_BRACE && !quote
|
237
|
+
@dimension += 1
|
238
|
+
if (@dimension > 1)
|
239
|
+
# Multi-dimensional array encounter, use a subparser
|
240
|
+
# to parse the next level down.
|
241
|
+
subparser = self.class.new(@source[@pos..-1], @converter)
|
242
|
+
@entries.push(subparser.parse(true))
|
243
|
+
@pos += subparser.pos - 1
|
244
|
+
end
|
245
|
+
elsif char == CLOSE_BRACE && !quote
|
246
|
+
@dimension -= 1
|
247
|
+
if (@dimension == 0)
|
248
|
+
new_entry
|
249
|
+
# Exit early if inside a subparser, since the
|
250
|
+
# text after parsing the current level should be
|
251
|
+
# ignored as it is handled by the parent parser.
|
252
|
+
return @entries if nested
|
253
|
+
end
|
254
|
+
elsif char == QUOTE && !escaped
|
255
|
+
# If already inside the quoted string, this is the
|
256
|
+
# ending quote, so add the entry. Otherwise, this
|
257
|
+
# is the opening quote, so set the quote flag.
|
258
|
+
new_entry(true) if quote
|
259
|
+
quote = !quote
|
260
|
+
elsif char == COMMA && !quote
|
261
|
+
# If not inside a string and a comma occurs, it indicates
|
262
|
+
# the end of the entry, so add the entry.
|
263
|
+
new_entry
|
264
|
+
else
|
265
|
+
# Add the character to the recorded character buffer.
|
266
|
+
record(char)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
raise Sequel::Error, "array dimensions not balanced" unless @dimension == 0
|
270
|
+
@entries
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Parse the string using the generalized parser, setting the type
|
275
|
+
# if given.
|
276
|
+
def self.parse(string, type=nil)
|
277
|
+
new(Parser.new(string, method(:convert_item)).parse, type)
|
278
|
+
end
|
279
|
+
|
280
|
+
# Return the item as-is by default, making conversion a no-op.
|
281
|
+
def self.convert_item(s)
|
282
|
+
s
|
283
|
+
end
|
284
|
+
private_class_method :convert_item
|
285
|
+
|
286
|
+
# The type of this array. May be nil if no type was given. If a type
|
287
|
+
# is provided, the array is automatically casted to this type when
|
288
|
+
# literalizing. This type is the underlying type, not the array type
|
289
|
+
# itself, so for an int4[] database type, it should be :int4 or 'int4'
|
290
|
+
attr_accessor :array_type
|
291
|
+
|
292
|
+
# Set the array to delegate to, and a database type.
|
293
|
+
def initialize(array, type=nil)
|
294
|
+
super(array)
|
295
|
+
self.array_type = type
|
296
|
+
end
|
297
|
+
|
298
|
+
# The delegated object is always an array.
|
299
|
+
alias to_a __getobj__
|
300
|
+
|
301
|
+
# Append the array SQL to the given sql string.
|
302
|
+
# If the receiver has a type, add a cast to the
|
303
|
+
# database array type.
|
304
|
+
def sql_literal_append(ds, sql)
|
305
|
+
sql << ARRAY
|
306
|
+
_literal_append(sql, ds, to_a)
|
307
|
+
if at = array_type
|
308
|
+
sql << DOUBLE_COLON << at.to_s << EMPTY_BRACKET
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
private
|
313
|
+
|
314
|
+
# Recursive method that handles multi-dimensional
|
315
|
+
# arrays, surrounding each with [] and interspersing
|
316
|
+
# entries with ,.
|
317
|
+
def _literal_append(sql, ds, array)
|
318
|
+
sql << OPEN_BRACKET
|
319
|
+
comma = false
|
320
|
+
commas = COMMA
|
321
|
+
array.each do |i|
|
322
|
+
sql << commas if comma
|
323
|
+
if i.is_a?(Array)
|
324
|
+
_literal_append(sql, ds, i)
|
325
|
+
else
|
326
|
+
ds.literal_append(sql, i)
|
327
|
+
end
|
328
|
+
comma = true
|
329
|
+
end
|
330
|
+
sql << CLOSE_BRACKET
|
331
|
+
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
|
+
|
410
|
+
# PGArray subclass for handling char/varchar/text arrays.
|
411
|
+
class PGStringArray < PGArray
|
412
|
+
CHAR = 'char'.freeze
|
413
|
+
VARCHAR = 'varchar'.freeze
|
414
|
+
TEXT = 'text'.freeze
|
415
|
+
|
416
|
+
# By default, use a text array. If char is given without
|
417
|
+
# a size, use varchar instead, as otherwise Postgres assumes
|
418
|
+
# length of 1, which is likely to cause data loss.
|
419
|
+
def array_type
|
420
|
+
case (c = super)
|
421
|
+
when nil
|
422
|
+
TEXT
|
423
|
+
when CHAR, :char
|
424
|
+
VARCHAR
|
425
|
+
else
|
426
|
+
c
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
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)}
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
class Array
|
453
|
+
# Return a PGArray proxy to the receiver, using a
|
454
|
+
# specific database type if given. This is mostly useful
|
455
|
+
# as a short cut for creating PGArray objects that didn't
|
456
|
+
# come from the database.
|
457
|
+
def pg_array(type=nil)
|
458
|
+
Sequel::Postgres::PGArray.new(self, type)
|
459
|
+
end
|
460
|
+
end
|