rom-sql 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,6 +17,7 @@ RSpec.describe 'PostgreSQL extension', :postgres do
17
17
 
18
18
  conf.commands(:people) do
19
19
  define(:create)
20
+ define(:update)
20
21
  end
21
22
  end
22
23
 
@@ -35,4 +36,24 @@ RSpec.describe 'PostgreSQL extension', :postgres do
35
36
  expect(people_relation.to_a).to eq([id: 1, name: 'John Doe', tags: []])
36
37
  end
37
38
  end
39
+
40
+ describe 'using retrurning' do
41
+ let(:create_person) { commands[:people].create }
42
+ let(:update_person) { commands[:people].update }
43
+ let(:composite_relation) { people_relation >> -> r { r.to_a.map { |x| x.fetch(:name).upcase } } }
44
+
45
+ context 'with pipeline' do
46
+ it 'works with create' do
47
+ mapped_people = create_person.new(composite_relation).call(name: 'John Doe', tags: ['foo'])
48
+ expect(mapped_people).to eql(['JOHN DOE'])
49
+ end
50
+
51
+ it 'works with update' do
52
+ create_person.call(name: 'John Doe', tags: ['foo'])
53
+
54
+ mapped_people = update_person.new(composite_relation).call(name: 'Jane Doe')
55
+ expect(mapped_people).to eql(['JANE DOE'])
56
+ end
57
+ end
58
+ end
38
59
  end
@@ -17,7 +17,7 @@ RSpec.describe 'Commands / Update', seeds: false do
17
17
 
18
18
  conf.relation(:users) do
19
19
  def by_id(id)
20
- where(id: id).limit(1)
20
+ where(id: id)
21
21
  end
22
22
 
23
23
  def by_name(name)
@@ -1,24 +1,55 @@
1
- RSpec.describe ROM::SQL, '.migration', :postgres do
1
+ RSpec.describe ROM::SQL, '.migration' do
2
2
  include_context 'database setup'
3
3
 
4
4
  before do
5
5
  inferrable_relations.concat %i(dragons schema_migrations)
6
6
  end
7
7
 
8
- before { conf }
8
+ with_adapters do
9
+ before { conf }
9
10
 
10
- it 'creates a migration for a specific gateway' do
11
- migration = ROM::SQL.migration do
12
- change do
13
- create_table :dragons do
14
- primary_key :id
15
- column :name, String
11
+ it 'creates a migration for a specific gateway' do
12
+ migration = ROM::SQL.migration(container) do
13
+ change do
14
+ create_table :dragons do
15
+ primary_key :id
16
+ column :name, String
17
+ end
16
18
  end
17
19
  end
20
+
21
+ migration.apply(conn, :up)
22
+
23
+ expect(conn.table_exists?(:dragons)).to be(true)
18
24
  end
25
+ end
26
+
27
+ context 'with non-default gateway' do
28
+ with_adapters(:postgres) do
29
+ let(:conf) do
30
+ ROM::Configuration.new(
31
+ default: [:sql, conn, inferrable_relations: %i(schema_migrations)],
32
+ in_memory: [:sql, DB_URIS[:sqlite], inferrable_relations: %i(schema_migrations)]
33
+ )
34
+ end
19
35
 
20
- migration.apply(conn, :up)
36
+ let(:in_memory_connection) { container.gateways[:in_memory].connection }
21
37
 
22
- expect(conn.table_exists?(:dragons)).to be(true)
38
+ it 'creates a migration for a specific gateway' do
39
+ in_memory_migration = ROM::SQL.migration(container, :in_memory) do
40
+ change do
41
+ create_table :turtles do
42
+ primary_key :id
43
+ column :name, String
44
+ end
45
+ end
46
+ end
47
+
48
+ in_memory_migration.apply(in_memory_connection, :up)
49
+
50
+ expect(in_memory_connection.table_exists?(:dragons)).to be(false)
51
+ expect(in_memory_connection.table_exists?(:turtles)).to be(true)
52
+ end
53
+ end
23
54
  end
24
55
  end
@@ -49,16 +49,63 @@ RSpec.describe 'Plugins / :auto_wrap' do
49
49
  end
50
50
  end
51
51
 
52
- context 'using association' do
52
+ context 'using association with inferred relation name' do
53
53
  before do
