sequel 3.37.0 → 3.38.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 (129) hide show
  1. data/CHANGELOG +56 -0
  2. data/README.rdoc +82 -58
  3. data/Rakefile +6 -5
  4. data/bin/sequel +1 -1
  5. data/doc/active_record.rdoc +67 -52
  6. data/doc/advanced_associations.rdoc +33 -48
  7. data/doc/association_basics.rdoc +41 -51
  8. data/doc/cheat_sheet.rdoc +21 -21
  9. data/doc/core_extensions.rdoc +374 -0
  10. data/doc/dataset_basics.rdoc +5 -5
  11. data/doc/dataset_filtering.rdoc +47 -43
  12. data/doc/mass_assignment.rdoc +1 -1
  13. data/doc/migration.rdoc +4 -5
  14. data/doc/model_hooks.rdoc +3 -3
  15. data/doc/object_model.rdoc +31 -25
  16. data/doc/opening_databases.rdoc +19 -5
  17. data/doc/prepared_statements.rdoc +2 -2
  18. data/doc/querying.rdoc +109 -52
  19. data/doc/reflection.rdoc +6 -6
  20. data/doc/release_notes/3.38.0.txt +234 -0
  21. data/doc/schema_modification.rdoc +22 -13
  22. data/doc/sharding.rdoc +8 -9
  23. data/doc/sql.rdoc +154 -112
  24. data/doc/testing.rdoc +47 -7
  25. data/doc/thread_safety.rdoc +1 -1
  26. data/doc/transactions.rdoc +1 -1
  27. data/doc/validations.rdoc +1 -1
  28. data/doc/virtual_rows.rdoc +29 -43
  29. data/lib/sequel/adapters/do/postgres.rb +1 -4
  30. data/lib/sequel/adapters/jdbc.rb +14 -3
  31. data/lib/sequel/adapters/jdbc/db2.rb +9 -0
  32. data/lib/sequel/adapters/jdbc/derby.rb +41 -4
  33. data/lib/sequel/adapters/jdbc/jtds.rb +11 -0
  34. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -6
  35. data/lib/sequel/adapters/mock.rb +10 -4
  36. data/lib/sequel/adapters/postgres.rb +1 -28
  37. data/lib/sequel/adapters/shared/mssql.rb +23 -13
  38. data/lib/sequel/adapters/shared/postgres.rb +46 -0
  39. data/lib/sequel/adapters/swift.rb +21 -13
  40. data/lib/sequel/adapters/swift/mysql.rb +1 -0
  41. data/lib/sequel/adapters/swift/postgres.rb +4 -5
  42. data/lib/sequel/adapters/swift/sqlite.rb +2 -1
  43. data/lib/sequel/adapters/tinytds.rb +14 -2
  44. data/lib/sequel/adapters/utils/pg_types.rb +5 -0
  45. data/lib/sequel/core.rb +29 -17
  46. data/lib/sequel/database/query.rb +1 -1
  47. data/lib/sequel/database/schema_generator.rb +3 -0
  48. data/lib/sequel/dataset/actions.rb +5 -6
  49. data/lib/sequel/dataset/query.rb +7 -7
  50. data/lib/sequel/dataset/sql.rb +5 -18
  51. data/lib/sequel/extensions/core_extensions.rb +8 -12
  52. data/lib/sequel/extensions/pg_array.rb +59 -33
  53. data/lib/sequel/extensions/pg_array_ops.rb +32 -4
  54. data/lib/sequel/extensions/pg_auto_parameterize.rb +1 -1
  55. data/lib/sequel/extensions/pg_hstore.rb +32 -17
  56. data/lib/sequel/extensions/pg_hstore_ops.rb +32 -3
  57. data/lib/sequel/extensions/pg_inet.rb +1 -2
  58. data/lib/sequel/extensions/pg_interval.rb +0 -1
  59. data/lib/sequel/extensions/pg_json.rb +41 -23
  60. data/lib/sequel/extensions/pg_range.rb +36 -11
  61. data/lib/sequel/extensions/pg_range_ops.rb +32 -4
  62. data/lib/sequel/extensions/pg_row.rb +572 -0
  63. data/lib/sequel/extensions/pg_row_ops.rb +164 -0
  64. data/lib/sequel/extensions/query.rb +3 -3
  65. data/lib/sequel/extensions/schema_dumper.rb +7 -8
  66. data/lib/sequel/extensions/select_remove.rb +1 -1
  67. data/lib/sequel/model/base.rb +1 -0
  68. data/lib/sequel/no_core_ext.rb +1 -1
  69. data/lib/sequel/plugins/pg_row.rb +121 -0
  70. data/lib/sequel/plugins/pg_typecast_on_load.rb +65 -0
  71. data/lib/sequel/plugins/validation_helpers.rb +31 -0
  72. data/lib/sequel/sql.rb +64 -44
  73. data/lib/sequel/version.rb +1 -1
  74. data/spec/adapters/mssql_spec.rb +37 -12
  75. data/spec/adapters/mysql_spec.rb +39 -75
  76. data/spec/adapters/oracle_spec.rb +11 -11
  77. data/spec/adapters/postgres_spec.rb +414 -237
  78. data/spec/adapters/spec_helper.rb +1 -1
  79. data/spec/adapters/sqlite_spec.rb +14 -14
  80. data/spec/core/database_spec.rb +6 -6
  81. data/spec/core/dataset_spec.rb +169 -205
  82. data/spec/core/expression_filters_spec.rb +182 -295
  83. data/spec/core/object_graph_spec.rb +6 -6
  84. data/spec/core/schema_spec.rb +14 -14
  85. data/spec/core/spec_helper.rb +1 -0
  86. data/spec/{extensions/core_extensions_spec.rb → core_extensions_spec.rb} +208 -14
  87. data/spec/extensions/columns_introspection_spec.rb +5 -5
  88. data/spec/extensions/hook_class_methods_spec.rb +28 -36
  89. data/spec/extensions/many_through_many_spec.rb +4 -4
  90. data/spec/extensions/pg_array_ops_spec.rb +15 -7
  91. data/spec/extensions/pg_array_spec.rb +81 -48
  92. data/spec/extensions/pg_auto_parameterize_spec.rb +2 -2
  93. data/spec/extensions/pg_hstore_ops_spec.rb +13 -9
  94. data/spec/extensions/pg_hstore_spec.rb +66 -65
  95. data/spec/extensions/pg_inet_spec.rb +2 -4
  96. data/spec/extensions/pg_interval_spec.rb +2 -3
  97. data/spec/extensions/pg_json_spec.rb +20 -18
  98. data/spec/extensions/pg_range_ops_spec.rb +11 -4
  99. data/spec/extensions/pg_range_spec.rb +30 -7
  100. data/spec/extensions/pg_row_ops_spec.rb +48 -0
  101. data/spec/extensions/pg_row_plugin_spec.rb +45 -0
  102. data/spec/extensions/pg_row_spec.rb +323 -0
  103. data/spec/extensions/pg_typecast_on_load_spec.rb +58 -0
  104. data/spec/extensions/query_literals_spec.rb +11 -11
  105. data/spec/extensions/query_spec.rb +3 -3
  106. data/spec/extensions/schema_dumper_spec.rb +20 -4
  107. data/spec/extensions/schema_spec.rb +18 -41
  108. data/spec/extensions/select_remove_spec.rb +4 -4
  109. data/spec/extensions/spec_helper.rb +4 -8
  110. data/spec/extensions/to_dot_spec.rb +5 -5
  111. data/spec/extensions/validation_class_methods_spec.rb +28 -16
  112. data/spec/integration/associations_test.rb +20 -20
  113. data/spec/integration/dataset_test.rb +98 -98
  114. data/spec/integration/eager_loader_test.rb +13 -27
  115. data/spec/integration/plugin_test.rb +5 -5
  116. data/spec/integration/prepared_statement_test.rb +22 -13
  117. data/spec/integration/schema_test.rb +28 -18
  118. data/spec/integration/spec_helper.rb +1 -1
  119. data/spec/integration/timezone_test.rb +2 -2
  120. data/spec/integration/type_test.rb +15 -6
  121. data/spec/model/association_reflection_spec.rb +1 -1
  122. data/spec/model/associations_spec.rb +4 -4
  123. data/spec/model/base_spec.rb +5 -5
  124. data/spec/model/eager_loading_spec.rb +15 -15
  125. data/spec/model/model_spec.rb +32 -32
  126. data/spec/model/record_spec.rb +16 -0
  127. data/spec/model/spec_helper.rb +2 -6
  128. data/spec/model/validations_spec.rb +1 -1
  129. metadata +16 -4
