sequel 3.36.1 → 3.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +13 -0
  3. data/bin/sequel +12 -16
  4. data/doc/advanced_associations.rdoc +36 -67
  5. data/doc/association_basics.rdoc +11 -16
  6. data/doc/release_notes/3.37.0.txt +338 -0
  7. data/doc/schema_modification.rdoc +4 -0
  8. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  9. data/lib/sequel/adapters/jdbc/postgresql.rb +26 -8
  10. data/lib/sequel/adapters/mysql2.rb +4 -3
  11. data/lib/sequel/adapters/odbc/mssql.rb +2 -2
  12. data/lib/sequel/adapters/postgres.rb +4 -60
  13. data/lib/sequel/adapters/shared/mssql.rb +2 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +0 -5
  15. data/lib/sequel/adapters/shared/postgres.rb +68 -2
  16. data/lib/sequel/adapters/shared/sqlite.rb +17 -1
  17. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +12 -1
  18. data/lib/sequel/adapters/utils/pg_types.rb +76 -0
  19. data/lib/sequel/core.rb +13 -0
  20. data/lib/sequel/database/misc.rb +41 -1
  21. data/lib/sequel/database/schema_generator.rb +23 -10
  22. data/lib/sequel/database/schema_methods.rb +26 -4
  23. data/lib/sequel/dataset/graph.rb +2 -1
  24. data/lib/sequel/dataset/query.rb +62 -2
  25. data/lib/sequel/extensions/_pretty_table.rb +7 -3
  26. data/lib/sequel/extensions/arbitrary_servers.rb +5 -4
  27. data/lib/sequel/extensions/blank.rb +4 -0
  28. data/lib/sequel/extensions/columns_introspection.rb +13 -2
  29. data/lib/sequel/extensions/core_extensions.rb +6 -0
  30. data/lib/sequel/extensions/eval_inspect.rb +158 -0
  31. data/lib/sequel/extensions/inflector.rb +4 -0
  32. data/lib/sequel/extensions/looser_typecasting.rb +5 -4
  33. data/lib/sequel/extensions/migration.rb +4 -1
  34. data/lib/sequel/extensions/named_timezones.rb +4 -0
  35. data/lib/sequel/extensions/null_dataset.rb +4 -0
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pg_array.rb +219 -168
  38. data/lib/sequel/extensions/pg_array_ops.rb +7 -2
  39. data/lib/sequel/extensions/pg_auto_parameterize.rb +10 -4
  40. data/lib/sequel/extensions/pg_hstore.rb +3 -1
  41. data/lib/sequel/extensions/pg_hstore_ops.rb +7 -2
  42. data/lib/sequel/extensions/pg_inet.rb +28 -3
  43. data/lib/sequel/extensions/pg_interval.rb +192 -0
  44. data/lib/sequel/extensions/pg_json.rb +21 -9
  45. data/lib/sequel/extensions/pg_range.rb +487 -0
  46. data/lib/sequel/extensions/pg_range_ops.rb +122 -0
  47. data/lib/sequel/extensions/pg_statement_cache.rb +3 -2
  48. data/lib/sequel/extensions/pretty_table.rb +12 -1
  49. data/lib/sequel/extensions/query.rb +4 -0
  50. data/lib/sequel/extensions/query_literals.rb +6 -6
  51. data/lib/sequel/extensions/schema_dumper.rb +39 -38
  52. data/lib/sequel/extensions/select_remove.rb +4 -0
  53. data/lib/sequel/extensions/server_block.rb +3 -2
  54. data/lib/sequel/extensions/split_array_nil.rb +65 -0
  55. data/lib/sequel/extensions/sql_expr.rb +4 -0
  56. data/lib/sequel/extensions/string_date_time.rb +4 -0
  57. data/lib/sequel/extensions/thread_local_timezones.rb +9 -3
  58. data/lib/sequel/extensions/to_dot.rb +4 -0
  59. data/lib/sequel/model/associations.rb +150 -91
  60. data/lib/sequel/plugins/identity_map.rb +2 -2
  61. data/lib/sequel/plugins/list.rb +1 -0
  62. data/lib/sequel/plugins/many_through_many.rb +33 -32
  63. data/lib/sequel/plugins/nested_attributes.rb +11 -3
  64. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  65. data/lib/sequel/plugins/schema.rb +1 -1
  66. data/lib/sequel/sql.rb +14 -14
  67. data/lib/sequel/version.rb +2 -2
  68. data/spec/adapters/mysql_spec.rb +25 -0
  69. data/spec/adapters/postgres_spec.rb +572 -28
  70. data/spec/adapters/sqlite_spec.rb +16 -1
  71. data/spec/core/database_spec.rb +61 -2
  72. data/spec/core/dataset_spec.rb +92 -0
  73. data/spec/core/expression_filters_spec.rb +12 -0
  74. data/spec/extensions/arbitrary_servers_spec.rb +1 -1
  75. data/spec/extensions/boolean_readers_spec.rb +25 -25
  76. data/spec/extensions/eval_inspect_spec.rb +58 -0
  77. data/spec/extensions/json_serializer_spec.rb +0 -6
  78. data/spec/extensions/list_spec.rb +1 -1
  79. data/spec/extensions/looser_typecasting_spec.rb +7 -7
  80. data/spec/extensions/many_through_many_spec.rb +81 -0
  81. data/spec/extensions/nested_attributes_spec.rb +21 -4
  82. data/spec/extensions/pg_array_ops_spec.rb +1 -11
  83. data/spec/extensions/pg_array_spec.rb +181 -90
  84. data/spec/extensions/pg_auto_parameterize_spec.rb +3 -3
  85. data/spec/extensions/pg_hstore_spec.rb +1 -3
  86. data/spec/extensions/pg_inet_spec.rb +6 -1
  87. data/spec/extensions/pg_interval_spec.rb +73 -0
  88. data/spec/extensions/pg_json_spec.rb +5 -9
  89. data/spec/extensions/pg_range_ops_spec.rb +49 -0
  90. data/spec/extensions/pg_range_spec.rb +372 -0
  91. data/spec/extensions/pg_statement_cache_spec.rb +1 -2
  92. data/spec/extensions/query_literals_spec.rb +1 -2
  93. data/spec/extensions/schema_dumper_spec.rb +48 -89
  94. data/spec/extensions/serialization_spec.rb +1 -5
  95. data/spec/extensions/server_block_spec.rb +2 -2
  96. data/spec/extensions/spec_helper.rb +12 -2
  97. data/spec/extensions/split_array_nil_spec.rb +24 -0
  98. data/spec/integration/associations_test.rb +4 -4
  99. data/spec/integration/database_test.rb +2 -2
  100. data/spec/integration/dataset_test.rb +4 -4
  101. data/spec/integration/eager_loader_test.rb +6 -6
  102. data/spec/integration/plugin_test.rb +2 -2
  103. data/spec/integration/spec_helper.rb +2 -2
  104. data/spec/model/association_reflection_spec.rb +5 -0
  105. data/spec/model/associations_spec.rb +156 -49
  106. data/spec/model/eager_loading_spec.rb +137 -2
  107. data/spec/model/model_spec.rb +10 -10
  108. metadata +15 -2
@@ -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