rom-sql 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,6 +2,27 @@ module ROM
2
2
  module SQL
3
3
  class Relation < ROM::Relation
4
4
  module Writing
5
+ # Add upsert option (only PostgreSQL >= 9.5)
6
+ # Uses internal Sequel implementation
7
+ # Default - ON CONFLICT DO NOTHING
8
+ # more options: http://sequel.jeremyevans.net/rdoc-adapters/classes/Sequel/Postgres/DatasetMethods.html#method-i-insert_conflict
9
+ #
10
+ # @example
11
+ # users.upsert({ name: 'Jane', email: 'jane@foo.com' },
12
+ # { target: :email, update: { name: :excluded__name } }
13
+ #
14
+ # @api public
15
+ def upsert(*args, &block)
16
+ if args.size > 1 && args[-1].is_a?(Hash)
17
+ *values, opts = args
18
+ else
19
+ values = args
20
+ opts = EMPTY_HASH
21
+ end
22
+
23
+ dataset.insert_conflict(opts).insert(*values, &block)
24
+ end
25
+
5
26
  # Insert tuple into relation
6
27
  #
7
28
  # @example
@@ -0,0 +1,35 @@
1
+ require 'rom/schema'
2
+ require 'rom/support/constants'
3
+
4
+ module ROM
5
+ module SQL
6
+ class Schema < ROM::Schema
7
+ # @!attribute [r] primary_key_name
8
+ # @return [Symbol] The name of the primary key. This is set because in
9
+ # most of the cases relations don't have composite pks
10
+ attr_reader :primary_key_name
11
+
12
+ # @!attribute [r] primary_key_names
13
+ # @return [Array<Symbol>] A list of all pk names
14
+ attr_reader :primary_key_names
15
+
16
+ def initialize(*)
17
+ super
18
+ @primary_key_name = nil
19
+ @primary_key_names = EMPTY_ARRAY
20
+ end
21
+
22
+ # @api private
23
+ def finalize!(*)
24
+ super do
25
+ if primary_key.size > 0
26
+ @primary_key_name = primary_key[0].meta[:name]
27
+ @primary_key_names = primary_key.map { |type| type.meta[:name] }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ require 'rom/sql/schema/dsl'
@@ -0,0 +1,68 @@
1
+ require 'rom/sql/association'
2
+
3
+ module ROM
4
+ module SQL
5
+ class Schema < ROM::Schema
6
+ class AssociationsDSL < BasicObject
7
+ attr_reader :source, :registry
8
+
9
+ def initialize(source, &block)
10
+ @source = source
11
+ @registry = {}
12
+ instance_exec(&block)
13
+ end
14
+
15
+ def one_to_many(target, options = {})
16
+ if options[:through]
17
+ many_to_many(target, options)
18
+ else
19
+ add(Association::OneToMany.new(source, target, options))
20
+ end
21
+ end
22
+ alias_method :has_many, :one_to_many
23
+
24
+ def one_to_one(target, options = {})
25
+ if options[:through]
26
+ one_to_one_through(target, options)
27
+ else
28
+ add(Association::OneToOne.new(source, target, options))
29
+ end
30
+ end
31
+
32
+ def one_to_one_through(target, options = {})
33
+ add(Association::OneToOneThrough.new(source, target, options))
34
+ end
35
+
36
+ def many_to_many(target, options = {})
37
+ add(Association::ManyToMany.new(source, target, options))
38
+ end
39
+
40
+ def many_to_one(target, options = {})
41
+ add(Association::ManyToOne.new(source, target, options))
42
+ end
43
+
44
+ def belongs_to(name, options = {})
45
+ many_to_one(dataset_name(name), options.merge(as: options[:as] || name))
46
+ end
47
+
48
+ def has_one(name, options = {})
49
+ one_to_one(dataset_name(name), options.merge(as: options[:as] || name))
50
+ end
51
+
52
+ def call
53
+ AssociationSet.new(registry)
54
+ end
55
+
56
+ private
57
+
58
+ def add(association)
59
+ registry[association.name] = association
60
+ end
61
+
62
+ def dataset_name(name)
63
+ Inflector.pluralize(name).to_sym
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,27 @@
1
+ require 'rom/sql/schema/inferrer'
2
+ require 'rom/sql/schema/associations_dsl'
3
+
4
+ module ROM
5
+ module SQL
6
+ class Schema < ROM::Schema
7
+ class DSL < ROM::Schema::DSL
8
+ attr_reader :associations_dsl
9
+
10
+ def associations(&block)
11
+ @associations_dsl = AssociationsDSL.new(name, &block)
12
+ end
13
+
14
+ def call
15
+ SQL::Schema.new(name, attributes, opts)
16
+ end
17
+
18
+ def opts
19
+ opts = {}
20
+ opts[:associations] = associations_dsl.call if associations_dsl
21
+ opts[:inferrer] = inferrer.new(self) if inferrer
22
+ opts
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,80 @@
1
+ module ROM
2
+ module SQL
3
+ class Schema < ROM::Schema
4
+ class Inferrer
5
+ extend ClassMacros
6
+
7
+ defines :type_mapping, :pk_type
8
+
9
+ type_mapping(
10
+ integer: Types::Strict::Int,
11
+ string: Types::Strict::String,
12
+ date: Types::Strict::Date,
13
+ datetime: Types::Strict::Time,
14
+ boolean: Types::Strict::Bool,
15
+ decimal: Types::Strict::Decimal,
16
+ blob: Types::Strict::String
17
+ ).freeze
18
+
19
+ pk_type Types::Serial
20
+
21
+ attr_reader :dsl
22
+
23
+ def initialize(dsl)
24
+ @dsl = dsl
25
+ end
26
+
27
+ # @api private
28
+ def call(dataset, gateway)
29
+ columns = gateway.connection.schema(dataset)
30
+ fks = fks_for(gateway, dataset)
31
+
32
+ columns.each do |(name, definition)|
33
+ dsl.attribute name, build_type(definition.merge(foreign_key: fks[name]))
34
+ end
35
+
36
+ pks = columns
37
+ .map { |(name, definition)| name if definition.fetch(:primary_key) }
38
+ .compact
39
+
40
+ dsl.primary_key(*pks) if pks.any?
41
+
42
+ dsl.attributes
43
+ end
44
+
45
+ private
46
+
47
+ # @api private
48
+ def build_type(primary_key: , type: , allow_null: , foreign_key: , **rest)
49
+ if primary_key
50
+ self.class.pk_type
51
+ else
52
+ type = self.class.type_mapping.fetch(type)
53
+ type = type.optional if allow_null
54
+ type = type.meta(foreign_key: true, relation: foreign_key) if foreign_key
55
+ type
56
+ end
57
+ end
58
+
59
+ # @api private
60
+ def fks_for(gateway, dataset)
61
+ gateway.connection.foreign_key_list(dataset).each_with_object({}) do |definition, fks|
62
+ column, fk = build_fk(definition)
63
+
64
+ fks[column] = fk if fk
65
+ end
66
+ end
67
+
68
+ # @api private
69
+ def build_fk(columns: , table: , **rest)
70
+ if columns.size == 1
71
+ [columns[0], table]
72
+ else
73
+ # We don't have support for multicolumn foreign keys
74
+ columns[0]
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,26 +1,36 @@
1
1
  require 'sequel/database/logging'
