rom-sql 2.0.0 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 33d9d062a6396ba9cf578abc3181d5d7aadfc2b9
4
- data.tar.gz: 5940f64e388cd42a0dbe28a0f6d3517622154fcc
3
+ metadata.gz: 0d964bf81ba3e921c992aff816f2c09bc1c91c3a
4
+ data.tar.gz: 1f8516a4f8420e4a009d41b98d9d1f1dd9bcb608
5
5
  SHA512:
6
- metadata.gz: 22a3d5592b184cb7de84439a7312c0ed97cee07f85e02a876c0c0d6e97758c9f4da65e3ba070183aa2e06640fac4dcf3ea9fdcf5b3989199d37441bbdf04385f
7
- data.tar.gz: d300f4387513f789f3840be6bd561657419ff572de6ceb1decf3a470207448acf36d3d039c2284658c8e11c1445bd0412c6a572e7788f8883d32cb574a868cdd
6
+ metadata.gz: 21ac8dc3534190ed1a72d6b0a798cdc60c0ff9207995a304a9edfc44356e41c53a0123fa49718b91b7c093cd53060d1c1adb15a2fd02694e85ceaf310b7647aa
7
+ data.tar.gz: 63dcf1fcc5c3bc2e5f4dc09335eb2da9138abf83790934daf725651df004c5333e4f15669a7a64c2c78160ffd9326a25fe4feec0d75d1dbbee19b7b41f88ba2d
@@ -1,3 +1,17 @@
1
+ ## v2.1.0 2017-10-23
2
+
3
+ ### Added
4
+
5
+ * Support for PG's range types (v-kolesnikov)
6
+ * Support for PG's `ltree` (GustavoCaso + solnic)
7
+
8
+ ### Fixed
9
+
10
+ * Schema inference works with primary keys that have custom types (ie an enum PK column) (v-kolesnikov)
11
+ * Ruby warnings are gone (solnic)
12
+
13
+ [Compare v2.0.0...v2.1.0](https://github.com/rom-rb/rom-sql/compare/v2.0.0...v2.1.0)
14
+
1
15
  ## v2.0.0 2017-10-18
2
16
 
3
17
  ### Added
@@ -10,9 +10,9 @@ module ROM
10
10
  #
11
11
  # @api private
12
12
  def insert(tuples)
13
- dataset = tuples.map do |tuple|
13
+ dataset = tuples.flat_map do |tuple|
14
14
  relation.dataset.returning.insert(tuple)
15
- end.flatten(1)
15
+ end
16
16
 
17
17
  wrap_dataset(dataset)
18
18
  end
@@ -27,16 +27,23 @@ module ROM
27
27
  'box' => Types::Box,
28
28
  'lseg' => Types::LineSegment,
29
29
  'polygon' => Types::Polygon,
30
- 'path' => Types::Path
30
+ 'path' => Types::Path,
31
+ 'int4range' => Types::Int4Range,
32
+ 'int8range' => Types::Int8Range,
33
+ 'numrange' => Types::NumRange,
34
+ 'tsrange' => Types::TsRange,
35
+ 'tstzrange' => Types::TsTzRange,
36
+ 'daterange' => Types::DateRange,
37
+ 'ltree' => Types::LTree
31
38
  ).freeze
32
39
 
33
40
  db_array_type_matcher '[]'.freeze
34
41
 
35
- def map_pk_type(type, db_type)
42
+ def map_pk_type(type, db_type, options = {})
36
43
  if numeric?(type, db_type)
37
44
  type = self.class.numeric_pk_type
38
45
  else
39
- type = map_type(type, db_type)
46
+ type = map_type(type, db_type, options)
40
47
  end
41
48
 
42
49
  type.meta(primary_key: true)
@@ -44,7 +51,10 @@ module ROM
44
51
 
45
52
  def map_type(ruby_type, db_type, enum_values: nil, **_)
46
53
  if db_type.end_with?(self.class.db_array_type_matcher)
47
- Types::Array(db_type[0...-2])
54
+ member_name = db_type[0...-2]
55
+ member_type = self.class.db_type_mapping[member_name]
56
+
57
+ Types::Array(member_name, member_type)
48
58
  elsif enum_values
49
59
  SQL::Types::String.enum(*enum_values)
50
60
  else
@@ -20,7 +20,14 @@ module ROM
20
20
  Types::Box => 'box',
21
21
  Types::LineSegment => 'lseg',
22
22
  Types::Polygon => 'polygon',
