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
@@ -1,6 +1,11 @@
1
1
  # The pg_array_ops extension adds support to Sequel's DSL to make
2
- # it easier to call PostgreSQL array functions and operators. The
3
- # most common usage is taking an object that represents an SQL
2
+ # it easier to call PostgreSQL array functions and operators.
3
+ #
4
+ # To load the extension:
5
+ #
6
+ # Sequel.extension :pg_array_ops
7
+ #
8
+ # The most common usage is taking an object that represents an SQL
4
9
  # identifier (such as a :symbol), and calling #pg_array on it:
5
10
  #
6
11
  # ia = :int_array_column.pg_array
@@ -9,7 +9,7 @@
9
9
  # DB[:test].where(:a=>1)
10
10
  # # SQL: SELECT * FROM test WHERE a = 1
11
11
  #
12
- # DB.extend Sequel::Postgres::AutoParameterize::DatabaseMethods
12
+ # DB.extension :pg_auto_parameterize
13
13
  # DB[:test].where(:a=>1)
14
14
  # # SQL: SELECT * FROM test WHERE a = $1 (args: [1])
15
15
  #
@@ -66,16 +66,20 @@ module Sequel
66
66
  module AutoParameterize
67
67
  # String that holds an array of parameters
68
68
  class StringWithArray < ::String
69
+ PLACEHOLDER = '$'.freeze
70
+ CAST = '::'.freeze
71
+
69
72
  # The array of parameters used by this query.
70
73
  attr_reader :args
71
74
 
72
75
  # Add a new parameter to this query, which adds
73
76
  # the parameter to the array of parameters, and an
74
77
  # SQL placeholder to the query itself.
75
- def add_arg(s, type)
78
+ def add_arg(s, type=nil)
76
79
  @args ||= []
77
80
  @args << s
78
- self << "$#{@args.length}::#{type}"
81
+ self << PLACEHOLDER << @args.length.to_s
82
+ self << CAST << type.to_s if type
79
83
  end
80
84
 
81
85
  # Show args when the string is inspected
@@ -120,7 +124,7 @@ module Sequel
120
124
  when Sequel::SQL::Blob
121
125
  sql.add_arg(v, :bytea)
122
126
  else
123
- sql.add_arg(v, :text)
127
+ sql.add_arg(v)
124
128
  end
125
129
  when Bignum
126
130
  sql.add_arg(v, :int8)
@@ -166,4 +170,6 @@ module Sequel
166
170
  end
167
171
  end
168
172
  end
173
+
174
+ Database.register_extension(:pg_auto_parameterize, Postgres::AutoParameterize::DatabaseMethods)
169
175
  end
@@ -67,7 +67,7 @@
67
67
  # recognizes and correctly handles the hstore columns, which you can
68
68
  # do by:
69
69
  #
70
- # DB.extend Sequel::Postgres::HStore::DatabaseMethods
70
+ # DB.extension :pg_hstore
71
71
  #
72
72
  # If you are not using the native postgres adapter, you probably
73
73
  # also want to use the typecast_on_load plugin in the model, and
@@ -281,6 +281,8 @@ module Sequel
281
281
  # Associate the named types by default.
282
282
  PG_NAMED_TYPES[:hstore] = HStore.method(:parse)
283
283
  end
284
+
285
+ Database.register_extension(:pg_hstore, Postgres::HStore::DatabaseMethods)
284
286
  end
285
287
 
286
288
  class Hash
@@ -1,6 +1,11 @@
1
1
  # The pg_hstore_ops extension adds support to Sequel's DSL to make
2
- # it easier to call PostgreSQL hstore functions and operators. The
3
- # most common usage is taking an object that represents an SQL
2
+ # it easier to call PostgreSQL hstore functions and operators.
3
+ #
4
+ # To load the extension:
5
+ #
6
+ # Sequel.extension :pg_hstore_ops
7
+ #
8
+ # The most common usage is taking an object that represents an SQL
4
9
  # expression (such as a :symbol), and calling #hstore on it:
5
10
  #
6
11
  # h = :hstore_column.hstore
@@ -8,17 +8,26 @@
8
8
  # After loading the extension, you should extend your dataset
9
9
  # with a module so that it correctly handles the inet/cidr type:
10
10
  #
11
- # DB.extend Sequel::Postgres::InetDatabaseMethods
11
+ # DB.extension :pg_inet
12
12
  #
13
13
  # If you are not using the native postgres adapter, you probably
14
14
  # also want to use the typecast_on_load plugin in the model, and
15
15
  # set it to typecast the inet/cidr column(s) on load.
16
16
  #
