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
@@ -6,195 +6,267 @@ describe ROM::Relation do
6
6
  let(:users) { container.relations.users }
7
7
  let(:tasks) { container.relations.tasks }
8
8
 
9
- before do
10
- configuration.relation(:users) do
11
- def sorted
12
- order(:id)
9
+ context 'with schema', adapter: :sqlite do
10
+ let(:uri) { SQLITE_DB_URI }
11
+
12
+ before do
13
+ conf.relation(:users) do
14
+ schema do
15
+ attribute :id, ROM::SQL::Types::Serial
16
+ attribute :name, ROM::SQL::Types::String
17
+ end
18
+
19
+ def sorted
20
+ order(:id)
21
+ end
13
22
  end
14
- end
15
-
16
- configuration.relation(:tasks)
17
- end
18
23
 
19
- describe '#dataset' do
20
- it 'selects all qualified columns and sorts by pk' do
21
- expect(users.dataset).to eql(
22
- users.select(*users.columns).order(:users__id).dataset
23
- )
24
+ conf.relation(:tasks)
24
25
  end
25
- end
26
26
 
27
- describe '#sum' do
28
- it 'delegates to dataset and return value' do
29
- expect(users.dataset).to receive(:sum).with(:id).and_call_original
30
- expect(users.sum(:id)).to eql(1)
27
+ describe '#dataset' do
28
+ it 'uses schema to infer default dataset' do
29
+ expect(container.relations[:users].dataset).to eql(
30
+ container.gateways[:default].dataset(:users).select(:id, :name).order(:users__id)
31
+ )
32
+ end
31
33
  end
32
34
  end
33
35
 
34
- describe '#min' do
35
- it 'delegates to dataset and return value' do
36
- users.insert id: 2, name: 'Oskar'
36
+ with_adapters do
37
+ context 'without schema' do
38
+ before do
39
+ conf.relation(:users) do
40
+ def sorted
41
+ order(:id)
42
+ end
43
+ end
37
44
 
38
- expect(users.dataset).to receive(:min).with(:id).and_call_original
39
- expect(users.min(:id)).to eql(1)
40
- end
41
- end
45
+ conf.relation(:tasks)
46
+ end
42
47
 
43
- describe '#max' do
44
- it 'delegates to dataset and return value' do
45
- users.insert id: 2, name: 'Oskar'
48
+ describe '#associations' do
49
+ it 'returns an empty association set' do
50
+ expect(users.associations.elements).to be_empty
51
+ end
52
+ end
46
53
 
47
- expect(users.dataset).to receive(:max).with(:id).and_call_original
48
- expect(users.max(:id)).to eql(2)
49
- end
50
- end
54
+ describe '#dataset' do
55
+ it 'selects all qualified columns and sorts by pk' do
56
+ expect(users.dataset).to eql(
57
+ users.select(*users.columns).order(:users__id).dataset
58
+ )
59
+ end
60
+ end
51
61
 
52
- describe '#avg' do
53
- it 'delegates to dataset and return value' do
54
- users.insert id: 2, name: 'Oskar'
62
+ describe '#primary_key' do
63
+ it 'returns :id by default' do
64
+ expect(users.primary_key).to be(:id)
65
+ end
55
66
 
56
- expect(users.dataset).to receive(:avg).with(:id).and_call_original
57
- expect(users.avg(:id)).to eql(1.5)
58
- end
59
- end
67
+ it 'returns configured primary key from the schema' do
68
+ conf.relation(:other_users) do
69
+ schema(:users) do
70
+ attribute :name, ROM::SQL::Types::String.meta(primary_key: true)
71
+ end
72
+ end
60
73
 
61
- describe '#distinct' do
62
- it 'delegates to dataset and returns a new relation' do
63
- expect(users.dataset).to receive(:distinct).with(:name).and_call_original
64
- expect(users.distinct(:name)).to_not eq(users)
65
- end
66
- end
74
+ expect(container.relations[:other_users].primary_key).to be(:name)
75
+ end
76
+ end
67
77
 
