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 +4 -4
- data/.travis.yml +9 -5
- data/CHANGELOG.md +30 -0
- data/lib/rom/plugins/relation/sql/auto_wrap.rb +2 -2
- data/lib/rom/sql/association/many_to_many.rb +12 -0
- data/lib/rom/sql/attribute.rb +99 -10
- data/lib/rom/sql/extensions/postgres/commands.rb +5 -2
- data/lib/rom/sql/extensions/postgres/types.rb +160 -0
- data/lib/rom/sql/gateway.rb +1 -9
- data/lib/rom/sql/migration.rb +91 -34
- data/lib/rom/sql/plugin/associates.rb +4 -8
- data/lib/rom/sql/relation.rb +10 -8
- data/lib/rom/sql/relation/reading.rb +21 -2
- data/lib/rom/sql/schema.rb +1 -1
- data/lib/rom/sql/schema/inferrer.rb +3 -1
- data/lib/rom/sql/tasks/migration_tasks.rake +17 -1
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +2 -2
- data/spec/extensions/postgres/attribute_spec.rb +128 -0
- data/spec/extensions/postgres/integration_spec.rb +21 -0
- data/spec/integration/commands/update_spec.rb +1 -1
- data/spec/integration/migration_spec.rb +41 -10
- data/spec/integration/plugins/auto_wrap_spec.rb +52 -5
- data/spec/integration/schema/inferrer_spec.rb +36 -5
- data/spec/shared/users.rb +1 -1
- data/spec/shared/users_and_tasks.rb +1 -1
- data/spec/spec_helper.rb +10 -4
- data/spec/unit/attribute_spec.rb +84 -4
- data/spec/unit/migration_tasks_spec.rb +12 -1
- data/spec/unit/order_dsl_spec.rb +8 -0
- data/spec/unit/plugin/associates_spec.rb +99 -0
- data/spec/unit/relation/by_pk_spec.rb +8 -0
- data/spec/unit/relation/exist_predicate_spec.rb +25 -0
- metadata +19 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 945fbfbd11ed73e702c332f2c86824cb32b97f7c
|
4
|
+
data.tar.gz: 7642f9bf09223210d49bf4b1440f42538ceeb38a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f74fbc365378212045ad117f5d5af04182e4957beaec11681328a410f20fb9ce817621b2116d3dd50b347f579acfd3d3d8d45bc251ae64ba2c904b15341d6aed
|
7
|
+
data.tar.gz: 88e1f6ed6f65489daca34b8a325e939d6dec3cf37fe40af80821a91319029bc404d888d436324638aa0540f67605435d9b744588d31f052c75e8a88e3eb98ea3
|
data/.travis.yml
CHANGED
@@ -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
|
-
- '[
|
14
|
+
- '[ -d coverage ] && bundle exec codeclimate-test-reporter'
|
15
15
|
script: "bundle exec rake ci"
|
16
16
|
rvm:
|
17
|
-
- 2.
|
18
|
-
- 2.3
|
19
|
-
- 2.
|
17
|
+
- 2.2.7
|
18
|
+
- 2.3.4
|
19
|
+
- 2.4.1
|
20
20
|
- rbx-3
|
21
|
-
- jruby-9.1.
|
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:
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
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]
|
data/lib/rom/sql/attribute.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
257
|
-
|
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
|
-
|
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)
|