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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/CHANGELOG.md +32 -0
  4. data/Gemfile +4 -1
  5. data/lib/rom/plugins/relation/sql/auto_wrap.rb +1 -3
  6. data/lib/rom/sql/association.rb +33 -14
  7. data/lib/rom/sql/association/many_to_many.rb +17 -10
  8. data/lib/rom/sql/association/many_to_one.rb +29 -13
  9. data/lib/rom/sql/association/name.rb +12 -4
  10. data/lib/rom/sql/association/one_to_many.rb +21 -10
  11. data/lib/rom/sql/commands/create.rb +0 -1
  12. data/lib/rom/sql/commands/update.rb +1 -49
  13. data/lib/rom/sql/dsl.rb +29 -0
  14. data/lib/rom/sql/expression.rb +26 -0
  15. data/lib/rom/sql/function.rb +23 -0
  16. data/lib/rom/sql/gateway.rb +24 -9
  17. data/lib/rom/sql/migration.rb +6 -7
  18. data/lib/rom/sql/migration/migrator.rb +7 -8
  19. data/lib/rom/sql/order_dsl.rb +20 -0
  20. data/lib/rom/sql/plugin/associates.rb +58 -45
  21. data/lib/rom/sql/plugin/pagination.rb +8 -11
  22. data/lib/rom/sql/plugins.rb +0 -2
  23. data/lib/rom/sql/projection_dsl.rb +41 -0
  24. data/lib/rom/sql/qualified_attribute.rb +2 -2
  25. data/lib/rom/sql/relation.rb +35 -67
  26. data/lib/rom/sql/relation/reading.rb +77 -25
  27. data/lib/rom/sql/restriction_dsl.rb +24 -0
  28. data/lib/rom/sql/schema.rb +73 -7
  29. data/lib/rom/sql/schema/associations_dsl.rb +4 -3
  30. data/lib/rom/sql/schema/dsl.rb +5 -2
  31. data/lib/rom/sql/schema/inferrer.rb +21 -11
  32. data/lib/rom/sql/transaction.rb +19 -0
  33. data/lib/rom/sql/type.rb +76 -0
  34. data/lib/rom/sql/version.rb +1 -1
  35. data/rom-sql.gemspec +3 -4
  36. data/spec/extensions/postgres/inferrer_spec.rb +19 -9
  37. data/spec/integration/association/many_to_many/custom_fks_spec.rb +73 -0
  38. data/spec/integration/association/many_to_many/from_view_spec.rb +81 -0
  39. data/spec/integration/association/many_to_many_spec.rb +2 -2
  40. data/spec/integration/association/many_to_one/custom_fks_spec.rb +59 -0
  41. data/spec/integration/association/many_to_one/from_view_spec.rb +74 -0
  42. data/spec/integration/association/many_to_one/self_ref_spec.rb +51 -0
  43. data/spec/integration/association/many_to_one_spec.rb +4 -2
  44. data/spec/integration/association/one_to_many/custom_fks_spec.rb +48 -0
  45. data/spec/integration/association/one_to_many/from_view_spec.rb +57 -0
  46. data/spec/integration/association/one_to_many/self_ref_spec.rb +52 -0
  47. data/spec/integration/association/one_to_many_spec.rb +1 -1
  48. data/spec/integration/association/one_to_one_spec.rb +1 -1
  49. data/spec/integration/association/one_to_one_through_spec.rb +2 -2
  50. data/spec/integration/commands/create_spec.rb +11 -27
  51. data/spec/integration/commands/update_spec.rb +54 -109
  52. data/spec/integration/gateway_spec.rb +31 -17
  53. data/spec/integration/plugins/associates_spec.rb +27 -0
  54. data/spec/integration/plugins/auto_wrap_spec.rb +8 -8
  55. data/spec/integration/schema/call_spec.rb +24 -0
  56. data/spec/integration/schema/prefix_spec.rb +18 -0
  57. data/spec/integration/schema/qualified_spec.rb +18 -0
  58. data/spec/integration/schema/rename_spec.rb +23 -0
  59. data/spec/integration/schema/view_spec.rb +29 -0
  60. data/spec/integration/schema_inference_spec.rb +31 -14
  61. data/spec/spec_helper.rb +2 -2
  62. data/spec/support/helpers.rb +7 -0
  63. data/spec/unit/gateway_spec.rb +5 -4
  64. data/spec/unit/projection_dsl_spec.rb +54 -0
  65. data/spec/unit/relation/dataset_spec.rb +3 -3
  66. data/spec/unit/relation/distinct_spec.rb +8 -7
  67. data/spec/unit/relation/exclude_spec.rb +2 -4
  68. data/spec/unit/relation/having_spec.rb +6 -4
  69. data/spec/unit/relation/inner_join_spec.rb +47 -2
  70. data/spec/unit/relation/invert_spec.rb +2 -3
  71. data/spec/unit/relation/left_join_spec.rb +44 -3
  72. data/spec/unit/relation/order_spec.rb +40 -0
  73. data/spec/unit/relation/prefix_spec.rb +2 -0
  74. data/spec/unit/relation/project_spec.rb +3 -1
  75. data/spec/unit/relation/qualified_columns_spec.rb +2 -0
  76. data/spec/unit/relation/rename_spec.rb +2 -0
  77. data/spec/unit/relation/right_join_spec.rb +59 -0
  78. data/spec/unit/relation/select_append_spec.rb +21 -0
  79. data/spec/unit/relation/select_spec.rb +41 -0
  80. data/spec/unit/relation/where_spec.rb +28 -0
  81. data/spec/unit/restriction_dsl_spec.rb +34 -0
  82. metadata +62 -40
  83. data/lib/rom/plugins/relation/sql/base_view.rb +0 -31
  84. data/lib/rom/sql/header.rb +0 -61
  85. data/lib/rom/sql/plugin/assoc_macros.rb +0 -133
  86. data/lib/rom/sql/plugin/assoc_macros/class_interface.rb +0 -128
  87. data/spec/integration/read_spec.rb +0 -111
  88. data/spec/unit/association_errors_spec.rb +0 -19
  89. data/spec/unit/plugin/assoc_macros/combined_associations_spec.rb +0 -73
  90. data/spec/unit/plugin/assoc_macros/many_to_many_spec.rb +0 -53
  91. data/spec/unit/plugin/assoc_macros/many_to_one_spec.rb +0 -61
  92. data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +0 -78
  93. 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.attributes).to eql(%i[id user_id title])
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.attributes).to eql(%i[id user_id number balance])
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.attributes).to eql(%i[id account_id pan user_id])
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.attributes).to eql(%i[id card_id service user_id])
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
- passed = false
84
-
85
- result = users.create.transaction {
86
- users.create.call(name: 'Jane')
87
- users.create.call(name: '')
88
- } >-> _value {
89
- passed = true
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
- Test::Params.attribute :bogus_field, Types::Int
223
- users.try { users.create.call(name: 'some name', bogus_field: 23) }
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
- context 'with a schema' do
16
- before do
17
- conf.relation(:users) do
18
- schema do
19
- attribute :id, ROM::SQL::Types::Serial
20
- attribute :name, ROM::SQL::Types::String
21
- end
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
- it 'uses relation schema for the default input handler' do
26
- conf.commands(:users) do
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
- context 'without a schema' do
39
- before do
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
- it 'respects configured input handler' do
68
- expect(update.input[foo: 'bar', id: 1, name: 'Jane']).to eql(
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
- context '#transaction' do
74
- it 'update record if there was no errors' do
75
- result = users.update.transaction do
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
- expect(result.value).to eq([{ id: 1, name: 'Peter' }])
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
- it 'updates nothing if error was raised' do
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
- describe '#call' do
93
- it 'updates everything when there is no original tuple' do
94
- result = users.try do
95
- users.update.by_id(piotr[:id]).call(peter)
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
- it 'updates when attributes changed' do
102
- result = users.try do
103
- users.as(:entity).update.by_id(piotr[:id]).change(Test::User.new(piotr)).call(peter)
104
- end
58
+ expect(relation.first[:name]).to eql('Piotr')
59
+ end
60
+ end
105
61
 
106
- expect(result.value.to_a).to match_array([Test::User.new(id: 1, name: 'Peter')])
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
- it 'does not update when attributes did not change' do
110
- result = users.try do
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
- command.call(name: piotr[:name])
116
- end
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
- expect(result.value.to_a).to be_empty
119
- end
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
- it 're-reaises database errors' do
122
- expect {
123
- users.try { users.update.by_id(piotr[:id]).call(bogus_field: '#trollface') }
124
- }.to raise_error(ROM::SQL::DatabaseError, /bogus_field/)
125
- end
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
- describe '#execute' do
128
- context 'with a single record' do
129
- it 'materializes the result' do
130
- result = users.update.by_name('Piotr').execute(name: 'Pete')
131
- expect(result).to eq([
132
- { id: 1, name: 'Pete' }
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
- context 'setting up' do
66
- it 'skips settings up associations when tables are missing' do
67
- conf = ROM::Configuration.new(:sql, uri) do |config|
68
- config.relation(:foos) do
69
- use :assoc_macros
70
- one_to_many :bars, key: :foo_id
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
- it 'skips finalization a relation when table is missing' do
77
- conf = ROM::Configuration.new(:sql, uri) do |config|
78
- class Foos < ROM::Relation[:sql]
79
- dataset :foos
80
- use :assoc_macros
81
- one_to_many :bars, key: :foo_id
82
- end
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 { ROM.container(conf) }.not_to raise_error
86
- expect { Foos.model.dataset }.to raise_error(Sequel::Error, /no dataset/i)
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) { container.relations[:tasks] }
20
+ subject(:tasks) { relations[:tasks] }
21
21
 
22
- let(:users) { container.relations[: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) { container.relations[:tasks] }
33
+ subject(:tasks) { relations[:tasks] }
34
34
 
35
- let(:users) { container.relations[:authors] }
35
+ let(:users) { relations[:authors] }
36
36
 
37
37
  before do
38
- conf.relation(:tasks)
39
- conf.relation(:authors) { dataset :users }
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