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
@@ -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