@@ -33,7 +33,7 @@
33
33
  # manually or use a literal string:
34
34
  #
35
35
  # DB[:table].select(:a, :b).order(:b, :a)
36
- # DB[:table].select(:a, :b).order('2, 1'.lit)
36
+ # DB[:table].select(:a, :b).order(Sequel.lit('2, 1'))
37
37
  #
38
38
  # 2. In order to avoid many type errors, it attempts to guess the
39
39
  # appropriate type and automatically casts all placeholders.
@@ -15,8 +15,13 @@
15
15
  # for HStore using the standard Sequel literalization callbacks, so
16
16
  # they work with on all adapters.
17
17
  #
18
- # To turn an existing Hash into an HStore:
18
+ # To turn an existing Hash into an HStore, use Sequel.hstore:
19
19
  #
20
+ # Sequel.hstore(hash)
21
+ #
22
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
23
+ # you can also use Hash#hstore:
24
+ #
20
25
  # hash.hstore
21
26
  #
22
27
  # Since the hstore type only supports strings, non string keys and
@@ -128,11 +133,6 @@ module Sequel
128
133
  end
129
134
 
130
135
  module DatabaseMethods
131
- # Reset the conversion procs if using the native postgres adapter.
132
- def self.extended(db)
133
- db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
134
- end
135
-
136
136
  # Handle hstores in bound variables
137
137
  def bound_variable_arg(arg, conn)
138
138
  case arg
@@ -163,8 +163,6 @@ module Sequel
163
163
  value
164
164
  when Hash
165
165
  HStore.new(value)
166
- when String
167
- HStore.parse(value)
168
166
  else
169
167
  raise Sequel::InvalidValue, "invalid value for hstore: #{value.inspect}"
170
168
  end
@@ -282,17 +280,34 @@ module Sequel
282
280
  PG_NAMED_TYPES[:hstore] = HStore.method(:parse)
283
281
  end
284
282
 
283
+ module SQL::Builders
284
+ # Return a Postgres::HStore proxy for the given hash.
285
+ def hstore(v)
286
+ case v
287
+ when Postgres::HStore
288
+ v
289
+ when Hash
290
+ Postgres::HStore.new(v)
291
+ else
292
+ # May not be defined unless the pg_hstore_ops extension is used
293
+ hstore_op(v)
294
+ end
295
+ end
296
+ end
297
+
285
298
  Database.register_extension(:pg_hstore, Postgres::HStore::DatabaseMethods)
286
299
  end
287
300
 
