rom-sql 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +12 -7
  4. data/CHANGELOG.md +28 -0
  5. data/Gemfile +6 -9
  6. data/README.md +5 -4
  7. data/circle.yml +10 -0
  8. data/lib/rom/plugins/relation/sql/auto_combine.rb +16 -3
  9. data/lib/rom/plugins/relation/sql/auto_wrap.rb +3 -2
  10. data/lib/rom/sql/association.rb +75 -0
  11. data/lib/rom/sql/association/many_to_many.rb +86 -0
  12. data/lib/rom/sql/association/many_to_one.rb +60 -0
  13. data/lib/rom/sql/association/name.rb +70 -0
  14. data/lib/rom/sql/association/one_to_many.rb +9 -0
  15. data/lib/rom/sql/association/one_to_one.rb +46 -0
  16. data/lib/rom/sql/association/one_to_one_through.rb +9 -0
  17. data/lib/rom/sql/commands.rb +2 -0
  18. data/lib/rom/sql/commands/create.rb +2 -2
  19. data/lib/rom/sql/commands/delete.rb +0 -1
  20. data/lib/rom/sql/commands/postgres.rb +76 -0
  21. data/lib/rom/sql/commands/update.rb +6 -3
  22. data/lib/rom/sql/commands_ext/postgres.rb +17 -0
  23. data/lib/rom/sql/gateway.rb +23 -15
  24. data/lib/rom/sql/header.rb +7 -1
  25. data/lib/rom/sql/plugin/assoc_macros.rb +3 -3
  26. data/lib/rom/sql/plugin/associates.rb +50 -9
  27. data/lib/rom/sql/qualified_attribute.rb +53 -0
  28. data/lib/rom/sql/relation.rb +76 -25
  29. data/lib/rom/sql/relation/reading.rb +138 -35
  30. data/lib/rom/sql/relation/writing.rb +21 -0
  31. data/lib/rom/sql/schema.rb +35 -0
  32. data/lib/rom/sql/schema/associations_dsl.rb +68 -0
  33. data/lib/rom/sql/schema/dsl.rb +27 -0
  34. data/lib/rom/sql/schema/inferrer.rb +80 -0
  35. data/lib/rom/sql/support/active_support_notifications.rb +27 -17
  36. data/lib/rom/sql/types.rb +11 -0
  37. data/lib/rom/sql/types/pg.rb +26 -0
  38. data/lib/rom/sql/version.rb +1 -1
  39. data/rom-sql.gemspec +4 -2
  40. data/spec/integration/association/many_to_many_spec.rb +137 -0
  41. data/spec/integration/association/many_to_one_spec.rb +110 -0
  42. data/spec/integration/association/one_to_many_spec.rb +58 -0
  43. data/spec/integration/association/one_to_one_spec.rb +57 -0
  44. data/spec/integration/association/one_to_one_through_spec.rb +90 -0
  45. data/spec/integration/combine_spec.rb +24 -24
  46. data/spec/integration/commands/create_spec.rb +215 -168
  47. data/spec/integration/commands/delete_spec.rb +88 -46
  48. data/spec/integration/commands/update_spec.rb +141 -60
  49. data/spec/integration/commands/upsert_spec.rb +83 -0
  50. data/spec/integration/gateway_spec.rb +9 -17
  51. data/spec/integration/migration_spec.rb +3 -5
  52. data/spec/integration/plugins/associates_spec.rb +168 -0
  53. data/spec/integration/plugins/auto_wrap_spec.rb +46 -0
  54. data/spec/integration/read_spec.rb +80 -77
  55. data/spec/integration/relation_schema_spec.rb +180 -0
  56. data/spec/integration/schema_inference_spec.rb +67 -0
  57. data/spec/integration/setup_spec.rb +22 -0
  58. data/spec/{support → integration/support}/active_support_notifications_spec.rb +0 -0
  59. data/spec/{support → integration/support}/rails_log_subscriber_spec.rb +0 -0
  60. data/spec/shared/database_setup.rb +46 -8
  61. data/spec/shared/relations.rb +8 -0
  62. data/spec/shared/users_and_accounts.rb +10 -0
  63. data/spec/shared/users_and_tasks.rb +20 -2
  64. data/spec/spec_helper.rb +64 -11
  65. data/spec/support/helpers.rb +9 -0
  66. data/spec/unit/association/many_to_many_spec.rb +89 -0
  67. data/spec/unit/association/many_to_one_spec.rb +81 -0
  68. data/spec/unit/association/name_spec.rb +68 -0
  69. data/spec/unit/association/one_to_many_spec.rb +62 -0
  70. data/spec/unit/association/one_to_one_spec.rb +62 -0
  71. data/spec/unit/association/one_to_one_through_spec.rb +69 -0
  72. data/spec/unit/association_errors_spec.rb +2 -4
  73. data/spec/unit/gateway_spec.rb +12 -3
  74. data/spec/unit/migration_tasks_spec.rb +3 -3
  75. data/spec/unit/migrator_spec.rb +2 -4
  76. data/spec/unit/{combined_associations_spec.rb → plugin/assoc_macros/combined_associations_spec.rb} +13 -19
  77. data/spec/unit/{many_to_many_spec.rb → plugin/assoc_macros/many_to_many_spec.rb} +9 -15
  78. data/spec/unit/{many_to_one_spec.rb → plugin/assoc_macros/many_to_one_spec.rb} +9 -14
  79. data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +78 -0
  80. data/spec/unit/plugin/base_view_spec.rb +11 -11
  81. data/spec/unit/plugin/pagination_spec.rb +62 -62
  82. data/spec/unit/relation_spec.rb +218 -146
  83. data/spec/unit/schema_spec.rb +15 -14
  84. data/spec/unit/types_spec.rb +40 -0
  85. metadata +105 -21
  86. data/.rubocop.yml +0 -74
  87. data/.rubocop_todo.yml +0 -21
  88. data/spec/unit/one_to_many_spec.rb +0 -83
