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
@@ -0,0 +1,9 @@
1
+ module ROM
2
+ module SQL
3
+ class Association
4
+ class OneToMany < OneToOne
5
+ result :many
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ module ROM
2
+ module SQL
3
+ class Association
4
+ class OneToOne < Association
5
+ result :one
6
+
7
+ # @api public
8
+ def call(relations)
9
+ with_keys(relations) do |left_pk, right_fk|
10
+ right = relations[target.relation]
11
+ columns = right.header.qualified.to_a
12
+
13
+ relation = right
14
+ .inner_join(source, left_pk => right_fk)
15
+ .select(*columns)
16
+ .order(*right.header.project(*right.primary_key).qualified)
17
+
18
+ relation.with(attributes: relation.header.names)
19
+ end
20
+ end
21
+
22
+ # @api public
23
+ def combine_keys(relations)
24
+ Hash[*with_keys(relations)]
25
+ end
26
+
27
+ # @api public
28
+ def join_keys(relations)
29
+ with_keys(relations) { |source_key, target_key|
30
+ { qualify(source, source_key) => qualify(target, target_key) }
31
+ }
32
+ end
33
+
34
+ protected
35
+
36
+ # @api private
37
+ def with_keys(relations, &block)
38
+ source_key = relations[source.relation].primary_key
39
+ target_key = relations[target.relation].foreign_key(source.relation)
40
+ return [source_key, target_key] unless block
41
+ yield(source_key, target_key)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ module ROM
2
+ module SQL
3
+ class Association
4
+ class OneToOneThrough < ManyToMany
5
+ result :one
6
+ end
7
+ end
8
+ end
9
+ end
@@ -3,4 +3,6 @@ require 'rom/commands'
3
3
  require 'rom/sql/commands/create'
4
4
  require 'rom/sql/commands/update'
5
5
  require 'rom/sql/commands/delete'
6
+
7
+ require 'rom/sql/commands/postgres'
6
8
  require 'rom/sql/commands_ext/postgres'
@@ -1,4 +1,3 @@
1
- require 'rom/sql/commands'
2
1
  require 'rom/sql/commands/error_wrapper'
3
2
  require 'rom/sql/commands/transaction'
4
3
 
@@ -15,6 +14,7 @@ module ROM
15
14
  include ErrorWrapper
16
15
 
17
16
  use :associates
17
+ use :schema
18
18
 
19
19
  # Inserts provided tuples into the database table
20
20
  #
@@ -40,7 +40,7 @@ module ROM
40
40
  # @api private
41
41
  def insert(tuples)
42
42
  pks = tuples.map { |tuple| relation.insert(tuple) }
43
- relation.where(relation.primary_key => pks)
43
+ relation.where(relation.primary_key => pks).to_a
44
44
  end
45
45
 
46
46
  # Executes multi_insert statement and returns inserted tuples
@@ -1,4 +1,3 @@
1
- require 'rom/sql/commands'
2
1
  require 'rom/sql/commands/error_wrapper'
3
2
  require 'rom/sql/commands/transaction'
4
3
 
@@ -0,0 +1,76 @@
1
+ module ROM
2
+ module SQL
3
+ module Commands
4
+ module Postgres
5
+ # Upsert command
6
+ #
7
+ # Uses a feature of PostgreSQL 9.5 commonly called an "upsert".
8
+ # The command been called attempts to perform an insert and
9
+ # can make an update (or silently do nothing) in case of
10
+ # the insertion was unsuccessful due to a violation of a unique
11
+ # constraint.
12
+ # Very important implementation detail is that the whole operation
13
+ # is atomic, i.e. aware of concurrent transactions, and doesn't raise
14
+ # exceptions if used properly.
15
+ #
16
+ # See PG's docs in INSERT statement for details
17
+ # https://www.postgresql.org/docs/current/static/sql-insert.html
18
+ #
19
+ # Normally, the command should configured via class level settings.
20
+ # By default, that is without any settings provided, the command
21
+ # uses ON CONFLICT DO NOTHING clause.
22
+ #
23
+ # This implementation uses Sequel's API underneath, the docs are available at
24
+ # http://sequel.jeremyevans.net/rdoc-adapters/classes/Sequel/Postgres/DatasetMethods.html#method-i-insert_conflict
25
+ #
26
+ # @api public
27
+ class Upsert < SQL::Commands::Create
28
+ adapter :sql
29
+
30
+ defines :constraint, :conflict_target, :update_statement, :update_where
31
+
32
+ # @!attribute [r] constraint
33
+ # @return [Symbol] the name of the constraint expected to be violated
34
+ option :constraint, reader: true, default: -> c { c.class.constraint }
35
+
36
+ # @!attribute [r] conflict_target
37
+ # @return [Object] the column or expression to handle a violation on
38
+ option :conflict_target, reader: true, default: -> c { c.class.conflict_target }
39
+
40
+ # @!attribute [r] update_statement
41
+ # @return [Object] the update statement which will be executed in case of a violation
42
+ option :update_statement, reader: true, default: -> c { c.class.update_statement }
43
+
44
+ # @!attribute [r] update_where
45
+ # @return [Object] the WHERE clause to be added to the update
46
+ option :update_where, reader: true, default: -> c { c.class.update_where }
47
+
48
+ # Tries to insert provided tuples and do an update (or nothing)
49
+ # when the inserted record violates a unique constraint and hence
50
+ # cannot be appended to the table
51
+ #
52
+ # @return [Array<Hash>]
53
+ #
54
+ # @api public
55
+ def execute(tuples)
56
+ inserted_tuples = with_input_tuples(tuples) do |tuple|
57
+ upsert(input[tuple], upsert_options)
58
+ end
59
+
60
+ inserted_tuples.flatten
61
+ end
62
+
63
+ # @api private
64
+ def upsert_options
65
+ @upsert_options ||= {
66
+ constraint: constraint,
67
+ target: conflict_target,
68
+ update_where: update_where,
69
+ update: update_statement
70
+ }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -1,6 +1,6 @@
1
1
  require 'rom/support/deprecations'