17
+ # This extension integrates with the pg_array extension. If you plan
18
+ # to use the inet[] or cidr[] types, load the pg_array extension before
19
+ # the pg_inet extension:
20
+ #
21
+ # DB.extension :pg_array, :pg_inet
22
+ #
17
23
  # This extension does not add special support for the macaddr
18
24
  # type. Ruby doesn't have a stdlib class that represents mac
19
- # addresses, so these will still be returned as strings.
25
+ # addresses, so these will still be returned as strings. The exception
26
+ # to this is that the pg_array extension integration will recognize
27
+ # macaddr[] types return them as arrays of strings.
20
28
 
21
29
  require 'ipaddr'
30
+ Sequel.require 'adapters/utils/pg_types'
22
31
 
23
32
  module Sequel
24
33
  module Postgres
@@ -56,6 +65,16 @@ module Sequel
56
65
 
57
66
  private
58
67
 
68
+ # Handle inet[]/cidr[] types in bound variables.
69
+ def bound_variable_array(a)
70
+ case a
71
+ when IPAddr
72
+ "\"#{a.to_s}/#{a.instance_variable_get(:@mask_addr).to_s(2).count('1')}\""
73
+ else
74
+ super
75
+ end
76
+ end
77
+
59
78
  # Typecast the given value to an IPAddr object.
60
79
  def typecast_value_ipaddr(value)
61
80
  case value
@@ -83,7 +102,13 @@ module Sequel
83
102
  end
84
103
  end
85
104
 
86
- PG_TYPES = {} unless defined?(PG_TYPES)
87
105
  PG_TYPES[869] = PG_TYPES[650] = IPAddr.method(:new)
106
+ if defined?(PGArray) && PGArray.respond_to?(:register)
107
+ PGArray.register('inet', :oid=>1041, :scalar_oid=>869)
108
+ PGArray.register('cidr', :oid=>651, :scalar_oid=>650)
109
+ PGArray.register('macaddr', :oid=>1040)
110
+ end
88
111
  end
112
+
113
+ Database.register_extension(:pg_inet, Postgres::InetDatabaseMethods)
89
114
  end