@@ -1,67 +1,109 @@
1
- require 'spec_helper'
2
-
3
- describe 'Commands / Delete' do
1
+ RSpec.describe 'Commands / Delete' do
4
2
  include_context 'users and tasks'
5
3
 
6
4
  subject(:users) { container.commands.users }
7
5
 
8
- before do
9
- configuration.relation(:users) do
10
- def by_name(name)
11
- where(name: name)
6
+ with_adapters do
7
+ before do
8
+ conf.relation(:users) do
9
+ def by_name(name)
10
+ where(name: name)
11
+ end
12
12
  end
13
- end
14
13
 
15
- configuration.commands(:users) do
16
- define(:delete) do
17
- result :one
14
+ conf.commands(:users) do
15
+ define(:delete) do
16
+ result :one
17
+ end
18
18
  end
19
+
20
+ container.relations.users.insert(id: 3, name: 'Jade')
21
+ container.relations.users.insert(id: 4, name: 'John')
19
22
  end
20
23
 
21
- container.relations.users.insert(id: 2, name: 'Jane')
22
- end
24
+ describe '#transaction' do
25
+ it 'deletes in normal way if no error raised' do
26
+ expect {
27
+ users.delete.transaction do
28
+ users.delete.by_name('Jade').call
29
+ end
30
+ }.to change { container.relations.users.count }.by(-1)
31
+ end
23
32
 
24
- context '#transaction' do
25
- it 'delete in normal way if no error raised' do
26
- expect {
27
- users.delete.transaction do
28
- users.delete.by_name('Jane').call
29
- end
30
- }.to change { container.relations.users.count }.by(-1)
33
+ it 'deletes nothing if error was raised' do
34
+ expect {
35
+ users.delete.transaction do
36
+ users.delete.by_name('Jade').call
37
+ raise ROM::SQL::Rollback
38
+ end
39
+ }.to_not change { container.relations.users.count }
40
+ end
31
41
  end
32
42
 
33
- it 'delete nothing if error was raised' do
34
- expect {
35
- users.delete.transaction do
36
- users.delete.by_name('Jane').call
37
- raise ROM::SQL::Rollback
38
- end
39
- }.to_not change { container.relations.users.count }
40
- end
41
- end
43
+ describe '#call' do
44
+ it 'deletes all tuples in a restricted relation' do
45
+ result = users.try { users.delete.by_name('Jade').call }
46
+
47
+ expect(result.value).to eql(id: 3, name: 'Jade')
48
+ end
42
49
 
