rom-sql 1.2.2 → 1.3.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: bc6edcd56b3aeda31a41f53c921aa2a79dd19ffc
4
- data.tar.gz: d954520aa88474fcf6d61897ea0f1fed50e876ee
3
+ metadata.gz: 945fbfbd11ed73e702c332f2c86824cb32b97f7c
4
+ data.tar.gz: 7642f9bf09223210d49bf4b1440f42538ceeb38a
5
5
  SHA512:
6
- metadata.gz: 9f9f16bab1aef147b4956217362a1e08ec3f08dda74fc624c1e82622b6614e7bf557f8a47026f94dad182f4c34fe9fddebb3f33e876902355938f051a9c239f2
7
- data.tar.gz: faa2da9349e0f26bfe53ed96a6e145a444a21ce100930191324650399d26a9aeeaa23ff1485b4cddc20532fda67e7667276bcd98ec460eecb7f0a1053f498347
6
+ metadata.gz: f74fbc365378212045ad117f5d5af04182e4957beaec11681328a410f20fb9ce817621b2116d3dd50b347f579acfd3d3d8d45bc251ae64ba2c904b15341d6aed
7
+ data.tar.gz: 88e1f6ed6f65489daca34b8a325e939d6dec3cf37fe40af80821a91319029bc404d888d436324638aa0540f67605435d9b744588d31f052c75e8a88e3eb98ea3
@@ -11,18 +11,22 @@ before_script:
11
11
  - mysql -u root -e 'create database rom_sql;'
12
12
  - rvm get master
13
13
  after_success:
14
- - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
14
+ - '[ -d coverage ] && bundle exec codeclimate-test-reporter'
15
15
  script: "bundle exec rake ci"
16
16
  rvm:
17
- - 2.4.0
18
- - 2.3
19
- - 2.2
17
+ - 2.2.7
18
+ - 2.3.4
19
+ - 2.4.1
20
20
  - rbx-3
21
- - jruby-9.1.7.0
21
+ - jruby-9.1.8.0
22
+ matrix:
23
+ allow_failures:
24
+ - rvm: rbx-3
22
25
  env:
23
26
  global:
24
27
  - CODECLIMATE_REPO_TOKEN=03d7f66589572702b12426d2bc71c4de6281a96139e33b335b894264b1f8f0b0
25
28
  - JRUBY_OPTS='--dev -J-Xmx1024M'
29
+ - COVERAGE='true'
26
30
  notifications:
27
31
  webhooks:
28
32
  urls:
@@ -1,3 +1,33 @@
1
+ ## v1.3.0 2017-05-02
2
+
3
+ ### Added
4
+
5
+ * New `Relation#exist?` predicate checks if the relation has at least one tuple (flash-gordon)
6
+ * Support for JSONB transformations and queries using native DSL (flash-gordon)
7
+ * Add `ROM::SQL::Attribute#not` for negated boolean equality expressions (AMHOL)
8
+ * Add `ROM::SQL::Attribute#!` for negated attribute's sql expressions (solnic)
9
+ * Inferrer gets limit constraints for string data types and stores them in type's meta (v-kolesnikov)
10
+
11
+ ### Fixed
12
+
13
+ * Fixed usage of PostgreSQL's commands with a composite relation (flash-gordon)
14
+ * Translation of `true/false/nil` equality checks to `is/is not` SQL statements in `ROM::SQL::Attribute#is` (AMHOL)
15
+
16
+ ### Changed
17
+
18
+ * Global private interface `SQL::Gateway.instance` has been deprecated. Now if you run migrations
19
+ with ROM you should set up a ROM config in the `db:setup` task with something similar to
20
+
21
+ ```ruby
22
+ namespace :db
23
+ task :setup do
24
+ ROM::SQL::RakeSupport.env = ROM::Configuration.new(:sql, ENV['DATABASE_URL'])
25
+ end
26
+ end
27
+ ```
28
+
29
+ [Compare v1.2.2...v1.3.0](https://github.com/rom-rb/rom-sql/compare/v1.2.2...v1.3.0)
30
+
1
31
  ## v1.2.2 2017-03-25
2
32
 
3
33
  ### Changed
@@ -35,9 +35,9 @@ module ROM
35
35
  rel, other =
36
36
  if associations.key?(name)
37
37
  assoc = associations[name]
38
- other = __registry__[assoc.target.to_sym]
38
+ other = __registry__[assoc.target.relation]
39
39
 
40
- [assoc.join(__registry__, :inner_join, self), other]
40
+ [assoc.join(__registry__, :inner_join, self, other), other]
41
41
  else
42
42
  # TODO: deprecate this before 2.0
43
43
  other = __registry__[name]
@@ -46,6 +46,18 @@ module ROM
46
46
  end
47
47
  end
48
48
 
49
+ # @api private
50
+ def persist(relations, children, parents)
51
+ join_tuples = associate(relations, children, parents)
52
+ join_relation = join_relation(relations)
53
+ join_relation.multi_insert(join_tuples)
54
+ end
55
+
56
+ # @api private
57
+ def parent_combine_keys(relations)
58
+ relations[target].associations[source].combine_keys(relations).to_a.flatten(1)
59
+ end
60
+
49
61
  # @api public
50
62
  def join(relations, type, source = relations[self.source], target = relations[self.target])
51
63
  through_assoc = source.associations[through]
@@ -1,4 +1,5 @@
1
1
  require 'sequel/core'
2
+ require 'dry/core/cache'
2
3
 
3
4
  require 'rom/schema/attribute'
4
5
  require 'rom/sql/projection_dsl'
@@ -10,10 +11,61 @@ module ROM
10
11
  # @api public
11
12
  class Attribute < ROM::Schema::Attribute
12
13
  OPERATORS = %i[>= <= > <].freeze
14
+ NONSTANDARD_EQUALITY_VALUES = [true, false, nil].freeze
13
15
 
14
16
  # Error raised when an attribute cannot be qualified
15
17
  QualifyError = Class.new(StandardError)
16
18
 
19
+ # Type-specific methods
20
+ #
21
+ # @api public
22
+ module TypeExtensions
23
+ class << self
24
+ # Gets extensions for a type
25
+ #
26
+ # @param [Dry::Types::Type] type
27
+ #
28
+ # @return [Hash]
29
+ #
30
+ # @api public
31
+ def [](type)
32
+ unwrapped = type.optional? ? type.right : type
33
+ @types[unwrapped.pristine] || EMPTY_HASH
34
+ end
35
+
36
+ # Registers a set of operations supported for a specific type
37
+ #
38
+ # @example
39
+ # ROM::SQL::Attribute::TypeExtensions.register(ROM::SQL::Types::PG::JSONB) do
40
+ # def contains(type, keys)
41
+ # Sequel::Postgres::JSONBOp.new(type.meta[:name]).contains(keys)
42
+ # end
43
+ # end
44
+ #
45
+ # @param [Dry::Types::Type] type Type
46
+ #
47
+ # @api public
48
+ def register(type, &block)
49
+ raise ArgumentError, "Type #{ type } already registered" if @types.key?(type)
50
+ mod = Module.new(&block)
51
+ ctx = Object.new.extend(mod)
52
+ functions = mod.public_instance_methods.each_with_object({}) { |m, ms| ms[m] = ctx.method(m) }
53
+ @types[type] = functions
54
+ end
55
+ end
56
+
57
+ @types = {}
58
+ end
59
+
60
+ extend Dry::Core::Cache
61
+
62
+ # @api private
63
+ def self.[](*args)
64
+ fetch_or_store(args) { new(*args) }
65
+ end
66
+
67
+ option :extensions, type: Types::Hash, default: -> { TypeExtensions[type] }
68
+
17
69
  # Return a new attribute with an alias
18
70
  #
19
71
  # @example
@@ -140,7 +192,7 @@ module ROM
140
192
  end
141
193
  end
142
194
 
143
- # Return a boolean expression with `=` operator
195
+ # Return a boolean expression with an equality operator
144
196
  #
145
197
  # @example
146
198
  # users.where { id.is(1) }
@@ -151,7 +203,38 @@ module ROM
151
203
  #
152
204
  # @api public
153
205
  def is(other)
154
- __cmp__(:'=', other)
206
+ self =~ other
207
+ end
208
+
209
+ # @api public
210
+ def =~(other)
211
+ meta(sql_expr: sql_expr =~ binary_operation_arg(other))
212
+ end
213
+
214
+ # Return a boolean expression with a negated equality operator
215
+ #
216
+ # @example
217
+ # users.where { id.not(1) }
218
+ #
219
+ # users.where(users[:id].not(1))
220
+ #
221
+ # @param [Object] other Any SQL-compatible object type
222
+ #
223
+ # @api public
224
+ def not(other)
225
+ !is(other)
226
+ end
227
+
228
+ # Negate the attribute's sql expression
229
+ #
230
+ # @example
231
+ # users.where(!users[:id].is(1))
232
+ #
233
+ # @return [Attribute]
234
+ #
235
+ # @api public
236
+ def !
237
+ ~self
155
238
  end
156
239
 
157
240
  # Return a boolean expression with an inclusion test
@@ -241,6 +324,8 @@ module ROM
241
324
  def method_missing(meth, *args, &block)
242
325
  if OPERATORS.include?(meth)
243
326
  __cmp__(meth, args[0])
327
+ elsif extensions.key?(meth)
328
+ extensions[meth].(type, sql_expr, *args, &block)
244
329
  elsif sql_expr.respond_to?(meth)
245
330
  meta(sql_expr: sql_expr.__send__(meth, *args, &block))
246
331
  else
@@ -253,15 +338,19 @@ module ROM
253
338
  #
254
339
  # @api private
255
340
  def __cmp__(op, other)
256
- value =
257
- case other
258
- when Sequel::SQL::Expression
259
- value
260
- else
261
- type[other]
262
- end
341
+ Sequel::SQL::BooleanExpression.new(op, self, binary_operation_arg(other))
342
+ end
263
343
 
264
- Sequel::SQL::BooleanExpression.new(op, self, value)
344
+ # Preprocess input value for binary operations
345
+ #
346
+ # @api private
347
+ def binary_operation_arg(value)
348
+ case value
349
+ when Sequel::SQL::Expression
350
+ value
351
+ else
352
+ type[value]
353
+ end
265
354
  end
266
355
  end
267
356
  end
@@ -10,9 +10,11 @@ module ROM
10
10
  #
11
11
  # @api private
12
12
  def insert(tuples)
13
- tuples.map do |tuple|
13
+ dataset = tuples.map do |tuple|
14
14
  relation.dataset.returning(*relation.columns).insert(tuple)
15
15
  end.flatten(1)
16
+
17
+ wrap_dataset(dataset)
16
18
  end
17
19
 
18
20
  # Executes multi_insert statement and returns inserted tuples
@@ -36,7 +38,8 @@ module ROM
36
38
  #
37
39
  # @api private
38
40
  def update(tuple)
39
- relation.dataset.returning(*relation.columns).update(tuple)
41
+ dataset = relation.dataset.returning(*relation.columns).update(tuple)
42
+ wrap_dataset(dataset)
40
43
  end
41
44
  end
42
45
 
@@ -40,6 +40,166 @@ module ROM
40
40
 
41
41
  JSONB = JSONBArray | JSONBHash | JSONBOp
42
42
 
43
+ Attribute::TypeExtensions.register(JSONB) do
44
+ # Checks whether the JSON value includes a json value
45
+ # Translates to the @> operator
46
+ #
47
+ # @example
48
+ # people.where { fields.contain(gender: 'Female') }
49
+ # people.where(people[:fields].contain([name: 'age']))
50
+ # people.select { fields.contain(gender: 'Female').as(:is_female) }
51
+ #
52
+ # @param [Hash,Array,Object] value
53
+ #
54
+ # @return [SQL::Attribute<Types::Bool>]
55
+ #
56
+ # @api public
57
+ def contain(type, expr, value)
58
+ Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contains(value))
59
+ end
60
+
61
+ # Checks whether the JSON value is contained by other value
62
+ # Translates to the <@ operator
63
+ #
64
+ # @example
65
+ # people.where { custom_values.contained_by(age: 25, foo: 'bar') }
66
+ #
67
+ # @param [Hash,Array] value
68
+ #
69
+ # @return [SQL::Attribute<Types::Bool>]
70
+ #
71
+ # @api public
72
+ def contained_by(type, expr, value)
73
+ Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contained_by(value))
74
+ end
75
+
76
+ # Extracts the JSON value using at the specified path
77
+ # Translates to -> or #> depending on the number of arguments
78
+ #
79
+ # @example
80
+ # people.select { data.get('age').as(:person_age) }
81
+ # people.select { fields.get(0).as(:first_field) }
82
+ # people.select { fields.get('0', 'value').as(:first_field_value) }
83
+ #
84
+ # @param [Array<Integer>,Array<String>] path Path to extract
85
+ #
86
+ # @return [SQL::Attribute<Types::PG::JSONB>]
87
+ #
88
+ # @api public
89
+ def get(type, expr, *path)
90
+ Attribute[JSONB].meta(sql_expr: expr.pg_jsonb[path_args(path)])
91
+ end
92
+
93
+ # Extracts the JSON value as text using at the specified path
94
+ # Translates to ->> or #>> depending on the number of arguments
95
+ #
96
+ # @example
97
+ # people.select { data.get('age').as(:person_age) }
98
+ # people.select { fields.get(0).as(:first_field) }
99
+ # people.select { fields.get('0', 'value').as(:first_field_value) }
100
+ #
101
+ # @param [Array<Integer>,Array<String>] path Path to extract
102
+ #
103
+ # @return [SQL::Attribute<Types::String>]
104
+ #
105
+ # @api public
106
+ def get_text(type, expr, *path)
107
+ Attribute[Types::String].meta(sql_expr: expr.pg_jsonb.get_text(path_args(path)))
108
+ end
109
+
110
+ # Does the JSON value has the specified top-level key
111
+ # Translates to ?
112
+ #
113
+ # @example
114
+ # people.where { data.has_key('age') }
115
+ #
116
+ # @param [String] key
117
+ #
118
+ # @return [SQL::Attribute<Types::Bool>]
119
+ #
120
+ # @api public
121
+ def has_key(type, expr, key)
122
+ Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.has_key?(key))
123
+ end
124
+
125
+ # Does the JSON value has any of the specified top-level keys
126
+ # Translates to ?|
127
+ #
128
+ # @example
129
+ # people.where { data.has_any_key('age', 'height') }
130
+ #
131
+ # @param [Array<String>] keys
132
+ #
133
+ # @return [SQL::Attribute<Types::Bool>]
134
+ #
135
+ # @api public
136
+ def has_any_key(type, expr, *keys)
137
+ Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contain_any(keys))
138
+ end
139
+
140
+ # Does the JSON value has all the specified top-level keys
141
+ # Translates to ?&
142
+ #
143
+ # @example
144
+ # people.where { data.has_all_keys('age', 'height') }
145
+ #
146
+ # @param [Array<String>] keys
147
+ #
148
+ # @return [SQL::Attribute<Types::Bool>]
149
+ #
150
+ # @api public
151
+ def has_all_keys(type, expr, *keys)
152
+ Attribute[Types::Bool].meta(sql_expr: expr.pg_jsonb.contain_all(keys))
153
+ end
154
+
155
+ # Concatenates two JSON values
156
+ # Translates to ||
157
+ #
158
+ # @example
159
+ # people.select { data.merge(fetched_at: Time.now).as(:data) }
160
+ # people.select { (fields + [name: 'height', value: 165]).as(:fields) }
161
+ #
162
+ # @param [Hash,Array] value
163
+ #
164
+ # @return [SQL::Attribute<Types::PG::JSONB>]
165
+ #
166
+ # @api public
167
+ def merge(type, expr, value)
168
+ Attribute[JSONB].meta(sql_expr: expr.pg_jsonb.concat(value))
169
+ end
170
+ alias_method :+, :merge
171
+
172
+ # Deletes the specified value by key, index, or path
173
+ # Translates to - or #- depending on the number of arguments
174
+ #
175
+ # @example
176
+ # people.select { data.delete('age').as(:data_without_age) }
177
+ # people.select { fields.delete(0).as(:fields_without_first) }
178
+ # people.select { fields.delete(-1).as(:fields_without_last) }
179
+ # people.select { data.delete('deeply', 'nested', 'value').as(:data) }
180
+ # people.select { fields.delete('0', 'name').as(:data) }
181
+ #
182
+ # @param [Array<String>] path
183
+ #
184
+ # @return [SQL::Attribute<Types::PG::JSONB>]
185
+ #
186
+ # @api public
187
+ def delete(type, expr, *path)
188
+ sql_expr = path.size == 1 ? expr.pg_jsonb - path : expr.pg_jsonb.delete_path(path)
189
+ Attribute[JSONB].meta(sql_expr: sql_expr)
190
+ end
191
+
192
+ private
193
+
194
+ def path_args(path)
195
+ case path.size
196
+ when 0 then raise ArgumentError, "wrong number of arguments (given 0, expected 1+)"
197
+ when 1 then path[0]
198
+ else path
199
+ end
200
+ end
201
+ end
202
+
43
203
  # HStore
44
204
 
45
205
  HStoreR = Types.Constructor(Hash, &:to_hash)