@@ -0,0 +1,192 @@
1
+ # The pg_interval extension adds support for PostgreSQL's interval type.
2
+ #
3
+ # This extension integrates with Sequel's native postgres adapter, so
4
+ # that when interval type values are retrieved, they are parsed and returned
5
+ # as instances of ActiveSupport::Duration.
6
+ #
7
+ # In addition to the parser, this extension adds literalizers for
8
+ # ActiveSupport::Duration ithat use the standard Sequel literalization
9
+ # callbacks, so they work on all adapters.
10
+ #
11
+ # If you would like to use interval columns in your model objects, you
12
+ # probably want to modify the typecasting so that it
13
+ # recognizes and correctly handles the interval columns, which you can
14
+ # do by:
15
+ #
16
+ # DB.extension :pg_interval
17
+ #
18
+ # If you are not using the native postgres adapter, you probably
19
+ # also want to use the typecast_on_load plugin in the model, and
20
+ # set it to typecast the interval type column(s) on load.
21
+ #
22
+ # This extension integrates with the pg_array extension. If you plan
23
+ # to use arrays of interval types, load the pg_array extension before the
24
+ # pg_interval extension:
25
+ #
26
+ # DB.extension :pg_array, :pg_interval
27
+ #
28
+ # The parser this extension uses requires that IntervalStyle for PostgreSQL
29
+ # is set to postgres (the default setting). If IntervalStyle is changed from
30
+ # the default setting, the parser will probably not work. The parser used is
31
+ # very simple, and is only designed to parse PostgreSQL's default output
32
+ # format, it is not designed to support all input formats that PostgreSQL
33
+ # supports.
34
+
35
+ require 'active_support/duration'
36
+ Sequel.require 'adapters/utils/pg_types'
37
+
38
+ module Sequel
39
+ module Postgres
40
+ module IntervalDatabaseMethods
41
+ EMPTY_INTERVAL = '0'.freeze
42
+ DURATION_UNITS = [:years, :months, :days, :minutes, :seconds].freeze
43
+
44
+ # Return an unquoted string version of the duration object suitable for
45
+ # use as a bound variable.
46
+ def self.literal_duration(duration)
47
+ h = Hash.new(0)
48
+ duration.parts.each{|unit, value| h[unit] += value}
49
+ s = ''
50
+
51
+ DURATION_UNITS.each do |unit|
52
+ if (v = h[unit]) != 0
53
+ s << "#{v.is_a?(Integer) ? v : sprintf('%0.6f', v)} #{unit} "
54
+ end
55
+ end
56
+
57
+ if s.empty?
58
+ EMPTY_INTERVAL
59
+ else
60
+ s
61
+ end
62
+ end
63
+
64
+ # Creates callable objects that convert strings into ActiveSupport::Duration instances.
65
+ class Parser
66
+ # Regexp that parses the full range of PostgreSQL interval type output.
67
+ PARSER = /\A([+-]?\d+ years?\s?)?([+-]?\d+ mons?\s?)?([+-]?\d+ days?\s?)?(?:(?:([+-])?(\d\d):(\d\d):(\d\d(\.\d+)?))|([+-]?\d+ hours?\s?)?([+-]?\d+ mins?\s?)?([+-]?\d+(\.\d+)? secs?\s?)?)?\z/o
68
+
69
+ # Parse the interval input string into an ActiveSupport::Duration instance.
70
+ def call(string)
71
+ raise(InvalidValue, "invalid or unhandled interval format: #{string.inspect}") unless matches = PARSER.match(string)
72
+
73
+ value = 0
74
+ parts = []
75
+
76
+ if v = matches[1]
77
+ v = v.to_i
78
+ value += 31557600 * v
79
+ parts << [:years, v]
80
+ end
81
+ if v = matches[2]
82
+ v = v.to_i
83
+ value += 2592000 * v
84
+ parts << [:months, v]
85
+ end
86
+ if v = matches[3]
87
+ v = v.to_i
88
+ value += 86400 * v
89
+ parts << [:days, v]
90
+ end
91
+ if matches[5]
92
+ seconds = matches[5].to_i * 3600 + matches[6].to_i * 60
93
+ seconds += matches[8] ? matches[7].to_f : matches[7].to_i
94
+ seconds *= -1 if matches[4] == '-'
95
+ value += seconds
96
+ parts << [:seconds, seconds]
97
+ elsif matches[9] || matches[10] || matches[11]
98
+ seconds = 0
99
+ if v = matches[9]
100
+ seconds += v.to_i * 3600
101
+ end
102
+ if v = matches[10]
103
+ seconds += v.to_i * 60
104
+ end
105
+ if v = matches[11]
106
+ seconds += matches[12] ? v.to_f : v.to_i
107
+ end
108
+ value += seconds
109
+ parts << [:seconds, seconds]
110
+ end
111
+
112
+ ActiveSupport::Duration.new(value, parts)
113
+ end
114
+ end
115
+
116
+ # Single instance of Parser used for parsing, to save on memory (since the parser has no state).
117
+ PARSER = Parser.new
118
+
119
+ # Reset the conversion procs if using the native postgres adapter,
120
+ # and extend the datasets to correctly literalize ActiveSupport::Duration values.
121
+ def self.extended(db)
122
+ db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
123
+ db.extend_datasets(IntervalDatasetMethods)
124
+ end
125
+
126
+ # Handle ActiveSupport::Duration values in bound variables
127
+ def bound_variable_arg(arg, conn)
128
+ case arg
129
+ when ActiveSupport::Duration
130
+ IntervalDatabaseMethods.literal_duration(arg)
131
+ else
132
+ super
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ # Handle arrays of interval types in bound variables.
139
+ def bound_variable_array(a)
140
+ case a
141
+ when ActiveSupport::Duration
142
+ "\"#{IntervalDatabaseMethods.literal_duration(a)}\""
143
+ else
144
+ super
145
+ end
146
+ end
147
+
148
+ # Typecast value correctly to an ActiveSupport::Duration instance.
149
+ # If already an ActiveSupport::Duration, return it.
150
+ # If a numeric argument is given, assume it represents a number
151
+ # of seconds, and create a new ActiveSupport::Duration instance
152
+ # representing that number of seconds.
153
+ # If a String, assume it is in PostgreSQL interval output format
154
+ # and attempt to parse it.
155
+ def typecast_value_interval(value)
156
+ case value
157
+ when ActiveSupport::Duration
158
+ value
159
+ when Numeric
160
+ ActiveSupport::Duration.new(value, [[:seconds, value]])
161
+ when String
162
+ PARSER.call(value)
163
+ else
164
+ raise Sequel::InvalidValue, "invalid value for interval type: #{value.inspect}"
165
+ end
166
+ end
167
+ end
168
+
169
+ module IntervalDatasetMethods
170
+ CAST_INTERVAL = '::interval'.freeze
171
+
172
+ # Handle literalization of ActiveSupport::Duration objects, treating them as
173
+ # PostgreSQL intervals.
174
+ def literal_other_append(sql, v)
175
+ case v
176
+ when ActiveSupport::Duration
177
+ literal_append(sql, IntervalDatabaseMethods.literal_duration(v))
178
+ sql << CAST_INTERVAL
179
+ else
180
+ super
181
+ end
182
+ end
183
+ end
184
+
185
+ PG_TYPES[1186] = Postgres::IntervalDatabaseMethods::PARSER
186
+ if defined?(PGArray) && PGArray.respond_to?(:register)
187
+ PGArray.register('interval', :oid=>1187, :scalar_oid=>1186)
188
+ end
189
+ end
190
+
191
+ Database.register_extension(:pg_interval, Postgres::IntervalDatabaseMethods)
192
+ end
@@ -32,25 +32,23 @@
32
32
  # so that it recognizes and correctly handles the json type, which