43
- it 'raises error when tuple count does not match expectation' do
44
- result = users.try { users.delete.call }
50
+ it 're-raises database error' do
51
+ command = users.delete.by_name('Jade')
45
52
 
46
- expect(result.value).to be(nil)
47
- expect(result.error).to be_instance_of(ROM::TupleCountMismatchError)
48
- end
53
+ expect(command.relation).to receive(:delete).and_raise(
54
+ Sequel::DatabaseError, 'totally wrong'
55
+ )
49
56
 
50
- it 'deletes all tuples in a restricted relation' do
51
- result = users.try { users.delete.by_name('Jane').call }
57
+ expect {
58
+ users.try { command.call }
59
+ }.to raise_error(ROM::SQL::DatabaseError, /totally wrong/)
60
+ end
61
+ end
52
62
 
53
- expect(result.value).to eql(id: 2, name: 'Jane')
54
- end
63
+ describe '#execute' do
64
+ context 'with postgres adapter' do
65
+ context 'with a single record' do
66
+ it 'materializes the result' do
67
+ result = container.command(:users).delete.by_name(%w(Jade)).execute
68
+ expect(result).to eq([
69
+ { id: 3, name: 'Jade' }
70
+ ])
71
+ end
72
+ end
55
73
 
56
- it 're-raises database error' do
57
- command = users.delete.by_name('Jane')
74
+ context 'with multiple records' do
75
+ it 'materializes the results' do
76
+ result = container.command(:users).delete.by_name(%w(Jade John)).execute
77
+ expect(result).to eq([
78
+ { id: 3, name: 'Jade' },
79
+ { id: 4, name: 'John' }
80
+ ])
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'with other adapter', adapter: :sqlite do
86
+ let(:uri) { SQLITE_DB_URI }
58
87
 
59
- expect(command.relation).to receive(:delete).and_raise(
60
- Sequel::DatabaseError, 'totally wrong'
61
- )
88
+ context 'with a single record' do
89
+ it 'materializes the result' do
90
+ result = container.command(:users).delete.by_name(%w(Jade)).execute
91
+ expect(result).to eq([
92
+ { id: 3, name: 'Jade' }
93
+ ])
94
+ end
95
+ end
62
96
 
63
- expect {
64
- users.try { command.call }
65
- }.to raise_error(ROM::SQL::DatabaseError, /totally wrong/)
97
+ context 'with multiple records' do
98
+ it 'materializes the results' do
99
+ result = container.command(:users).delete.by_name(%w(Jade John)).execute
100
+ expect(result).to eq([
101
+ { id: 3, name: 'Jade' },
102
+ { id: 4, name: 'John' }
103
+ ])
104
+ end
105
+ end
106
+ end
107
+ end
66
108
  end
67
109
  end
@@ -1,91 +1,172 @@
1
- require 'spec_helper'
2
1
  require 'anima'
3
2
 
4
- describe 'Commands / Update' do
3
+ RSpec.describe 'Commands / Update' do
5
4
  include_context 'database setup'
6
5
 
7
6
  subject(:users) { container.command(:users) }
8
7
 
8
+ let(:update) { container.commands[:users][:update] }
9
+
9
10
  let(:relation) { container.relations.users }
10
11
  let(:piotr) { relation.by_name('Piotr').one }
11
12
  let(:peter) { { name: 'Peter' } }
12
13
 
13
- before do
14
- configuration.relation(:users) do
15
- def by_id(id)
16
- where(id: id).limit(1)
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
22
+ end
17
23
  end
18
24
 
19
- def by_name(name)
20
- where(name: name)
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
30
+ end
31
+
32
+ expect(update.input[foo: 'bar', id: 1, name: 'Jane']).to eql(
33
+ id: 1, name: 'Jane'
34
+ )
21
35
  end
22
36
  end
23
37
 
24
- configuration.commands(:users) do
25
- define(:update)
26
- end
38
+ context 'without a schema' do
39
+ before do
40
+ conf.relation(:users) do
41
+ def by_id(id)
42
+ where(id: id).limit(1)
43
+ end
27
44
 
28
- User = Class.new { include Anima.new(:id, :name) }
45
+ def by_name(name)
46
+ where(name: name)
47
+ end
48
+ end
29
49
 
30
- configuration.mappers do
31
- register :users, entity: -> tuples { tuples.map { |tuple| User.new(tuple) } }
32
- end
50
+ conf.commands(:users) do
51
+ define(:update)
52
+ end
33
53
 