23
- Types::Path => 'path'
23
+ Types::Path => 'path',
24
+ Types::Int4Range => 'int4range',
25
+ Types::Int8Range => 'int8range',
26
+ Types::NumRange => 'numrange',
27
+ Types::TsRange => 'tsrange',
28
+ Types::TsTzRange => 'tstzrange',
29
+ Types::DateRange => 'daterange',
30
+ Types::LTree => 'ltree'
24
31
  )
25
32
  )
26
33
 
@@ -41,3 +41,5 @@ require 'rom/sql/extensions/postgres/types/array'
41
41
  require 'rom/sql/extensions/postgres/types/json'
42
42
  require 'rom/sql/extensions/postgres/types/geometric'
43
43
  require 'rom/sql/extensions/postgres/types/network'
44
+ require 'rom/sql/extensions/postgres/types/range'
45
+ require 'rom/sql/extensions/postgres/types/ltree'
@@ -2,6 +2,8 @@ require 'sequel/core'
2
2
 
3
3
  Sequel.extension(*%i(pg_array pg_array_ops))
4
4
 
5
+ require 'rom/sql/extensions/postgres/types/array_types'
6
+
5
7
  module ROM
6
8
  module SQL
7
9
  module Postgres
@@ -10,18 +12,14 @@ module ROM
10
12
 
11
13
  ArrayRead = Array.constructor { |v| v.respond_to?(:to_ary) ? v.to_ary : v }
12
14
 
13
- constructor = -> type { -> value { Sequel.pg_array(value, type) } }
14
-
15
- @array_types = ::Hash.new do |hash, type|
16
- name = "#{ type }[]"
17
- array_type = Type(name, Array.constructor(constructor.(type))).
18
- meta(type: type, read: ArrayRead)
19
- TypeExtensions.register(array_type) { include ArrayMethods }
20
- hash[type] = array_type
15
+ # @api private
16
+ def self.array_types
17
+ @array_types ||= ArrayTypes.new(Postgres::Types::Array, Postgres::Types::ArrayRead)
21
18
  end
22
19
 
23
- def self.Array(db_type)
24
- @array_types[db_type]
20
+ # @api private
21
+ def self.Array(db_type, member_type = nil)
22
+ array_types[db_type, member_type]
25
23
  end
26
24
 
27
25
  # @!parse
