rom-sql 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +16 -12
  3. data/CHANGELOG.md +23 -0
  4. data/Gemfile +11 -3
  5. data/README.md +1 -7
  6. data/lib/rom/sql.rb +4 -7
  7. data/lib/rom/sql/association.rb +1 -1
  8. data/lib/rom/sql/association/one_to_many.rb +44 -1
  9. data/lib/rom/sql/association/one_to_one.rb +1 -38
  10. data/lib/rom/sql/commands.rb +0 -3
  11. data/lib/rom/sql/commands/error_wrapper.rb +1 -1
  12. data/lib/rom/sql/errors.rb +4 -1
  13. data/lib/rom/sql/extensions.rb +19 -0
  14. data/lib/rom/sql/{support → extensions}/active_support_notifications.rb +0 -0
  15. data/lib/rom/sql/extensions/postgres.rb +3 -0
  16. data/lib/rom/sql/{commands/postgres.rb → extensions/postgres/commands.rb} +38 -0
  17. data/lib/rom/sql/extensions/postgres/inferrer.rb +64 -0
  18. data/lib/rom/sql/extensions/postgres/types.rb +65 -0
  19. data/lib/rom/sql/{support → extensions}/rails_log_subscriber.rb +0 -0
  20. data/lib/rom/sql/gateway.rb +15 -4
  21. data/lib/rom/sql/relation.rb +6 -2
  22. data/lib/rom/sql/relation/reading.rb +18 -0
  23. data/lib/rom/sql/schema/dsl.rb +7 -4
  24. data/lib/rom/sql/schema/inferrer.rb +44 -31
  25. data/lib/rom/sql/types.rb +5 -1
  26. data/lib/rom/sql/version.rb +1 -1
  27. data/rom-sql.gemspec +14 -13
  28. data/spec/extensions/postgres/inferrer_spec.rb +40 -0
  29. data/spec/extensions/postgres/integration_spec.rb +38 -0
  30. data/spec/extensions/postgres/types_spec.rb +115 -0
  31. data/spec/integration/association/many_to_many_spec.rb +2 -1
  32. data/spec/integration/association/one_to_one_spec.rb +6 -4
  33. data/spec/integration/combine_spec.rb +1 -1
  34. data/spec/integration/commands/create_spec.rb +46 -21
  35. data/spec/integration/commands/delete_spec.rb +13 -38
  36. data/spec/integration/commands/update_spec.rb +19 -41
  37. data/spec/integration/commands/upsert_spec.rb +1 -1
  38. data/spec/integration/gateway_spec.rb +5 -9
  39. data/spec/integration/migration_spec.rb +6 -7
  40. data/spec/integration/read_spec.rb +30 -38
  41. data/spec/integration/schema_inference_spec.rb +211 -49
  42. data/spec/integration/setup_spec.rb +5 -5
  43. data/spec/integration/support/active_support_notifications_spec.rb +4 -3
  44. data/spec/integration/support/rails_log_subscriber_spec.rb +5 -4
  45. data/spec/shared/database_setup.rb +21 -6
  46. data/spec/spec_helper.rb +44 -35
  47. data/spec/unit/association/one_to_many_spec.rb +20 -0
  48. data/spec/unit/association/one_to_one_spec.rb +23 -2
  49. data/spec/unit/association_errors_spec.rb +1 -1
  50. data/spec/unit/gateway_spec.rb +9 -8
  51. data/spec/unit/logger_spec.rb +1 -1
  52. data/spec/unit/migration_tasks_spec.rb +3 -3
  53. data/spec/unit/migrator_spec.rb +3 -2
  54. data/spec/unit/plugin/assoc_macros/combined_associations_spec.rb +1 -1
  55. data/spec/unit/plugin/assoc_macros/many_to_many_spec.rb +1 -1
  56. data/spec/unit/plugin/assoc_macros/many_to_one_spec.rb +1 -1
  57. data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +1 -1
  58. data/spec/unit/relation/associations_spec.rb +27 -0
  59. data/spec/unit/relation/avg_spec.rb +11 -0
  60. data/spec/unit/relation/by_pk_spec.rb +15 -0
  61. data/spec/unit/relation/dataset_spec.rb +48 -0
  62. data/spec/unit/relation/distinct_spec.rb +14 -0
  63. data/spec/unit/relation/exclude_spec.rb +13 -0
  64. data/spec/unit/relation/fetch_spec.rb +21 -0
  65. data/spec/unit/relation/having_spec.rb +20 -0
  66. data/spec/unit/relation/inner_join_spec.rb +22 -0
  67. data/spec/unit/relation/inspect_spec.rb +11 -0
  68. data/spec/unit/relation/invert_spec.rb +12 -0
  69. data/spec/unit/relation/left_join_spec.rb +16 -0
  70. data/spec/unit/relation/map_spec.rb +16 -0
  71. data/spec/unit/relation/max_spec.rb +11 -0
  72. data/spec/unit/relation/min_spec.rb +11 -0
  73. data/spec/unit/relation/pluck_spec.rb +11 -0
  74. data/spec/unit/relation/prefix_spec.rb +27 -0
  75. data/spec/unit/relation/primary_key_spec.rb +27 -0
  76. data/spec/unit/relation/project_spec.rb +22 -0
  77. data/spec/unit/relation/qualified_columns_spec.rb +27 -0
  78. data/spec/unit/relation/rename_spec.rb +21 -0
  79. data/spec/unit/relation/sum_spec.rb +11 -0
  80. data/spec/unit/relation/union_spec.rb +19 -0
  81. data/spec/unit/relation/unique_predicate_spec.rb +18 -0
  82. data/spec/unit/schema_spec.rb +1 -1
  83. data/spec/unit/types_spec.rb +4 -21
  84. metadata +79 -11
  85. data/lib/rom/sql/commands_ext/postgres.rb +0 -45
  86. data/lib/rom/sql/types/pg.rb +0 -26
  87. data/spec/unit/relation_spec.rb +0 -272