33
33
  # you can do by:
34
34
  #
35
- # DB.extend Sequel::Postgres::JSONDatabaseMethods
35
+ # DB.extension :pg_json
36
36
  #
37
37
  # If you are not using the native postgres adapter, you probably
38
38
  # also want to use the typecast_on_load plugin in the model, and
39
39
  # set it to typecast the json column(s) on load.
40
40
  #
41
- # The extension is designed to be used with the json type natively
42
- # supported by PostgreSQL 9.2+. There was also a PostgreSQL extension released
43
- # that allowed the json type to be used on PostgreSQL 9.1. To make
44
- # this extension support that type in the native adapter, do the
45
- # following after loading this extension:
41
+ # This extension integrates with the pg_array extension. If you plan
42
+ # to use the json[] type, load the pg_array extension before the
43
+ # pg_json extension:
46
44
  #
47
- # Sequel::Postgres::PG_NAMED_TYPES = {} unless defined?(Sequel::Postgres::PG_NAMED_TYPES)
48
- # Sequel::Postgres::PG_NAMED_TYPES[:json] = Sequel::Postgres::PG_TYPES[114]
45
+ # DB.extension :pg_array, :pg_json
49
46
  #
50
47
  # This extension requires both the json and delegate libraries.
51
48
 
52
49
  require 'delegate'
53
50
  require 'json'
51
+ Sequel.require 'adapters/utils/pg_types'
54
52
 
55
53
  module Sequel
56
54
  module Postgres
@@ -132,6 +130,16 @@ module Sequel
132
130
 
133
131
  private
134
132
 
133
+ # Handle json[] types in bound variables.
134
+ def bound_variable_array(a)
135
+ case a
136
+ when JSONHash, JSONArray
137
+ "\"#{a.to_json.gsub('"', '\\"')}\""
138
+ else
139
+ super
140
+ end
141
+ end
142
+
135
143
  # Given a value to typecast to the json column
136
144
  # * If given a JSONArray or JSONHash, just return the value
137
145
  # * If given an Array, return a JSONArray
@@ -154,9 +162,13 @@ module Sequel
154
162
  end
155
163
  end
156
164
 
157
- PG_TYPES = {} unless defined?(PG_TYPES)
158
165
  PG_TYPES[114] = JSONDatabaseMethods.method(:parse_json)
166
+ if defined?(PGArray) && PGArray.respond_to?(:register)
167
+ PGArray.register('json', :oid=>199, :scalar_oid=>114)
168
+ end
159
169
  end
170
+
171
+ Database.register_extension(:pg_json, Postgres::JSONDatabaseMethods)
160
172
  end
161
173
 
162
174
  class Array