@@ -0,0 +1,70 @@
1
+ require 'rom/sql/type_extensions'
2
+
3
+ module ROM
4
+ module SQL
5
+ module Postgres
6
+ module Types
7
+ # @api private
8
+ class ArrayTypes
9
+ attr_reader :elements
10
+
11
+ attr_reader :constructor
12
+
13
+ attr_reader :base_write_type
14
+
15
+ attr_reader :base_read_type
16
+
17
+ def initialize(base_write_type, base_read_type)
18
+ @elements = {}
19
+ @base_write_type = base_write_type
20
+ @base_read_type = base_read_type
21
+ @constructor = proc { |db_type, member|
22
+ if member
23
+ -> arr { Sequel.pg_array(arr.map { |v| member[v] }, db_type) }
24
+ else
25
+ -> arr { Sequel.pg_array(arr, db_type) }
26
+ end
27
+ }
28
+ end
29
+
30
+ def [](db_type, member_type = nil)
31
+ elements.fetch(db_type) do
32
+ name = "#{db_type}[]"
33
+
34
+ write_type = build_write_type(db_type, member_type)
35
+ read_type = build_read_type(db_type, member_type)
36
+
37
+ array_type = Types.Type(name, write_type).meta(type: db_type, read: read_type)
38
+
39
+ register_extension(array_type)
40
+
41
+ elements[db_type] = array_type
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def build_write_type(db_type, member_type)
48
+ if member_type
49
+ base_write_type.constructor(constructor[db_type, member_type])
50
+ else
51
+ base_write_type.constructor(constructor[db_type])
52
+ end
53
+ end
54
+
55
+ def build_read_type(db_type, member_type)
56
+ if member_type && member_type.meta[:read]
57
+ base_read_type.of(member_type.meta[:read])
58
+ else
59
+ base_read_type
60
+ end
61
+ end
62
+
63
+ def register_extension(type)
64
+ TypeExtensions.register(type) { include ArrayMethods }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,329 @@
1
+ require 'rom/types/values'
2
+
3
+ module ROM
4
+ module SQL
5
+ module Postgres
6
+ # @api public
7
+ module Types
8
+ # @see https://www.postgresql.org/docs/current/static/ltree.html
9
+
10
+ LTree = Type('ltree') do
11
+ SQL::Types.define(ROM::Types::Values::TreePath) do
12
+ input do |label_path|
13
+ label_path.to_s
14
+ end
15
+
16
+ output do |label_path|
17
+ ROM::Types::Values::TreePath.new(label_path.to_s) if label_path
18
+ end
19
+ end
20
+ end
21
+
22
+ # @!parse
23
+ # class SQL::Attribute
24
+ # # @!method match(value)
25
+ # # Check whether the LTree match a lquery value
26
+ # # Translates to the ~ operator
27
+ # #
28
+ # # @example
29
+ # # people.select(:name).where { ltree_tags.match('Bottom.Cities') }
30
+ # #
31
+ # # @param [String] value
32
+ # #
33
+ # # @return [SQL::Attribute<Types::Bool>]
34
+ # #
35
+ # # @api public
36
+ #
37
+ # # @!method match_any(value)
38
+ # # Check whether the LTree match any of the lquery values
39
+ # # Translates to the ? operator
40
+ # #
41
+ # # @example
42
+ # # people.select(:name).where { ltree_tags.match_any(['Bottom', 'Bottom.Cities.*']) }
43
+ # # people.select(:name).where { ltree_tags.match_any('Bottom,Bottom.Cities.*') }
44
+ # #
45
+ # # @param [Array,String] value
46
+ # #
47
+ # # @return [SQL::Attribute<Types::Bool>]
48
+ # #
49
+ # # @api public
50
+ #
51
+ # # @!method match_ltextquery(value)
52
+ # # Check whether the LTree match a ltextquery
53
+ # # Translates to the @ operator
54
+ # #
55
+ # # @example
56
+ # # people.select(:name).where { ltree_tags.match_ltextquery('Countries & Brasil') }
57
+ # #
58
+ # # @param [String] value
59
+ # #
60
+ # # @return [SQL::Attribute<Types::Bool>]
61
+ # #
62
+ # # @api public
63
+ #
64
+ # # @!method contain_descendant(value)
65
+ # # Check whether the LTree is a descendant of the LTree values
66
+ # # Translates to the <@ operator
67
+ # #
68
+ # # @example
69
+ # # people.select(:name).where { ltree_tags.contain_descendant(['Bottom.Cities']) }
70
+ # # people.select(:name).where { ltree_tags.contain_descendant('Bottom.Cities, Bottom.Parks') }
71
+ # #
72
+ # # @param [Array<String>, String] value
73
+ # #
74
+ # # @return [SQL::Attribute<Types::Bool>]
75
+ # #
76
+ # # @api public
77
+ #
78
+ # # @!method descendant(value)
79
+ # # Check whether the LTree is a descendant of the LTree value
80
+ # # Translates to the <@ operator
81
+ # #
82
+ # # @example
83
+ # # people.select(:name).where { ltree_tags.descendant('Bottom.Cities') }
84
+ # #
85
+ # # @param [String] value
86
+ # #
87
+ # # @return [SQL::Attribute<Types::Bool>]
88
+ # #
89
+ # # @api public
90
+ #
91
+ # # @!method contain_ascendant(value)
92
+ # # Check whether the LTree is a ascendant of the LTree values
93
+ # # Translates to the @> operator
94
+ # #
95
+ # # @example
96
+ # # people.select(:name).where { ltree_tags.contain_ascendant(['Bottom.Cities']) }
97
+ # # people.select(:name).where { ltree_tags.contain_ascendant('Bottom.Cities, Bottom.Parks') }
98
+ # #
99
+ # # @param [Array<String>, String] value
100
+ # #
101
+ # # @return [SQL::Attribute<Types::Bool>]
102
+ # #
103
+ # # @api public
104
+ #
105
+ # # @!method ascendant(value)
106
+ # # Check whether the LTree is a ascendant of the LTree value
107
+ # # Translates to the @> operator
108
+ # #
109
+ # # @example
110
+ # # people.select(:name).where { ltree_tags.ascendant('Bottom.Cities') }
111
+ # #
112
+ # # @param [String] value
113
+ # #
114
+ # # @return [SQL::Attribute<Types::Bool>]
115
+ # #
116
+ # # @api public
117
+ #
118
+ # # @!method +(value)
119
+ # # Concatenate two LTree values
120
+ # # Translates to ||
121
+ # #
122
+ # # @example
123
+ # # people.select { (ltree_tags + ROM::Types::Values::TreePath.new('Moscu')).as(:ltree_tags) }.where { name.is('Jade Doe') }
124
+ # # people.select { (ltree_tags + 'Moscu').as(:ltree_tags) }.where { name.is('Jade Doe') }
125
+ # #
126
+ # # @param [LTree, String] keys
127
+ # #
128
+ # # @return [SQL::Attribute<Types::LTree>]
129
+ # #
130
+ # # @api public
131
+ #
132
+ # # @!method contain_any_ltextquery(value)
133
+ # # Does LTree array contain any path matching ltxtquery
134
+ # # Translates to @
135
+ # #
136
+ # # @example
137
+ # # people.select(:name).where { parents_tags.contain_any_ltextquery('Parks')}
138
+ # #
139
+ # # @param [String] value
140
+ # #
141
+ # # @return [SQL::Attribute<Types::Bool>]
142
+ # #
143
+ # # @api public
144
+ #
145
+ # # @!method contain_ancestor(value)
146
+ # # Does LTree array contain an ancestor of ltree
147
+ # # Translates to @>
148
+ # #
149
+ # # @example
150
+ # # people.select(:name).where { parents_tags.contain_ancestor('Top.Building.EmpireState.381')}
151
+ # #
152
+ # # @param [String] value
153
+ # #
154
+ # # @return [SQL::Attribute<Types::PG::Bool>]
155
+ # #
156
+ # # @api public
157
+ #
158
+ # # @!method contain_descendant(value)
159
+ # # Does LTree array contain an descendant of ltree
160
+ # # Translates to <@
161
+ # #
162
+ # # @example
163
+ # # people.select(:name).where { parents_tags.contain_descendant('Top.Building.EmpireState.381')}
164
+ # #
165
+ # # @param [String] value
166
+ # #
167
+ # # @return [SQL::Attribute<Types::PG::Bool>]
168
+ # #
169
+ # # @api public
170
+ #
171
+ # # @!method find_ancestor(value)
172
+ # # Return first LTree array entry that is an ancestor of ltree, NULL if none
173
+ # # Translates to ?@>
174
+ # #
175
+ # # @example
176
+ # # people.select(:name).where { parents_tags.find_ancestor('Left.Parks').not(nil)}
177
+ # #
178
+ # # @param [String] value
179
+ # #
180
+ # # @return [SQL::Attribute<Types::PG::LTree>]
181
+ # #
182
+ # # @api public
183
+ #
184
+ # # @!method find_descendant(value)
185
+ # # Return first LTree array entry that is an descendant of ltree, NULL if none
186
+ # # Translates to ?<@
187
+ # #
188
+ # # @example
189
+ # # people.select(:name).where { parents_tags.find_descendant('Left.Parks').not(nil)}
190
+ # #
191
+ # # @param [String] value
192
+ # #
193
+ # # @return [SQL::Attribute<Types::PG::LTree>]
194
+ # #
195
+ # # @api public
196
+ #
197
+ # # @!method match_any_lquery(value)
198
+ # # Return first LTree array entry that matches lquery, NULL if none
199
+ # # Translates to ?~
200
+ # #
201
+ # # @example
202
+ # # people.select(:name).where { parents_tags.match_any_lquery('Right.*').not(nil)}
203
+ # #
204
+ # # @param [String] value
205
+ # #
206
+ # # @return [SQL::Attribute<Types::PG::LTree>]
207
+ # #
208
+ # # @api public
209
+ #
210
+ # # @!method match_any_ltextquery(value)
211
+ # # Return first LTree array entry that matches ltextquery, NULL if none
212
+ # # Translates to ?@
213
+ # #
214
+ # # @example
215
+ # # people.select(:name).where { parents_tags.match_any_ltextquery('EmpireState').not(nil)}
216
+ # #
217
+ # # @param [String] value
218
+ # #
219
+ # # @return [SQL::Attribute<Types::PG::LTree>]
220
+ # #
221
+ # # @api public
222
+ #
223
+ # end
224
+ module LTreeMethods
225
+ ASCENDANT = ["(".freeze, " @> ".freeze, ")".freeze].freeze
226
+ FIND_ASCENDANT = ["(".freeze, " ?@> ".freeze, ")".freeze].freeze
227
+ DESCENDANT = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
228
+ FIND_DESCENDANT = ["(".freeze, " ?<@ ".freeze, ")".freeze].freeze
229
+ MATCH_ANY = ["(".freeze, " ? ".freeze, ")".freeze].freeze
230
+ MATCH_ANY_LQUERY = ["(".freeze, " ?~ ".freeze, ")".freeze].freeze
231
+ MATCH_LTEXTQUERY = ["(".freeze, " @ ".freeze, ")".freeze].freeze
232
+ MATCH_ANY_LTEXTQUERY = ["(".freeze, " ?@ ".freeze, ")".freeze].freeze
233
+
234
+ def match(type, expr, query)
235
+ Attribute[SQL::Types::Bool].meta(sql_expr: Sequel::SQL::BooleanExpression.new(:'~', expr, query))
236
+ end
237
+
238
+ def match_any(type, expr, query)
239
+ array = build_array_query(query)
240
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(MATCH_ANY, expr, array))
241
+ end
242
+
243
+ private
244
+
245
+ def custom_operator_expr(string, expr, query)
246
+ Sequel::SQL::PlaceholderLiteralString.new(string, [expr, query])
247
+ end
248
+
249
+ def build_array_query(query, array_type = 'lquery')
250
+ case query
251
+ when Array
252
+ ROM::SQL::Types::PG::Array(array_type)[query]
253
+ when String
254
+ ROM::SQL::Types::PG::Array(array_type)[query.split(',')]
255
+ end
256
+ end
257
+ end
258
+
259
+ TypeExtensions.register(ROM::SQL::Types::PG::Array('ltree', LTree)) do
260
+ include LTreeMethods
261
+
262
+ def contain_any_ltextquery(type, expr, query)
263
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::MATCH_LTEXTQUERY, expr, query))
264
+ end
265
+
266
+ def contain_ancestor(type, expr, query)
267
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::ASCENDANT, expr, query))
268
+ end
269
+
270
+ def contain_descendant(type, expr, query)
271
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::DESCENDANT, expr, query))
272
+ end
273
+
274
+ def find_ancestor(type, expr, query)
275
+ Attribute[LTree].meta(sql_expr: custom_operator_expr(LTreeMethods::FIND_ASCENDANT, expr, query))
276
+ end
277
+
278
+ def find_descendant(type, expr, query)
279
+ Attribute[LTree].meta(sql_expr: custom_operator_expr(LTreeMethods::FIND_DESCENDANT, expr, query))
280
+ end
281
+
282
+ def match_any_lquery(type, expr, query)
283
+ Attribute[LTree].meta(sql_expr: custom_operator_expr(LTreeMethods::MATCH_ANY_LQUERY, expr, query))
284
+ end
285
+
286
+ def match_any_ltextquery(type, expr, query)
287
+ Attribute[LTree].meta(sql_expr: custom_operator_expr(LTreeMethods::MATCH_ANY_LTEXTQUERY, expr, query))
288
+ end
289
+ end
290
+
291
+ TypeExtensions.register(LTree) do
292
+ include LTreeMethods
293
+
294
+ def match_ltextquery(type, expr, query)
295
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::MATCH_LTEXTQUERY, expr, query))
296
+ end
297
+
298
+ def contain_descendant(type, expr, query)
299
+ array = build_array_query(query, 'ltree')
300
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::DESCENDANT, expr, array))
301
+ end
302
+
303
+ def descendant(type, expr, query)
304
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::DESCENDANT, expr, query))
305
+ end
306
+
307
+ def contain_ascendant(type, expr, query)
308
+ array = build_array_query(query, 'ltree')
309
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::ASCENDANT, expr, array))
310
+ end
311
+
312
+ def ascendant(type, expr, query)
313
+ Attribute[SQL::Types::Bool].meta(sql_expr: custom_operator_expr(LTreeMethods::ASCENDANT, expr, query))
314
+ end
315
+
316
+ def +(type, expr, other)
317
+ other_value = case other
318
+ when ROM::Types::Values::TreePath
319
+ other
320
+ else
321
+ ROM::Types::Values::TreePath.new(other)
322
+ end
323
+ Attribute[LTree].meta(sql_expr: Sequel::SQL::StringExpression.new(:'||', expr, other_value.to_s))
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
@@ -0,0 +1,207 @@
1
+ require 'sequel/core'
2
+
3
+ Sequel.extension(:pg_range, :pg_range_ops)
4
+
5
+ module ROM
6
+ module SQL
7
+ module Postgres
8
+ module Values
9
+ Range = ::Struct.new(:lower, :upper, :bounds) do
10
+ PAREN_LEFT = '('.freeze
11
+ PAREN_RIGHT = ')'.freeze
12
+
13
+ def initialize(lower, upper, bounds = :'[)')
14
+ super
15
+ end
16
+
17
+ def exclude_begin?
18
+ bounds[0] == PAREN_LEFT
19
+ end
20
+
21
+ def exclude_end?
22
+ bounds[1] == PAREN_RIGHT
23
+ end
24
+ end
25
+ end
26
+
27
+ # @api public
28
+ module Types
29
+ # The list of range types supported by PostgreSQL
30
+ # @see https://www.postgresql.org/docs/current/static/rangetypes.html
31
+
32
+ @range_parsers = {
33
+ int4range: Sequel::Postgres::PGRange::Parser.new(
34
+ 'int4range', SQL::Types::Coercible::Int
35
+ ),
36
+ int8range: Sequel::Postgres::PGRange::Parser.new(
37
+ 'int8range', SQL::Types::Coercible::Int
38
+ ),
39
+ numrange: Sequel::Postgres::PGRange::Parser.new(
40
+ 'numrange', SQL::Types::Coercible::Int
41
+ ),
42
+ tsrange: Sequel::Postgres::PGRange::Parser.new(
43
+ 'tsrange', SQL::Types::Form::Time
44
+ ),
45
+ tstzrange: Sequel::Postgres::PGRange::Parser.new(
46
+ 'tstzrange', SQL::Types::Form::Time
47
+ ),
48
+ daterange: Sequel::Postgres::PGRange::Parser.new(
49
+ 'daterange', SQL::Types::Form::Date
50
+ )
51
+ }.freeze
52
+
53
+ # @api private
54
+ def self.range_read_type(name)
55
+ SQL::Types.Constructor(Values::Range) do |value|
56
+ pg_range =
57
+ if value.is_a?(Sequel::Postgres::PGRange)
58
+ value
59
+ elsif value && value.respond_to?(:to_s)
60
+ @range_parsers[name].(value.to_s)
61
+ else
62
+ value
63
+ end
64
+
65
+ Values::Range.new(
66
+ pg_range.begin,
67
+ pg_range.end,
68
+ [pg_range.exclude_begin? ? :'(' : :'[',
69
+ pg_range.exclude_end? ? :')' : :']']
70
+ .join('').to_sym
71
+ )
72
+ end
73
+ end
74
+
75
+ # @api private
76
+ def self.range(name, read_type)
77
+ Type(name) do
78
+ type = SQL::Types.Definition(Values::Range).constructor do |range|
79
+ format('%s%s,%s%s',
80
+ range.exclude_begin? ? :'(' : :'[',
81
+ range.lower,
82
+ range.upper,
83
+ range.exclude_end? ? :')' : :']')
84
+ end
85
+
86
+ type.meta(read: read_type)
87
+ end
88
+ end
89
+
90
+ Int4Range = range('int4range', range_read_type(:int4range))
91
+ Int8Range = range('int8range', range_read_type(:int8range))
92
+ NumRange = range('numrange', range_read_type(:numrange))
93
+ TsRange = range('tsrange', range_read_type(:tsrange))
94
+ TsTzRange = range('tstzrange', range_read_type(:tstzrange))
95
+ DateRange = range('daterange', range_read_type(:daterange))
96
+
97
+ module RangeOperators
98
+ def contain(_type, expr, value)
99
+ Attribute[SQL::Types::Bool].meta(
100
+ sql_expr: Sequel.pg_range(expr).contains(value)
101
+ )
102
+ end
103
+
104
+ def contained_by(_type, expr, value)
105
+ Attribute[SQL::Types::Bool].meta(
106
+ sql_expr: Sequel.pg_range(expr).contained_by(value)
107
+ )
108
+ end
109
+
110
+ def overlap(_type, expr, value)
111
+ Attribute[SQL::Types::Bool].meta(
112
+ sql_expr: Sequel.pg_range(expr).overlaps(value)
113
+ )
114
+ end
115
+
116
+ def left_of(_type, expr, value)
117
+ Attribute[SQL::Types::Bool].meta(
118
+ sql_expr: Sequel.pg_range(expr).left_of(value)
119
+ )
120
+ end
121
+
122
+ def right_of(_type, expr, value)
123
+ Attribute[SQL::Types::Bool].meta(
124
+ sql_expr: Sequel.pg_range(expr).right_of(value)
125
+ )
126
+ end
127
+
128
+ def starts_after(_type, expr, value)
129
+ Attribute[SQL::Types::Bool].meta(
130
+ sql_expr: Sequel.pg_range(expr).starts_after(value)
131
+ )
132
+ end
133
+
134
+ def ends_before(_type, expr, value)
135
+ Attribute[SQL::Types::Bool].meta(
136
+ sql_expr: Sequel.pg_range(expr).ends_before(value)
137
+ )
138
+ end
139
+
140
+ def adjacent_to(_type, expr, value)
141
+ Attribute[SQL::Types::Bool].meta(
142
+ sql_expr: Sequel.pg_range(expr).adjacent_to(value)
143
+ )
144
+ end
145
+ end
146
+
147
+ module RangeFunctions
148
+ def lower(_type, expr)
149
+ Attribute[SQL::Types::Bool].meta(
150
+ sql_expr: Sequel.pg_range(expr).lower
151
+ )
152
+ end
153
+
154
+ def upper(_type, expr)
155
+ Attribute[SQL::Types::Bool].meta(
156
+ sql_expr: Sequel.pg_range(expr).upper
157
+ )
158
+ end
159
+
160
+ def is_empty(_type, expr)
161
+ Attribute[SQL::Types::Bool].meta(
162
+ sql_expr: Sequel.pg_range(expr).isempty
163
+ )
164
+ end
165
+
166
+ def lower_inc(_type, expr)
167
+ Attribute[SQL::Types::Bool].meta(
168
+ sql_expr: Sequel.pg_range(expr).lower_inc
169
+ )
170
+ end
171
+
172
+ def upper_inc(_type, expr)
173
+ Attribute[SQL::Types::Bool].meta(
174
+ sql_expr: Sequel.pg_range(expr).upper_inc
175
+ )
176
+ end
177
+
178
+ def lower_inf(_type, expr)
179
+ Attribute[SQL::Types::Bool].meta(
180
+ sql_expr: Sequel.pg_range(expr).lower_inf
181
+ )
182
+ end
183
+
184
+ def upper_inf(_type, expr)
185
+ Attribute[SQL::Types::Bool].meta(
186
+ sql_expr: Sequel.pg_range(expr).upper_inf
187
+ )
188
+ end
189
+ end
190
+
191
+ [
192
+ Int4Range,
193
+ Int8Range,
194
+ NumRange,
195
+ TsRange,
196
+ TsTzRange,
197
+ DateRange
198
+ ].each do |type|
199
+ TypeExtensions.register(type) do
200
+ include RangeOperators
201
+ include RangeFunctions
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -132,8 +132,39 @@ module ROM
132
132
  Attribute[type].meta(sql_expr: ::Sequel.cast(expr, db_type))