288
- class Hash
289
- # Create a new HStore using the receiver as the input
290
- # hash. Note that the HStore created will not use the
291
- # receiver as the backing store, since it has to
292
- # modify the hash. To get the new backing store, use:
293
- #
294
- # hash.hstore.to_hash
295
- def hstore
296
- Sequel::Postgres::HStore.new(self)
301
+ if Sequel.core_extensions?
302
+ class Hash
303
+ # Create a new HStore using the receiver as the input
304
+ # hash. Note that the HStore created will not use the
305
+ # receiver as the backing store, since it has to
306
+ # modify the hash. To get the new backing store, use:
307
+ #
308
+ # hash.hstore.to_hash
309
+ def hstore
310
+ Sequel::Postgres::HStore.new(self)
311
+ end
297
312
  end
298
313
  end
@@ -6,7 +6,22 @@
6
6
  # Sequel.extension :pg_hstore_ops
7
7
  #
8
8
  # The most common usage is taking an object that represents an SQL
9
- # expression (such as a :symbol), and calling #hstore on it:
9
+ # expression (such as a :symbol), and calling Sequel.hstore_op with it:
10
+ #
11
+ # h = Sequel.hstore_op(:hstore_column)
12
+ #
13
+ # If you have also loaded the pg_hstore extension, you can use
14
+ # Sequel.hstore as well:
15
+ #
16
+ # h = Sequel.hstore(:hstore_column)
17
+ #
18
+ # Also, on most Sequel expression objects, you can call the hstore
19
+ # method:
20
+ #
21
+ # h = Sequel.expr(:hstore_column).hstore
22
+ #
23
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
24
+ # you can also call Symbol#hstore:
10
25
  #
11
26
  # h = :hstore_column.hstore
12
27
  #
@@ -250,6 +265,18 @@ module Sequel
250
265
  end
251
266
  end
252
267
 
268
+ module SQL::Builders
269
+ # Return the object wrapped in an Postgres::HStoreOp.
270
+ def hstore_op(v)
271
+ case v
272
+ when Postgres::HStoreOp
273
+ v
274
+ else
275
+ Postgres::HStoreOp.new(v)
276
+ end
277
+ end
278
+ end
279
+
253
280
  class SQL::GenericExpression
254
281
  include Sequel::Postgres::HStoreOpMethods
255
282
  end
@@ -259,6 +286,8 @@ module Sequel
259
286
  end
260
287
  end
261
288
 
262
- class Symbol
263
- include Sequel::Postgres::HStoreOpMethods
289
+ if Sequel.core_extensions?
290
+ class Symbol
291
+ include Sequel::Postgres::HStoreOpMethods
292
+ end
264
293
  end
@@ -35,10 +35,9 @@ module Sequel
35
35
  module InetDatabaseMethods
36
36
 
37
37
  # Reset the conversion procs when extending the Database object, so
38
- # it will pick up the inet/cidr convertor. Also, extend the datasets
38
+ # it will pick up the inet/cidr converter. Also, extend the datasets
39
39
  # with support for literalizing the IPAddr types.
40
40
  def self.extended(db)
41
- db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
42
41
  db.extend_datasets(InetDatasetMethods)
43
42
  end
44
43
 
@@ -119,7 +119,6 @@ module Sequel
119
119
  # Reset the conversion procs if using the native postgres adapter,
120
120
  # and extend the datasets to correctly literalize ActiveSupport::Duration values.
121
121
  def self.extended(db)
122
- db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
123
122
  db.extend_datasets(IntervalDatasetMethods)
124
123
  end
125
124
 
@@ -17,15 +17,22 @@
17
17
  # This is done so that Sequel does not treat JSONArray and JSONHash
18
18
  # like Array and Hash by default, which would cause issues.
19
19
  #
20
- # To turn an existing Array or Hash into a JSONArray or JSONHash:
20
+ # To turn an existing Array or Hash into a JSONArray or JSONHash,
21
+ # use Sequel.pg_json:
22
+ #
23
+ # Sequel.pg_json(array)
24
+ # Sequel.pg_json(hash)
25
+ #
26
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
27
+ # you can also use Array#pg_json and Hash#pg_json:
21
28
  #
22
29
  # array.pg_json
23
30
  # hash.pg_json
24
31
  #
25
32
  # So if you want to insert an array or hash into an json database column:
26
33
  #
27
- # DB[:table].insert(:column=>[1, 2, 3].pg_json)
28
- # DB[:table].insert(:column=>{'a'=>1, 'b'=>2}.pg_json)
34
+ # DB[:table].insert(:column=>Sequel.pg_json([1, 2, 3]))
35
+ # DB[:table].insert(:column=>Sequel.pg_json({'a'=>1, 'b'=>2}))
29
36
  #
30
37
  # If you would like to use PostgreSQL json columns in your model
31
38
  # objects, you probably want to modify the schema parsing/typecasting
@@ -101,13 +108,6 @@ module Sequel
101
108
  end
102
109
  end
103
110
 
104
- # Reset the conversion procs when extending the Database object, so
105
- # it will pick up the json convertor. This is only done for the native
106
- # postgres adapter.
107
- def self.extended(db)
108
- db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
109
- end
110
-
111
111
  # Handle JSONArray and JSONHash in bound variables
112
112
  def bound_variable_arg(arg, conn)
113
113
  case arg
@@ -168,23 +168,41 @@ module Sequel
168
168
  end
169
169
  end
170
170
 
171
+ module SQL::Builders
172
+ # Wrap the array or hash in a Postgres::JSONArray or Postgres::JSONHash.
173
+ def pg_json(v)
174
+ case v
175
+ when Postgres::JSONArray, Postgres::JSONHash
176
+ v
177
+ when Array
178
+ Postgres::JSONArray.new(v)
179
+ when Hash
180
+ Postgres::JSONHash.new(v)
181
+ else
182
+ raise Error, "Sequel.pg_json requires a hash or array argument"
183
+ end
184
+ end
185
+ end
186
+
171
187
  Database.register_extension(:pg_json, Postgres::JSONDatabaseMethods)
172
188
  end
173
189
 
