sequel 3.37.0 → 3.38.0

Sign up to get free protection for your applications and to get access to all the features.
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