133
133
  end
134
134
 
135
+ # Add a FILTER clause to aggregate function (supported by PostgreSQL 9.4+)
136
+ # @see https://www.postgresql.org/docs/current/static/sql-expressions.html
137
+ #
138
+ # Filter aggregate using the specified conditions
139
+ #
140
+ # @example
141
+ # users.project { int::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
142
+ # users.project { int::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
143
+ #
144
+ # @param [Hash,SQL::Attribute] Conditions
145
+ # @yield [block] A block with restrictions
146
+ #
147
+ # @return [SQL::Function]
148
+ #
149
+ # @api public
150
+ def filter(condition = Undefined, &block)
151
+ if block
152
+ conditions = schema.restriction(&block)
153
+ conditions = conditions & condition unless condition.equal?(Undefined)
154
+ else
155
+ conditions = condition
156
+ end
157
+
158
+ super(conditions)
159
+ end
160
+
135
161
  private
136
162
 
163
+ # @api private
164
+ def schema
165
+ meta[:schema]
166
+ end
167
+
137
168
  # @api private
138
169
  def func
139
170
  meta[:func]
@@ -2,7 +2,7 @@ require 'pathname'
2
2
 
3
3
  require 'rom/types'
4
4
  require 'rom/initializer'
5
- require 'rom/sql/migration'
5
+
6
6
  require 'rom/sql/migration/runner'