@@ -8,6 +8,26 @@ RSpec.describe ROM::SQL::Association::OneToMany, helpers: true do
8
8
  let(:users) { double(:users, primary_key: :id) }
9
9
  let(:tasks) { double(:tasks) }
10
10
 
11
+ describe '#associate' do
12
+ let(:source) { :users }
13
+ let(:target) { :tasks }
14
+
15
+ let(:relations) do
16
+ { users: users, tasks: tasks }
17
+ end
18
+
19
+ it 'returns child tuple with FK set' do
20
+ expect(tasks).to receive(:foreign_key).with(:users).and_return(:user_id)
21
+
22
+ task_tuple = { title: 'Task' }
23
+ user_tuple = { id: 3 }
24
+
25
+ expect(assoc.associate(relations, task_tuple, user_tuple)).to eql(
26
+ user_id: 3, title: 'Task'
27
+ )
28
+ end
29
+ end
30
+
11
31
  shared_examples_for 'one-to-many association' do
12
32
  describe '#combine_keys' do
13
33
  it 'returns a hash with combine keys' do
@@ -5,8 +5,29 @@ RSpec.describe ROM::SQL::Association::OneToOne, helpers: true do
5
5
 
6
6
  let(:options) { {} }
7
7
 
8
- let(:users) { double(:users, primary_key: :id) }
9
- let(:tasks) { double(:tasks) }
8
+ let(:users) { double(:users, primary_key: :id) }
9
+ let(:tasks) { double(:tasks) }
10
+ let(:avatars) { double(:avatars) }
11
+
12
+ describe '#associate' do
13
+ let(:source) { :users }
14
+ let(:target) { :avatar }
15
+
16
+ let(:relations) do
17
+ { users: users, avatar: avatars }
18
+ end
19
+
20
+ it 'returns child tuple with FK set' do
21
+ expect(avatars).to receive(:foreign_key).with(:users).and_return(:user_id)
22
+
23
+ avatar_tuple = { url: 'http://rom-rb.org/images/logo.svg' }
24
+ user_tuple = { id: 3 }
25
+
26
+ expect(assoc.associate(relations, avatar_tuple, user_tuple)).to eql(
27
+ user_id: 3, url: 'http://rom-rb.org/images/logo.svg'
28
+ )
29
+ end
30
+ end
10
31
 
11
32
  shared_examples_for 'one-to-one association' do
12
33
  describe '#combine_keys' do
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Association errors' do
1
+ RSpec.describe 'Association errors', :postgres do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  describe 'accessing an undefined association' do
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  require 'rom/lint/spec'
4
4
 
5
- describe ROM::SQL::Gateway do
5
+ RSpec.describe ROM::SQL::Gateway, :postgres do
6
6
  include_context 'users and tasks'
7
7
 
8
8
  let(:gateway) { container.gateways[:default] }
@@ -10,13 +10,14 @@ describe ROM::SQL::Gateway do
10
10
  it_behaves_like 'a rom gateway' do
11
11
  let(:identifier) { :sql }
12
12
  let(:gateway) { ROM::SQL::Gateway }
13
- let(:uri) { POSTGRES_DB_URI }
14
13
  end
15
14
 
16
- describe 'sqlite with a file db' do
15
+ describe 'sqlite with a file db', :sqlite, postgres: false do
16
+ before do
17
+ Tempfile.new('test.sqlite')
18
+ end
19
+
17
20
  it 'establishes an sqlite connection' do
18
- db_file = Tempfile.new('test.sqlite')
19
- uri = "#{defined?(JRUBY_VERSION) ? 'jdbc:sqlite' : 'sqlite'}://#{db_file.path}"
20
21
  gateway = ROM::SQL::Gateway.new(uri)
