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