7
7
  require 'rom/sql/migration/inline_runner'
8
8
  require 'rom/sql/migration/writer'
@@ -121,9 +121,9 @@ module ROM
121
121
 
122
122
  case parent
123
123
  when Array
124
- parent.map do |p|
124
+ parent.flat_map do |p|
125
125
  tuples.map { |tuple| Hash(tuple).merge(fk => p[pk]) }
126
- end.flatten(1)
126
+ end
127
127
  else
128
128
  tuples.map { |tuple| Hash(tuple).update(fk => parent[pk]) }
129
129
  end
@@ -38,7 +38,7 @@ module ROM
38
38
  type = type(meth)
39
39
 
40
40
  if type
41
- ::ROM::SQL::Function.new(type)
41
+ ::ROM::SQL::Function.new(type, schema: schema)
42
42
  else
43
43
  super
44
44
  end
@@ -33,6 +33,8 @@ module ROM
33
33
  [inferred, missing]
34
34
  end
35
35
 
36
+ undef :with
37
+
36
38
  # @api private
37
39
  def with(new_options)
38
40
  self.class.new(options.merge(new_options))
@@ -37,7 +37,7 @@ module ROM
37
37
 
38
38
  def call(primary_key:, db_type:, type:, allow_null:, **rest)
39
39
  if primary_key