21
22
  expect(gateway).to be_instance_of(ROM::SQL::Gateway)
22
23
  end
@@ -37,10 +38,10 @@ describe ROM::SQL::Gateway do
37
38
  migrator = double('migrator')
38
39
 
39
40
  expect(Sequel).to receive(:connect)
40
- .with(POSTGRES_DB_URI, host: '127.0.0.1')
41
+ .with(uri, host: '127.0.0.1')
41
42
  .and_return(conn)
42
43
 
43
- gateway = ROM::SQL::Gateway.new(POSTGRES_DB_URI, migrator: migrator, host: '127.0.0.1')
44
+ gateway = ROM::SQL::Gateway.new(uri, migrator: migrator, host: '127.0.0.1')
44
45
 
45
46
  expect(gateway.options).to eql(migrator: migrator)
46
47
  end
@@ -49,7 +50,7 @@ describe ROM::SQL::Gateway do
49
50
  extensions = [:pg_array, :pg_enum]
50
51
  connection = Sequel.connect uri
51
52
 
52
- expect(connection).to receive(:extension).with(:pg_array, :pg_enum)
53
+ expect(connection).to receive(:extension).with(:pg_array, :pg_json, :pg_enum)
53
54
 
54
55
  ROM::SQL::Gateway.new(connection, extensions: extensions)
55
56
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe 'Logger' do
3
+ RSpec.describe 'Logger', :postgres do
4
4
  include_context 'database setup'
5
5
 
6
6
  it 'sets up a logger for sequel' do
@@ -6,9 +6,9 @@ namespace :db do
6
6
  end
7
7
  end
8
8
 
9
- describe 'MigrationTasks' do
10
- let(:conf) { ROM::Configuration.new(:sql, POSTGRES_DB_URI) }
11
- let!(:container) { ROM.container(conf) }
9
+ RSpec.describe 'MigrationTasks', :postgres, skip_tables: true do
10
+ include_context 'database setup'
11
+
12
12
  let(:migrator) { container.gateways[:default].migrator }
13
13
 
14
14
  before do
@@ -1,7 +1,8 @@
1
- RSpec.describe ROM::SQL::Migration::Migrator do
1
+ RSpec.describe ROM::SQL::Migration::Migrator, :postgres, skip_tables: true do
2
+ include_context 'database setup'
3
+
2
4
  subject(:migrator) { ROM::SQL::Migration::Migrator.new(conn, options) }
3
5
 
4
- let(:conn) { Sequel.connect(POSTGRES_DB_URI) }
5
6
  let(:options) { { path: TMP_PATH.join('test/migrations') } }
6
7
 
7
8
  describe '#create_file' do
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Defining multiple associations' do
1
+ RSpec.describe 'Defining multiple associations', :postgres do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  it 'extends relation with association methods' do
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Defining many-to-one association' do
1
+ RSpec.describe 'Defining many-to-one association', :postgres do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  it 'extends relation with association methods' do
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Defining many-to-one association' do
1
+ RSpec.describe 'Defining many-to-one association', :postgres do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  before do
@@ -1,4 +1,4 @@
1
- RSpec.describe 'Defining one-to-many association' do
1
+ RSpec.describe 'Defining one-to-many association', :postgres do
2
2
  include_context 'users and tasks'
3
3
 
4
4
  before do
