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 +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)
|