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
@@ -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