174
- class Array
175
- # Return a Sequel::Postgres::JSONArray proxy to the receiver.
176
- # This is mostly useful as a short cut for creating JSONArray
177
- # objects that didn't come from the database.
178
- def pg_json
179
- Sequel::Postgres::JSONArray.new(self)
190
+ if Sequel.core_extensions?
191
+ class Array
192
+ # Return a Sequel::Postgres::JSONArray proxy to the receiver.
193
+ # This is mostly useful as a short cut for creating JSONArray
194
+ # objects that didn't come from the database.
195
+ def pg_json
196
+ Sequel::Postgres::JSONArray.new(self)
197
+ end
180
198
  end
181
- end
182
199
 
183
- class Hash
184
- # Return a Sequel::Postgres::JSONHash proxy to the receiver.
185
- # This is mostly useful as a short cut for creating JSONHash
186
- # objects that didn't come from the database.
187
- def pg_json
188
- Sequel::Postgres::JSONHash.new(self)
200
+ class Hash
201
+ # Return a Sequel::Postgres::JSONHash proxy to the receiver.
202
+ # This is mostly useful as a short cut for creating JSONHash
203
+ # objects that didn't come from the database.
204
+ def pg_json
205
+ Sequel::Postgres::JSONHash.new(self)
206
+ end
189
207
  end
190
208
  end
@@ -20,12 +20,18 @@
20
20
  # for both PGRange and Range that use the standard Sequel literalization
21
21
  # callbacks, so they work on all adapters.
22
22
  #
23
- # To turn an existing Range into a PGRange:
23
+ # To turn an existing Range into a PGRange, use Sequel.pg_range:
24
+ #
25
+ # Sequel.pg_range(range)
26
+ #
27
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
28
+ # you can also use Range#pg_range:
24
29
  #
25
30
  # range.pg_range
26
31
  #
27
32
  # You may want to specify a specific range type:
28
33
  #
34
+ # Sequel.pg_range(range, :daterange)
29
35
  # range.pg_range(:daterange)
30
36
  #
31
37
  # If you specify the range database type, Sequel will automatically cast
@@ -177,7 +183,6 @@ module Sequel
177
183
  # Reset the conversion procs if using the native postgres adapter,
178
184
  # and extend the datasets to correctly literalize ruby Range values.
179
185
  def self.extended(db)
180
- db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
181
186
  db.extend_datasets(DatasetMethods)
182
187
  end
183
188
 
@@ -216,12 +221,11 @@ module Sequel
216
221
  # Manually override the typecasting for tsrange and tstzrange types so that
217
222
  # they use the database's timezone instead of the global Sequel
218
223
  # timezone.
219
- def get_conversion_procs(conn)
224
+ def get_conversion_procs
220
225
  procs = super
221
226
 
222
- converter = method(:to_application_timestamp)
223
- procs[3908] = Parser.new("tsrange", converter)
224
- procs[3910] = Parser.new("tstzrange", converter)
227
+ procs[3908] = Parser.new("tsrange", procs[1114])
228
+ procs[3910] = Parser.new("tstzrange", procs[1184])
225
229
  if defined?(PGArray::Creator)
226
230
  procs[3909] = PGArray::Creator.new("tsrange", procs[3908])
227
231
  procs[3911] = PGArray::Creator.new("tstzrange", procs[3910])
@@ -475,13 +479,34 @@ module Sequel
475
479
  end
476
480
  end
477
481
 
482
+ module SQL::Builders
483
+ # Convert the object to a Postgres::PGRange.
484
+ def pg_range(v, db_type=nil)
485
+ case v
486
+ when Postgres::PGRange
487
+ if db_type.nil? || v.db_type == db_type
488
+ v
489
+ else
490
+ Postgres::PGRange.new(v.begin, v.end, :exclude_begin=>v.exclude_begin?, :exclude_end=>v.exclude_end?, :db_type=>db_type)
491
+ end
492
+ when Range
493
+ Postgres::PGRange.from_range(v, db_type)
494
+ else
495
+ # May not be defined unless the pg_range_ops extension is used
496
+ pg_range_op(v)
497
+ end
498
+ end
499
+ end
500
+
478
501
  Database.register_extension(:pg_range, Postgres::PGRange::DatabaseMethods)
479
502
  end
480
503
 
481
- class Range
482
- # Create a new PGRange using the receiver as the input range,
483
- # with the given database type.
484
- def pg_range(db_type=nil)
485
- Sequel::Postgres::PGRange.from_range(self, db_type)
504
+ if Sequel.core_extensions?
505
+ class Range
506
+ # Create a new PGRange using the receiver as the input range,
507
+ # with the given database type.
508
+ def pg_range(db_type=nil)
509
+ Sequel::Postgres::PGRange.from_range(self, db_type)
510
+ end
486
511
  end
487
512
  end
@@ -5,8 +5,22 @@
5
5
  #
6
6
  # Sequel.extension :pg_range_ops
7
7
  #
8
- # The most common usage is taking an object that represents an SQL
9
- # identifier (such as a :symbol), and calling #pg_range on it:
8
+ # The most common usage is passing an expression to Sequel.pg_range_op:
9
+ #
10
+ # r = Sequel.pg_range_op(:range)
11
+ #
12
+ # If you have also loaded the pg_range extension, you can use
13
+ # Sequel.pg_range as well:
14
+ #
15
+ # r = Sequel.pg_range(:range)
16
+ #
17
+ # Also, on most Sequel expression objects, you can call the pg_range
18
+ # method:
19
+ #
20
+ # r = Sequel.expr(:range).pg_range
21
+ #
22
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
23
+ # you can also call Symbol#pg_range:
10
24
  #
11
25
  # r = :range.pg_range
12
26
  #
@@ -108,6 +122,18 @@ module Sequel
108
122
  end