68
- describe '#exclude' do
69
- it 'delegates to dataset and returns a new relation' do
70
- expect(users.dataset)
71
- .to receive(:exclude).with(name: 'Piotr').and_call_original
72
- expect(users.exclude(name: 'Piotr')).to_not eq(users)
73
- end
74
- end
78
+ describe '#sum' do
79
+ it 'returns a sum' do
80
+ expect(users.sum(:id)).to eql(3)
81
+ end
82
+ end
75
83
 
76
- describe '#invert' do
77
- it 'delegates to dataset and returns a new relation' do
78
- expect(users.dataset).to receive(:invert).and_call_original
79
- expect(users.invert).to_not eq(users)
80
- end
81
- end
84
+ describe '#min' do
85
+ it 'returns a min' do
86
+ expect(users.min(:id)).to eql(1)
87
+ end
88
+ end
82
89
 
83
- describe '#map' do
84
- it 'yields tuples' do
85
- result = users.map { |tuple| tuple[:name] }
86
- expect(result).to eql(%w(Piotr))
87
- end
88
- end
90
+ describe '#max' do
91
+ it 'delegates to dataset and return value' do
92
+ expect(users.max(:id)).to eql(2)
93
+ end
94
+ end
89
95
 
90
- describe '#inner_join' do
91
- it 'joins relations using inner join' do
92
- conn[:users].insert(id: 2, name: 'Jane')
96
+ describe '#avg' do
97
+ it 'delegates to dataset and return value' do
98
+ expect(users.avg(:id)).to eql(1.5)
99
+ end
100
+ end
93
101
 
94
- result = users.inner_join(:tasks, user_id: :id).select(:name, :title)
102
+ describe '#distinct' do
103
+ it 'delegates to dataset and returns a new relation' do
104
+ expect(users.dataset).to receive(:distinct).with(:name).and_call_original
105
+ expect(users.distinct(:name)).to_not eq(users)
106
+ end
107
+ end
95
108
 
96
- expect(result.to_a).to match_array([
97
- { name: 'Piotr', title: 'Finish ROM' }
98
- ])
99
- end
109
+ describe '#exclude' do
110
+ it 'delegates to dataset and returns a new relation' do
111
+ expect(users.dataset)
112
+ .to receive(:exclude).with(name: 'Jane').and_call_original
113
+ expect(users.exclude(name: 'Jane')).to_not eq(users)
114
+ end
115
+ end
100
116
 
101
- it 'raises error when column names are ambiguous' do
102
- expect {
103
- users.inner_join(:tasks, user_id: :id).to_a
104
- }.to raise_error(Sequel::DatabaseError, /column reference "id" is ambiguous/)
105
- end
106
- end
117
+ describe '#invert' do
118
+ it 'delegates to dataset and returns a new relation' do
119
+ expect(users.dataset).to receive(:invert).and_call_original
120
+ expect(users.invert).to_not eq(users)
121
+ end
122
+ end
107
123
 
108
- describe '#left_join' do
109
- it 'joins relations using left outer join' do
110
- conn[:users].insert(id: 2, name: 'Jane')
124
+ describe '#map' do
125
+ it 'yields tuples' do
126
+ result = users.map { |tuple| tuple[:name] }
127
+ expect(result).to eql(%w(Jane Joe))
128
+ end
111
129
 
112
- result = users.left_join(:tasks, user_id: :id).select(:name, :title)
130
+ it 'plucks value' do
131
+ expect(users.map(:name)).to eql(%w(Jane Joe))
132
+ end
133
+ end
113
134
 
114
- expect(result.to_a).to match_array([
115
- { name: 'Piotr', title: 'Finish ROM' },
116
- { name: 'Jane', title: nil }
117
- ])
118
- end
119
- end
135
+ describe '#inner_join' do
136
+ it 'joins relations using inner join' do
137
+ result = users.inner_join(:tasks, user_id: :id).select(:name, :title)
138
+
139
+ expect(result.to_a).to eql([
140
+ { name: 'Jane', title: "Jane's task" },
141
+ { name: 'Joe', title: "Joe's task" }
142
+ ])
143
+ end
144
+
145
+ it 'raises error when column names are ambiguous' do
146
+ expect {
147
+ users.inner_join(:tasks, user_id: :id).to_a
148
+ }.to raise_error(Sequel::DatabaseError, /is ambiguous/)
149
+ end
150
+ end
120
151
 
