rom-sql 1.2.2 → 1.3.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.
@@ -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