rom-sql 0.7.0 → 0.8.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 (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