109
123
  end
110
124
 
125
+ module SQL::Builders
126
+ # Return the expression wrapped in the Postgres::RangeOp.
127
+ def pg_range_op(v)
128
+ case v
129
+ when Postgres::RangeOp
130
+ v
131
+ else
132
+ Postgres::RangeOp.new(v)
133
+ end
134
+ end
135
+ end
136
+
111
137
  class SQL::GenericExpression
112
138
  include Sequel::Postgres::RangeOpMethods
113
139
  end
@@ -117,6 +143,8 @@ module Sequel
117
143
  end
118
144
  end
119
145
 
120
- class Symbol
121
- include Sequel::Postgres::RangeOpMethods
146
+ if Sequel.core_extensions?
147
+ class Symbol
148
+ include Sequel::Postgres::RangeOpMethods
149
+ end
122
150
  end
@@ -0,0 +1,572 @@
1
+ # The pg_row extension adds support for Sequel to handle
2
+ # PostgreSQL's row-valued/composite types.
3
+ #
4
+ # This extension integrates with Sequel's native postgres adapter, so
5
+ # that when composite fields are retrieved, they are parsed and returned
6
+ # as instances of Sequel::Postgres::PGRow::(HashRow|ArrayRow), or
7
+ # optionally a custom type. HashRow and ArrayRow are DelegateClasses of
8
+ # of Hash and Array, so they mostly act like a hash or array, but not
9
+ # completely (is_a?(Hash) and is_a?(Array) are false). If you want the
10
+ # actual hash for a HashRow, call HashRow#to_hash, and if you want the
11
+ # actual array for an ArrayRow, call ArrayRow#to_a. This is done so
12
+ # that Sequel does not treat a values like an Array or Hash by default,
13
+ # which would cause issues.
14
+ #
15
+ # In addition to the parsers, this extension comes with literalizers
16
+ # for HashRow and ArrayRow using the standard Sequel literalization callbacks, so
17
+ # they work with on all adapters.
18
+ #
19
+ # The first thing you are going to want to do is to load the extension into
20
+ # your Database object. Make sure you load the :pg_array extension first
21
+ # if you plan to use composite types in bound variables:
22
+ #
23
+ # DB.extension(:pg_array, :pg_row)
24
+ #
25
+ # You can create an anonymous row type by calling the Sequel.pg_row with
26
+ # an array:
27
+ #
28
+ # Sequel.pg_row(array)
29
+ #
30
+ # If you have loaded the {core_extensions extension}[link:files/doc/core_extensions_rdoc.html]),
31
+ # you can also use Array#pg_row:
32
+ #
33
+ # array.pg_row
34
+ #
35
+ # However, in most cases you are going to want something beyond anonymous
36
+ # row types. This extension allows you to register row types on a per
37
+ # database basis, using Database#register_row_type:
38
+ #
39
+ # DB.register_row_type(:foo)
40
+ #
41
+ # When you register the row type, Sequel will query the PostgreSQL
42
+ # system tables to find the related metadata, and will setup
43
+ # a custom HashRow subclass for that type. This includes looking up
44
+ # conversion procs for each column in the type, so that when the composite
45
+ # type is returned from the database, the members of the type have
46
+ # the correct type. Additionally, if the composite type also has an
47
+ # array form, Sequel registers an array type for the composite type,
48
+ # so that array columns of the composite type are converted correctly.
49
+ #
50
+ # You can then create values of that type by using Database#row_type:
51
+ #
52
+ # DB.row_type(:address, ['123 Sesame St.', 'Some City', '12345'])
53
+ #
54
+ # Let's say table address has columns street, city, and zip. This would return
55
+ # something similar to:
56
+ #
57
+ # {:street=>'123 Sesame St.', :city=>'Some City', :zip=>'12345'}
58
+ #
59
+ # You can also use a hash:
60
+ #
61
+ # DB.row_type(:address, :street=>'123 Sesame St.', :city=>'Some City', :zip=>'12345')
62
+ #
63
+ # So if you have a person table that has an address column, here's how you
64
+ # could insert into the column:
65
+ #
66
+ # DB[:table].insert(:address=>DB.row_type(:address, :street=>'123 Sesame St.', :city=>'Some City', :zip=>'12345'))
67
+ #
68
+ # This extension requires both the strscan and delegate libraries.
69
+
70
+ require 'delegate'
71
+ require 'strscan'
72
+ Sequel.require 'adapters/utils/pg_types'
73
+
74
+ module Sequel
75
+ module Postgres
76
+ module PGRow
77
+ ROW = 'ROW'.freeze
78
+ CAST = '::'.freeze
79
+
80
+ # Class for row-valued/composite types that are treated as arrays. By default,
81
+ # this is only used for generic PostgreSQL record types, as registered
82
+ # types use HashRow by default.
83
+ class ArrayRow < DelegateClass(Array)
84
+ class << self
85
+ # The database type for this class. May be nil if this class
86
+ # done not have a specific database type.
87
+ attr_accessor :db_type
88
+
89
+ # Alias new to call, so that the class itself can be used
90
+ # directly as a converter.
91
+ alias call new
92
+ end
93
+
94
+ # Create a subclass associated with a specific database type.
95
+ # This is done so that instances of this subclass are
96
+ # automatically casted to the database type when literalizing.
97
+ def self.subclass(db_type)
98
+ sc = Class.new(self) do
99
+ @db_type = db_type
100
+ end
101
+ end
102
+
103
+ # Sets the database type associated with this instance. This is
104
+ # used to override the class's default database type.
105
+ attr_writer :db_type
106
+
107
+ # Return the instance's database type, or the class's database
108
+ # type if the instance has not overridden it.
109
+ def db_type
110
+ @db_type || self.class.db_type
111
+ end
112
+
113
+ # Append SQL fragment related to this object to the sql.
114
+ def sql_literal_append(ds, sql)
115
+ sql << ROW
116
+ ds.literal_append(sql, to_a)
117
+ if db_type
118
+ sql << CAST
119
+ ds.quote_schema_table_append(sql, db_type)
120
+ end
121
+ end
122
+ end
123
+
124
+ # Class for row-valued/composite types that are treated as hashes.
125
+ # Types registered via Database#register_row_type will use this
126
+ # class by default.
127
+ class HashRow < DelegateClass(Hash)
128
+ class << self
129
+ # The columns associated with this class.
130
+ attr_accessor :columns
131
+
132
+ # The database type for this class. May be nil if this class
133
+ # done not have a specific database type.
134
+ attr_accessor :db_type
135
+
136
+ # Alias new to call, so that the class itself can be used
137
+ # directly as a converter.
138
+ alias call new
139
+ end
140
+
141
+ # Create a new subclass of this class with the given database
142
+ # type and columns.
143
+ def self.subclass(db_type, columns)
144
+ sc = Class.new(self) do
145
+ @db_type = db_type
146
+ @columns = columns
147
+ end
148
+ end
149
+
150
+ # Return the underlying hash for this delegate object.
151
+ alias to_hash __getobj__
152
+
153
+ # Sets the columns associated with this instance. This is
154
+ # used to override the class's default columns.
155
+ attr_writer :columns
156
+
157
+ # Sets the database type associated with this instance. This is
158
+ # used to override the class's default database type.
159
+ attr_writer :db_type
160
+
161
+ # Return the instance's columns, or the class's columns
162
+ # if the instance has not overridden it.
163
+ def columns
164
+ @columns || self.class.columns
165
+ end
166
+
167
+ # Return the instance's database type, or the class's columns
168
+ # if the instance has not overridden it.
169
+ def db_type
170
+ @db_type || self.class.db_type
171
+ end
172
+
173
+ # Check that the HashRow has valid columns. This should be used
174
+ # before all attempts to literalize the object, since literalization
175
+ # depends on the columns to get the column order.
176
+ def check_columns!
177
+ if columns.nil? || columns.empty?
178
+ raise Error, 'cannot literalize HashRow without columns'
179
+ end
180
+ end
181
+
182
+ # Append SQL fragment related to this object to the sql.
183
+ def sql_literal_append(ds, sql)
184
+ check_columns!
185
+ sql << ROW
186
+ ds.literal_append(sql, values_at(*columns))
187
+ if db_type
188
+ sql << CAST
189
+ ds.quote_schema_table_append(sql, db_type)
190
+ end
191
+ end
192
+ end
193
+
194
+ # This parser-like class splits the PostgreSQL
195
+ # row-valued/composite type output string format
196
+ # into an array of strings. Note this class makes
197
+ # no attempt to handle all input formats that PostgreSQL
198
+ # will accept, it only handles the output format that
199
+ # PostgreSQL uses.
200
+ class Splitter < StringScanner
201
+ OPEN_PAREN = /\(/.freeze
202
+ CLOSE_PAREN = /\)/.freeze
203
+ UNQUOTED_RE = /[^,)]*/.freeze
204
+ SEP_RE = /[,)]/.freeze
205
+ QUOTE_RE = /"/.freeze
206
+ QUOTE_SEP_RE = /"[,)]/.freeze
207
+ QUOTED_RE = /(\\.|""|[^"])*/.freeze
208
+ REPLACE_RE = /\\(.)|"(")/.freeze
209
+ REPLACE_WITH = '\1\2'.freeze
210
+
211
+ # Split the stored string into an array of strings, handling
212
+ # the different types of quoting.
213
+ def parse
214
+ return @result if @result
215
+ values = []
216
+ skip(OPEN_PAREN)
217
+ if skip(CLOSE_PAREN)
218
+ values << nil
219
+ else
220
+ until eos?
221
+ if skip(QUOTE_RE)
222
+ values << scan(QUOTED_RE).gsub(REPLACE_RE, REPLACE_WITH)
223
+ skip(QUOTE_SEP_RE)
224
+ else
225
+ v = scan(UNQUOTED_RE)
226
+ values << (v unless v.empty?)
227
+ skip(SEP_RE)
228
+ end
229
+ end
230
+ end
231
+ values
232
+ end
233
+ end
234
+
235
+ # The Parser is responsible for taking the input string
236
+ # from PostgreSQL, and returning an appropriate ruby
237
+ # object that the type represents, such as an ArrayRow or
238
+ # HashRow.
239
+ class Parser
240
+ # The columns for the parser, if any. If the parser has
241
+ # no columns, it will treat the input as an array. If
242
+ # it has columns, it will treat the input as a hash.
243
+ # If present, should be an array of strings.
244
+ attr_reader :columns
245
+
246
+ # Converters for each member in the composite type. If
247
+ # not present, no conversion will be done, so values will
248
+ # remain strings. If present, should be an array of
249
+ # callable objects.
250
+ attr_reader :column_converters
251
+
252
+ # The OIDs for each member in the composite type. Not
253
+ # currently used, but made available for user code.
254
+ attr_reader :column_oids
255
+
256
+ # A converter for the object as a whole. Used to wrap
257
+ # the returned array/hash in another object, such as an
258
+ # ArrayRow or HashRow. If present, should be callable.
259
+ attr_reader :converter
260
+
261
+ # The oid for the composite type itself.
262
+ attr_reader :oid
263
+
264
+ # A callable object used for typecasting the object. This
265
+ # is similar to the converter, but it is called by the
266
+ # typecasting code, which has different assumptions than
267
+ # the converter. For instance, the converter should be
268
+ # called with all of the member values already typecast,
269
+ # but the typecaster may not be.
270
+ attr_reader :typecaster
271
+
272
+ # Sets each of the parser's attributes, using options with
273
+ # the same name (e.g. :columns sets the columns attribute).
274
+ def initialize(h={})
275
+ @columns = h[:columns]
276
+ @column_converters = h[:column_converters]
277
+ @column_oids = h[:column_oids]
278
+ @converter = h[:converter]
279
+ @typecaster = h[:typecaster]
280
+ @oid = h[:oid]
281
+ end
282
+
283
+ # Convert the PostgreSQL composite type input format into
284
+ # an appropriate ruby object.
285
+ def call(s)
286
+ convert(convert_format(convert_columns(Splitter.new(s).parse)))
287
+ end
288
+
289
+ # Typecast the given object to the appropriate type using the
290
+ # typecaster. Note that this does not conversion for the members
291
+ # of the composite type, since those conversion expect strings and
292
+ # strings may not be provided.
293
+ def typecast(obj)
294
+ case obj
295
+ when Array
296
+ _typecast(convert_format(obj))
297
+ when Hash
298
+ unless @columns
299
+ raise Error, 'PGRow::Parser without columns cannot typecast from a hash'
300
+ end
301
+ _typecast(obj)
302
+ else
303
+ raise Error, 'PGRow::Parser can only typecast arrays and hashes'
304
+ end
305
+ end
306
+
307
+ private
308
+
309
+ # If the parser has a typecaster, call it with
310
+ # the object, otherwise return the object as is.
311
+ def _typecast(obj)
312
+ if t = @typecaster
313
+ t.call(obj)
314
+ else
315
+ obj
316
+ end
317
+ end
318
+
319
+ # If the parser has column converters, map the
320
+ # array of strings input to a array of appropriate
321
+ # ruby objects, one for each converter.
322
+ def convert_columns(arr)
323
+ if ccs = @column_converters
324
+ arr.zip(ccs).map{|v, pr| pr ? pr.call(v) : v}
325
+ else
326
+ arr
327
+ end
328
+ end
329
+
330
+ # If the parser has columns, return a hash assuming
331
+ # that the array is ordered by the columns.
332
+ def convert_format(arr)
333
+ if cs = @columns
334
+ h = {}
335
+ arr.zip(cs).each{|v, c| h[c] = v}
336
+ h
337
+ else
338
+ arr
339
+ end
340
+ end
341
+
342
+ # If the parser has a converter, call it with the object,
343
+ # otherwise return the object as is.
344
+ def convert(obj)
345
+ if c = @converter
346
+ c.call(obj)
347
+ else
348
+ obj
349
+ end
350
+ end
351
+ end
352
+
353
+ module DatabaseMethods
354
+ ESCAPE_RE = /("|\\)/.freeze
355
+ ESCAPE_REPLACEMENT = '\\\\\1'.freeze
356
+ COMMA = ','.freeze
357
+
358
+ # A hash mapping row type keys (usually symbols), to option
359
+ # hashes. At the least, the values will contain the :parser
360
+ # option for the Parser instance that the type will use.
361
+ attr_reader :row_types
362
+
363
+ # Do some setup for the data structures the module uses.
364
+ def self.extended(db)
365
+ # Return right away if row_types has already been set. This
366
+ # makes things not break if a user extends the database with
367
+ # this module more than once (since extended is called every
368
+ # time).
369
+ return if db.row_types
370
+
371
+ db.instance_eval do
372
+ @row_types = {}
373
+ @row_schema_types = {}
374
+ extend(@row_type_method_module = Module.new)
375
+ end
376
+ end
377
+
378
+ # Handle ArrayRow and HashRow values in bound variables.
379
+ def bound_variable_arg(arg, conn)
380
+ case arg
381
+ when ArrayRow
382
+ "(#{arg.map{|v| bound_variable_array(v)}.join(COMMA)})"
383
+ when HashRow
384
+ arg.check_columns!
385
+ "(#{arg.values_at(*arg.columns).map{|v| bound_variable_array(v)}.join(COMMA)})"
386
+ else
387
+ super
388
+ end
389
+ end
390
+
391
+ # Register a new row type for the Database instance. db_type should be the type
392
+ # symbol. This parses the PostgreSQL system tables to get information the
393
+ # composite type, and by default has the type return instances of a subclass
394
+ # of HashRow.
395
+ #
396
+ # The following options are supported:
397
+ #
398
+ # :converter :: Use a custom converter for the parser.
399
+ # :typecaster :: Use a custom typecaster for the parser.
400
+ def register_row_type(db_type, opts={})
401
+ procs = @conversion_procs
402
+ rel_oid = nil
403
+ array_oid = nil
404
+ parser_opts = {}
405
+
406
+ # Try to handle schema-qualified types.
407
+ type_schema, type_name = schema_and_table(db_type)
408
+ schema_type_string = type_name.to_s
409
+
410
+ # Get basic oid information for the composite type.
411
+ ds = from(:pg_type).
412
+ select(:pg_type__oid, :typrelid, :typarray).
413
+ where([[:typtype, 'c'], [:typname, type_name.to_s]])
414
+ if type_schema
415
+ ds = ds.join(:pg_namespace, [[:oid, :typnamespace], [:nspname, type_schema.to_s]])
416
+ schema_type_symbol = :"pg_row_#{type_schema}__#{type_name}"
417
+ else
418
+ schema_type_symbol = :"pg_row_#{type_name}"
419
+ end
420
+ unless row = ds.first
421
+ raise Error, "row type #{db_type.inspect} not found in database"
422
+ end
423
+ # Manually cast to integer using to_i, because adapter may not cast oid type
424
+ # correctly (e.g. swift)
425
+ parser_opts[:oid], rel_oid, array_oid = row.values_at(:oid, :typrelid, :typarray).map{|i| i.to_i}
426
+
427
+ # Get column names and oids for each of the members of the composite type.
428
+ res = from(:pg_attribute).
429
+ where(:attrelid=>rel_oid).
430
+ where{attnum > 0}.
431
+ exclude(:attisdropped).
432
+ order(:attnum).
433
+ select_map([:attname, :atttypid])
434
+ if res.empty?
435
+ raise Error, "no columns for row type #{db_type.inspect} in database"
436
+ end
437
+ parser_opts[:columns] = res.map{|r| r[0].to_sym}
438
+ parser_opts[:column_oids] = res.map{|r| r[1].to_i}
439
+
440
+ # Using the conversion_procs, lookup converters for each member of the composite type
441
+ parser_opts[:column_converters] = parser_opts[:column_oids].map do |oid|
442
+ if pr = procs[oid]
443
+ pr
444
+ elsif !Sequel::Postgres::STRING_TYPES.include?(oid)
445
+ # It's not a string type, and it's possible a conversion proc for this
446
+ # oid will be added later, so do a runtime check for it.
447
+ lambda{|s| (pr = procs[oid]) ? pr.call(s) : s}
448
+ end
449
+ end
450
+
451
+ # Setup the converter and typecaster
452
+ parser_opts[:converter] = opts.fetch(:converter){HashRow.subclass(db_type, parser_opts[:columns])}
453
+ parser_opts[:typecaster] = opts.fetch(:typecaster, parser_opts[:converter])
454
+
455
+ parser = Parser.new(parser_opts)
456
+ @conversion_procs[parser.oid] = parser
457
+
458
+ if defined?(PGArray) && PGArray.respond_to?(:register) && array_oid && array_oid > 0
459
+ PGArray.register(db_type, :oid=>array_oid, :converter=>parser, :type_procs=>@conversion_procs, :scalar_typecast=>schema_type_symbol)
460
+ end
461
+
462
+ @row_types[db_type] = opts.merge(:parser=>parser)
463
+ @row_schema_types[schema_type_string] = schema_type_symbol
464
+ @row_type_method_module.class_eval do
465
+ meth = :"typecast_value_#{schema_type_symbol}"
466
+ define_method(meth) do |v|
467
+ row_type(db_type, v)
468
+ end
469
+ private meth
470
+ end
471
+
472
+ nil
473
+ end
474
+
475
+ # When reseting conversion procs, reregister all the row types so that
476
+ # the system tables are introspected again, picking up database changes.
477
+ def reset_conversion_procs
478
+ procs = super
479
+
480
+ row_types.each do |db_type, opts|
481
+ register_row_type(db_type, opts)
482
+ end
483
+
484
+ procs
485
+ end
486
+
487
+ # Handle typecasting of the given object to the given database type.
488
+ # In general, the given database type should already be registered,
489
+ # but if obj is an array, this will handled unregistered types.
490
+ def row_type(db_type, obj)
491
+ (type_hash = @row_types[db_type]) &&
492
+ (parser = type_hash[:parser])
493
+
494
+ case obj
495
+ when ArrayRow, HashRow
496
+ obj
497
+ when Array
498
+ if parser
499
+ parser.typecast(obj)
500
+ else
501
+ obj = ArrayRow.new(obj)
502
+ obj.db_type = db_type
503
+ obj
504
+ end
505
+ when Hash
506
+ if parser
507
+ parser.typecast(obj)
508
+ else
509
+ raise InvalidValue, "Database#row_type requires the #{db_type.inspect} type have a registered parser and typecaster when called with a hash"
510
+ end
511
+ else
512
+ raise InvalidValue, "cannot convert #{obj.inspect} to row type #{db_type.inspect}"
513
+ end
514
+ end
515
+
516
+ # Make the column type detection handle registered row types.
517
+ def schema_column_type(db_type)
518
+ if type = @row_schema_types[db_type]
519
+ type
520
+ else
521
+ super
522
+ end
523
+ end
524
+
525
+ private
526
+
527
+ # Format composite types used in bound variable arrays.
528
+ def bound_variable_array(arg)
529
+ case arg
530
+ when ArrayRow
531
+ "\"(#{arg.map{|v| bound_variable_array(v)}.join(COMMA).gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)})\""
532
+ when HashRow
533
+ arg.check_columns!
534
+ "\"(#{arg.values_at(*arg.columns).map{|v| bound_variable_array(v)}.join(COMMA).gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)})\""
535
+ else
536
+ super
537
+ end
538
+ end
539
+ end
540
+ end
541
+
542
+ # Register the default anonymous record type
543
+ PG_TYPES[2249] = PGRow::Parser.new(:converter=>PGRow::ArrayRow)
544
+ if defined?(PGArray) && PGArray.respond_to?(:register)
545
+ PGArray.register('record', :oid=>2287, :scalar_oid=>2249)
546
+ end
547
+ end
548
+
549
+ module SQL::Builders
550
+ # Wraps the expr array in an anonymous Postgres::PGRow::ArrayRow instance.
551
+ def pg_row(expr)
552
+ case expr
553
+ when Array
554
+ Postgres::PGRow::ArrayRow.new(expr)
555
+ else
556
+ # Will only work if pg_row_ops extension is loaded
557
+ pg_row_op(expr)
558
+ end
559
+ end
560
+ end
561
+
562
+ Database.register_extension(:pg_row, Postgres::PGRow::DatabaseMethods)
563
+ end
564
+
565
+ if Sequel.core_extensions?
566
+ class Array
567
+ # Wraps the receiver in an anonymous Sequel::Postgres::PGRow::ArrayRow instance.
568
+ def pg_row
569
+ Sequel::Postgres::PGRow::ArrayRow.new(self)
570
+ end
571
+ end
572
+ end