121
- describe '#project' do
122
- it 'projects the dataset using new column names' do
123
- projected = users.sorted.project(:name)
152
+ describe '#left_join' do
153
+ it 'joins relations using left outer join' do
154
+ result = users.left_join(:tasks, user_id: :id).select(:name, :title)
124
155
 
125
- expect(projected.header).to match_array([:name])
126
- expect(projected.to_a).to eql([{ name: 'Piotr' }])
127
- end
128
- end
156
+ expect(result.to_a).to match_array([
157
+ { name: 'Joe', title: "Joe's task" },
158
+ { name: 'Jane', title: "Jane's task" }
159
+ ])
160
+ end
161
+ end
129
162
 
130
- describe '#rename' do
131
- it 'projects the dataset using new column names' do
132
- renamed = users.sorted.rename(id: :user_id, name: :user_name)
163
+ describe '#project' do
164
+ it 'projects the dataset using new column names' do
165
+ projected = users.sorted.project(:name)
133
166
 
134
- expect(renamed.to_a).to eql([{ user_id: 1, user_name: 'Piotr' }])
135
- end
136
- end
167
+ expect(projected.header).to match_array([:name])
168
+ expect(projected.first).to eql(name: 'Jane')
169
+ end
170
+ end
137
171
 
138
- describe '#prefix' do
139
- it 'projects the dataset using new column names' do
140
- prefixed = users.sorted.prefix(:user)
172
+ describe '#rename' do
173
+ it 'projects the dataset using new column names' do
174
+ renamed = users.sorted.rename(id: :user_id, name: :user_name)
141
175
 
142
- expect(prefixed.to_a).to eql([{ user_id: 1, user_name: 'Piotr' }])
143
- end
176
+ expect(renamed.first).to eql(user_id: 1, user_name: 'Jane')
177
+ end
178
+ end
144
179
 
145
- it 'uses singularized table name as the default prefix' do
146
- prefixed = users.sorted.prefix
180
+ describe '#prefix' do
181
+ it 'projects the dataset using new column names' do
182
+ prefixed = users.sorted.prefix(:user)
147
183
 
148
- expect(prefixed.to_a).to eql([{ user_id: 1, user_name: 'Piotr' }])
149
- end
150
- end
184
+ expect(prefixed.first).to eql(user_id: 1, user_name: 'Jane')
185
+ end
151
186
 
152
- describe '#qualified_columns' do
153
- it 'returns qualified column names' do
154
- columns = users.sorted.prefix(:user).qualified_columns
187
+ it 'uses singularized table name as the default prefix' do
188
+ prefixed = users.sorted.prefix
155
189
 
156
- expect(columns).to eql([:users__id___user_id, :users__name___user_name])
157
- end
190
+ expect(prefixed.first).to eql(user_id: 1, user_name: 'Jane')
191
+ end
192
+ end
158
193
 
159
- it 'returns projected qualified column names' do
160
- columns = users.sorted.project(:id).prefix(:user).qualified_columns
194
+ describe '#qualified_columns' do
195
+ it 'returns qualified column names' do
196
+ columns = users.sorted.prefix(:user).qualified_columns
161
197
 
162
- expect(columns).to eql([:users__id___user_id])
163
- end
164
- end
198
+ expect(columns).to eql([:users__id___user_id, :users__name___user_name])
199
+ end
165
200
 
166
- describe '#inspect' do
167
- it 'includes dataset' do
168
- expect(users.inspect).to include('dataset')
169
- end
170
- end
201
+ it 'returns projected qualified column names' do
202
+ columns = users.sorted.project(:id).prefix(:user).qualified_columns
171
203
 
172
- describe '#unique?' do
173
- before { tasks.delete }
204
+ expect(columns).to eql([:users__id___user_id])
205
+ end
206
+ end
174
207
 
175
- it 'returns true when there is only one tuple matching criteria' do
176
- expect(tasks.unique?(title: 'Task One')).to be(true)
177
- end
208
+ describe '#inspect' do
209
+ it 'includes dataset' do
210
+ expect(users.inspect).to include('dataset')
211
+ end
212
+ end
178
213
 