2
2
  require 'active_support/notifications'
3
3
 
4
- module Sequel
5
- class Database
6
- def log_yield_with_instrumentation(sql, args = nil, &block)
7
- ActiveSupport::Notifications.instrument(
8
- 'sql.rom',
9
- sql: sql,
10
- name: instrumentation_name,
11
- binds: args
12
- ) do
13
- log_yield_without_instrumentation(sql, args, &block)
4
+ module ROM
5
+ module SQL
6
+ module ActiveSupportInstrumentation
7
+ if Sequel::MAJOR == 4 && Sequel::MINOR < 35
8
+ def log_yield(sql, args = nil)
9
+ ActiveSupport::Notifications.instrument(
10
+ 'sql.rom',
11
+ sql: sql,
12
+ name: instrumentation_name,
13
+ binds: args
14
+ ) { super }
15
+ end
16
+ else
17
+ def log_connection_yield(sql, _conn, args = nil)
18
+ ActiveSupport::Notifications.instrument(
19
+ 'sql.rom',
20
+ sql: sql,
21
+ name: instrumentation_name,
22
+ binds: args
23
+ ) { super }
24
+ end
14
25
  end
15
- end
16
-
17
- alias_method :log_yield_without_instrumentation, :log_yield
18
- alias_method :log_yield, :log_yield_with_instrumentation
19
26
 