@@ -0,0 +1,487 @@
1
+ # The pg_range extension adds support for the PostgreSQL 9.2+ range
2
+ # types to Sequel. PostgreSQL range types are similar to ruby's
3
+ # Range class, representating an array of values. However, they
4
+ # are more flexible than ruby's ranges, allowing exclusive beginnings
5
+ # and endings (ruby's range only allows exclusive endings), and
6
+ # unbounded beginnings and endings (which ruby's range does not
7
+ # support).
8
+ #
9
+ # This extension integrates with Sequel's native postgres adapter, so
10
+ # that when range type values are retrieved, they are parsed and returned
11
+ # as instances of Sequel::Postgres::PGRange. PGRange mostly acts
12
+ # like a Range, but it's not a Range as not all PostgreSQL range
13
+ # type values would be valid ruby ranges. If the range type value
14
+ # you are using is a valid ruby range, you can call PGRange#to_range
15
+ # to get a Range. However, if you call PGRange#to_range on a range
16
+ # type value uses features that ruby's Range does not support, an
17
+ # exception will be raised.
18
+ #
19
+ # In addition to the parser, this extension comes with literalizers
20
+ # for both PGRange and Range that use the standard Sequel literalization
21
+ # callbacks, so they work on all adapters.
22
+ #
23
+ # To turn an existing Range into a PGRange:
24
+ #
25
+ # range.pg_range
26
+ #
27
+ # You may want to specify a specific range type:
28
+ #
29
+ # range.pg_range(:daterange)
30
+ #
31
+ # If you specify the range database type, Sequel will automatically cast
32
+ # the value to that type when literalizing.
33
+ #
34
+ # If you would like to use range columns in your model objects, you
35
+ # probably want to modify the schema parsing/typecasting so that it
36
+ # recognizes and correctly handles the range type columns, which you can
37
+ # do by:
38
+ #
39
+ # DB.extension :pg_range
40
+ #
41
+ # If you are not using the native postgres adapter, you probably
42
+ # also want to use the typecast_on_load plugin in the model, and
43
+ # set it to typecast the range type column(s) on load.
44
+ #
45
+ # This extension integrates with the pg_array extension. If you plan
46
+ # to use arrays of range types, load the pg_array extension before the
47
+ # pg_range extension:
48
+ #
49
+ # DB.extension :pg_array, :pg_range
50
+
51
+ Sequel.require 'adapters/utils/pg_types'
52
+
53
+ module Sequel
54
+ module Postgres
55
+ class PGRange
56
+
57
+ # Map of string database type names to type symbols (e.g. 'int4range' => :int4range),
58
+ # used in the schema parsing.
59
+ RANGE_TYPES = {}
60
+
61
+ EMPTY = 'empty'.freeze
62
+ EMPTY_STRING = ''.freeze
63
+ QUOTED_EMPTY_STRING = '""'.freeze
64
+ OPEN_PAREN = "(".freeze
65
+ CLOSE_PAREN = ")".freeze
66
+ OPEN_BRACKET = "[".freeze
67
+ CLOSE_BRACKET = "]".freeze
68
+ ESCAPE_RE = /("|,|\\|\[|\]|\(|\))/.freeze
69
+ ESCAPE_REPLACE = '\\\\\1'.freeze
70
+ CAST = '::'.freeze
71
+
72
+ # Registers a range type that the extension should handle. Makes a Database instance that
73
+ # has been extended with DatabaseMethods recognize the range type given and set up the
74
+ # appropriate typecasting. Also sets up automatic typecasting for the native postgres
75
+ # adapter, so that on retrieval, the values are automatically converted to PGRange instances.
76
+ # The db_type argument should be the name of the range type. Accepts the following options:
77
+ #
78
+ # :converter :: A callable object (e.g. Proc), that is called with the start or end of the range
79
+ # (usually a string), and should return the appropriate typecasted object.
80
+ # :oid :: The PostgreSQL OID for the range type. This is used by the Sequel postgres adapter
81
+ # to set up automatic type conversion on retrieval from the database.
82
+ # :subtype_oid :: Should be the PostgreSQL OID for the range's subtype. If given,
83
+ # automatically sets the :converter option by looking for scalar conversion
84
+ # proc.
85
+ #
86
+ # If a block is given, it is treated as the :converter option.
87
+ def self.register(db_type, opts={}, &block)
88
+ db_type = db_type.to_s.dup.freeze
89
+
90
+ if converter = opts[:converter]
91
+ raise Error, "can't provide both a block and :converter option to register" if block
92
+ else
93
+ converter = block
94
+ end
95
+
96
+ if soid = opts[:subtype_oid]
97
+ raise Error, "can't provide both a converter and :scalar_oid option to register" if converter
98
+ raise Error, "no conversion proc for :scalar_oid=>#{soid.inspect} in PG_TYPES" unless converter = PG_TYPES[soid]
99
+ end
100
+
101
+ parser = Parser.new(db_type, converter)
102
+
103
+ RANGE_TYPES[db_type] = db_type.to_sym
104
+
105
+ DatabaseMethods.define_range_typecast_method(db_type, parser)
106
+
107
+ if oid = opts[:oid]
108
+ Sequel::Postgres::PG_TYPES[oid] = parser
109
+ end
110
+
111
+ nil
112
+ end
113
+
114
+ # Creates callable objects that convert strings into PGRange instances.
115
+ class Parser
116
+ # Regexp that parses the full range of PostgreSQL range type output,
117
+ # except for empty ranges.
118
+ PARSER = /\A(\[|\()("((?:\\"|[^"])*)"|[^"]*),("((?:\\"|[^"])*)"|[^"]*)(\]|\))\z/o
119
+
120
+ REPLACE_RE = /\\(.)/.freeze
121
+ REPLACE_WITH = '\1'.freeze
122
+
123
+ # The database range type for this parser (e.g. 'int4range'),
124
+ # automatically setting the db_type for the returned PGRange instances.
125
+ attr_reader :db_type
126
+
127
+ # A callable object to convert the beginning and ending of the range into
128
+ # the appropriate ruby type.
129
+ attr_reader :converter
130
+
131
+ # Set the db_type and converter on initialization.
132
+ def initialize(db_type, converter=nil)
133
+ @db_type = db_type.to_s.dup.freeze if db_type
134
+ @converter = converter
135
+ end
136
+
137
+ # Parse the range type input string into a PGRange value.
138
+ def call(string)
139
+ if string == EMPTY
140
+ return PGRange.empty(db_type)
141
+ end
142
+
143
+ raise(InvalidValue, "invalid or unhandled range format: #{string.inspect}") unless matches = PARSER.match(string)
144
+
145
+ exclude_begin = matches[1] == '('
146
+ exclude_end = matches[6] == ')'
147
+
148
+ # If the input is quoted, it needs to be unescaped. Also, quoted input isn't
149
+ # checked for emptiness, since the empty quoted string is considered an
150
+ # element that happens to be the empty string, while an unquoted empty string
151
+ # is considered unbounded.
152
+ #
153
+ # While PostgreSQL allows pure escaping for input (without quoting), it appears
154
+ # to always use the quoted output form when characters need to be escaped, so
155
+ # there isn't a need to unescape unquoted output.
156
+ if beg = matches[3]
157
+ beg.gsub!(REPLACE_RE, REPLACE_WITH)
158
+ else
159
+ beg = matches[2] unless matches[2].empty?
160
+ end
161
+ if en = matches[5]
162
+ en.gsub!(REPLACE_RE, REPLACE_WITH)
163
+ else
164
+ en = matches[4] unless matches[4].empty?
165
+ end
166
+
167
+ if c = converter
168
+ beg = c.call(beg) if beg
169
+ en = c.call(en) if en
170
+ end
171
+
172
+ PGRange.new(beg, en, :exclude_begin=>exclude_begin, :exclude_end=>exclude_end, :db_type=>db_type)
173
+ end
174
+ end
175
+
176
+ module DatabaseMethods
177
+ # Reset the conversion procs if using the native postgres adapter,
178
+ # and extend the datasets to correctly literalize ruby Range values.
179
+ def self.extended(db)
180
+ db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
181
+ db.extend_datasets(DatasetMethods)
182
+ end
183
+
184
+ # Define a private range typecasting method for the given type that uses
185
+ # the parser argument to do the type conversion.
186
+ def self.define_range_typecast_method(type, parser)
187
+ meth = :"typecast_value_#{type}"
188
+ define_method(meth){|v| typecast_value_pg_range(v, parser)}
189
+ private meth
190
+ end
191
+
192
+ # Handle Range and PGRange values in bound variables
193
+ def bound_variable_arg(arg, conn)
194
+ case arg
195
+ when PGRange
196
+ arg.unquoted_literal(schema_utility_dataset)
197
+ when Range
198
+ PGRange.from_range(arg).unquoted_literal(schema_utility_dataset)
199
+ else
200
+ super
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ # Handle arrays of range types in bound variables.
207
+ def bound_variable_array(a)
208
+ case a
209
+ when PGRange, Range
210
+ "\"#{bound_variable_arg(a, nil)}\""
211
+ else
212
+ super
213
+ end
214
+ end
215
+
216
+ # Manually override the typecasting for tsrange and tstzrange types so that
217
+ # they use the database's timezone instead of the global Sequel
218
+ # timezone.
219
+ def get_conversion_procs(conn)
220
+ procs = super
221
+
222
+ converter = method(:to_application_timestamp)
223
+ procs[3908] = Parser.new("tsrange", converter)
224
+ procs[3910] = Parser.new("tstzrange", converter)
225
+ if defined?(PGArray::Creator)
226
+ procs[3909] = PGArray::Creator.new("tsrange", procs[3908])
227
+ procs[3911] = PGArray::Creator.new("tstzrange", procs[3910])
228
+ end
229
+
230
+ procs
231
+ end
232
+
233
+ # Recognize the registered database range types.
234
+ def schema_column_type(db_type)
235
+ if type = RANGE_TYPES[db_type]
236
+ type
237
+ else
238
+ super
239
+ end
240
+ end
241
+
242
+ # Typecast value correctly to a PGRange. If already an
243
+ # PGRange instance with the same db_type, return as is.
244
+ # If a PGRange with a different subtype, return a new
245
+ # PGRange with the same values and the expected subtype.
246
+ # If a Range object, create a PGRange with the given
247
+ # db_type. If a string, assume it is in PostgreSQL
248
+ # output format and parse it using the parser.
249
+ def typecast_value_pg_range(value, parser)
250
+ case value
251
+ when PGRange
252
+ if value.db_type.to_s == parser.db_type
253
+ value
254
+ elsif value.empty?
255
+ PGRange.empty(parser.db_type)
256
+ else
257
+ PGRange.new(value.begin, value.end, :exclude_begin=>value.exclude_begin?, :exclude_end=>value.exclude_end?, :db_type=>parser.db_type)
258
+ end
259
+ when Range
260
+ PGRange.from_range(value, parser.db_type)
261
+ when String
262
+ parser.call(value)
263
+ else
264
+ raise Sequel::InvalidValue, "invalid value for range type: #{value.inspect}"
265
+ end
266
+ end
267
+ end
268
+
269
+ module DatasetMethods
270
+ # Handle literalization of ruby Range objects, treating them as
271
+ # PostgreSQL ranges.
272
+ def literal_other_append(sql, v)
273
+ case v
274
+ when Range
275
+ super(sql, Sequel::Postgres::PGRange.from_range(v))
276
+ else
277
+ super
278
+ end
279
+ end
280
+ end
281
+
282
+ include Enumerable
283
+
284
+ # The beginning of the range. If nil, the range has an unbounded beginning.
285
+ attr_reader :begin
286
+
287
+ # The end of the range. If nil, the range has an unbounded ending.
288
+ attr_reader :end
289
+
290
+ # The PostgreSQL database type for the range (e.g. 'int4range').
291
+ attr_reader :db_type
292
+
293
+ # Create a new PGRange instance using the beginning and ending of the ruby Range,
294
+ # with the given db_type.
295
+ def self.from_range(range, db_type=nil)
296
+ new(range.begin, range.end, :exclude_end=>range.exclude_end?, :db_type=>db_type)
297
+ end
298
+
299
+ # Create an empty PGRange with the given database type.
300
+ def self.empty(db_type=nil)
301
+ new(nil, nil, :empty=>true, :db_type=>db_type)
302
+ end
303
+
304
+ # Initialize a new PGRange instance. Accepts the following options:
305
+ #
306
+ # :db_type :: The PostgreSQL database type for the range.
307
+ # :empty :: Whether the range is empty (has no points)
308
+ # :exclude_begin :: Whether the beginning element is excluded from the range.
309
+ # :exclude_end :: Whether the ending element is excluded from the range.
310
+ def initialize(beg, en, opts={})
311
+ @begin = beg
312
+ @end = en
313
+ @empty = !!opts[:empty]
314
+ @exclude_begin = !!opts[:exclude_begin]
315
+ @exclude_end = !!opts[:exclude_end]
316
+ @db_type = opts[:db_type]
317
+ if @empty
318
+ raise(Error, 'cannot have an empty range with either a beginning or ending') unless @begin.nil? && @end.nil? && opts[:exclude_begin].nil? && opts[:exclude_end].nil?
319
+ end
320
+ end
321
+
322
+ # Delegate to the ruby range object so that the object mostly acts like a range.
323
+ range_methods = %w'each last first step'
324
+ range_methods << 'cover?' if RUBY_VERSION >= '1.9'
325
+ range_methods.each do |m|
326
+ class_eval("def #{m}(*a, &block) to_range.#{m}(*a, &block) end", __FILE__, __LINE__)
327
+ end
328
+
329
+ # Consider the receiver equal to other PGRange instances with the
330
+ # same beginning, ending, exclusions, and database type. Also consider
331
+ # it equal to Range instances if this PGRange can be converted to a
332
+ # a Range and those ranges are equal.
333
+ def eql?(other)
334
+ case other
335
+ when PGRange
336
+ if db_type == other.db_type
337
+ if empty?
338
+ other.empty?
339
+ elsif other.empty?
340
+ false
341
+ else
342
+ [:@begin, :@end, :@exclude_begin, :@exclude_end].all?{|v| instance_variable_get(v) == other.instance_variable_get(v)}
343
+ end
344
+ else
345
+ false
346
+ end
347
+ when Range
348
+ if valid_ruby_range?
349
+ to_range.eql?(other)
350
+ else
351
+ false
352
+ end
353
+ else
354
+ false
355
+ end
356
+ end
357
+ alias == eql?
358
+
359
+ # Allow PGRange values in case statements, where they return true if they
360
+ # are equal to each other using eql?, or if this PGRange can be converted
361
+ # to a Range, delegating to that range.
362
+ def ===(other)
363
+ if eql?(other)
364
+ true
365
+ else
366
+ if valid_ruby_range?
367
+ to_range === other
368
+ else
369
+ false
370
+ end
371
+ end
372
+ end
373
+
374
+ # Whether this range is empty (has no points).
375
+ def empty?
376
+ @empty
377
+ end
378
+
379
+ # Whether the beginning element is excluded from the range.
380
+ def exclude_begin?
381
+ @exclude_begin
382
+ end
383
+
384
+ # Whether the ending element is excluded from the range.
385
+ def exclude_end?
386
+ @exclude_end
387
+ end
388
+
389
+ # Append a literalize version of the receiver to the sql.
390
+ def sql_literal_append(ds, sql)
391
+ ds.literal_append(sql, unquoted_literal(ds))
392
+ if s = @db_type
393
+ sql << CAST << s.to_s
394
+ end
395
+ end
396
+
397
+ # Return a ruby Range object for this instance, if one can be created.
398
+ def to_range
399
+ return @range if @range
400
+ raise(Error, "cannot create ruby range for an empty PostgreSQL range") if empty?
401
+ raise(Error, "cannot create ruby range when PostgreSQL range excludes beginning element") if exclude_begin?
402
+ raise(Error, "cannot create ruby range when PostgreSQL range has unbounded beginning") unless self.begin
403
+ raise(Error, "cannot create ruby range when PostgreSQL range has unbounded ending") unless self.end
404
+ @range = Range.new(self.begin, self.end, exclude_end?)
405
+ end
406
+
407
+ # Whether or not this PGRange is a valid ruby range. In order to be a valid ruby range,
408
+ # it must have a beginning and an ending (no unbounded ranges), and it cannot exclude
409
+ # the beginning element.
410
+ def valid_ruby_range?
411
+ !(empty? || exclude_begin? || !self.begin || !self.end)
412
+ end
413
+
414
+ # Whether the beginning of the range is unbounded.
415
+ def unbounded_begin?
416
+ self.begin.nil? && !empty?
417
+ end
418
+
419
+ # Whether the end of the range is unbounded.
420
+ def unbounded_end?
421
+ self.end.nil? && !empty?
422
+ end
423
+
424
+ # Return a string containing the unescaped version of the range.
425
+ # Separated out for use by the bound argument code.
426
+ def unquoted_literal(ds)
427
+ if empty?
428
+ EMPTY
429
+ else
430
+ "#{exclude_begin? ? OPEN_PAREN : OPEN_BRACKET}#{escape_value(self.begin, ds)},#{escape_value(self.end, ds)}#{exclude_end? ? CLOSE_PAREN : CLOSE_BRACKET}"
431
+ end
432
+ end
433
+
434
+ private
435
+
436
+ # Escape common range types. Instead of quoting, just backslash escape all
437
+ # special characters.
438
+ def escape_value(k, ds)
439
+ case k
440
+ when nil
441
+ EMPTY_STRING
442
+ when Date, Time
443
+ ds.literal(k)[1...-1]
444
+ when Integer, Float
445
+ k.to_s
446
+ when BigDecimal
447
+ k.to_s('F')
448
+ when LiteralString
449
+ k
450
+ when String
451
+ if k.empty?
452
+ QUOTED_EMPTY_STRING
453
+ else
454
+ k.gsub(ESCAPE_RE, ESCAPE_REPLACE)
455
+ end
456
+ else
457
+ ds.literal(k).gsub(ESCAPE_RE, ESCAPE_REPLACE)
458
+ end
459
+ end
460
+ end
461
+
462
+ PGRange.register('int4range', :oid=>3904, :subtype_oid=>23)
463
+ PGRange.register('numrange', :oid=>3906, :subtype_oid=>1700)
464
+ PGRange.register('tsrange', :oid=>3908, :subtype_oid=>1114)
465
+ PGRange.register('tstzrange', :oid=>3910, :subtype_oid=>1184)
466
+ PGRange.register('daterange', :oid=>3912, :subtype_oid=>1082)
467
+ PGRange.register('int8range', :oid=>3926, :subtype_oid=>20)
468
+ if defined?(PGArray) && PGArray.respond_to?(:register)
469
+ PGArray.register('int4range', :oid=>3905, :scalar_oid=>3904, :scalar_typecast=>:int4range)
470
+ PGArray.register('numrange', :oid=>3907, :scalar_oid=>3906, :scalar_typecast=>:numrange)
471
+ PGArray.register('tsrange', :oid=>3909, :scalar_oid=>3908, :scalar_typecast=>:tsrange)
472
+ PGArray.register('tstzrange', :oid=>3911, :scalar_oid=>3910, :scalar_typecast=>:tstzrange)
473
+ PGArray.register('daterange', :oid=>3913, :scalar_oid=>3912, :scalar_typecast=>:daterange)
474
+ PGArray.register('int8range', :oid=>3927, :scalar_oid=>3926, :scalar_typecast=>:int8range)
475
+ end
476
+ end
477
+
478
+ Database.register_extension(:pg_range, Postgres::PGRange::DatabaseMethods)
479
+ end
480
+
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)
486
+ end
487
+ end