rom-sql 1.0.3 → 1.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/lib/rom/plugins/relation/sql/auto_restrictions.rb +74 -0
- data/lib/rom/plugins/relation/sql/instrumentation.rb +31 -0
- data/lib/rom/sql/association/many_to_many.rb +1 -3
- data/lib/rom/sql/association/many_to_one.rb +1 -3
- data/lib/rom/sql/association/one_to_many.rb +1 -3
- data/lib/rom/sql/attribute.rb +15 -3
- data/lib/rom/sql/plugins.rb +3 -0
- data/lib/rom/sql/relation/reading.rb +20 -5
- data/lib/rom/sql/relation/sequel_api.rb +14 -0
- data/lib/rom/sql/schema.rb +9 -0
- data/lib/rom/sql/schema/inferrer.rb +25 -2
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +1 -1
- data/spec/integration/association/many_to_many_spec.rb +15 -0
- data/spec/integration/association/many_to_one_spec.rb +10 -6
- data/spec/integration/association/one_to_many_spec.rb +9 -0
- data/spec/integration/commands/create_spec.rb +7 -1
- data/spec/integration/commands/upsert_spec.rb +2 -0
- data/spec/integration/plugins/auto_restrictions_spec.rb +54 -0
- data/spec/integration/schema/inferrer_spec.rb +63 -20
- data/spec/integration/sequel_api_spec.rb +6 -0
- data/spec/shared/users_and_tasks.rb +1 -1
- data/spec/unit/relation/instrument_spec.rb +45 -0
- data/spec/unit/relation/read_spec.rb +4 -0
- data/spec/unit/relation/where_spec.rb +83 -37
- metadata +10 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4ee8c56fc0c1b34efb44c1d32c844765bc6c891
|
4
|
+
data.tar.gz: a2aeb62deb514cc3912f5fd512c9c02e2c9a24df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0942e1aeec2211371d871e1510f2668187581abcd5f4cbe447f2666a125678c4392c4e39fd0e8ceaecae227562d691b3ef8ab715a9ef111ed1ebb260ba5d64d4'
|
7
|
+
data.tar.gz: 3f08eac6477071a28cabb927690eaef0a0db41ee1bba0804f01a45e5317f5ba882765a06001313bed14309c8c169198071b45f07e8e22579a7956db669a0bf32
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
|
+
## v1.1.0 2017-03-01
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Added inferring for database indices (flash-gordon)
|
6
|
+
* Restriction conditions are now coerced using schema attributes (solnic)
|
7
|
+
* `:instrumentation` relation plugin that can be configured with any instrumentation backend (solnic)
|
8
|
+
* `:auto_restrictions` relation plugin, which defines `by_*` views restricting relations by their indexed attributes (solnic)
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
* Missing `group` method was added to legacy `SequelAPI` module (solnic)
|
13
|
+
* Associations properly maintain `order` if it was set (solnic)
|
14
|
+
|
15
|
+
[Compare v1.0.3...v1.1.0](https://github.com/rom-rb/rom-sql/compare/v1.0.3...v1.1.0)
|
16
|
+
|
1
17
|
## v1.0.3 2017-02-23
|
2
18
|
|
3
19
|
### Changed
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ROM
|
2
|
+
module Plugins
|
3
|
+
module Relation
|
4
|
+
module SQL
|
5
|
+
# Generates methods for restricting relations by their indexed attributes
|
6
|
+
#
|
7
|
+
# This plugin must be enabled for the whole adapter, `use` won't work as
|
8
|
+
# schema is not yet available, unless it was defined explicitly.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# rom = ROM.container(:sql, 'sqlite::memory') do |config|
|
12
|
+
# config.create_table(:users) do
|
13
|
+
# primary_key :id
|
14
|
+
# column :email, String, null: false, unique: true
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# config.plugin(:sql, relations: :auto_restrictions)
|
18
|
+
#
|
19
|
+
# config.relation(:users) do
|
20
|
+
# schema(infer: true)
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # now `by_email` is available automatically
|
25
|
+
# rom.relations[:users].by_email('jane@doe.org')
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
module AutoRestrictions
|
29
|
+
EmptySchemaError = Class.new(ArgumentError) do
|
30
|
+
def initialize(klass)
|
31
|
+
super("#{klass} relation has no schema. " \
|
32
|
+
"Make sure :auto_restrictions is enabled after defining a schema")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.included(klass)
|
37
|
+
super
|
38
|
+
schema = klass.schema
|
39
|
+
raise EmptySchemaError, klass if schema.nil?
|
40
|
+
methods, mod = restriction_methods(schema)
|
41
|
+
klass.include(mod)
|
42
|
+
methods.each { |meth| klass.auto_curry(meth) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.restriction_methods(schema)
|
46
|
+
mod = Module.new
|
47
|
+
|
48
|
+
indexed_attrs = schema.select { |attr| attr.meta[:index] }
|
49
|
+
|
50
|
+
methods = indexed_attrs.map do |attr|
|
51
|
+
meth_name = :"by_#{attr.name}"
|
52
|
+
|
53
|
+
mod.module_eval do
|
54
|
+
define_method(meth_name) do |value|
|
55
|
+
where(attr.is(value))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
meth_name
|
60
|
+
end
|
61
|
+
|
62
|
+
[methods, mod]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ROM.plugins do
|
71
|
+
adapter :sql do
|
72
|
+
register :auto_restrictions, ROM::Plugins::Relation::SQL::AutoRestrictions, type: :relation
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rom/plugins/relation/instrumentation'
|
2
|
+
|
3
|
+
module ROM
|
4
|
+
module Plugins
|
5
|
+
module Relation
|
6
|
+
module SQL
|
7
|
+
# @api private
|
8
|
+
module Instrumentation
|
9
|
+
def self.included(klass)
|
10
|
+
super
|
11
|
+
|
12
|
+
klass.class_eval do
|
13
|
+
include ROM::Plugins::Relation::Instrumentation
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
def notification_payload(relation)
|
17
|
+
super.merge(query: relation.dataset.sql)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ROM.plugins do
|
28
|
+
adapter :sql do
|
29
|
+
register :instrumentation, ROM::Plugins::Relation::SQL::Instrumentation, type: :relation
|
30
|
+
end
|
31
|
+
end
|
@@ -37,9 +37,7 @@ module ROM
|
|
37
37
|
right.schema.merge(join_rel.schema.project(left_fk))
|
38
38
|
end.qualified
|
39
39
|
|
40
|
-
relation = left
|
41
|
-
.inner_join(source, join_keys(relations))
|
42
|
-
.order(*right.schema.project_pk.qualified)
|
40
|
+
relation = left.inner_join(source, join_keys(relations))
|
43
41
|
|
44
42
|
if view
|
45
43
|
apply_view(schema, relation)
|
@@ -21,9 +21,7 @@ module ROM
|
|
21
21
|
left_schema.merge(right_schema.project_fk(left_pk => right_fk))
|
22
22
|
end.qualified
|
23
23
|
|
24
|
-
relation = left
|
25
|
-
.inner_join(source_table, join_keys(relations))
|
26
|
-
.order(*right_schema.qualified)
|
24
|
+
relation = left.inner_join(source_table, join_keys(relations))
|
27
25
|
|
28
26
|
if view
|
29
27
|
apply_view(schema, relation)
|
@@ -8,9 +8,7 @@ module ROM
|
|
8
8
|
def call(relations, right = relations[target.relation])
|
9
9
|
schema = right.schema.qualified
|
10
10
|
|
11
|
-
relation = right
|
12
|
-
.inner_join(source_table, join_keys(relations))
|
13
|
-
.order(*right.schema.project_pk.qualified)
|
11
|
+
relation = right.inner_join(source_table, join_keys(relations))
|
14
12
|
|
15
13
|
if view
|
16
14
|
apply_view(schema, relation)
|
data/lib/rom/sql/attribute.rb
CHANGED
@@ -9,6 +9,8 @@ module ROM
|
|
9
9
|
#
|
10
10
|
# @api public
|
11
11
|
class Attribute < ROM::Schema::Attribute
|
12
|
+
OPERATORS = %i[>= <= > <].freeze
|
13
|
+
|
12
14
|
# Error raised when an attribute cannot be qualified
|
13
15
|
QualifyError = Class.new(StandardError)
|
14
16
|
|
@@ -237,7 +239,9 @@ module ROM
|
|
237
239
|
#
|
238
240
|
# @api private
|
239
241
|
def method_missing(meth, *args, &block)
|
240
|
-
if
|
242
|
+
if OPERATORS.include?(meth)
|
243
|
+
__cmp__(meth, args[0])
|
244
|
+
elsif sql_expr.respond_to?(meth)
|
241
245
|
meta(sql_expr: sql_expr.__send__(meth, *args, &block))
|
242
246
|
else
|
243
247
|
super
|
@@ -248,8 +252,16 @@ module ROM
|
|
248
252
|
# the left part is the attribute value
|
249
253
|
#
|
250
254
|
# @api private
|
251
|
-
def __cmp__(op,
|
252
|
-
|
255
|
+
def __cmp__(op, other)
|
256
|
+
value =
|
257
|
+
case other
|
258
|
+
when Sequel::SQL::Expression
|
259
|
+
value
|
260
|
+
else
|
261
|
+
type[other]
|
262
|
+
end
|
263
|
+
|
264
|
+
Sequel::SQL::BooleanExpression.new(op, self, value)
|
253
265
|
end
|
254
266
|
end
|
255
267
|
end
|
data/lib/rom/sql/plugins.rb
CHANGED
@@ -45,7 +45,7 @@ module ROM
|
|
45
45
|
#
|
46
46
|
# @api public
|
47
47
|
def first
|
48
|
-
|
48
|
+
limit(1).to_a.first
|
49
49
|
end
|
50
50
|
|
51
51
|
# Get last tuple from the relation
|
@@ -58,7 +58,7 @@ module ROM
|
|
58
58
|
#
|
59
59
|
# @api public
|
60
60
|
def last
|
61
|
-
|
61
|
+
reverse.limit(1).first
|
62
62
|
end
|
63
63
|
|
64
64
|
# Prefix all columns in a relation
|
@@ -338,9 +338,11 @@ module ROM
|
|
338
338
|
# @api public
|
339
339
|
def where(*args, &block)
|
340
340
|
if block
|
341
|
-
|
341
|
+
where(*args).where(self.class.schema.restriction(&block))
|
342
|
+
elsif args.size == 1 && args[0].is_a?(Hash)
|
343
|
+
new(dataset.where(coerce_conditions(args[0])))
|
342
344
|
else
|
343
|
-
new(dataset.
|
345
|
+
new(dataset.where(*args))
|
344
346
|
end
|
345
347
|
end
|
346
348
|
|
@@ -784,11 +786,24 @@ module ROM
|
|
784
786
|
#
|
785
787
|
# @api public
|
786
788
|
def read(sql)
|
787
|
-
new(dataset.db[sql])
|
789
|
+
new(dataset.db[sql], schema: schema.empty)
|
788
790
|
end
|
789
791
|
|
790
792
|
private
|
791
793
|
|
794
|
+
# Apply input types to condition values
|
795
|
+
#
|
796
|
+
# @api private
|
797
|
+
def coerce_conditions(conditions)
|
798
|
+
conditions.each_with_object({}) { |(k, v), h|
|
799
|
+
if k.is_a?(Symbol) && self.class.schema.key?(k)
|
800
|
+
h[k] = self.class.schema[k][v]
|
801
|
+
else
|
802
|
+
h[k] = v
|
803
|
+
end
|
804
|
+
}
|
805
|
+
end
|
806
|
+
|
792
807
|
# Common join method used by other join methods
|
793
808
|
#
|
794
809
|
# @api private
|
@@ -114,6 +114,20 @@ module ROM
|
|
114
114
|
def left_join(*args, &block)
|
115
115
|
new(dataset.__send__(__method__, *args, &block))
|
116
116
|
end
|
117
|
+
|
118
|
+
# Group by specific columns
|
119
|
+
#
|
120
|
+
# @example
|
121
|
+
# tasks.group(:user_id)
|
122
|
+
#
|
123
|
+
# @param [Array<Symbol>] *args A list of column names
|
124
|
+
#
|
125
|
+
# @return [Relation]
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def group(*args, &block)
|
129
|
+
new(dataset.__send__(__method__, *args, &block))
|
130
|
+
end
|
117
131
|
end
|
118
132
|
end
|
119
133
|
end
|
data/lib/rom/sql/schema.rb
CHANGED
@@ -95,6 +95,15 @@ module ROM
|
|
95
95
|
relation.new(relation.dataset.select(*self), schema: self)
|
96
96
|
end
|
97
97
|
|
98
|
+
# Return an empty schema
|
99
|
+
#
|
100
|
+
# @return [Schema]
|
101
|
+
#
|
102
|
+
# @api public
|
103
|
+
def empty
|
104
|
+
new(EMPTY_ARRAY)
|
105
|
+
end
|
106
|
+
|
98
107
|
# @api private
|
99
108
|
def finalize!(*)
|
100
109
|
super do
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'dry/core/class_attributes'
|
2
3
|
|
3
4
|
module ROM
|
@@ -55,10 +56,12 @@ module ROM
|
|
55
56
|
dataset = source.dataset
|
56
57
|
|
57
58
|
columns = filter_columns(gateway.connection.schema(dataset))
|
59
|
+
all_indexes = indexes_for(gateway, dataset)
|
58
60
|
fks = fks_for(gateway, dataset)
|
59
61
|
|
60
62
|
inferred = columns.map do |(name, definition)|
|
61
|
-
|
63
|
+
indexes = column_indexes(all_indexes, name)
|
64
|
+
type = build_type(**definition, foreign_key: fks[name], indexes: indexes)
|
62
65
|
|
63
66
|
if type
|
64
67
|
type.meta(name: name, source: source)
|
@@ -74,7 +77,7 @@ module ROM
|
|
74
77
|
schema.reject { |(_, definition)| definition[:db_type] == CONSTRAINT_DB_TYPE }
|
75
78
|
end
|
76
79
|
|
77
|
-
def build_type(primary_key:, db_type:, type:, allow_null:, foreign_key:, **rest)
|
80
|
+
def build_type(primary_key:, db_type:, type:, allow_null:, foreign_key:, indexes:, **rest)
|
78
81
|
if primary_key
|
79
82
|
map_pk_type(type, db_type)
|
80
83
|
else
|
@@ -84,6 +87,8 @@ module ROM
|
|
84
87
|
read_type = mapped_type.meta[:read]
|
85
88
|
mapped_type = mapped_type.optional if allow_null
|
86
89
|
mapped_type = mapped_type.meta(foreign_key: true, target: foreign_key) if foreign_key
|
90
|
+
mapped_type = mapped_type.meta(index: indexes) unless indexes.empty?
|
91
|
+
|
87
92
|
if read_type && allow_null
|
88
93
|
mapped_type.meta(read: read_type.optional)
|
89
94
|
elsif read_type
|
@@ -118,6 +123,24 @@ module ROM
|
|
118
123
|
end
|
119
124
|
end
|
120
125
|
|
126
|
+
# @api private
|
127
|
+
def indexes_for(gateway, dataset)
|
128
|
+
if gateway.connection.respond_to?(:indexes)
|
129
|
+
gateway.connection.indexes(dataset)
|
130
|
+
else
|
131
|
+
# index listing is not implemented
|
132
|
+
EMPTY_HASH
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# @api private
|
137
|
+
def column_indexes(indexes, column)
|
138
|
+
indexes.each_with_object(Set.new) do |(name, idx), indexes|
|
139
|
+
indexes << name if idx[:columns][0] == column
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# @api private
|
121
144
|
def build_fk(columns: , table: , **rest)
|
122
145
|
if columns.size == 1
|
123
146
|
[columns[0], table]
|
data/lib/rom/sql/version.rb
CHANGED
data/rom-sql.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
22
22
|
spec.add_runtime_dependency 'dry-types', '~> 0.9', '>= 0.9.4'
|
23
23
|
spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
|
24
|
-
spec.add_runtime_dependency 'rom', '~> 3.
|
24
|
+
spec.add_runtime_dependency 'rom', '~> 3.1'
|
25
25
|
|
26
26
|
spec.add_development_dependency 'bundler'
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
@@ -79,6 +79,21 @@ RSpec.describe ROM::SQL::Association::ManyToMany do
|
|
79
79
|
|
80
80
|
expect(relation.to_a).to eql([id: 1, tag: 'important', name: 'important', task_id: 1])
|
81
81
|
end
|
82
|
+
|
83
|
+
it 'respects custom order' do
|
84
|
+
conn[:tags].insert id: 2, name: 'boring'
|
85
|
+
conn[:task_tags].insert(tag_id: 2, task_id: 1)
|
86
|
+
|
87
|
+
relation = tags.
|
88
|
+
order(tags[:name].qualified).
|
89
|
+
for_combine(assoc).call(tasks.call)
|
90
|
+
|
91
|
+
expect(relation.to_a).
|
92
|
+
to eql([
|
93
|
+
{ id: 2, name: 'boring', task_id: 1 },
|
94
|
+
{ id: 1, name: 'important', task_id: 1 }
|
95
|
+
])
|
96
|
+
end
|
82
97
|
end
|
83
98
|
end
|
84
99
|
|
@@ -52,8 +52,10 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
|
|
52
52
|
expect(relation.where(user_id: 2).one).to eql(id: 2, task_id: 1, name: 'Joe')
|
53
53
|
|
54
54
|
expect(relation.to_a).
|
55
|
-
to eql([
|
56
|
-
|
55
|
+
to eql([
|
56
|
+
{ id: 1, task_id: 2, name: 'Jane' },
|
57
|
+
{ id: 2, task_id: 1, name: 'Joe' }
|
58
|
+
])
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -62,12 +64,14 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
|
|
62
64
|
relation = users.for_combine(assoc).call(tasks.call)
|
63
65
|
|
64
66
|
expect(relation.to_a).
|
65
|
-
to eql([
|
66
|
-
|
67
|
+
to eql([
|
68
|
+
{ id: 1, task_id: 2, name: 'Jane' },
|
69
|
+
{ id: 2, task_id: 1, name: 'Joe' }
|
70
|
+
])
|
67
71
|
end
|
68
72
|
|
69
73
|
it 'maintains original relation' do
|
70
|
-
users.accounts.insert(user_id: 2, number: '
|
74
|
+
users.accounts.insert(user_id: 2, number: '31', balance: 0)
|
71
75
|
|
72
76
|
relation = users.
|
73
77
|
join(:accounts, user_id: :id).
|
@@ -76,7 +80,7 @@ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
|
|
76
80
|
for_combine(assoc).call(tasks.call)
|
77
81
|
|
78
82
|
expect(relation.to_a).
|
79
|
-
to eql([{ id: 2, task_id: 1, name: 'Joe', account_num: '
|
83
|
+
to eql([{ id: 2, task_id: 1, name: 'Joe', account_num: '31' },
|
80
84
|
{ id: 1, task_id: 2, name: 'Jane', account_num: '42' }])
|
81
85
|
end
|
82
86
|
end
|
@@ -59,6 +59,15 @@ RSpec.describe ROM::SQL::Association::OneToMany do
|
|
59
59
|
|
60
60
|
expect(relation.to_a).to eql([{ id: 1, user_id: 2, title: "Joe's task", tag_id: 1 }])
|
61
61
|
end
|
62
|
+
|
63
|
+
it 'respects custom order' do
|
64
|
+
relation = tasks.
|
65
|
+
order(tasks[:title].qualified).
|
66
|
+
for_combine(assoc).call(users.call)
|
67
|
+
|
68
|
+
expect(relation.to_a).
|
69
|
+
to eql([{ id: 2, user_id: 1, title: "Jane's task" }, { id: 1, user_id: 2, title: "Joe's task" }])
|
70
|
+
end
|
62
71
|
end
|
63
72
|
end
|
64
73
|
end
|
@@ -6,7 +6,7 @@ RSpec.describe 'Commands / Create', :postgres, seeds: false do
|
|
6
6
|
let(:users) { commands[:users] }
|
7
7
|
let(:tasks) { commands[:tasks] }
|
8
8
|
|
9
|
-
before do
|
9
|
+
before do |ex|
|
10
10
|
module Test
|
11
11
|
class Params < Dry::Struct
|
12
12
|
attribute :name, Types::Strict::String.optional
|
@@ -19,6 +19,12 @@ RSpec.describe 'Commands / Create', :postgres, seeds: false do
|
|
19
19
|
|
20
20
|
conn.add_index :users, :name, unique: true
|
21
21
|
|
22
|
+
if sqlite?(ex)
|
23
|
+
conn.add_index :tasks, :title, unique: true
|
24
|
+
else
|
25
|
+
conn.execute "ALTER TABLE tasks add CONSTRAINT tasks_title_key UNIQUE (title)"
|
26
|
+
end
|
27
|
+
|
22
28
|
conf.commands(:users) do
|
23
29
|
define(:create) do
|
24
30
|
input Test::Params
|
@@ -6,6 +6,8 @@ RSpec.describe 'Commands / Postgres / Upsert', :postgres, seeds: false do
|
|
6
6
|
let(:tasks) { commands[:tasks] }
|
7
7
|
|
8
8
|
before do
|
9
|
+
conn.execute "ALTER TABLE tasks add CONSTRAINT tasks_title_key UNIQUE (title)"
|
10
|
+
|
9
11
|
conn[:users].insert id: 1, name: 'Jane'
|
10
12
|
conn[:users].insert id: 2, name: 'Joe'
|
11
13
|
conn[:users].insert id: 3, name: 'Jean'
|
@@ -0,0 +1,54 @@
|
|
1
|
+
RSpec.describe 'Plugins / :auto_restrictions', seeds: true do
|
2
|
+
include_context 'users and tasks'
|
3
|
+
|
4
|
+
with_adapters do
|
5
|
+
before do
|
6
|
+
conn.add_index :tasks, :title, unique: true
|
7
|
+
end
|
8
|
+
|
9
|
+
shared_context 'auto-generated restriction view' do
|
10
|
+
it 'defines restriction views for all indexed attributes' do
|
11
|
+
expect(tasks.select(:id).by_title("Jane's task").one).to eql(id: 2)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'defines curried methods' do
|
15
|
+
expect(tasks.by_title.("Jane's task").first).to eql(id: 2, user_id: 1, title: "Jane's task")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'with an inferred schema' do
|
20
|
+
before do
|
21
|
+
conf.plugin(:sql, relations: :auto_restrictions)
|
22
|
+
end
|
23
|
+
|
24
|
+
include_context 'auto-generated restriction view'
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with explicit schema' do
|
28
|
+
before do
|
29
|
+
conf.relation(:tasks) do
|
30
|
+
schema do
|
31
|
+
attribute :id, ROM::SQL::Types::Serial
|
32
|
+
attribute :user_id, ROM::SQL::Types::Int
|
33
|
+
attribute :title, ROM::SQL::Types::String.meta(index: true)
|
34
|
+
end
|
35
|
+
|
36
|
+
use :auto_restrictions
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
include_context 'auto-generated restriction view'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'raises error when enabled w/o a schema' do
|
44
|
+
expect {
|
45
|
+
conf.relation(:tasks) do
|
46
|
+
use :auto_restrictions
|
47
|
+
end
|
48
|
+
}.to raise_error(
|
49
|
+
ROM::Plugins::Relation::SQL::AutoRestrictions::EmptySchemaError,
|
50
|
+
"ROM::Relation[Tasks] relation has no schema. Make sure :auto_restrictions is enabled after defining a schema"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -38,7 +38,13 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
|
|
38
38
|
let(:dataset) { :tasks }
|
39
39
|
let(:source) { ROM::Relation::Name[:tasks] }
|
40
40
|
|
41
|
-
it 'can infer attributes for dataset' do
|
41
|
+
it 'can infer attributes for dataset' do |ex|
|
42
|
+
if mysql?(ex)
|
43
|
+
indexes = { index: %i(user_id).to_set }
|
44
|
+
else
|
45
|
+
indexes = {}
|
46
|
+
end
|
47
|
+
|
42
48
|
expect(schema.to_h).
|
43
49
|
to eql(
|
44
50
|
id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
|
@@ -47,7 +53,8 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
|
|
47
53
|
name: :user_id,
|
48
54
|
foreign_key: true,
|
49
55
|
source: source,
|
50
|
-
target: :users
|
56
|
+
target: :users,
|
57
|
+
**indexes
|
51
58
|
)
|
52
59
|
)
|
53
60
|
end
|
@@ -87,14 +94,15 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
|
|
87
94
|
it 'can infer attributes for dataset' do |ex|
|
88
95
|
date_type = oracle?(ex) ? ROM::SQL::Types::Time : ROM::SQL::Types::Date
|
89
96
|
|
90
|
-
expect(schema.to_h).
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
97
|
+
expect(schema.to_h).
|
98
|
+
to eql(
|
99
|
+
id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
|
100
|
+
text: ROM::SQL::Types::String.meta(name: :text, source: source),
|
101
|
+
time: ROM::SQL::Types::Time.optional.meta(name: :time, source: source),
|
102
|
+
date: date_type.optional.meta(name: :date, source: source),
|
103
|
+
datetime: ROM::SQL::Types::Time.meta(name: :datetime, source: source),
|
104
|
+
data: ROM::SQL::Types::Blob.optional.meta(name: :data, source: source),
|
105
|
+
)
|
98
106
|
end
|
99
107
|
end
|
100
108
|
|
@@ -130,16 +138,17 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
|
|
130
138
|
|
131
139
|
pending 'Add precision inferrence for Oracle' if oracle?(example)
|
132
140
|
|
133
|
-
expect(schema.to_h).
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
141
|
+
expect(schema.to_h).
|
142
|
+
to eql(
|
143
|
+
id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
|
144
|
+
dec: default_precision,
|
145
|
+
dec_prec: decimal.meta(name: :dec_prec, precision: 12, scale: 0),
|
146
|
+
num: decimal.meta(name: :num, precision: 5, scale: 2),
|
147
|
+
small: ROM::SQL::Types::Int.optional.meta(name: :small, source: source),
|
148
|
+
int: ROM::SQL::Types::Int.optional.meta(name: :int, source: source),
|
149
|
+
floating: ROM::SQL::Types::Float.optional.meta(name: :floating, source: source),
|
150
|
+
double_p: ROM::SQL::Types::Float.optional.meta(name: :double_p, source: source),
|
151
|
+
)
|
143
152
|
end
|
144
153
|
end
|
145
154
|
end
|
@@ -300,5 +309,39 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
|
|
300
309
|
end
|
301
310
|
end
|
302
311
|
end
|
312
|
+
|
313
|
+
describe 'inferring indices', oracle: false do
|
314
|
+
before do |ex|
|
315
|
+
ctx = self
|
316
|
+
|
317
|
+
conn.create_table :test_inferrence do
|
318
|
+
primary_key :id
|
319
|
+
Integer :foo
|
320
|
+
Integer :bar, null: false
|
321
|
+
Integer :baz, null: false
|
322
|
+
|
323
|
+
index :foo, name: :foo_idx
|
324
|
+
index :bar, name: :bar_idx
|
325
|
+
index :baz, name: :baz1_idx
|
326
|
+
index :baz, name: :baz2_idx
|
327
|
+
|
328
|
+
index %i(bar baz), name: :composite_idx
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
let(:dataset) { :test_inferrence }
|
333
|
+
let(:source) { ROM::Relation::Name[dataset] }
|
334
|
+
|
335
|
+
it 'infers types with indices' do
|
336
|
+
int = ROM::SQL::Types::Int
|
337
|
+
expect(schema.to_h).
|
338
|
+
to eql(
|
339
|
+
id: int.meta(name: :id, source: source, primary_key: true),
|
340
|
+
foo: int.optional.meta(name: :foo, source: source, index: %i(foo_idx).to_set),
|
341
|
+
bar: int.meta(name: :bar, source: source, index: %i(bar_idx composite_idx).to_set),
|
342
|
+
baz: int.meta(name: :baz, source: source, index: %i(baz1_idx baz2_idx).to_set)
|
343
|
+
)
|
344
|
+
end
|
345
|
+
end
|
303
346
|
end
|
304
347
|
end
|
@@ -28,4 +28,10 @@ RSpec.describe 'Using legacy sequel api', :sqlite do
|
|
28
28
|
expect(users.where(name: 'Jane').first).to eql(id: 1, name: 'Jane')
|
29
29
|
end
|
30
30
|
end
|
31
|
+
|
32
|
+
describe '#order' do
|
33
|
+
it 'orders relation' do
|
34
|
+
expect(users.order(:users__name).first).to eql(id: 1, name: 'Jane')
|
35
|
+
end
|
36
|
+
end
|
31
37
|
end
|
@@ -13,7 +13,7 @@ RSpec.shared_context 'users and tasks' do
|
|
13
13
|
conn.create_table :tasks do
|
14
14
|
primary_key :id
|
15
15
|
foreign_key :user_id, :users
|
16
|
-
String :title
|
16
|
+
String :title
|
17
17
|
constraint(:title_length) { char_length(title) > 1 } if ctx.postgres?(example)
|
18
18
|
constraint(:title_length) { length(title) > 1 } if ctx.sqlite?(example)
|
19
19
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
RSpec.describe ROM::SQL::Relation, '#instrument', :sqlite do
|
2
|
+
include_context 'users and tasks'
|
3
|
+
|
4
|
+
subject(:relation) do
|
5
|
+
relations[:users]
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:notifications) do
|
9
|
+
spy(:notifications)
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
conf.plugin(:sql, relations: :instrumentation) do |p|
|
14
|
+
p.notifications = notifications
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'instruments relation materialization' do
|
19
|
+
users.to_a
|
20
|
+
|
21
|
+
expect(notifications).
|
22
|
+
to have_received(:instrument).with(:sql, name: :users, query: users.dataset.sql)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'instruments methods that return a single tuple' do
|
26
|
+
users.first
|
27
|
+
|
28
|
+
expect(notifications).
|
29
|
+
to have_received(:instrument).with(:sql, name: :users, query: users.limit(1).dataset.sql)
|
30
|
+
|
31
|
+
users.last
|
32
|
+
|
33
|
+
expect(notifications).
|
34
|
+
to have_received(:instrument).with(:sql, name: :users, query: users.reverse.limit(1).dataset.sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'instruments aggregation methods' do
|
38
|
+
pending "no idea how to make this work with sequel"
|
39
|
+
|
40
|
+
users.count
|
41
|
+
|
42
|
+
expect(notifications).
|
43
|
+
to have_received(:instrument).with(:sql, name: :users, query: 'SELECT COUNT(*) FROM users')
|
44
|
+
end
|
45
|
+
end
|
@@ -3,54 +3,100 @@ RSpec.describe ROM::Relation, '#where' do
|
|
3
3
|
|
4
4
|
include_context 'users and tasks'
|
5
5
|
|
6
|
-
before do
|
7
|
-
conf.relation(:tasks) { schema(infer: true) }
|
8
|
-
end
|
9
|
-
|
10
6
|
with_adapters do
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
context 'without :read types' do
|
8
|
+
it 'restricts relation using provided conditions' do
|
9
|
+
expect(relation.where(id: 1).to_a).
|
10
|
+
to eql([{ id: 1, title: "Joe's task" }])
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
it 'restricts relation using provided conditions and block' do
|
14
|
+
expect(relation.where(id: 1) { title.like("%Jane%") }.to_a).to be_empty
|
15
|
+
end
|
19
16
|
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
it 'restricts relation using provided conditions in a block' do
|
18
|
+
expect(relation.where { (id > 2) & title.like("%Jane%") }.to_a).to be_empty
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
it 'restricts relation using canonical attributes' do
|
22
|
+
expect(relation.rename(id: :user_id).where { id > 3 }.to_a).to be_empty
|
23
|
+
end
|
27
24
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
it 'restricts with or condition' do
|
26
|
+
expect(relation.where { id.is(1) | id.is(2) }.to_a).
|
27
|
+
to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
|
28
|
+
end
|
32
29
|
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
it 'restricts with a range condition' do
|
31
|
+
expect(relation.where { id.in(-1...2) }.to_a).
|
32
|
+
to eql([{ id: 1, title: "Joe's task" }])
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
expect(relation.where { id.in(0...3) }.to_a).
|
35
|
+
to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
|
36
|
+
end
|
40
37
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
it 'restricts with an inclusive range' do
|
39
|
+
expect(relation.where { id.in(0..2) }.to_a).
|
40
|
+
to eql([{ id: 1, title: "Joe's task" }, { id: 2, title: "Jane's task" }])
|
41
|
+
end
|
45
42
|
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
it 'restricts with an ordinary enum' do
|
44
|
+
expect(relation.where { id.in(2, 3) }.to_a).
|
45
|
+
to eql([{ id: 2, title: "Jane's task" }])
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'restricts with enum using self syntax' do
|
49
|
+
expect(relation.where(relation[:id].in(2, 3)).to_a).
|
50
|
+
to eql([{ id: 2, title: "Jane's task" }])
|
51
|
+
end
|
49
52
|
end
|
50
53
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
+
context 'with :read types' do
|
55
|
+
before do
|
56
|
+
conf.relation(:tasks) do
|
57
|
+
schema(infer: true) do
|
58
|
+
attribute :id, ROM::SQL::Types::Serial.constructor(&:to_i)
|
59
|
+
attribute :title, ROM::SQL::Types::Coercible::String
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Test
|
64
|
+
Id = Struct.new(:v) do
|
65
|
+
def to_i
|
66
|
+
v.to_i
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Title = Struct.new(:v) do
|
71
|
+
def to_s
|
72
|
+
v.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'applies write_schema to hash conditions' do
|
79
|
+
rel = tasks.where(id: Test::Id.new('2'), title: Test::Title.new(:"Jane's task"))
|
80
|
+
|
81
|
+
expect(rel.first).
|
82
|
+
to eql(id: 2, user_id: 1, title: "Jane's task")
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'applies write_schema to conditions with operators other than equality' do
|
86
|
+
rel = tasks.where { id >= Test::Id.new('2') }
|
87
|
+
|
88
|
+
expect(rel.first).
|
89
|
+
to eql(id: 2, user_id: 1, title: "Jane's task")
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'applies write_schema to conditions in a block' do
|
93
|
+
rel = tasks.where {
|
94
|
+
id.is(Test::Id.new('2')) & title.is(Test::Title.new(:"Jane's task"))
|
95
|
+
}
|
96
|
+
|
97
|
+
expect(rel.first).
|
98
|
+
to eql(id: 2, user_id: 1, title: "Jane's task")
|
99
|
+
end
|
54
100
|
end
|
55
101
|
end
|
56
102
|
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: 1.0
|
4
|
+
version: 1.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-
|
11
|
+
date: 2017-03-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -84,14 +84,14 @@ dependencies:
|
|
84
84
|
requirements:
|
85
85
|
- - "~>"
|
86
86
|
- !ruby/object:Gem::Version
|
87
|
-
version: '3.
|
87
|
+
version: '3.1'
|
88
88
|
type: :runtime
|
89
89
|
prerelease: false
|
90
90
|
version_requirements: !ruby/object:Gem::Requirement
|
91
91
|
requirements:
|
92
92
|
- - "~>"
|
93
93
|
- !ruby/object:Gem::Version
|
94
|
-
version: '3.
|
94
|
+
version: '3.1'
|
95
95
|
- !ruby/object:Gem::Dependency
|
96
96
|
name: bundler
|
97
97
|
requirement: !ruby/object:Gem::Requirement
|
@@ -154,7 +154,9 @@ files:
|
|
154
154
|
- circle.yml
|
155
155
|
- lib/rom-sql.rb
|
156
156
|
- lib/rom/plugins/relation/sql/auto_combine.rb
|
157
|
+
- lib/rom/plugins/relation/sql/auto_restrictions.rb
|
157
158
|
- lib/rom/plugins/relation/sql/auto_wrap.rb
|
159
|
+
- lib/rom/plugins/relation/sql/instrumentation.rb
|
158
160
|
- lib/rom/sql.rb
|
159
161
|
- lib/rom/sql/association.rb
|
160
162
|
- lib/rom/sql/association/many_to_many.rb
|
@@ -241,6 +243,7 @@ files:
|
|
241
243
|
- spec/integration/migration_spec.rb
|
242
244
|
- spec/integration/plugins/associates/many_to_many_spec.rb
|
243
245
|
- spec/integration/plugins/associates_spec.rb
|
246
|
+
- spec/integration/plugins/auto_restrictions_spec.rb
|
244
247
|
- spec/integration/plugins/auto_wrap_spec.rb
|
245
248
|
- spec/integration/relation_schema_spec.rb
|
246
249
|
- spec/integration/schema/call_spec.rb
|
@@ -297,6 +300,7 @@ files:
|
|
297
300
|
- spec/unit/relation/having_spec.rb
|
298
301
|
- spec/unit/relation/inner_join_spec.rb
|
299
302
|
- spec/unit/relation/inspect_spec.rb
|
303
|
+
- spec/unit/relation/instrument_spec.rb
|
300
304
|
- spec/unit/relation/invert_spec.rb
|
301
305
|
- spec/unit/relation/left_join_spec.rb
|
302
306
|
- spec/unit/relation/map_spec.rb
|
@@ -372,6 +376,7 @@ test_files:
|
|
372
376
|
- spec/integration/migration_spec.rb
|
373
377
|
- spec/integration/plugins/associates/many_to_many_spec.rb
|
374
378
|
- spec/integration/plugins/associates_spec.rb
|
379
|
+
- spec/integration/plugins/auto_restrictions_spec.rb
|
375
380
|
- spec/integration/plugins/auto_wrap_spec.rb
|
376
381
|
- spec/integration/relation_schema_spec.rb
|
377
382
|
- spec/integration/schema/call_spec.rb
|
@@ -428,6 +433,7 @@ test_files:
|
|
428
433
|
- spec/unit/relation/having_spec.rb
|
429
434
|
- spec/unit/relation/inner_join_spec.rb
|
430
435
|
- spec/unit/relation/inspect_spec.rb
|
436
|
+
- spec/unit/relation/instrument_spec.rb
|
431
437
|
- spec/unit/relation/invert_spec.rb
|
432
438
|
- spec/unit/relation/left_join_spec.rb
|
433
439
|
- spec/unit/relation/map_spec.rb
|