20
- private
27
+ private
21
28
 
22
- def instrumentation_name
23
- "ROM[#{database_type}]"
29
+ def instrumentation_name
30
+ "ROM[#{database_type}]"
31
+ end
24
32
  end
25
33
  end
26
34
  end
35
+
36
+ Sequel::Database.send(:prepend, ROM::SQL::ActiveSupportInstrumentation)
@@ -0,0 +1,11 @@
1
+ require 'rom/types'
2
+
3
+ module ROM
4
+ module SQL
5
+ module Types
6
+ include ROM::Types
7
+
8
+ Serial = Strict::Int.constrained(gt: 0).meta(primary_key: true)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,26 @@
1
+ require 'dry-types'
2
+ require 'sequel'
3
+
4
+ module ROM
5
+ module SQL
6
+ module Types
7
+ module PG
8
+ Sequel.extension(:pg_json)
9
+
10
+ Array = Dry::Types::Definition
11
+ .new(Sequel::Postgres::JSONArray)
12
+ .constructor(Sequel.method(:pg_json))
13
+
14
+ Hash = Dry::Types::Definition
15
+ .new(Sequel::Postgres::JSONHash)
16
+ .constructor(Sequel.method(:pg_json))
17
+
18
+ JSON = Array | Hash
19
+
20
+ Bytea = Dry::Types::Definition
21
+ .new(Sequel::SQL::Blob)
22
+ .constructor(Sequel::SQL::Blob.method(:new))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '0.7.0'.freeze
3
+ VERSION = '0.8.0'.freeze
4
4
  end
5
5
  end
data/rom-sql.gemspec CHANGED
@@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency "sequel", "~> 4.18"
21
+ spec.add_runtime_dependency "sequel", "~> 4.25"
22
22
  spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
23
- spec.add_runtime_dependency "rom", "~> 1.0.0"
23
+ spec.add_runtime_dependency "dry-types", "~> 0.8"
24
+ spec.add_runtime_dependency "rom", "~> 2.0"
25
+ spec.add_runtime_dependency "rom-support", "~> 2.0"
24
26
 
25
27
  spec.add_development_dependency "bundler"
26
28
  spec.add_development_dependency "rake", "~> 10.0"