54
- conf.relation(:tasks) {
55
- schema(infer: true) { associations { belongs_to :users, as: :assignee } }
56
- }
54
+ conf.relation(:tasks) do
55
+ schema(infer: true) do
56
+ associations do
57
+ belongs_to :user
58
+ end
59
+ end
60
+ end
57
61
 
58
- conf.relation(:users) { schema(infer: true) }
62
+ conf.relation(:users) do
63
+ schema(infer: true)
64
+ end
65
+ end
66
+
67
+ include_context 'joined tuple' do
68
+ let(:name) { :user }
69
+ end
70
+ end
71
+
72
+ context 'using association with an alias' do
73
+ before do
74
+ conf.relation(:tasks) do
75
+ schema(infer: true) do
76
+ associations do
77
+ belongs_to :users, as: :assignee
78
+ end
79
+ end
80
+ end
81
+
82
+ conf.relation(:users) do
83
+ schema(infer: true)
84
+ end
85
+ end
86
+
87
+ include_context 'joined tuple' do
88
+ let(:name) { :assignee }
89
+ end
90
+ end
91
+
92
+ context 'using association with an aliased relation' do
93
+ before do
94
+ conf.relation(:tasks) do
95
+ schema(infer: true) do
96
+ associations do
97
+ belongs_to :users, as: :assignee, relation: :people
98
+ end
99
+ end
100
+ end
101
+
102
+ conf.relation(:people) do
103
+ schema(:users, infer: true)
104
+ end
59
105
  end
60
106
 
61
107
  include_context 'joined tuple' do
108
+ let(:users) { relations[:people] }
62
109
  let(:name) { :assignee }
63
110
  end
64
111
  end
@@ -2,7 +2,7 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  before do
5
- inferrable_relations.concat %i(test_inferrence test_numeric)
5
+ inferrable_relations.concat %i(test_characters test_inferrence test_numeric)
6
6
  end
7
7
 
8
8
  let(:schema) { container.relations[dataset].schema }
@@ -29,7 +29,7 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
29
29
  expect(schema.to_h).
30
30
  to eql(
31
31
  id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
32
- name: ROM::SQL::Types::String.meta(name: :name, source: source)
32
+ name: ROM::SQL::Types::String.meta(name: :name, limit: 255, source: source)
33
33
  )
34
34
  end
35
35
  end
@@ -48,7 +48,7 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
48
48
  expect(schema.to_h).