34
- relation.insert(name: 'Piotr')
35
- end
54
+ Test::User = Class.new { include Anima.new(:id, :name) }
36
55
 
37
- after { Object.send(:remove_const, :User) }
56
+ conf.mappers do
57
+ register :users, entity: -> tuples { tuples.map { |tuple| Test::User.new(tuple) } }
58
+ end
38
59
 
39
- context '#transaction' do
40
- it 'update record if there was no errors' do
41
- result = users.update.transaction do
42
- users.update.by_id(piotr[:id]).call(peter)
60
+ relation.insert(name: 'Piotr')
61
+ relation.insert(name: 'Jane')
43
62
  end
44
63
 
45
- expect(result.value).to eq([{ id: 1, name: 'Peter' }])
46
- end
47
-
48
- it 'updates nothing if error was raised' do
49
- users.update.transaction do
50
- users.update.by_id(piotr[:id]).call(peter)
51
- raise ROM::SQL::Rollback
64
+ it 'respects configured input handler' do
65
+ expect(update.input[foo: 'bar', id: 1, name: 'Jane']).to eql(
66
+ foo: 'bar', id: 1, name: 'Jane'
67
+ )
52
68
  end
53
69
 
54
- expect(relation.one[:name]).to eql('Piotr')
55
- end
56
- end
70
+ context '#transaction' do
71
+ it 'update record if there was no errors' do
72
+ result = users.update.transaction do
73
+ users.update.by_id(piotr[:id]).call(peter)
74
+ end
57
75
 
58
- it 'updates everything when there is no original tuple' do
59
- result = users.try do
60
- users.update.by_id(piotr[:id]).call(peter)
61
- end
76
+ expect(result.value).to eq([{ id: 1, name: 'Peter' }])
77
+ end
62
78
 
63
- expect(result.value.to_a).to match_array([{ id: 1, name: 'Peter' }])
64
- end
79
+ it 'updates nothing if error was raised' do
80
+ users.update.transaction do
81
+ users.update.by_id(piotr[:id]).call(peter)
82
+ raise ROM::SQL::Rollback
83
+ end
65
84
 
66
- it 'updates when attributes changed' do
67
- result = users.try do
68
- users.as(:entity).update.by_id(piotr[:id]).change(User.new(piotr)).call(peter)
69
- end
70
-
71
- expect(result.value.to_a).to match_array([User.new(id: 1, name: 'Peter')])
72
- end
73
-
74
- it 'does not update when attributes did not change' do
75
- result = users.try do
76
- command = users.update.by_id(piotr[:id]).change(piotr)
77
-
78
- expect(command.relation).not_to receive(:update)
85
+ expect(relation.first[:name]).to eql('Piotr')
86
+ end
87
+ end
79
88
 
80
- command.call(name: piotr[:name])
89
+ describe '#call' do
90
+ it 'updates everything when there is no original tuple' do
91
+ result = users.try do
92
+ users.update.by_id(piotr[:id]).call(peter)
93
+ end
94
+
95
+ expect(result.value.to_a).to match_array([{ id: 1, name: 'Peter' }])
96
+ end
97
+
98
+ it 'updates when attributes changed' do
99
+ result = users.try do
100
+ users.as(:entity).update.by_id(piotr[:id]).change(Test::User.new(piotr)).call(peter)
101
+ end
102
+
103
+ expect(result.value.to_a).to match_array([Test::User.new(id: 1, name: 'Peter')])
104
+ end
105
+
106
+ it 'does not update when attributes did not change' do
107
+ result = users.try do
108
+ command = users.update.by_id(piotr[:id]).change(piotr)
109
+
110
+ expect(command.relation).not_to receive(:update)
111
+
112
+ command.call(name: piotr[:name])
113
+ end
114
+
115
+ expect(result.value.to_a).to be_empty
116
+ end
117
+
118
+ it 're-reaises database errors' do
119
+ expect {
120
+ users.try { users.update.by_id(piotr[:id]).call(bogus_field: '#trollface') }
121
+ }.to raise_error(ROM::SQL::DatabaseError, /bogus_field/)
122
+ end
123
+
124
+ describe '#execute' do
125
+ context 'with postgres adapter' do
126
+ context 'with a single record' do
127
+ it 'materializes the result' do
128
+ result = users.update.by_name('Piotr').execute(name: 'Pete')
129
+ expect(result).to eq([
130
+ { id: 1, name: 'Pete' }
131
+ ])
132
+ end
133
+ end
134
+
135
+ context 'with multiple records' do
136
+ it 'materializes the results' do
137
+ result = users.update.by_name(%w(Piotr Jane)).execute(name: 'Josie')
138
+ expect(result).to eq([
139
+ { id: 1, name: 'Josie' },
140
+ { id: 2, name: 'Josie' }
141
+ ])
142
+ end
143
+ end
144
+ end
145
+
146
+ context 'with other adapter', adapter: :sqlite do
147
+ let(:uri) { SQLITE_DB_URI }
148
+
149
+ context 'with a single record' do
150
+ it 'materializes the result' do
151
+ result = users.update.by_name('Piotr').execute(name: 'Pete')
152
+ expect(result).to eq([
153
+ { id: 1, name: 'Pete' }
154
+ ])
155
+ end
156
+ end
157
+
158
+ context 'with multiple records' do
159
+ it 'materializes the results' do
160
+ result = users.update.by_name(%w(Piotr Jane)).execute(name: 'Josie')
161
+ expect(result).to eq([
162
+ { id: 1, name: 'Josie' },
163
+ { id: 2, name: 'Josie' }
164
+ ])
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
81
170
  end