@@ -0,0 +1,137 @@
1
+ RSpec.describe ROM::SQL::Association::ManyToMany do
2
+ include_context 'users and tasks'
3
+
4
+ with_adapters :sqlite do
5
+ context 'with two associations pointing to the same target relation' do
6
+ let(:container) do
7
+ ROM.container(:sql, uri) do |conf|
8
+ conf.default.create_table(:users_tasks) do
9
+ foreign_key :user_id, :users
10
+ foreign_key :task_id, :tasks
11
+ primary_key [:user_id, :task_id]
12
+ end
13
+
14
+ conf.relation(:users) do
15
+ schema(infer: true) do
16
+ associations do
17
+ has_many :users_tasks
18
+ has_many :tasks, through: :users_tasks
19
+ has_many :tasks, as: :priv_tasks
20
+ end
21
+ end
22
+ end
23
+
24
+ conf.relation(:users_tasks) do
25
+ schema(infer: true) do
26
+ associations do
27
+ belongs_to :user
28
+ belongs_to :task
29
+ end
30
+ end
31
+ end
32
+
33
+ conf.relation(:tasks) do
34
+ schema(infer: true) do
35
+ associations do
36
+ has_many :users_tasks
37
+ has_many :users, through: :users_tasks
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ it 'does not conflict with two FKs' do
45
+ users = container.relations[:users]
46
+ tasks = container.relations[:tasks]
47
+ assoc = users.associations[:tasks]
48
+
49
+ relation = tasks.for_combine(assoc).call(users.call)
50
+
51
+ expect(relation.to_a).to be_empty
52
+ end
53
+
54
+ it 'preloads using FK' do
55
+ users = container.relations[:users]
56
+ tasks = container.relations[:tasks]
57
+ assoc = users.associations[:priv_tasks]
58
+
59
+ relation = tasks.for_combine(assoc).call(users.where(id: 2).call)
60
+
61
+ expect(relation.to_a).to eql([id: 1, user_id: 2, title: "Joe's task"])
62
+ end
63
+ end
64
+ end
65
+
66
+ with_adapters do
67
+ subject(:assoc) {
68
+ ROM::SQL::Association::ManyToMany.new(:tasks, :tags, through: :task_tags)
69
+ }
70
+
71
+ let(:tasks) { container.relations[:tasks] }
72
+ let(:tags) { container.relations[:tags] }
73
+
74
+ before do
75
+ conf.relation(:task_tags) do
76
+ schema do
77
+ attribute :task_id, ROM::SQL::Types::ForeignKey(:tasks)
78
+ attribute :tag_id, ROM::SQL::Types::ForeignKey(:tags)
79
+
80
+ primary_key :task_id, :tag_id
81
+
82
+ associations do
83
+ many_to_one :tasks
84
+ many_to_one :tags
85
+ end
86
+ end
87
+ end
88
+
89
+ conf.relation(:tasks) do
90
+ schema do
91
+ attribute :id, ROM::SQL::Types::Serial
92
+ attribute :user_id, ROM::SQL::Types::ForeignKey(:users)
93
+ attribute :title, ROM::SQL::Types::String
94
+
95
+ associations do
96
+ one_to_many :task_tags
97
+ one_to_many :tags, through: :task_tags
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ describe '#result' do
104
+ specify { expect(ROM::SQL::Association::ManyToMany.result).to be(:many) }
105
+ end
106
+
107
+ describe '#call' do
108
+ it 'prepares joined relations' do
109
+ relation = assoc.call(container.relations)
110
+
111
+ expect(relation.attributes).to eql(%i[id name task_id])
112
+ expect(relation.to_a).to eql([id: 1, name: 'important', task_id: 1])
113
+ end
114
+ end
115
+
116
+ describe ':through another assoc' do
117
+ subject(:assoc) do
118
+ ROM::SQL::Association::ManyToMany.new(:users, :tags, through: :tasks)
119
+ end
120
+
121
+ it 'prepares joined relations through other association' do
122
+ relation = assoc.call(container.relations)
123
+
124
+ expect(relation.attributes).to eql(%i[id name user_id])
125
+ expect(relation.to_a).to eql([id: 1, name: 'important', user_id: 2])
126
+ end
127
+ end
128
+
129
+ describe ROM::Plugins::Relation::SQL::AutoCombine, '#for_combine' do
130
+ it 'preloads relation based on association' do
131
+ relation = tags.for_combine(assoc).call(tasks.call)
132
+
133
+ expect(relation.to_a).to eql([id: 1, name: 'important', task_id: 1])
134
+ end
135
+ end
136
+ end
137
+ end