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