179
- it 'returns true when there are more than one tuple matching criteria' do
180
- tasks.insert(title: 'Task One')
181
- expect(tasks.unique?(title: 'Task One')).to be(false)
182
- end
183
- end
214
+ describe '#unique?' do
215
+ before { tasks.delete }
184
216
 
185
- describe '#union' do
186
- let(:relation1) { users.where(id: 1).select(:id, :name) }
187
- let(:relation2) { users.where(id: 2).select(:id, :name) }
217
+ it 'returns true when there is only one tuple matching criteria' do
218
+ expect(tasks.unique?(title: 'Task One')).to be(true)
219
+ end
188
220
 
189
- it 'unions two relations and returns a new relation' do
190
- conn[:users].insert(id: 2, name: 'Jane')
221
+ it 'returns true when there are more than one tuple matching criteria' do
222
+ tasks.insert(title: 'Task One')
223
+ expect(tasks.unique?(title: 'Task One')).to be(false)
224
+ end
225
+ end
226
+
227
+ describe '#union' do
228
+ let(:relation1) { users.where(id: 1).select(:id, :name) }
229
+ let(:relation2) { users.where(id: 2).select(:id, :name) }
191
230
 
192
- result = relation1.union(relation2)
231
+ it 'unions two relations and returns a new relation' do
232
+ result = relation1.union(relation2)
233
+
234
+ expect(result.to_a).to match_array([
235
+ { id: 1, name: 'Jane' },
236
+ { id: 2, name: 'Joe' }
237
+ ])
238
+ end
239
+ end
193
240
 
194
- expect(result.to_a).to match_array([
195
- { id: 1, name: 'Piotr' },
196
- { id: 2, name: 'Jane' }
197
- ])
241
+ describe '#pluck' do
242
+ it 'returns a list of values from a specific column' do
243
+ expect(users.pluck(:id)).to eql([1, 2])
244
+ end
245
+ end
246
+
247
+ describe '#by_pk' do
248
+ it 'restricts a relation by its PK' do
249
+ expect(users.by_pk(1).to_a).to eql([id: 1, name: 'Jane'])
250
+ end
251
+
252
+ it 'is available as a view' do
253
+ expect(users.by_pk).to be_curried
254
+ end
255
+ end
256
+
257
+ describe '#fetch' do
258
+ it 'returns a single tuple identified by the pk' do
259
+ expect(users.fetch(1)).to eql(id: 1, name: 'Jane')
260
+ end
261
+
262
+ it 'raises when tuple was not found' do
263
+ expect { users.fetch(535315412) }.to raise_error(ROM::TupleCountMismatchError)
264
+ end
265
+
266
+ it 'raises when more tuples were returned' do
267
+ expect { users.fetch([1, 2]) }.to raise_error(ROM::TupleCountMismatchError)
268
+ end
269
+ end
198
270
  end
199
271
  end
200
272
  end
@@ -1,22 +1,23 @@
1
- require 'spec_helper'
1
+ RSpec.describe ROM::SQL::Schema do
2
+ describe '#primary_key' do
3
+ it 'returns primary key attributes' do
4
+ schema = Class.new(ROM::Relation[:sql]).schema do
5
+ attribute :id, ROM::SQL::Types::Serial
6
+ end
2
7
 
3
- describe 'Inferring schema from database' do
4
- include_context 'database setup'
8
+ schema.finalize!
5
9
 
6
- context "when database schema exists" do
7
- it "infers the schema from the database relations" do
8
- configuration.relation(:users)
9
-
10
- expect(container.relations.users.to_a)
11
- .to eql(container.gateways[:default][:users].to_a)
10
+ expect(schema.primary_key).to eql([schema[:id]])
12
11
  end
13
- end
14
12
 
15
- context "for empty database schemas" do
16
- it "returns an empty schema" do
17
- drop_tables
13
+ it 'returns empty array when there is no PK defined' do
14
+ schema = Class.new(ROM::Relation[:sql]).schema do
15
+ attribute :id, ROM::SQL::Types::Int
16
+ end
17
+
18
+ schema.finalize!
18
19
 
19
- expect { container.not_here }.to raise_error(NoMethodError)
20
+ expect(schema.primary_key).to eql([])
20
21
  end
21
22
  end
22
23
  end