49
49
  to eql(
50
50
  id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
51
- title: ROM::SQL::Types::String.optional.meta(name: :title, source: source),
51
+ title: ROM::SQL::Types::String.meta(limit: 255).optional.meta(name: :title, source: source),
52
52
  user_id: ROM::SQL::Types::Int.optional.meta(
53
53
  name: :user_id,
54
54
  foreign_key: true,
@@ -66,7 +66,7 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
66
66
 
67
67
  conn.create_table :test_inferrence do
68
68
  primary_key :id
69
- String :text, null: false
69
+ String :text, text: false, null: false
70
70
  Time :time
71
71
  Date :date
72
72
 
@@ -97,7 +97,7 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
97
97
  expect(schema.to_h).
98
98
  to eql(
99
99
  id: ROM::SQL::Types::Serial.meta(name: :id, source: source),
100
- text: ROM::SQL::Types::String.meta(name: :text, source: source),
100
+ text: ROM::SQL::Types::String.meta(name: :text, limit: 255, source: source),
101
101
  time: ROM::SQL::Types::Time.optional.meta(name: :time, source: source),
102
102
  date: date_type.optional.meta(name: :date, source: source),
103
103
  datetime: ROM::SQL::Types::Time.meta(name: :datetime, source: source),
@@ -106,6 +106,37 @@ RSpec.describe 'Schema inference for common datatypes', seeds: false do
106
106
  end
107
107
  end
108
108
 
109
+ context 'character datatypes' do
110
+ before do
111
+ conn.create_table :test_characters do
112
+ String :text1, text: false, null: false
113
+ String :text2, size: 100, null: false
114
+ column :text3, 'char(100)', null: false
115
+ column :text4, 'varchar', null: false
116
+ column :text5, 'varchar(100)', null: false
117
+ String :text6, size: 100
118
+ end
119
+ end
120
+
121
+ let(:dataset) { :test_characters }
122
+ let(:source) { ROM::Relation::Name[dataset] }
123
+
124
+ let(:char_t) { ROM::SQL::Types::String.meta(source: source) }
125
+
126
+ it 'infers attributes with limit' do
127
+ expect(schema.to_h).to eql(
128
+ text1: char_t.meta(name: :text1, limit: 255),
129
+ text2: char_t.meta(name: :text2, limit: 100),
130
+ text3: char_t.meta(name: :text3, limit: 100),
131
+ text4: char_t.meta(name: :text4, limit: 255),
132
+ text5: char_t.meta(name: :text5, limit: 100),
133
+ text6: ROM::SQL::Types::String.meta(limit: 100).optional.meta(
134
+ name: :text6, source: source
135
+ )
136
+ )
137
+ end
138
+ end
139
+
109
140
  context 'numeric datatypes' do
110
141
  before do
111
142
  conn.create_table :test_numeric do
@@ -16,7 +16,7 @@ RSpec.shared_context 'users' do
16
16
 
17
17
  conn.create_table :users do
18
18
  primary_key :id
19
- String :name, null: false
19
+ String :name, text: false, null: false
20
20
  check { char_length(name) > 2 } if ctx.postgres?(example)
21
21
  end
22
22
  end
@@ -17,7 +17,7 @@ RSpec.shared_context 'users and tasks' do
17
17
  conn.create_table :tasks do
18
18
  primary_key :id
19
19
  foreign_key :user_id, :users
20
- String :title
20
+ String :title, text: false
21
21
  constraint(:title_length) { char_length(title) > 1 } if ctx.postgres?(example)
22
22
  constraint(:title_length) { length(title) > 1 } if ctx.sqlite?(example)
23
23
  end
@@ -1,10 +1,16 @@
1
1
  require 'bundler'
2
2
  Bundler.setup
3
3
 
4
- if RUBY_ENGINE == 'ruby' && ENV['CI'] == 'true'
5
- require 'simplecov'
6
- SimpleCov.start do
7
- add_filter '/spec/'
4
+ if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
5
+ require 'yaml'
6
+ rubies = YAML.load(File.read(File.join(__dir__, '..', '.travis.yml')))['rvm']
7
+ latest_mri = rubies.select { |v| v =~ /\A\d+\.\d+.\d+\z/ }.max
8
+
9
+ if RUBY_VERSION == latest_mri
10
+ require 'simplecov'
11
+ SimpleCov.start do
12
+ add_filter '/spec/'
13
+ end
8
14
  end
9
15
  end
10
16
 
@@ -6,12 +6,92 @@ RSpec.describe ROM::SQL::Attribute, :postgres do
6
6
  let(:ds) { users.dataset }
7
7
 
8
8
  describe '#is' do
9
- it 'returns a boolean expression' do
10
- expect(users[:id].is(1).sql_literal(ds)).to eql('("id" = 1)')
9
+ context 'with a standard value' do
10
+ it 'returns a boolean expression' do
11
+ expect(users[:id].is(1).sql_literal(ds)).to eql('("id" = 1)')
12
+ end
13
+
14
+ it 'returns a boolean equality expression for qualified attribute' do
15
+ expect((users[:id].qualified.is(1)).sql_literal(ds)).to eql('("users"."id" = 1)')
16
+ end
17
+ end
18
+
19
+ context 'with a nil value' do
20
+ it 'returns an IS NULL expression' do
21
+ expect(users[:id].is(nil).sql_literal(ds)).to eql('("id" IS NULL)')
22
+ end
23
+
24
+ it 'returns an IS NULL expression for qualified attribute' do
25
+ expect((users[:id].qualified.is(nil)).sql_literal(ds)).to eql('("users"."id" IS NULL)')
26
+ end
27
+ end
28
+
29
+ context 'with a boolean true' do
30
+ it 'returns an IS TRUE expression' do
31
+ expect(users[:id].is(true).sql_literal(ds)).to eql('("id" IS TRUE)')
32
+ end
33
+
34
+ it 'returns an IS TRUE expression for qualified attribute' do
35
+ expect((users[:id].qualified.is(true)).sql_literal(ds)).to eql('("users"."id" IS TRUE)')
36
+ end
37
+ end
38
+
39
+ context 'with a boolean false' do
40
+ it 'returns an IS FALSE expression' do
41
+ expect(users[:id].is(false).sql_literal(ds)).to eql('("id" IS FALSE)')
42
+ end
43
+
44
+ it 'returns an IS FALSE expression for qualified attribute' do
45
+ expect((users[:id].qualified.is(false)).sql_literal(ds)).to eql('("users"."id" IS FALSE)')
46
+ end
47
+ end
48
+ end
49
+
50
+ describe '#not' do
51
+ context 'with a standard value' do
52
+ it 'returns a negated boolean equality expression' do
53
+ expect(users[:id].not(1).sql_literal(ds)).to eql('("id" != 1)')
54
+ end
55
+
56
+ it 'returns a negated boolean equality expression for qualified attribute' do
57
+ expect((users[:id].qualified.not(1)).sql_literal(ds)).to eql('("users"."id" != 1)')
58
+ end
59
+ end
60
+
61
+ context 'with a nil value' do
62
+ it 'returns an IS NOT NULL expression' do
63
+ expect(users[:id].not(nil).sql_literal(ds)).to eql('("id" IS NOT NULL)')
64
+ end
65
+
66
+ it 'returns an IS NOT NULL expression for qualified attribute' do
67
+ expect((users[:id].qualified.not(nil)).sql_literal(ds)).to eql('("users"."id" IS NOT NULL)')
68
+ end
69
+ end
70
+
71
+ context 'with a boolean true' do
72
+ it 'returns an IS NOT TRUE expression' do
73
+ expect(users[:id].not(true).sql_literal(ds)).to eql('("id" IS NOT TRUE)')
74
+ end
75
+
76
+ it 'returns an IS NOT TRUE expression for qualified attribute' do
77
+ expect((users[:id].qualified.not(true)).sql_literal(ds)).to eql('("users"."id" IS NOT TRUE)')
78
+ end
79
+ end
80
+
81
+ context 'with a boolean false' do
82
+ it 'returns an IS NOT FALSE expression' do
83
+ expect(users[:id].not(false).sql_literal(ds)).to eql('("id" IS NOT FALSE)')
84
+ end
85
+
86
+ it 'returns an IS NOT FALSE expression for qualified attribute' do
87
+ expect((users[:id].qualified.not(false)).sql_literal(ds)).to eql('("users"."id" IS NOT FALSE)')
88
+ end
11
89
  end
90
+ end
12
91
 
13
- it 'returns a boolean expression for qualified attribute' do
14
- expect((users[:id].qualified.is(1)).sql_literal(ds)).to eql('("users"."id" = 1)')
92
+ describe '#!' do
93
+ it 'returns a new attribute with negated sql expr' do
94
+ expect((!users[:id].is(1)).sql_literal(ds)).to eql('("id" != 1)')
15
95
  end
16
96
  end
17
97
 
@@ -12,7 +12,9 @@ RSpec.describe 'MigrationTasks', :postgres, skip_tables: true do
12
12
  let(:migrator) { container.gateways[:default].migrator }
13
13
 
14
14
  before do
15
- allow(ROM::SQL::RakeSupport).to receive(:env) { conf }
15
+ ROM::SQL::Gateway.instance = nil
16
+ ROM::SQL::RakeSupport.env = nil
17
+ conf
16
18
  end
17
19
 
18
20
  context 'db:reset' do
@@ -46,6 +48,15 @@ RSpec.describe 'MigrationTasks', :postgres, skip_tables: true do
46
48
  }.to output("<= db:migrate executed\n").to_stdout
47
49
  end
48
50
  end
51
+
52
+ it 'raises an error on missing both env and Gateway.instance' do
53
+ ROM::SQL::RakeSupport.env = nil
54
+ ROM::SQL::Gateway.instance = nil
55
+
56
+ expect {
57
+ Rake::Task["db:migrate"].execute
58
+ }.to raise_error(ROM::SQL::RakeSupport::MissingEnv)
59
+ end
49
60
  end
50
61
 
51
62
  context 'db:clean' do
@@ -31,5 +31,13 @@ RSpec.describe ROM::SQL::OrderDSL, :postgres, helpers: true do
31
31
  expect(dsl.call { nullif(id.qualified, `''`).desc }.first.sql_literal(conn[:users])).
32
32
  to eql(%(NULLIF("users"."id", '') DESC))
33
33
  end
34
+
35
+ it 'allows to set nulls first/last' do
36
+ expect(dsl.call { id.desc(nulls: :first) }.first.sql_literal(conn[:users])).
37
+ to eql(%("id" DESC NULLS FIRST))
38
+
39
+ expect(dsl.call { id.desc(nulls: :last) }.first.sql_literal(conn[:users])).
40
+ to eql(%("id" DESC NULLS LAST))
41
+ end
34
42
  end
35
43
  end
@@ -0,0 +1,99 @@
1
+ require 'ostruct'
2
+ require 'rom/sql/commands'
3
+
4
+ RSpec.describe ROM::SQL::Plugin::Associates do
5
+ subject(:command) do
6
+ command_class.build(posts).with_association(:tags)
7
+ end
8
+
9
+ let(:posts) do
10
+ instance_double(Class.new(ROM::SQL::Relation), schema?: false, associations: associations)
11
+ end
12
+
13
+ let(:tags) do
14
+ instance_double(ROM::SQL::Relation, associations: associations)
15
+ end
16
+
17
+ let(:join_relation) do
18
+ instance_double(ROM::SQL::Relation)
19
+ end
20
+
21
+ let(:registry) do
22
+ Hash.new { |h, k| h.fetch(k.to_sym) }.update(posts: posts, tags: tags)
23
+ end
24
+
25
+ let(:command_class) do
26
+ Class.new(ROM::SQL::Commands::Create) do
27
+ use :associates, tags: []
28
+ end
29
+ end
30
+
31
+ let(:associations) do
32
+ Hash.new { |h, k| h.fetch(k.to_sym) }.update(posts: posts_assoc)
33
+ end
34
+
35
+ let(:tags_assoc) do
36
+ ROM::SQL::Association::ManyToMany.new(:posts, :tags, through: :posts_tags)
37
+ end
38
+
39
+ let(:posts_assoc) do
40
+ ROM::SQL::Association::ManyToMany.new(:tags, :posts, through: :posts_tags)
41
+ end
42
+
43
+ before do
44
+ allow(posts).to receive(:__registry__).and_return(registry)
45
+ allow(associations).to receive(:try).and_yield(tags_assoc)
46
+ allow(tags_assoc).to receive(:join_keys).and_return({})
47
+ end
48
+
49
+ shared_context 'associates result' do
50
+ it 'inserts join tuples and returns child tuples with combine keys' do
51
+ expect(tags_assoc).to receive(:persist).with(registry, post_tuples, tag_tuples)
52
+ expect(tags_assoc).to receive(:parent_combine_keys).with(registry).and_return(%i[name tag])
53
+
54
+ result = command.associate(post_tuples, tag_tuples, assoc: tags_assoc, keys: {})
55
+
56
+ expect(result).
57
+ to match_array([
58
+ { title: 'post 1', tag: 'red' }, { title: 'post 1', tag: 'green'},
59
+ { title: 'post 2', tag: 'red' }, { title: 'post 2', tag: 'green'}
60
+ ])
61
+ end
62
+ end
63
+
64
+ describe '#associate' do
65
+ context 'with plain hash tuples' do
66
+ let(:post_tuples) do
67
+ [{ title: 'post 1' }, { title: 'post 2' }]
68
+ end
69
+
70
+ let(:tag_tuples) do
71
+ [{ name: 'red' }, { name: 'green' }]
72
+ end
73
+
74
+ include_context 'associates result'
75
+ end
76
+
77
+ context 'with tuples coercible to a hash' do
78
+ before do
79
+ module Test
80
+ class Post < OpenStruct
81
+ def to_hash
82
+ { title: title }
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ let(:post_tuples) do
89
+ [Test::Post.new(title: 'post 1'), Test::Post.new(title: 'post 2')]
90
+ end
91
+
92
+ let(:tag_tuples) do
93
+ [{ name: 'red' }, { name: 'green' }]
94
+ end
95
+
96
+ include_context 'associates result'
97
+ end
98
+ end
99
+ end