40
- map_pk_type(type, db_type)
40
+ map_pk_type(type, db_type, **rest)
41
41
  else
42
42
  mapped_type = map_type(type, db_type, rest)
43
43
 
@@ -56,7 +56,7 @@ module ROM
56
56
  end
57
57
 
58
58
  # @api private
59
- def map_pk_type(_ruby_type, _db_type)
59
+ def map_pk_type(_ruby_type, _db_type, **)
60
60
  self.class.numeric_pk_type.meta(primary_key: true)
61
61
  end
62
62
 
@@ -34,11 +34,10 @@ module ROM
34
34
  extensions = @types[type.meta[:database]]
35
35
  db_type = type.meta[:db_type]
36
36
 
37
- raise ArgumentError, "Type #{ db_type } already registered" if extensions.key?(db_type)
38
37
  mod = Module.new(&block)
39
38
  ctx = Object.new.extend(mod)
40
39
  functions = mod.public_instance_methods.each_with_object({}) { |m, ms| ms[m] = ctx.method(m) }
41
- extensions[db_type] = functions
40
+ extensions[db_type] = (extensions[db_type] || {}).merge(functions)
42
41
  end
43
42
  end
44
43
 
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '2.0.0'.freeze
3
+ VERSION = '2.1.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,22 @@
1
+ require 'rom/sql/types'
2
+
3
+ module ROM
4
+ module Types
5
+ module Values
6
+ class TreePath < ::Struct.new(:value, :separator)
7
+ DEFAULT_SEPARATOR = '.'.freeze
8
+
9
+ # @api public
10
+ def self.new(value, separator = DEFAULT_SEPARATOR)
11
+ super
12
+ end
13
+
14
+ # @api public
15
+ def to_s
16
+ value
17
+ end
18
+ alias_method :to_str, :to_s
19
+ end
20
+ end
21
+ end
22
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rom-sql
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-18 00:00:00.000000000 Z
11
+ date: 2017-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -169,9 +169,12 @@ files:
169
169
  - lib/rom/sql/extensions/postgres/type_serializer.rb
