rom-sql 0.9.1 → 1.0.0.beta1

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