2
+ require 'rom/support/constants'
2
3
 
3
- require 'rom/sql/commands'
4
4
  require 'rom/sql/commands/error_wrapper'
5
5
  require 'rom/sql/commands/transaction'
6
6
 
@@ -20,6 +20,8 @@ module ROM
20
20
 
21
21
  option :original, reader: true
22
22
 
23
+ use :schema
24
+
23
25
  deprecate :set, :call
24
26
  deprecate :to, :call
25
27
 
@@ -34,10 +36,10 @@ module ROM
34
36
 
35
37
  changed = diff(attributes.to_h)
36
38
 
37
- if changed.any?
39
+ if changed.size > 0
38
40
  update(changed)
39
41
  else
40
- []
42
+ EMPTY_ARRAY
41
43
  end
42
44
  end
43
45
 
@@ -55,6 +57,7 @@ module ROM
55
57
  #
56
58
  # @api public
57
59
  def change(original)
60
+ Deprecations.warn("#{self.class}#change is deprecated. Use repositories with changesets instead")
58
61
  self.class.build(relation, options.merge(original: original.to_h))
59
62
  end
60
63
 
@@ -6,18 +6,35 @@ module ROM
6
6
  module Commands
7
7
  module Postgres
8
8
  module Create
9
+ # Executes insert statement and returns inserted tuples
10
+ #
11
+ # @api private
9
12
  def insert(tuples)
10
13
  tuples.map do |tuple|
11
14
  relation.dataset.returning(*relation.columns).insert(tuple)
12
15
  end.flatten
13
16
  end
14
17
 
18
+ # Executes multi_insert statement and returns inserted tuples
19
+ #
20
+ # @api private
15
21
  def multi_insert(tuples)
16
22
  relation.dataset.returning(*relation.columns).multi_insert(tuples)
17
23
  end
24
+
25
+ # Executes upsert statement (INSERT with ON CONFLICT clause)
26
+ # and returns inserted/updated tuples
27
+ #
28
+ # @api private
29
+ def upsert(tuple, opts = EMPTY_HASH)
30
+ relation.dataset.returning(*relation.columns).insert_conflict(opts).insert(tuple)
31
+ end
18
32
  end
19
33
 
20
34
  module Update
35
+ # Executes update statement and returns updated tuples
36
+ #
37
+ # @api private
21
38
  def update(tuple)
22
39
  relation.dataset.returning(*relation.columns).update(tuple)
23
40
  end
@@ -30,20 +30,6 @@ module ROM
30
30
  # @api public
31
31
  attr_reader :logger
32
32
 
33
- # Returns a list of datasets inferred from table names
34
- #
35
- # @return [Array] array with table names
36
- #
37
- # @api public
38
- attr_reader :schema
39
-
40
- # @param [String,Symbol] scheme
41
- #
42
- # @api public
43
- def self.database_file?(scheme)
44
- scheme.to_s.include?('sqlite')
45
- end
46
-
47
33
  # SQL gateway interface
48
34
  #
49
35
  # @overload connect(uri, options)
@@ -70,7 +56,6 @@ module ROM
70
56
  conn_options = options.reject { |k, _| repo_options.include?(k) }
71
57
 
72
58
  @connection = connect(uri, conn_options)
73
- @schema = connection.tables
74
59
  add_extensions(Array(options[:extensions])) if options[:extensions]
75
60
 
76
61
  super(uri, options.reject { |k, _| conn_options.keys.include?(k) })
@@ -153,6 +138,29 @@ module ROM
153
138
  klass
154
139
  end
155
140
 
141
+ # Create a table using the configured connection
142
+ #
143
+ # @api public
144
+ def create_table(*args, &block)
145
+ connection.create_table(*args, &block)
146
+ end
147
+
148
+ # Drops a table
149
+ #
150
+ # @api public
151
+ def drop_table(*args, &block)
152
+ connection.drop_table(*args, &block)
153
+ end
154
+
155
+ # Returns a list of datasets inferred from table names
156
+ #
157
+ # @return [Array] array with table names
158
+ #
159
+ # @api public
160
+ def schema
161
+ @schema ||= connection.tables
162
+ end
163
+
156
164
  private
