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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +12 -7
- data/CHANGELOG.md +28 -0
- data/Gemfile +6 -9
- data/README.md +5 -4
- data/circle.yml +10 -0
- data/lib/rom/plugins/relation/sql/auto_combine.rb +16 -3
- data/lib/rom/plugins/relation/sql/auto_wrap.rb +3 -2
- data/lib/rom/sql/association.rb +75 -0
- data/lib/rom/sql/association/many_to_many.rb +86 -0
- data/lib/rom/sql/association/many_to_one.rb +60 -0
- data/lib/rom/sql/association/name.rb +70 -0
- data/lib/rom/sql/association/one_to_many.rb +9 -0
- data/lib/rom/sql/association/one_to_one.rb +46 -0
- data/lib/rom/sql/association/one_to_one_through.rb +9 -0
- data/lib/rom/sql/commands.rb +2 -0
- data/lib/rom/sql/commands/create.rb +2 -2
- data/lib/rom/sql/commands/delete.rb +0 -1
- data/lib/rom/sql/commands/postgres.rb +76 -0
- data/lib/rom/sql/commands/update.rb +6 -3
- data/lib/rom/sql/commands_ext/postgres.rb +17 -0
- data/lib/rom/sql/gateway.rb +23 -15
- data/lib/rom/sql/header.rb +7 -1
- data/lib/rom/sql/plugin/assoc_macros.rb +3 -3
- data/lib/rom/sql/plugin/associates.rb +50 -9
- data/lib/rom/sql/qualified_attribute.rb +53 -0
- data/lib/rom/sql/relation.rb +76 -25
- data/lib/rom/sql/relation/reading.rb +138 -35
- data/lib/rom/sql/relation/writing.rb +21 -0
- data/lib/rom/sql/schema.rb +35 -0
- data/lib/rom/sql/schema/associations_dsl.rb +68 -0
- data/lib/rom/sql/schema/dsl.rb +27 -0
- data/lib/rom/sql/schema/inferrer.rb +80 -0
- data/lib/rom/sql/support/active_support_notifications.rb +27 -17
- data/lib/rom/sql/types.rb +11 -0
- data/lib/rom/sql/types/pg.rb +26 -0
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +4 -2
- data/spec/integration/association/many_to_many_spec.rb +137 -0
- data/spec/integration/association/many_to_one_spec.rb +110 -0
- data/spec/integration/association/one_to_many_spec.rb +58 -0
- data/spec/integration/association/one_to_one_spec.rb +57 -0
- data/spec/integration/association/one_to_one_through_spec.rb +90 -0
- data/spec/integration/combine_spec.rb +24 -24
- data/spec/integration/commands/create_spec.rb +215 -168
- data/spec/integration/commands/delete_spec.rb +88 -46
- data/spec/integration/commands/update_spec.rb +141 -60
- data/spec/integration/commands/upsert_spec.rb +83 -0
- data/spec/integration/gateway_spec.rb +9 -17
- data/spec/integration/migration_spec.rb +3 -5
- data/spec/integration/plugins/associates_spec.rb +168 -0
- data/spec/integration/plugins/auto_wrap_spec.rb +46 -0
- data/spec/integration/read_spec.rb +80 -77
- data/spec/integration/relation_schema_spec.rb +180 -0
- data/spec/integration/schema_inference_spec.rb +67 -0
- data/spec/integration/setup_spec.rb +22 -0
- data/spec/{support → integration/support}/active_support_notifications_spec.rb +0 -0
- data/spec/{support → integration/support}/rails_log_subscriber_spec.rb +0 -0
- data/spec/shared/database_setup.rb +46 -8
- data/spec/shared/relations.rb +8 -0
- data/spec/shared/users_and_accounts.rb +10 -0
- data/spec/shared/users_and_tasks.rb +20 -2
- data/spec/spec_helper.rb +64 -11
- data/spec/support/helpers.rb +9 -0
- data/spec/unit/association/many_to_many_spec.rb +89 -0
- data/spec/unit/association/many_to_one_spec.rb +81 -0
- data/spec/unit/association/name_spec.rb +68 -0
- data/spec/unit/association/one_to_many_spec.rb +62 -0
- data/spec/unit/association/one_to_one_spec.rb +62 -0
- data/spec/unit/association/one_to_one_through_spec.rb +69 -0
- data/spec/unit/association_errors_spec.rb +2 -4
- data/spec/unit/gateway_spec.rb +12 -3
- data/spec/unit/migration_tasks_spec.rb +3 -3
- data/spec/unit/migrator_spec.rb +2 -4
- data/spec/unit/{combined_associations_spec.rb → plugin/assoc_macros/combined_associations_spec.rb} +13 -19
- data/spec/unit/{many_to_many_spec.rb → plugin/assoc_macros/many_to_many_spec.rb} +9 -15
- data/spec/unit/{many_to_one_spec.rb → plugin/assoc_macros/many_to_one_spec.rb} +9 -14
- data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +78 -0
- data/spec/unit/plugin/base_view_spec.rb +11 -11
- data/spec/unit/plugin/pagination_spec.rb +62 -62
- data/spec/unit/relation_spec.rb +218 -146
- data/spec/unit/schema_spec.rb +15 -14
- data/spec/unit/types_spec.rb +40 -0
- metadata +105 -21
- data/.rubocop.yml +0 -74
- data/.rubocop_todo.yml +0 -21
- data/spec/unit/one_to_many_spec.rb +0 -83
@@ -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
|
data/lib/rom/sql/commands.rb
CHANGED
@@ -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
|
@@ -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.
|
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
|
data/lib/rom/sql/gateway.rb
CHANGED
@@ -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
|
data/lib/rom/sql/header.rb
CHANGED
@@ -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(
|
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
|
-
|
43
|
+
input_tuples =
|
44
|
+
case assoc
|
45
|
+
when Array
|
46
|
+
fk, pk = assoc
|
27
47
|
|
28
|
-
|
29
|
-
|
30
|
-
|
48
|
+
input_tuples = with_input_tuples(tuples).map { |tuple|
|
49
|
+
tuple.merge(fk => parent.fetch(pk))
|
50
|
+
}
|
31
51
|
|
32
|
-
|
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:
|
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
|