170
170
  - lib/rom/sql/extensions/postgres/types.rb
171
171
  - lib/rom/sql/extensions/postgres/types/array.rb
172
+ - lib/rom/sql/extensions/postgres/types/array_types.rb
172
173
  - lib/rom/sql/extensions/postgres/types/geometric.rb
173
174
  - lib/rom/sql/extensions/postgres/types/json.rb
175
+ - lib/rom/sql/extensions/postgres/types/ltree.rb
174
176
  - lib/rom/sql/extensions/postgres/types/network.rb
177
+ - lib/rom/sql/extensions/postgres/types/range.rb
175
178
  - lib/rom/sql/extensions/rails_log_subscriber.rb
176
179
  - lib/rom/sql/extensions/sqlite.rb
177
180
  - lib/rom/sql/extensions/sqlite/type_builder.rb
@@ -216,6 +219,7 @@ files:
216
219
  - lib/rom/sql/types.rb
217
220
  - lib/rom/sql/version.rb
218
221
  - lib/rom/sql/wrap.rb
222
+ - lib/rom/types/values.rb
219
223
  homepage: http://rom-rb.org
220
224
  licenses:
221
225
  - MIT
@@ -236,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
236
240
  version: '0'
237
241
  requirements: []
238
242
  rubyforge_project:
239
- rubygems_version: 2.6.11
243
+ rubygems_version: 2.6.13
240
244
  signing_key:
241
245
  specification_version: 4
242
246
  summary: SQL databases support for ROM