157
165
 
158
166
  # Connect to database or reuse established connection instance
@@ -4,6 +4,8 @@ module ROM
4
4
  class Header
5
5
  include Dry::Equalizer(:columns, :table)
6
6
 
7
+ SEP_REGEX = /_{2,3}/.freeze
8
+
7
9
  attr_reader :columns, :table
8
10
 
9
11
  def initialize(columns, table)
@@ -24,7 +26,11 @@ module ROM
24
26
  end
25
27
 
26
28
  def names
27
- columns.map { |col| :"#{col.to_s.split('___').last}" }
29
+ columns.map { |col| :"#{col.to_s.split(SEP_REGEX).last}" }
30
+ end
31
+
32
+ def exclude(*names)
33
+ self.class.new(columns.find_all { |col| !names.include?(col) }, table)
28
34
  end
29
35
 
30
36
  def project(*names)
@@ -66,7 +66,7 @@ module ROM
66
66
  if assoc.nil?
67
67
  raise NoAssociationError,
68
68
  "Association #{assoc_name.inspect} has not been " \
69
- "defined for relation #{name.inspect}"
69
+ "defined for relation #{name.relation.inspect}"
70
70
  end
71
71
 
72
72
  type = assoc[:type]
@@ -103,7 +103,7 @@ module ROM
103
103
  l_graph = graph(
104
104
  assoc[:join_table],
105
105
  { assoc[:left_key] => primary_key },
106
- select: l_select, implicit_qualifier: self.name
106
+ select: l_select, implicit_qualifier: self.name.dataset
107
107
  )
108
108
 
109
109
  l_graph.graph(
@@ -124,7 +124,7 @@ module ROM
124
124
 
125
125
  graph(
126
126
  name, join_keys,
127
- options.merge(join_type: join_type, implicit_qualifier: self.name)
127
+ options.merge(join_type: join_type, implicit_qualifier: self.name.dataset)
128
128
  )
129
129
  end
130
130
  end
@@ -1,3 +1,5 @@
1
+ require 'rom/support/deprecations'
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Plugin
@@ -12,6 +14,21 @@ module ROM
12
14
  end
13
15
 
14
16
  module InstanceMethods
17
+ attr_reader :assoc, :__registry__
18
+
19
+ # @api private
20
+ def initialize(*)
21
+ super
22
+ @__registry__ = relation.__registry__
23
+ assoc_name, assoc_opts = self.class.associations[0]
24
+ @assoc =
25
+ if assoc_opts.any?
26
+ assoc_opts[:key]
27
+ else
28
+ relation.associations[assoc_name]
29
+ end
30
+ end
31
+
15
32
  # Set fk on tuples from parent tuple
16
33
  #
17
34
  # @param [Array<Hash>, Hash] tuples The input tuple(s)
@@ -23,13 +40,36 @@ module ROM
23
40
  #
24
41
  # @api public
25
42
  def execute(tuples, parent)
26
- fk, pk = association[:key]
43
+ input_tuples =
44
+ case assoc
45
+ when Array
46
+ fk, pk = assoc
27
47
 
28
- input_tuples = with_input_tuples(tuples).map { |tuple|
29
- tuple.merge(fk => parent.fetch(pk))
30
- }
48
+ input_tuples = with_input_tuples(tuples).map { |tuple|
49
+ tuple.merge(fk => parent.fetch(pk))
50
+ }
31
51
 
32
- super(input_tuples)
52
+ super(input_tuples)
53
+ when Association::ManyToMany
54
+ new_tuples = super(tuples)
55
+
56
+ join_tuples = assoc.associate(__registry__, new_tuples, parent)
57
+ join_relation = assoc.join_relation(__registry__)
58
+ join_relation.multi_insert(join_tuples)
59
+
60
+ pk, fk = __registry__[assoc.target]
61
+ .associations[assoc.source]
62
+ .combine_keys(__registry__).to_a.flatten
63
+
64
+ pk_extend = { fk => parent[pk] }
65
+
66
+ new_tuples.map { |tuple| tuple.update(pk_extend) }
67
+ when Association
68
+ input_tuples = with_input_tuples(tuples).map { |tuple|
69
+ assoc.associate(relation.__registry__, tuple, parent)
70
+ }
71
+ super(input_tuples)
72
+ end
33
73
  end
34
74
  end
35
75
 
@@ -62,16 +102,17 @@ module ROM
62
102
  # @option options [Array] :key The association keys
63
103
  #
64
104
  # @api public
65
- def associates(name, options)
66
- if associations.include?(name)
105
+ def associates(name, options = {})
106
+ if associations.map(&:first).include?(name)
67
107
  raise ArgumentError,
68
108
  "#{name} association is already defined for #{self.class}"
69
109
  end
70
110
 
71
- option :association, reader: true, default: -> _command { options }
111
+ option :association, reader: true, default: {}
112
+
72
113
  include InstanceMethods
73
114
 
74
- associations << name
115
+ associations << [name, options]
75
116
  end
76
117
  end
77
118
  end