@@ -0,0 +1,27 @@
1
+ RSpec.describe ROM::Relation, '#associations' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ context 'with schema' do
8
+ it 'returns configured primary key from the schema' do
9
+ conf.relation(:users) do
10
+ schema(infer: true) do
11
+ associations do
12
+ has_many :tasks
13
+ end
14
+ end
15
+ end
16
+
17
+ expect(relation.associations[:tasks]).to be(container.relations.users.schema.associations[:tasks])
18
+ end
19
+ end
20
+
21
+ context 'without schema' do
22
+ it 'returns an empty association set' do
23
+ expect(relation.associations.elements).to be_empty
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ RSpec.describe ROM::Relation, '#avg' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'delegates to dataset and return value' do
8
+ expect(relation.avg(:id)).to eql(1.5)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe ROM::Relation, '#by_pk' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'restricts a relation by its PK' do
8
+ expect(relation.by_pk(1).to_a).to eql([id: 1, name: 'Jane'])
9
+ end
10
+
11
+ it 'is available as a view' do
12
+ expect(relation.by_pk).to be_curried
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ RSpec.describe ROM::Relation, '#dataset' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ let(:dataset) { container.gateways[:default].dataset(:users) }
7
+
8
+ with_adapters do
9
+ context 'with schema' do
10
+ before do
11
+ conf.relation(:users) do
12
+ schema do
13
+ attribute :id, ROM::SQL::Types::Serial
14
+ attribute :name, ROM::SQL::Types::String
15
+ end
16
+ end
17
+ end
18
+
19
+ it 'uses schema to infer default dataset' do
20
+ expect(relation.dataset).to eql(dataset.select(:id, :name).order(:users__id))
21
+ end
22
+ end
23
+
24
+ context 'with cherry-picked attributes in schema' do
25
+ before do
26
+ conf.relation(:users) do
27
+ schema do
28
+ attribute :id, ROM::SQL::Types::Serial
29
+ end
30
+ end
31
+ end
32
+
33
+ it 'uses schema to infer default dataset' do
34
+ expect(relation.dataset).to eql(dataset.select(:id).order(:users__id))
35
+ end
36
+ end
37
+
38
+ context 'without schema' do
39
+ before do
40
+ conf.relation(:users)
41
+ end
42
+
43
+ it 'selects all qualified columns and sorts by pk' do
44
+ expect(relation.dataset).to eql(dataset.select(*relation.columns).order(:users__id))
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ RSpec.describe ROM::Relation, '#distinct' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ if !metadata[:sqlite]
8
+ it 'delegates to dataset and returns a new relation' do
9
+ expect(relation.dataset).to receive(:distinct).with(:name).and_call_original
10
+ expect(relation.distinct(:name)).to_not eql(relation)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.describe ROM::Relation, '#exclude' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'delegates to dataset and returns a new relation' do
8
+ expect(relation.dataset)
9
+ .to receive(:exclude).with(name: 'Jane').and_call_original
10
+ expect(relation.exclude(name: 'Jane')).to_not eq(relation)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ RSpec.describe ROM::Relation, '#fetch' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ describe '#fetch' do
8
+ it 'returns a single tuple identified by the pk' do
9
+ expect(relation.fetch(1)).to eql(id: 1, name: 'Jane')
10
+ end
11
+
12
+ it 'raises when tuple was not found' do
13
+ expect { relation.fetch(535315412) }.to raise_error(ROM::TupleCountMismatchError)
14
+ end
15
+
16
+ it 'raises when more tuples were returned' do
17
+ expect { relation.fetch([1, 2]) }.to raise_error(ROM::TupleCountMismatchError)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ RSpec.describe ROM::Relation, '#having' do
2
+ subject(:relation) do
3
+ container.relations.users
4
+ .inner_join(:tasks, user_id: :id)
5
+ .select_group(:users__id, :users__name)
6
+ .select_append { count(:tasks).as(:task_count) }
7
+ end
8
+
9
+ include_context 'users and tasks'
10
+
11
+ with_adapters :postgres do
12
+ before do
13
+ conn[:tasks].insert(id: 3, user_id: 2, title: "Joe's another task")
14
+ end
15
+
16
+ it 'restricts a relation using HAVING clause' do
17
+ expect(relation.having { count(:tasks__id) >= 2 }.to_a).to eq([{ id: 2, name: 'Joe', task_count: 2 }])
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ RSpec.describe ROM::Relation, '#inner_join' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'joins relations using inner join' do
8
+ result = relation.inner_join(:tasks, user_id: :id).select(:name, :title)
9
+
10
+ expect(result.to_a).to eql([
11
+ { name: 'Jane', title: "Jane's task" },
12
+ { name: 'Joe', title: "Joe's task" }
13
+ ])
14
+ end
15
+
16
+ it 'raises error when column names are ambiguous' do
17
+ expect {
18
+ relation.inner_join(:tasks, user_id: :id).to_a
19
+ }.to raise_error(Sequel::DatabaseError, /ambiguous/)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ RSpec.describe ROM::Relation, '#inspect' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'includes dataset' do
8
+ expect(relation.inspect).to include('dataset')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ RSpec.describe ROM::Relation, '#invert' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'delegates to dataset and returns a new relation' do
8
+ expect(relation.dataset).to receive(:invert).and_call_original
9
+ expect(relation.invert).to_not eq(relation)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe ROM::Relation, '#left_join' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'joins relations using left outer join' do
8
+ result = relation.left_join(:tasks, user_id: :id).select(:name, :title)
9
+
10
+ expect(result.to_a).to match_array([
11
+ { name: 'Joe', title: "Joe's task" },
12
+ { name: 'Jane', title: "Jane's task" }
13
+ ])
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe ROM::Relation, '#map' do
2
+ subject(:relation) { container.relations.users }
3
+
4
+ include_context 'users and tasks'
5
+
6
+ with_adapters do
7
+ it 'yields tuples' do
8
+ result = relation.map { |tuple| tuple[:name] }
9
+ expect(result).to eql(%w(Jane Joe))
10
+ end
11
+
12
+ it 'plucks value' do
13
+ expect(relation.map(:name)).to eql(%w(Jane Joe))
14
+ end
15
+ end
16
+ end