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.
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