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
@@ -0,0 +1,110 @@
1
+ RSpec.describe ROM::SQL::Association::ManyToOne, helpers: true do
2
+ subject(:assoc) {
3
+ ROM::SQL::Association::ManyToOne.new(:tasks, :users)
4
+ }
5
+
6
+ include_context 'users and tasks'
7
+
8
+ let(:users) { container.relations[:users] }
9
+ let(:tasks) { container.relations[:tasks] }
10
+ let(:articles) { container.relations[:articles] }
11
+
12
+ with_adapters do
13
+ before do
14
+ conf.relation(:tasks) do
15
+ schema do
16
+ attribute :id, ROM::SQL::Types::Serial
17
+ attribute :user_id, ROM::SQL::Types::ForeignKey(:users)
18
+ attribute :title, ROM::SQL::Types::String
19
+ end
20
+ end
21
+ end
22
+
23
+ describe '#result' do
24
+ specify { expect(ROM::SQL::Association::ManyToOne.result).to be(:one) }
25
+ end
26
+
27
+ describe '#name' do
28
+ it 'uses target by default' do
29
+ expect(assoc.name).to be(:users)
30
+ end
31
+ end
32
+
33
+ describe '#target' do
34
+ it 'builds full relation name' do
35
+ assoc = ROM::SQL::Association::ManyToOne.new(:users, :tasks, relation: :foo)
36
+
37
+ expect(assoc.name).to be(:tasks)
38
+ expect(assoc.target).to eql(ROM::SQL::Association::Name[:foo, :tasks])
39
+ end
40
+ end
41
+
42
+ describe '#call' do
43
+ it 'prepares joined relations' do
44
+ relation = assoc.call(container.relations)
45
+
46
+ expect(relation.attributes).to eql(%i[id name task_id])
47
+
48
+ expect(relation.where(user_id: 1).one).to eql(id: 1, task_id: 2, name: 'Jane')
49
+
50
+ expect(relation.where(user_id: 2).one).to eql(id: 2, task_id: 1, name: 'Joe')
51
+
52
+ expect(relation.to_a).to eql([
53
+ { id: 2, task_id: 1, name: 'Joe' },
54
+ { id: 1, task_id: 2, name: 'Jane' }
55
+ ])
56
+ end
57
+ end
58
+
59
+ describe ROM::Plugins::Relation::SQL::AutoCombine, '#for_combine' do
60
+ it 'preloads relation based on association' do
61
+ relation = users.for_combine(assoc).call(tasks.call)
62
+
63
+ expect(relation.to_a).to eql([
64
+ { id: 2, task_id: 1, name: 'Joe' },
65
+ { id: 1, task_id: 2, name: 'Jane' }
66
+ ])
67
+ end
68
+ end
69
+
70
+ context 'arbitrary name conventions' do
71
+ let(:articles_name) { ROM::Relation::Name[:articles, :posts] }
72
+
73
+ subject(:assoc) do
74
+ ROM::SQL::Association::ManyToOne.new(articles_name, :users)
75
+ end
76
+
77
+ before do
78
+ conf.relation(:articles) do
79
+ schema(:posts) do
80
+ attribute :post_id, ROM::SQL::Types::Serial
81
+ attribute :author_id, ROM::SQL::Types::ForeignKey(:users)
82
+ attribute :title, ROM::SQL::Types::Strict::String
83
+ attribute :body, ROM::SQL::Types::Strict::String
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#call' do
89
+ it 'prepares joined relations' do
90
+ relation = assoc.call(container.relations)
91
+
92
+ expect(relation.attributes).to eql(%i[id name post_id])
93
+
94
+ expect(relation.order(:id).to_a).to eql([
95
+ { id: 1, name: 'Jane', post_id: 2 },
96
+ { id: 2, name: 'Joe', post_id: 1 }
97
+ ])
98
+
99
+ expect(relation.where(author_id: 1).to_a).to eql(
100
+ [id: 1, name: 'Jane', post_id: 2]
101
+ )
102
+
103
+ expect(relation.where(author_id: 2).to_a).to eql(
104
+ [id: 2, name: 'Joe', post_id: 1]
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,58 @@
1
+ RSpec.describe ROM::SQL::Association::OneToMany do
2
+ subject(:assoc) {
3
+ ROM::SQL::Association::OneToMany.new(:users, :tasks)
4
+ }
5
+
6
+ include_context 'users and tasks'
7
+
8
+ let(:users) { container.relations[:users] }
9
+ let(:tasks) { container.relations[:tasks] }
10
+
11
+ with_adapters do
12
+ before do
13
+ conf.relation(:tasks) do
14
+ schema do
15
+ attribute :id, ROM::SQL::Types::Serial
16
+ attribute :user_id, ROM::SQL::Types::ForeignKey(:users)
17
+ attribute :title, ROM::SQL::Types::String
18
+ end
19
+ end
20
+ end
21
+
22
+ describe '#result' do
23
+ specify { expect(ROM::SQL::Association::OneToMany.result).to be(:many) }
24
+ end
25
+
26
+ describe '#call' do
27
+ it 'prepares joined relations' do
28
+ relation = assoc.call(container.relations)
29
+
30
+ expect(relation.attributes).to eql(%i[id user_id title])
31
+
32
+ expect(relation.order(:tasks__id).to_a).to eql([
33
+ { id: 1, user_id: 2, title: "Joe's task" },
34
+ { id: 2, user_id: 1, title: "Jane's task" }
35
+ ])
36
+
37
+ expect(relation.where(user_id: 1).to_a).to eql([
38
+ { id: 2, user_id: 1, title: "Jane's task" }
39
+ ])
40
+
41
+ expect(relation.where(user_id: 2).to_a).to eql([
42
+ { id: 1, user_id: 2, title: "Joe's task" }
43
+ ])
44
+ end
45
+ end
46
+
47
+ describe ROM::Plugins::Relation::SQL::AutoCombine, '#for_combine' do
48
+ it 'preloads relation based on association' do
49
+ relation = tasks.for_combine(assoc).call(users.call)
50
+
51
+ expect(relation.to_a).to eql([
52
+ { id: 1, user_id: 2, title: "Joe's task" },
53
+ { id: 2, user_id: 1, title: "Jane's task" }
54
+ ])
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ RSpec.describe ROM::SQL::Association::OneToOne do
2
+ subject(:assoc) {
3
+ ROM::SQL::Association::OneToOne.new(:users, :accounts)
4
+ }
5
+
6
+ include_context 'users and accounts'
7
+
8
+ let(:users) { container.relations[:users] }
9
+ let(:accounts) { container.relations[:accounts] }
10
+
11
+ with_adapters do
12
+ before do
13
+ conf.relation(:accounts) do
14
+ schema do
15
+ attribute :id, ROM::SQL::Types::Serial
16
+ attribute :user_id, ROM::SQL::Types::ForeignKey(:users)
17
+ attribute :number, ROM::SQL::Types::String
18
+ attribute :balance, ROM::SQL::Types::Decimal
19
+ end
20
+ end
21
+ end
22
+
23
+ describe '#result' do
24
+ specify { expect(ROM::SQL::Association::OneToOne.result).to be(:one) }
25
+ end
26
+
27
+ describe '#call' do
28
+ it 'prepares joined relations' do
29
+ relation = assoc.call(container.relations)
30
+
31
+ expect(relation.attributes).to eql(%i[id user_id number balance])
32
+
33
+ # TODO: this if caluse should be removed when (and if) https://github.com/xerial/sqlite-jdbc/issues/112
34
+ # will be resolved. See https://github.com/rom-rb/rom-sql/issues/49 for details
35
+ if defined? JRUBY_VERSION && SQLITE_DB_URI == db_uri
36
+ expect(relation.to_a).to eql([id: 1, user_id: 1, number: '42', balance: 10_000])
37
+ else
38
+ expect(relation.to_a).to eql([id: 1, user_id: 1, number: '42', balance: 10_000.to_d])
39
+ end
40
+ end
41
+ end
42
+
43
+ describe ROM::Plugins::Relation::SQL::AutoCombine, '#for_combine' do
44
+ it 'preloads relation based on association' do
45
+ relation = accounts.for_combine(assoc).call(users.call)
46
+
47
+ # TODO: this if caluse should be removed when (and if) https://github.com/xerial/sqlite-jdbc/issues/112
48
+ # will be resolved. See https://github.com/rom-rb/rom-sql/issues/49 for details
49
+ if defined? JRUBY_VERSION && SQLITE_DB_URI == db_uri
50
+ expect(relation.to_a).to eql([id: 1, user_id: 1, number: '42', balance: 10_000])
51
+ else
52
+ expect(relation.to_a).to eql([id: 1, user_id: 1, number: '42', balance: 10_000.to_d])
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,90 @@
1
+ RSpec.describe ROM::SQL::Association::OneToOneThrough do
2
+ subject(:assoc) {
3
+ ROM::SQL::Association::OneToOneThrough.new(:users, :cards, through: :accounts)
4
+ }
5
+
6
+ include_context 'users and accounts'
7
+
8
+ let(:users) { container.relations[:users] }
9
+ let(:cards) { container.relations[:cards] }
10
+
11
+ with_adapters do
12
+ before do
13
+ conf.relation(:accounts) do
14
+ schema do
15
+ attribute :id, ROM::SQL::Types::Serial
16
+ attribute :user_id, ROM::SQL::Types::ForeignKey(:users)
17
+ attribute :number, ROM::SQL::Types::String
18
+ attribute :balance, ROM::SQL::Types::Decimal
19
+
20
+ associations do
21
+ one_to_many :cards
22
+ one_to_many :subscriptions, through: :cards
23
+ end
24
+ end
25
+ end
26
+
27
+ conf.relation(:cards) do
28
+ schema do
29
+ attribute :id, ROM::SQL::Types::Serial
30
+ attribute :account_id, ROM::SQL::Types::ForeignKey(:accounts)
31
+ attribute :pan, ROM::SQL::Types::String
32
+
33
+ associations do
34
+ one_to_many :subscriptions
35
+ end
36
+ end
37
+ end
38
+
39
+ conf.relation(:subscriptions) do
40
+ schema do
41
+ attribute :id, ROM::SQL::Types::Serial
42
+ attribute :card_id, ROM::SQL::Types::ForeignKey(:cards)
43
+ attribute :service, ROM::SQL::Types::String
44
+
45
+ associations do
46
+ many_to_one :cards
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#result' do
53
+ specify { expect(ROM::SQL::Association::OneToOneThrough.result).to be(:one) }
54
+ end
55
+
56
+ describe '#call' do
57
+ it 'prepares joined relations' do
58
+ relation = assoc.call(container.relations)
59
+
60
+ expect(relation.attributes).to eql(%i[id account_id pan user_id])
61
+ expect(relation.to_a).to eql([id: 1, account_id: 1, pan: '*6789', user_id: 1])
62
+ end
63
+ end
64
+
65
+ describe ':through another assoc' do
66
+ subject(:assoc) do
67
+ ROM::SQL::Association::OneToOneThrough.new(:users, :subscriptions, through: :accounts)
68
+ end
69
+
70
+ let(:account_assoc) do
71
+ ROM::SQL::Association::OneToOneThrough.new(:accounts, :subscriptions, through: :cards)
72
+ end
73
+
74
+ it 'prepares joined relations through other association' do
75
+ relation = assoc.call(container.relations)
76
+
77
+ expect(relation.attributes).to eql(%i[id card_id service user_id])
78
+ expect(relation.to_a).to eql([id: 1, card_id: 1, service: 'aws', user_id: 1])
79
+ end
80
+ end
81
+
82
+ describe ROM::Plugins::Relation::SQL::AutoCombine, '#for_combine' do
83
+ it 'preloads relation based on association' do
84
+ relation = cards.for_combine(assoc).call(users.call)
85
+
86
+ expect(relation.to_a).to eql([id: 1, account_id: 1, pan: '*6789', user_id: 1])
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,37 +1,37 @@
1
- require 'spec_helper'
2
-
3
- describe 'Eager loading' do
1
+ describe 'Eager loading', adapters: :all do
4
2
  include_context 'users and tasks'
5
3
 
6
- before do
7
- configuration.relation(:users) do
8
- def by_name(name)
9
- where(name: name)
4
+ with_adapters do
5
+ before do
6
+ conf.relation(:users) do
7
+ def by_name(name)
8
+ where(name: name)
9
+ end
10
10
  end
11
- end
12
11
 
13
- configuration.relation(:tasks) do
14
- def for_users(users)
15
- where(user_id: users.map { |tuple| tuple[:id] })
12
+ conf.relation(:tasks) do
13
+ def for_users(users)
14
+ where(user_id: users.map { |tuple| tuple[:id] })
15
+ end
16
16
  end
17
- end
18
17
 
19
- configuration.relation(:tags) do
20
- def for_tasks(tasks)
21
- inner_join(:task_tags, task_id: :id)
22
- .where(task_id: tasks.map { |tuple| tuple[:id] })
18
+ conf.relation(:tags) do
19
+ def for_tasks(tasks)
20
+ inner_join(:task_tags, task_id: :id)
21
+ .where(task_id: tasks.map { |tuple| tuple[:id] })
22
+ end
23
23
  end
24
24
  end
25
- end
26
25
 
27
- it 'issues 3 queries for 3 combined relations' do
28
- users = container.relation(:users).by_name('Piotr')
29
- tasks = container.relation(:tasks)
30
- tags = container.relation(:tags)
26
+ it 'issues 3 queries for 3 combined relations' do
27
+ users = container.relation(:users).by_name('Piotr')
28
+ tasks = container.relation(:tasks)
29
+ tags = container.relation(:tags)
31
30
 
32
- relation = users.combine(tasks.for_users.combine(tags.for_tasks))
31
+ relation = users.combine(tasks.for_users.combine(tags.for_tasks))
33
32
 
34
- # TODO: figure out a way to assert correct number of issued queries
35
- expect(relation.call).to be_instance_of(ROM::Relation::Loaded)
33
+ # TODO: figure out a way to assert correct number of issued queries
34
+ expect(relation.call).to be_instance_of(ROM::Relation::Loaded)
35
+ end
36
36
  end
37
37
  end
@@ -1,11 +1,10 @@
1
- require 'spec_helper'
2
1
  require 'virtus'
3
2
 
4
- describe 'Commands / Create' do
5
- include_context 'database setup'
3
+ RSpec.describe 'Commands / Create' do
4
+ include_context 'relations'
6
5
 
7
- subject(:users) { container.commands.users }
8
- subject(:tasks) { container.commands.tasks }
6
+ let(:users) { commands[:users] }
7
+ let(:tasks) { commands[:tasks] }
9
8
 
10
9
  before do
11
10
  class Params
@@ -18,7 +17,9 @@ describe 'Commands / Create' do
18
17
  end
19
18
  end
20
19
 
21
- configuration.commands(:users) do
20
+ conn.add_index :users, :name, unique: true
21
+
22
+ conf.commands(:users) do
22
23
  define(:create) do
23
24
  input Params
24
25
 
@@ -34,225 +35,271 @@ describe 'Commands / Create' do
34
35
  end
35
36
  end
36
37
 
37
- configuration.commands(:tasks) do
38
+ conf.commands(:tasks) do
38
39
  define(:create)
39
40
  end
40
-
41
- configuration.relation(:users)
42
- configuration.relation(:tasks)
43
41
  end
44
42
 
45
- context '#transaction' do
46
- it 'creates record if nothing was raised' do
47
- result = users.create.transaction {
48
- users.create.call(name: 'Jane')
49
- }
50
-
51
- expect(result.value).to eq(id: 1, name: 'Jane')
52
- end
53
-
54
- it 'creates multiple records if nothing was raised' do
55
- result = users.create.transaction {
56
- users.create_many.call([{ name: 'Jane' }, { name: 'Jack' }])
57
- }
58
-
59
- expect(result.value).to match_array([
60
- { id: 1, name: 'Jane' }, { id: 2, name: 'Jack' }
61
- ])
62
- end
63
-
64
- it 'allows for nested transactions' do
65
- result = users.create.transaction {
66
- users.create.transaction {
43
+ with_adapters do
44
+ describe '#transaction' do
45
+ it 'creates record if nothing was raised' do
46
+ result = users.create.transaction {
67
47
  users.create.call(name: 'Jane')
68
48
  }
69
- }
70
49
 
71
- expect(result.value).to eq(id: 1, name: 'Jane')
72
- end
73
-
74
- it 'creates nothing if command error was raised' do
75
- expect {
76
- passed = false
50
+ expect(result.value).to eq(id: 1, name: 'Jane')
51
+ end
77
52
 
53
+ it 'creates multiple records if nothing was raised' do
78
54
  result = users.create.transaction {
79
- users.create.call(name: 'Jane')
80
- users.create.call(name: '')
81
- } >-> _value {
82
- passed = true
55
+ users.create_many.call([{ name: 'Jane' }, { name: 'Jack' }])
83
56
  }
84
57
 
85
- expect(result.value).to be(nil)
86
- expect(result.error.message).to eql('name cannot be empty')
87
- expect(passed).to be(false)
88
- }.to_not change { container.relations.users.count }
89
- end
90
-
91
- it 'creates nothing if rollback was raised' do
92
- expect {
93
- passed = false
58
+ expect(result.value).to match_array([
59
+ { id: 1, name: 'Jane' }, { id: 2, name: 'Jack' }
60
+ ])
61
+ end
94
62
 
63
+ it 'allows for nested transactions' do
95
64
  result = users.create.transaction {
96
- users.create.call(name: 'Jane')
97
- users.create.call(name: 'John')
98
- raise ROM::SQL::Rollback
99
- } >-> _value {
100
- passed = true
65
+ users.create.transaction {
66
+ users.create.call(name: 'Jane')
67
+ }
101
68
  }
102
69
 
103
- expect(result.value).to be(nil)
104
- expect(result.error).to be(nil)
105
- expect(passed).to be(false)
106
- }.to_not change { container.relations.users.count }
107
- end
70
+ expect(result.value).to eq(id: 1, name: 'Jane')
71
+ end
108
72
 
109
- it 'creates nothing if constraint error was raised' do
110
- expect {
111
- begin
73
+ it 'creates nothing if command error was raised' do
74
+ expect {
112
75
  passed = false
113
76
 
114
- users.create.transaction {
115
- users.create.call(name: 'Jane')
77
+ result = users.create.transaction {
116
78
  users.create.call(name: 'Jane')
79
+ users.create.call(name: '')
117
80
  } >-> _value {
118
81
  passed = true
119
82
  }
120
- rescue => error
121
- expect(error).to be_instance_of(ROM::SQL::UniqueConstraintError)
83
+
84
+ expect(result.value).to be(nil)
85
+ expect(result.error.message).to eql('name cannot be empty')
122
86
  expect(passed).to be(false)
123
- end
124
- }.to_not change { container.relations.users.count }
125
- end
87
+ }.to_not change { container.relations.users.count }
88
+ end
126
89
 
127
- it 'creates nothing if anything was raised in any nested transaction' do
128
- expect {
90
+ it 'creates nothing if rollback was raised' do
129
91
  expect {
130
- users.create.transaction {
92
+ passed = false
93
+
94
+ result = users.create.transaction {
95
+ users.create.call(name: 'Jane')
131
96
  users.create.call(name: 'John')
97
+ raise ROM::SQL::Rollback
98
+ } >-> _value {
99
+ passed = true
100
+ }
101
+
102
+ expect(result.value).to be(nil)
103
+ expect(result.error).to be(nil)
104
+ expect(passed).to be(false)
105
+ }.to_not change { container.relations.users.count }
106
+ end
107
+
108
+ it 'creates nothing if constraint error was raised' do
109
+ expect {
110
+ begin
111
+ passed = false
112
+
132
113
  users.create.transaction {
133
114
  users.create.call(name: 'Jane')
134
- raise Exception
115
+ users.create.call(name: 'Jane')
116
+ } >-> _value {
117
+ passed = true
135
118
  }
136
- }
137
- }.to raise_error(Exception)
138
- }.to_not change { container.relations.users.count }
119
+ rescue => error
120
+ expect(error).to be_instance_of(ROM::SQL::UniqueConstraintError)
121
+ expect(passed).to be(false)
122
+ end
123
+ }.to_not change { container.relations.users.count }
124
+ end
125
+
126
+ it 'creates nothing if anything was raised in any nested transaction' do
127
+ expect {
128
+ expect {
129
+ users.create.transaction {
130
+ users.create.call(name: 'John')
131
+ users.create.transaction {
132
+ users.create.call(name: 'Jane')
133
+ raise Exception
134
+ }
135
+ }
136
+ }.to raise_error(Exception)
137
+ }.to_not change { container.relations.users.count }
138
+ end
139
139
  end
140
- end
141
140
 
142
- it 'returns a single tuple when result is set to :one' do
143
- result = users.try { users.create.call(name: 'Jane') }
141
+ it 'uses relation schema for the default input handler' do
142
+ conf.relation(:users) do
143
+ register_as :users_with_schema
144
144
 
145
- expect(result.value).to eql(id: 1, name: 'Jane')
146
- end
145
+ schema do
146
+ attribute :id, ROM::SQL::Types::Serial
147
+ attribute :name, ROM::SQL::Types::String
148
+ end
149
+ end
147
150
 
148
- it 'returns tuples when result is set to :many' do
149
- result = users.try do
150
- users.create_many.call([{ name: 'Jane' }, { name: 'Jack' }])
151
+ conf.commands(:users_with_schema) do
152
+ define(:create) do
153
+ result :one
154
+ end
155
+ end
156
+
157
+ create = container.commands[:users_with_schema][:create]
158
+
159
+ expect(create.input[foo: 'bar', id: 1, name: 'Jane']).to eql(
160
+ id: 1, name: 'Jane'
161
+ )
151
162
  end
152
163
 
153
- expect(result.value.to_a).to match_array([
154
- { id: 1, name: 'Jane' }, { id: 2, name: 'Jack' }
155
- ])
156
- end
164
+ it 'returns a single tuple when result is set to :one' do
165
+ result = users.try { users.create.call(name: 'Jane') }
157
166
 
158
- it 're-raises not-null constraint violation error' do
159
- expect {
160
- users.try { users.create.call(name: nil) }
161
- }.to raise_error(ROM::SQL::NotNullConstraintError)
162
- end
167
+ expect(result.value).to eql(id: 1, name: 'Jane')
168
+ end
163
169
 
164
- it 're-raises uniqueness constraint violation error' do
165
- expect {
166
- users.try {
167
- users.create.call(name: 'Jane')
168
- } >-> user {
169
- users.try { users.create.call(name: user[:name]) }
170
- }
171
- }.to raise_error(ROM::SQL::UniqueConstraintError)
172
- end
170
+ it 'returns tuples when result is set to :many' do
171
+ result = users.try do
172
+ users.create_many.call([{ name: 'Jane' }, { name: 'Jack' }])
173
+ end
173
174
 
174
- it 're-raises check constraint violation error' do
175
- expect {
176
- users.try {
177
- users.create.call(name: 'J')
178
- }
179
- }.to raise_error(ROM::SQL::CheckConstraintError, /name/)
180
- end
175
+ expect(result.value.to_a).to match_array([
176
+ { id: 1, name: 'Jane' }, { id: 2, name: 'Jack' }
177
+ ])
178
+ end
181
179
 
182
- it 're-raises fk constraint violation error' do
183
- expect {
184
- tasks.try {
185
- tasks.create.call(user_id: 918_273_645)
186
- }
187
- }.to raise_error(ROM::SQL::ForeignKeyConstraintError, /user_id/)
188
- end
180
+ it 're-raises not-null constraint violation error' do
181
+ expect {
182
+ users.try { users.create.call(name: nil) }
183
+ }.to raise_error(ROM::SQL::NotNullConstraintError)
184
+ end
189
185
 
190
- it 're-raises database errors' do
191
- expect {
192
- Params.attribute :bogus_field
193
- users.try { users.create.call(name: 'some name', bogus_field: 23) }
194
- }.to raise_error(ROM::SQL::DatabaseError)
195
- end
186
+ it 're-raises uniqueness constraint violation error' do
187
+ expect {
188
+ users.try {
189
+ users.create.call(name: 'Jane')
190
+ } >-> user {
191
+ users.try { users.create.call(name: user[:name]) }
192
+ }
193
+ }.to raise_error(ROM::SQL::UniqueConstraintError)
194
+ end
196
195
 
197
- it 'supports [] syntax instead of call' do
198
- expect {
199
- Params.attribute :bogus_field
200
- users.try { users.create[name: 'some name', bogus_field: 23] }
201
- }.to raise_error(ROM::SQL::DatabaseError)
202
- end
196
+ it 're-raises fk constraint violation error' do
197
+ expect {
198
+ tasks.try {
199
+ tasks.create.call(user_id: 918_273_645)
200
+ }
201
+ }.to raise_error(ROM::SQL::ForeignKeyConstraintError, /user_id/)
202
+ end
203
203
 
204
- describe '.associates' do
205
- it 'sets foreign key prior execution for many tuples' do
206
- configuration.commands(:tasks) do
207
- define(:create) do
208
- associates :user, key: [:user_id, :id]
204
+ it 're-raises database errors' do
205
+ expect {
206
+ Params.attribute :bogus_field
207
+ users.try { users.create.call(name: 'some name', bogus_field: 23) }
208
+ }.to raise_error(ROM::SQL::DatabaseError)
209
+ end
210
+
211
+ it 'supports [] syntax instead of call' do
212
+ expect {
213
+ Params.attribute :bogus_field
214
+ users.try { users.create[name: 'some name', bogus_field: 23] }
215
+ }.to raise_error(ROM::SQL::DatabaseError)
216
+ end
217
+
218
+ describe '#execute' do
219
+ context 'with a single record' do
220
+ it 'materializes the result' do
221
+ result = container.command(:users).create.execute(name: 'Jane')
222
+ expect(result).to eq([
223
+ { id: 1, name: 'Jane' }
224
+ ])
225
+ end
226
+ end
227
+
228
+ context 'with multiple records' do
229
+ it 'materializes the results' do
230
+ result = container.command(:users).create.execute([
231
+ { name: 'Jane' },
232
+ { name: 'John' }
233
+ ])
234
+ expect(result).to eq([
235
+ { id: 1, name: 'Jane' },
236
+ { id: 2, name: 'John' }
237
+ ])
209
238
  end
210
239
  end
211
240
 
212
- create_user = container.command(:users).create.with(name: 'Jade')
241
+ context 'with a composite pk' do
242
+ before do
243
+ conn.create_table?(:user_group) do
244
+ primary_key [:user_id, :group_id]
245
+ column :user_id, Integer, null: false
246
+ column :group_id, Integer, null: false
247
+ end
213
248
 
214
- create_task = container.command(:tasks).create.with([
215
- { title: 'Task one' }, { title: 'Task two' }
216
- ])
249
+ conf.relation(:user_group) do
250
+ schema(infer: true)
251
+ end
217
252
 
218
- command = create_user >> create_task
253
+ conf.commands(:user_group) do
254
+ define(:create) { result :one }
255
+ end
256
+ end
219
257
 
220
- result = command.call
258
+ after do
259
+ conn.drop_table(:user_group)
260
+ end
221
261
 
222
- expect(result).to match_array([
223
- { id: 1, user_id: 1, title: 'Task one' },
224
- { id: 2, user_id: 1, title: 'Task two' }
225
- ])
226
- end
262
+ it 'materializes the result' do
263
+ pending 'TODO: with a composite pk sequel returns 0 when inserting' if mysql?
227
264
 
228
- it 'sets foreign key prior execution for one tuple' do
229
- configuration.commands(:tasks) do
230
- define(:create) do
231
- result :one
232
- associates :user, key: [:user_id, :id]
265
+ command = container.commands[:user_group][:create]
266
+ result = command.call(user_id: 1, group_id: 2)
267
+
268
+ expect(result).to eql(user_id: 1, group_id: 2)
233
269
  end
234
270
  end
271
+ end
272
+ end
235
273
 
236
- create_user = container.command(:users).create.with(name: 'Jade')
237
- create_task = container.command(:tasks).create.with(title: 'Task one')
274
+ describe '#call', adapter: :postgres do
275
+ it 're-raises check constraint violation error', adapter: :postgres do
276
+ expect {
277
+ users.try {
278
+ users.create.call(name: 'J')
279
+ }
280
+ }.to raise_error(ROM::SQL::CheckConstraintError, /name/)
281
+ end
282
+ end
238
283
 
239
- command = create_user >> create_task
284
+ describe '#upsert', adapter: :postgres do
285
+ let(:task) { { title: 'task 1' } }
240
286
 
241
- result = command.call
287
+ before { tasks.create.call(task) }
242
288
 
243
- expect(result).to match_array(id: 1, user_id: 1, title: 'Task one')
289
+ it 'raises error without upsert marker' do
290
+ expect {
291
+ tasks.create.call(task)
292
+ }.to raise_error(ROM::SQL::UniqueConstraintError)
244
293
  end
245
294
 
246
- it 'raises when already defined' do
247
- expect {
248
- configuration.commands(:tasks) do
249
- define(:create) do
250
- result :one
251
- associates :user, key: [:user_id, :id]
252
- associates :user, key: [:user_id, :id]
253
- end
254
- end
255
- }.to raise_error(ArgumentError, /user/)
295
+ it 'raises no error for duplicated data' do
296
+ expect { tasks.create.upsert(task) }.to_not raise_error
256
297
  end
257
- end
298
+
299
+ it 'returns record data' do
300
+ expect(tasks.create.upsert(task, constraint: :tasks_title_key, update: { user_id: nil })).to eql([
301
+ id: 1, user_id: nil, title: 'task 1'
302
+ ])
303
+ end
304
+ end if PG_LTE_95
258
305
  end