82
-
83
- expect(result.value.to_a).to be_empty
84
- end
85
-
86
- it 're-reaises database errors' do
87
- expect {
88
- users.try { users.update.by_id(piotr[:id]).call(bogus_field: '#trollface') }
89
- }.to raise_error(ROM::SQL::DatabaseError, /bogus_field/)
90
171
  end
91
172
  end
@@ -0,0 +1,83 @@
1
+ RSpec.describe 'Commands / Postgres / Upsert', adapter: :postgres do
2
+ subject(:command) { commands[:tasks][:create_or_update] }
3
+
4
+ include_context 'relations'
5
+
6
+ let(:tasks) { commands[:tasks] }
7
+
8
+ before do
9
+ conn[:users].insert id: 1, name: 'Jane'
10
+ conn[:users].insert id: 2, name: 'Joe'
11
+ conn[:users].insert id: 3, name: 'Jean'
12
+ end
13
+
14
+ describe '#call' do
15
+ let(:task) { { title: 'task 1', user_id: 1 } }
16
+ let(:excluded) { task.merge(user_id: 3) }
17
+
18
+ before do
19
+ command_config = self.command_config
20
+
21
+ conf.commands(:tasks) do
22
+ define('Postgres::Upsert') do
23
+ register_as :create_or_update
24
+ result :one
25
+
26
+ instance_exec(&command_config)
27
+ end
28
+ end
29
+ end
30
+
31
+ before { command.relation.upsert(task) }
32
+
33
+ context 'on conflict do nothing' do
34
+ let(:command_config) { -> { } }
35
+
36
+ it 'returns nil' do
37
+ expect(command.call(excluded)).to be nil
38
+ end
39
+ end
40
+
41
+ context 'on conflict do update' do
42
+ context 'with conflict target' do
43
+ let(:command_config) do
44
+ -> do
45
+ conflict_target :title
46
+ update_statement user_id: 2
47
+ end
48
+ end
49
+
50
+ it 'returns updated data' do
51
+ expect(command.call(excluded)).to eql(id: 1, user_id: 2, title: 'task 1')
52
+ end
53
+ end
54
+
55
+ context 'with constraint name' do
56
+ let(:command_config) do
57
+ -> do
58
+ constraint :tasks_title_key
59
+ update_statement user_id: :excluded__user_id
60
+ end
61
+ end
62
+
63
+ it 'returns updated data' do
64
+ expect(command.call(excluded)).to eql(id: 1, user_id: 3, title: 'task 1')
65
+ end
66
+ end
67
+
68
+ context 'with where clause' do
69
+ let(:command_config) do
70
+ -> do
71
+ conflict_target :title
72
+ update_statement user_id: nil
73
+ update_where tasks__id: 2
74
+ end
75
+ end
76
+
77
+ it 'returns nil' do
78
+ expect(command.call(excluded)).to be nil
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end if PG_LTE_95