rom-sql 1.2.2 → 1.3.0

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