rom-sql 0.8.0 → 0.9.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 (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