sequel 3.36.1 → 3.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +13 -0
  3. data/bin/sequel +12 -16
  4. data/doc/advanced_associations.rdoc +36 -67
  5. data/doc/association_basics.rdoc +11 -16
  6. data/doc/release_notes/3.37.0.txt +338 -0
  7. data/doc/schema_modification.rdoc +4 -0
  8. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  9. data/lib/sequel/adapters/jdbc/postgresql.rb +26 -8
  10. data/lib/sequel/adapters/mysql2.rb +4 -3
  11. data/lib/sequel/adapters/odbc/mssql.rb +2 -2
  12. data/lib/sequel/adapters/postgres.rb +4 -60
  13. data/lib/sequel/adapters/shared/mssql.rb +2 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +0 -5
  15. data/lib/sequel/adapters/shared/postgres.rb +68 -2
  16. data/lib/sequel/adapters/shared/sqlite.rb +17 -1
  17. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +12 -1
  18. data/lib/sequel/adapters/utils/pg_types.rb +76 -0
  19. data/lib/sequel/core.rb +13 -0
  20. data/lib/sequel/database/misc.rb +41 -1
  21. data/lib/sequel/database/schema_generator.rb +23 -10
  22. data/lib/sequel/database/schema_methods.rb +26 -4
  23. data/lib/sequel/dataset/graph.rb +2 -1
  24. data/lib/sequel/dataset/query.rb +62 -2
  25. data/lib/sequel/extensions/_pretty_table.rb +7 -3
  26. data/lib/sequel/extensions/arbitrary_servers.rb +5 -4
  27. data/lib/sequel/extensions/blank.rb +4 -0
  28. data/lib/sequel/extensions/columns_introspection.rb +13 -2
  29. data/lib/sequel/extensions/core_extensions.rb +6 -0
  30. data/lib/sequel/extensions/eval_inspect.rb +158 -0
  31. data/lib/sequel/extensions/inflector.rb +4 -0
  32. data/lib/sequel/extensions/looser_typecasting.rb +5 -4
  33. data/lib/sequel/extensions/migration.rb +4 -1
  34. data/lib/sequel/extensions/named_timezones.rb +4 -0
  35. data/lib/sequel/extensions/null_dataset.rb +4 -0
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pg_array.rb +219 -168
  38. data/lib/sequel/extensions/pg_array_ops.rb +7 -2
  39. data/lib/sequel/extensions/pg_auto_parameterize.rb +10 -4
  40. data/lib/sequel/extensions/pg_hstore.rb +3 -1
  41. data/lib/sequel/extensions/pg_hstore_ops.rb +7 -2
  42. data/lib/sequel/extensions/pg_inet.rb +28 -3
  43. data/lib/sequel/extensions/pg_interval.rb +192 -0
  44. data/lib/sequel/extensions/pg_json.rb +21 -9
  45. data/lib/sequel/extensions/pg_range.rb +487 -0
  46. data/lib/sequel/extensions/pg_range_ops.rb +122 -0
  47. data/lib/sequel/extensions/pg_statement_cache.rb +3 -2
  48. data/lib/sequel/extensions/pretty_table.rb +12 -1
  49. data/lib/sequel/extensions/query.rb +4 -0
  50. data/lib/sequel/extensions/query_literals.rb +6 -6
  51. data/lib/sequel/extensions/schema_dumper.rb +39 -38
  52. data/lib/sequel/extensions/select_remove.rb +4 -0
  53. data/lib/sequel/extensions/server_block.rb +3 -2
  54. data/lib/sequel/extensions/split_array_nil.rb +65 -0
  55. data/lib/sequel/extensions/sql_expr.rb +4 -0
  56. data/lib/sequel/extensions/string_date_time.rb +4 -0
  57. data/lib/sequel/extensions/thread_local_timezones.rb +9 -3
  58. data/lib/sequel/extensions/to_dot.rb +4 -0
  59. data/lib/sequel/model/associations.rb +150 -91
  60. data/lib/sequel/plugins/identity_map.rb +2 -2
  61. data/lib/sequel/plugins/list.rb +1 -0
  62. data/lib/sequel/plugins/many_through_many.rb +33 -32
  63. data/lib/sequel/plugins/nested_attributes.rb +11 -3
  64. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  65. data/lib/sequel/plugins/schema.rb +1 -1
  66. data/lib/sequel/sql.rb +14 -14
  67. data/lib/sequel/version.rb +2 -2
  68. data/spec/adapters/mysql_spec.rb +25 -0
  69. data/spec/adapters/postgres_spec.rb +572 -28
  70. data/spec/adapters/sqlite_spec.rb +16 -1
  71. data/spec/core/database_spec.rb +61 -2
  72. data/spec/core/dataset_spec.rb +92 -0
  73. data/spec/core/expression_filters_spec.rb +12 -0
  74. data/spec/extensions/arbitrary_servers_spec.rb +1 -1
  75. data/spec/extensions/boolean_readers_spec.rb +25 -25
  76. data/spec/extensions/eval_inspect_spec.rb +58 -0
  77. data/spec/extensions/json_serializer_spec.rb +0 -6
  78. data/spec/extensions/list_spec.rb +1 -1
  79. data/spec/extensions/looser_typecasting_spec.rb +7 -7
  80. data/spec/extensions/many_through_many_spec.rb +81 -0
  81. data/spec/extensions/nested_attributes_spec.rb +21 -4
  82. data/spec/extensions/pg_array_ops_spec.rb +1 -11
  83. data/spec/extensions/pg_array_spec.rb +181 -90
  84. data/spec/extensions/pg_auto_parameterize_spec.rb +3 -3
  85. data/spec/extensions/pg_hstore_spec.rb +1 -3
  86. data/spec/extensions/pg_inet_spec.rb +6 -1
  87. data/spec/extensions/pg_interval_spec.rb +73 -0
  88. data/spec/extensions/pg_json_spec.rb +5 -9
  89. data/spec/extensions/pg_range_ops_spec.rb +49 -0
  90. data/spec/extensions/pg_range_spec.rb +372 -0
  91. data/spec/extensions/pg_statement_cache_spec.rb +1 -2
  92. data/spec/extensions/query_literals_spec.rb +1 -2
  93. data/spec/extensions/schema_dumper_spec.rb +48 -89
  94. data/spec/extensions/serialization_spec.rb +1 -5
  95. data/spec/extensions/server_block_spec.rb +2 -2
  96. data/spec/extensions/spec_helper.rb +12 -2
  97. data/spec/extensions/split_array_nil_spec.rb +24 -0
  98. data/spec/integration/associations_test.rb +4 -4
  99. data/spec/integration/database_test.rb +2 -2
  100. data/spec/integration/dataset_test.rb +4 -4
  101. data/spec/integration/eager_loader_test.rb +6 -6
  102. data/spec/integration/plugin_test.rb +2 -2
  103. data/spec/integration/spec_helper.rb +2 -2
  104. data/spec/model/association_reflection_spec.rb +5 -0
  105. data/spec/model/associations_spec.rb +156 -49
  106. data/spec/model/eager_loading_spec.rb +137 -2
  107. data/spec/model/model_spec.rb +10 -10
  108. 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 use it, you should extend the database with the
4
- # Sequel::LooserTypecasting module after loading the extension:
3
+ # Kernel.Integer. To load the extension into the database:
5
4
  #
6
- # Sequel.extension :looser_typecasting
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 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.
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 subclasses. PGArray is
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 :int4, :"double precision", etc.
22
+ # array.pg_array(:varchar) # or :integer, :"double precision", etc.
28
23
  #
29
- # So if you want to insert an array into an int4[] database column:
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.extend Sequel::Postgres::PGArray::DatabaseMethods
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
- # 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.
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 deal with string and numeric array types.
212
+ # Make the column type detection handle registered array types.
120
213
  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
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, just return the value (even if different subclass)
151
- # * If given an Array, create a new instance of the subclass
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, klass)
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
- klass.new(value)
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
- klass.parse(value)
275
+ creator.call(value)
161
276
  else
162
- raise Sequel::InvalidValue, "invalid value for #{klass}: #{value.inspect}"
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 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.
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
- # 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)
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
- # Return the item as-is by default, making conversion a no-op.
281
- def self.convert_item(s)
282
- s
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
- self.array_type = type
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
- # 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)}
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