rom-sql 0.7.0 → 0.8.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.
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