rom-sql 0.9.1 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +32 -0
- data/Gemfile +4 -1
- data/lib/rom/plugins/relation/sql/auto_wrap.rb +1 -3
- data/lib/rom/sql/association.rb +33 -14
- data/lib/rom/sql/association/many_to_many.rb +17 -10
- data/lib/rom/sql/association/many_to_one.rb +29 -13
- data/lib/rom/sql/association/name.rb +12 -4
- data/lib/rom/sql/association/one_to_many.rb +21 -10
- data/lib/rom/sql/commands/create.rb +0 -1
- data/lib/rom/sql/commands/update.rb +1 -49
- data/lib/rom/sql/dsl.rb +29 -0
- data/lib/rom/sql/expression.rb +26 -0
- data/lib/rom/sql/function.rb +23 -0
- data/lib/rom/sql/gateway.rb +24 -9
- data/lib/rom/sql/migration.rb +6 -7
- data/lib/rom/sql/migration/migrator.rb +7 -8
- data/lib/rom/sql/order_dsl.rb +20 -0
- data/lib/rom/sql/plugin/associates.rb +58 -45
- data/lib/rom/sql/plugin/pagination.rb +8 -11
- data/lib/rom/sql/plugins.rb +0 -2
- data/lib/rom/sql/projection_dsl.rb +41 -0
- data/lib/rom/sql/qualified_attribute.rb +2 -2
- data/lib/rom/sql/relation.rb +35 -67
- data/lib/rom/sql/relation/reading.rb +77 -25
- data/lib/rom/sql/restriction_dsl.rb +24 -0
- data/lib/rom/sql/schema.rb +73 -7
- data/lib/rom/sql/schema/associations_dsl.rb +4 -3
- data/lib/rom/sql/schema/dsl.rb +5 -2
- data/lib/rom/sql/schema/inferrer.rb +21 -11
- data/lib/rom/sql/transaction.rb +19 -0
- data/lib/rom/sql/type.rb +76 -0
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +3 -4
- data/spec/extensions/postgres/inferrer_spec.rb +19 -9
- data/spec/integration/association/many_to_many/custom_fks_spec.rb +73 -0
- data/spec/integration/association/many_to_many/from_view_spec.rb +81 -0
- data/spec/integration/association/many_to_many_spec.rb +2 -2
- data/spec/integration/association/many_to_one/custom_fks_spec.rb +59 -0
- data/spec/integration/association/many_to_one/from_view_spec.rb +74 -0
- data/spec/integration/association/many_to_one/self_ref_spec.rb +51 -0
- data/spec/integration/association/many_to_one_spec.rb +4 -2
- data/spec/integration/association/one_to_many/custom_fks_spec.rb +48 -0
- data/spec/integration/association/one_to_many/from_view_spec.rb +57 -0
- data/spec/integration/association/one_to_many/self_ref_spec.rb +52 -0
- data/spec/integration/association/one_to_many_spec.rb +1 -1
- data/spec/integration/association/one_to_one_spec.rb +1 -1
- data/spec/integration/association/one_to_one_through_spec.rb +2 -2
- data/spec/integration/commands/create_spec.rb +11 -27
- data/spec/integration/commands/update_spec.rb +54 -109
- data/spec/integration/gateway_spec.rb +31 -17
- data/spec/integration/plugins/associates_spec.rb +27 -0
- data/spec/integration/plugins/auto_wrap_spec.rb +8 -8
- data/spec/integration/schema/call_spec.rb +24 -0
- data/spec/integration/schema/prefix_spec.rb +18 -0
- data/spec/integration/schema/qualified_spec.rb +18 -0
- data/spec/integration/schema/rename_spec.rb +23 -0
- data/spec/integration/schema/view_spec.rb +29 -0
- data/spec/integration/schema_inference_spec.rb +31 -14
- data/spec/spec_helper.rb +2 -2
- data/spec/support/helpers.rb +7 -0
- data/spec/unit/gateway_spec.rb +5 -4
- data/spec/unit/projection_dsl_spec.rb +54 -0
- data/spec/unit/relation/dataset_spec.rb +3 -3
- data/spec/unit/relation/distinct_spec.rb +8 -7
- data/spec/unit/relation/exclude_spec.rb +2 -4
- data/spec/unit/relation/having_spec.rb +6 -4
- data/spec/unit/relation/inner_join_spec.rb +47 -2
- data/spec/unit/relation/invert_spec.rb +2 -3
- data/spec/unit/relation/left_join_spec.rb +44 -3
- data/spec/unit/relation/order_spec.rb +40 -0
- data/spec/unit/relation/prefix_spec.rb +2 -0
- data/spec/unit/relation/project_spec.rb +3 -1
- data/spec/unit/relation/qualified_columns_spec.rb +2 -0
- data/spec/unit/relation/rename_spec.rb +2 -0
- data/spec/unit/relation/right_join_spec.rb +59 -0
- data/spec/unit/relation/select_append_spec.rb +21 -0
- data/spec/unit/relation/select_spec.rb +41 -0
- data/spec/unit/relation/where_spec.rb +28 -0
- data/spec/unit/restriction_dsl_spec.rb +34 -0
- metadata +62 -40
- data/lib/rom/plugins/relation/sql/base_view.rb +0 -31
- data/lib/rom/sql/header.rb +0 -61
- data/lib/rom/sql/plugin/assoc_macros.rb +0 -133
- data/lib/rom/sql/plugin/assoc_macros/class_interface.rb +0 -128
- data/spec/integration/read_spec.rb +0 -111
- data/spec/unit/association_errors_spec.rb +0 -19
- data/spec/unit/plugin/assoc_macros/combined_associations_spec.rb +0 -73
- data/spec/unit/plugin/assoc_macros/many_to_many_spec.rb +0 -53
- data/spec/unit/plugin/assoc_macros/many_to_one_spec.rb +0 -61
- data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +0 -78
- data/spec/unit/plugin/base_view_spec.rb +0 -18
@@ -27,7 +27,7 @@ RSpec.describe ROM::SQL::Association::OneToMany do
|
|
27
27
|
it 'prepares joined relations' do
|
28
28
|
relation = assoc.call(container.relations)
|
29
29
|
|
30
|
-
expect(relation.
|
30
|
+
expect(relation.schema.map(&:name)).to eql(%i[id user_id title])
|
31
31
|
|
32
32
|
expect(relation.order(:tasks__id).to_a).to eql([
|
33
33
|
{ id: 1, user_id: 2, title: "Joe's task" },
|
@@ -28,7 +28,7 @@ RSpec.describe ROM::SQL::Association::OneToOne do
|
|
28
28
|
it 'prepares joined relations' do |example|
|
29
29
|
relation = assoc.call(container.relations)
|
30
30
|
|
31
|
-
expect(relation.
|
31
|
+
expect(relation.schema.map(&:name)).to eql(%i[id user_id number balance])
|
32
32
|
|
33
33
|
# TODO: this if caluse should be removed when (and if) https://github.com/xerial/sqlite-jdbc/issues/112
|
34
34
|
# will be resolved. See https://github.com/rom-rb/rom-sql/issues/49 for details
|
@@ -57,7 +57,7 @@ RSpec.describe ROM::SQL::Association::OneToOneThrough do
|
|
57
57
|
it 'prepares joined relations' do
|
58
58
|
relation = assoc.call(container.relations)
|
59
59
|
|
60
|
-
expect(relation.
|
60
|
+
expect(relation.schema.map(&:name)).to eql(%i[id account_id pan user_id])
|
61
61
|
expect(relation.to_a).to eql([id: 1, account_id: 1, pan: '*6789', user_id: 1])
|
62
62
|
end
|
63
63
|
end
|
@@ -74,7 +74,7 @@ RSpec.describe ROM::SQL::Association::OneToOneThrough do
|
|
74
74
|
it 'prepares joined relations through other association' do
|
75
75
|
relation = assoc.call(container.relations)
|
76
76
|
|
77
|
-
expect(relation.
|
77
|
+
expect(relation.schema.map(&:name)).to eql(%i[id card_id service user_id])
|
78
78
|
expect(relation.to_a).to eql([id: 1, card_id: 1, service: 'aws', user_id: 1])
|
79
79
|
end
|
80
80
|
end
|
@@ -27,10 +27,6 @@ RSpec.describe 'Commands / Create', :postgres do
|
|
27
27
|
define(:create) do
|
28
28
|
input Test::Params
|
29
29
|
|
30
|
-
validator -> tuple {
|
31
|
-
raise ROM::CommandError, 'name cannot be empty' if tuple[:name] == ''
|
32
|
-
}
|
33
|
-
|
34
30
|
result :one
|
35
31
|
end
|
36
32
|
|
@@ -80,18 +76,13 @@ RSpec.describe 'Commands / Create', :postgres do
|
|
80
76
|
|
81
77
|
it 'creates nothing if command error was raised' do
|
82
78
|
expect {
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
}
|
91
|
-
|
92
|
-
expect(result.value).to be(nil)
|
93
|
-
expect(result.error.message).to eql('name cannot be empty')
|
94
|
-
expect(passed).to be(false)
|
79
|
+
begin
|
80
|
+
users.create.transaction {
|
81
|
+
users.create.call(name: 'Jane')
|
82
|
+
users.create.call(name: nil)
|
83
|
+
}
|
84
|
+
rescue ROM::SQL::Error
|
85
|
+
end
|
95
86
|
}.to_not change { container.relations.users.count }
|
96
87
|
end
|
97
88
|
|
@@ -209,7 +200,8 @@ RSpec.describe 'Commands / Create', :postgres do
|
|
209
200
|
}.to raise_error(ROM::SQL::UniqueConstraintError)
|
210
201
|
end
|
211
202
|
|
212
|
-
it 're-raises fk constraint violation error' do
|
203
|
+
it 're-raises fk constraint violation error' do |ex|
|
204
|
+
pending 'Waits for https://github.com/jeremyevans/sequel/pull/1283' if jruby? && sqlite?(ex)
|
213
205
|
expect {
|
214
206
|
tasks.try {
|
215
207
|
tasks.create.call(user_id: 918_273_645)
|
@@ -219,16 +211,8 @@ RSpec.describe 'Commands / Create', :postgres do
|
|
219
211
|
|
220
212
|
it 're-raises database errors' do
|
221
213
|
expect {
|
222
|
-
|
223
|
-
|
224
|
-
}.to raise_error(ROM::SQL::DatabaseError)
|
225
|
-
end
|
226
|
-
|
227
|
-
it 'supports [] syntax instead of call' do
|
228
|
-
expect {
|
229
|
-
Test::Params.attribute :bogus_field, Types::Int
|
230
|
-
users.try { users.create[name: 'some name', bogus_field: 23] }
|
231
|
-
}.to raise_error(ROM::SQL::DatabaseError)
|
214
|
+
users.try { users.create.call(name: nil) }
|
215
|
+
}.to raise_error(ROM::SQL::NotNullConstraintError)
|
232
216
|
end
|
233
217
|
|
234
218
|
describe '#execute' do
|
@@ -12,138 +12,83 @@ RSpec.describe 'Commands / Update' do
|
|
12
12
|
let(:peter) { { name: 'Peter' } }
|
13
13
|
|
14
14
|
with_adapters do
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
15
|
+
before do
|
16
|
+
Test::User = Class.new(Dry::Struct) {
|
17
|
+
attribute :id, Types::Strict::Int
|
18
|
+
attribute :name, Types::Strict::String
|
19
|
+
}
|
20
|
+
|
21
|
+
conf.relation(:users) do
|
22
|
+
def by_id(id)
|
23
|
+
where(id: id).limit(1)
|
22
24
|
end
|
23
|
-
end
|
24
25
|
|
25
|
-
|
26
|
-
|
27
|
-
define(:update) do
|
28
|
-
result :one
|
29
|
-
end
|
26
|
+
def by_name(name)
|
27
|
+
where(name: name)
|
30
28
|
end
|
31
|
-
|
32
|
-
expect(update.input[foo: 'bar', id: 1, name: 'Jane']).to eql(
|
33
|
-
id: 1, name: 'Jane'
|
34
|
-
)
|
35
29
|
end
|
36
|
-
end
|
37
30
|
|
38
|
-
|
39
|
-
|
40
|
-
Test::User = Class.new(Dry::Struct) {
|
41
|
-
attribute :id, Types::Strict::Int
|
42
|
-
attribute :name, Types::Strict::String
|
43
|
-
}
|
44
|
-
|
45
|
-
conf.relation(:users) do
|
46
|
-
def by_id(id)
|
47
|
-
where(id: id).limit(1)
|
48
|
-
end
|
49
|
-
|
50
|
-
def by_name(name)
|
51
|
-
where(name: name)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
conf.commands(:users) do
|
56
|
-
define(:update)
|
57
|
-
end
|
58
|
-
|
59
|
-
conf.mappers do
|
60
|
-
register :users, entity: -> tuples { tuples.map { |tuple| Test::User.new(tuple) } }
|
61
|
-
end
|
62
|
-
|
63
|
-
relation.insert(name: 'Piotr')
|
64
|
-
relation.insert(name: 'Jane')
|
31
|
+
conf.commands(:users) do
|
32
|
+
define(:update)
|
65
33
|
end
|
66
34
|
|
67
|
-
|
68
|
-
|
69
|
-
foo: 'bar', id: 1, name: 'Jane'
|
70
|
-
)
|
35
|
+
conf.mappers do
|
36
|
+
register :users, entity: -> tuples { tuples.map { |tuple| Test::User.new(tuple) } }
|
71
37
|
end
|
72
38
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
users.update.by_id(piotr[:id]).call(peter)
|
77
|
-
end
|
39
|
+
relation.insert(name: 'Piotr')
|
40
|
+
relation.insert(name: 'Jane')
|
41
|
+
end
|
78
42
|
|
79
|
-
|
43
|
+
context '#transaction' do
|
44
|
+
it 'update record if there was no errors' do
|
45
|
+
result = users.update.transaction do
|
46
|
+
users.update.by_id(piotr[:id]).call(peter)
|
80
47
|
end
|
81
48
|
|
82
|
-
|
83
|
-
users.update.transaction do
|
84
|
-
users.update.by_id(piotr[:id]).call(peter)
|
85
|
-
raise ROM::SQL::Rollback
|
86
|
-
end
|
87
|
-
|
88
|
-
expect(relation.first[:name]).to eql('Piotr')
|
89
|
-
end
|
49
|
+
expect(result.value).to eq([{ id: 1, name: 'Peter' }])
|
90
50
|
end
|
91
51
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
end
|
97
|
-
|
98
|
-
expect(result.value.to_a).to match_array([{ id: 1, name: 'Peter' }])
|
52
|
+
it 'updates nothing if error was raised' do
|
53
|
+
users.update.transaction do
|
54
|
+
users.update.by_id(piotr[:id]).call(peter)
|
55
|
+
raise ROM::SQL::Rollback
|
99
56
|
end
|
100
57
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
58
|
+
expect(relation.first[:name]).to eql('Piotr')
|
59
|
+
end
|
60
|
+
end
|
105
61
|
|
106
|
-
|
62
|
+
describe '#call' do
|
63
|
+
it 'updates relation tuples' do
|
64
|
+
result = users.try do
|
65
|
+
users.update.by_id(piotr[:id]).call(peter)
|
107
66
|
end
|
108
67
|
|
109
|
-
|
110
|
-
|
111
|
-
command = users.update.by_id(piotr[:id]).change(piotr)
|
112
|
-
|
113
|
-
expect(command.relation).not_to receive(:update)
|
68
|
+
expect(result.value.to_a).to match_array([{ id: 1, name: 'Peter' }])
|
69
|
+
end
|
114
70
|
|
115
|
-
|
116
|
-
|
71
|
+
it 're-raises database errors' do |example|
|
72
|
+
pending 'why is it failing on travis?' if ENV['TRAVIS'] && mysql?(example) && !jruby?
|
117
73
|
|
118
|
-
|
119
|
-
|
74
|
+
expect {
|
75
|
+
users.update.by_id(piotr[:id]).call(name: nil)
|
76
|
+
}.to raise_error(ROM::SQL::NotNullConstraintError, /name/)
|
77
|
+
end
|
120
78
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
79
|
+
it 'materializes single result' do
|
80
|
+
result = users.update.by_name('Piotr').call(name: 'Pete')
|
81
|
+
expect(result).to eq([
|
82
|
+
{ id: 1, name: 'Pete' }
|
83
|
+
])
|
84
|
+
end
|
126
85
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
])
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
context 'with multiple records' do
|
138
|
-
it 'materializes the results' do
|
139
|
-
result = users.update.by_name(%w(Piotr Jane)).execute(name: 'Josie')
|
140
|
-
expect(result).to eq([
|
141
|
-
{ id: 1, name: 'Josie' },
|
142
|
-
{ id: 2, name: 'Josie' }
|
143
|
-
])
|
144
|
-
end
|
145
|
-
end
|
146
|
-
end
|
86
|
+
it 'materializes multiple results' do
|
87
|
+
result = users.update.by_name(%w(Piotr Jane)).call(name: 'Josie')
|
88
|
+
expect(result).to eq([
|
89
|
+
{ id: 1, name: 'Josie' },
|
90
|
+
{ id: 2, name: 'Josie' }
|
91
|
+
])
|
147
92
|
end
|
148
93
|
end
|
149
94
|
end
|
@@ -62,28 +62,42 @@ RSpec.describe ROM::SQL::Gateway, :postgres, skip_tables: true do
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
end
|
65
|
+
describe 'transactions' do
|
66
|
+
before do
|
67
|
+
conn.drop_table?(:names)
|
68
|
+
|
69
|
+
conn.create_table(:names) do
|
70
|
+
String :name
|
72
71
|
end
|
73
|
-
expect { ROM.container(conf) }.not_to raise_error
|
74
72
|
end
|
75
73
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
74
|
+
let(:gw) { container.gateways[:default] }
|
75
|
+
let(:names) { gw.dataset(:names) }
|
76
|
+
|
77
|
+
it 'can run the code inside a transaction' do
|
78
|
+
names.insert name: 'Jade'
|
79
|
+
|
80
|
+
gw.transaction do |t|
|
81
|
+
names.insert name: 'John'
|
82
|
+
|
83
|
+
t.rollback!
|
84
|
+
names.insert name: 'Jack'
|
83
85
|
end
|
84
86
|
|
85
|
-
expect
|
86
|
-
|
87
|
+
expect(names.to_a).to eql([name: 'Jade'])
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'sets isolation level to read commited' do
|
91
|
+
gw = container.gateways[:default]
|
92
|
+
names = gw.dataset(:names)
|
93
|
+
|
94
|
+
gw.transaction do |t|
|
95
|
+
names.insert name: 'John'
|
96
|
+
concurrent_names = nil
|
97
|
+
Thread.new { concurrent_names = names.to_a }.join
|
98
|
+
|
99
|
+
expect(concurrent_names).to eql([])
|
100
|
+
end
|
87
101
|
end
|
88
102
|
end
|
89
103
|
end
|
@@ -13,6 +13,33 @@ RSpec.describe 'Plugins / :associates' do
|
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
describe '#with_association' do
|
17
|
+
let(:user) do
|
18
|
+
users[:create].call(name: 'Jane')
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:task) do
|
22
|
+
{ title: 'Task one' }
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
conf.commands(:users) do
|
27
|
+
define(:create) { result :one }
|
28
|
+
end
|
29
|
+
|
30
|
+
conf.commands(:tasks) do
|
31
|
+
define(:create) { result :one }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns a command prepared for the given association' do
|
36
|
+
command = tasks[:create].with_association(:user, key: %i[user_id id])
|
37
|
+
|
38
|
+
expect(command.call(task, user)).
|
39
|
+
to eql(id: 1, title: 'Task one', user_id: user[:id])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
16
43
|
shared_context 'automatic FK setting' do
|
17
44
|
it 'sets foreign key prior execution for many tuples' do
|
18
45
|
create_user = users[:create].with(name: 'Jade')
|
@@ -17,26 +17,26 @@ RSpec.describe 'Plugins / :auto_wrap' do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
context 'when parent relation is registered under dataset name' do
|
20
|
-
subject(:tasks) {
|
20
|
+
subject(:tasks) { relations[:tasks] }
|
21
21
|
|
22
|
-
let(:users) {
|
22
|
+
let(:users) { relations[:users] }
|
23
23
|
|
24
24
|
before do
|
25
|
-
conf.relation(:tasks)
|
26
|
-
conf.relation(:users)
|
25
|
+
conf.relation(:tasks) { schema(infer: true) }
|
26
|
+
conf.relation(:users) { schema(infer: true) }
|
27
27
|
end
|
28
28
|
|
29
29
|
include_context 'joined tuple'
|
30
30
|
end
|
31
31
|
|
32
32
|
context 'when parent relation is registered under a custom name' do
|
33
|
-
subject(:tasks) {
|
33
|
+
subject(:tasks) { relations[:tasks] }
|
34
34
|
|
35
|
-
let(:users) {
|
35
|
+
let(:users) { relations[:authors] }
|
36
36
|
|
37
37
|
before do
|
38
|
-
conf.relation(:tasks)
|
39
|
-
conf.relation(:authors) {
|
38
|
+
conf.relation(:tasks) { schema(infer: true) }
|
39
|
+
conf.relation(:authors) { schema(:users, infer: true) }
|
40
40
|
end
|
41
41
|
|
42
42
|
include_context 'joined tuple'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe ROM::SQL::Schema, '#call' do
|
4
|
+
include_context 'database setup'
|
5
|
+
|
6
|
+
with_adapters :postgres do
|
7
|
+
before do
|
8
|
+
conf.relation(:users) do
|
9
|
+
schema(infer: true)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:schema) { relations[:users].schema }
|
14
|
+
|
15
|
+
it 'auto-projects a relation' do
|
16
|
+
expect(schema.(relations[:users]).dataset.sql).to eql('SELECT "id", "name" FROM "users" ORDER BY "users"."id"')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'maintains schema' do
|
20
|
+
projected = relations[:users].schema.project(:name)
|
21
|
+
expect(projected.(relations[:users]).schema).to be(projected)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|