rom-sql 1.0.0.beta3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -5
- data/.yardopts +2 -0
- data/CHANGELOG.md +4 -1
- data/lib/rom/plugins/relation/sql/auto_combine.rb +4 -0
- data/lib/rom/plugins/relation/sql/auto_wrap.rb +4 -0
- data/lib/rom/sql/{type.rb → attribute.rb} +8 -5
- data/lib/rom/sql/commands/update.rb +10 -0
- data/lib/rom/sql/dsl.rb +1 -0
- data/lib/rom/sql/extensions/postgres/inferrer.rb +2 -1
- data/lib/rom/sql/extensions/sqlite.rb +1 -0
- data/lib/rom/sql/extensions/sqlite/inferrer.rb +9 -0
- data/lib/rom/sql/extensions/sqlite/types.rb +11 -0
- data/lib/rom/sql/function.rb +4 -1
- data/lib/rom/sql/gateway.rb +64 -28
- data/lib/rom/sql/migration.rb +21 -29
- data/lib/rom/sql/migration/migrator.rb +8 -0
- data/lib/rom/sql/order_dsl.rb +1 -0
- data/lib/rom/sql/plugin/associates.rb +21 -0
- data/lib/rom/sql/plugin/timestamps.rb +131 -0
- data/lib/rom/sql/plugins.rb +3 -0
- data/lib/rom/sql/projection_dsl.rb +1 -0
- data/lib/rom/sql/relation/reading.rb +256 -75
- data/lib/rom/sql/restriction_dsl.rb +1 -0
- data/lib/rom/sql/schema/associations_dsl.rb +119 -1
- data/lib/rom/sql/schema/dsl.rb +44 -2
- data/lib/rom/sql/version.rb +1 -1
- data/rom-sql.gemspec +2 -2
- data/spec/extensions/sqlite/types_spec.rb +11 -0
- data/spec/integration/plugins/associates_spec.rb +79 -0
- data/spec/integration/schema/inferrer/postgres_spec.rb +3 -1
- data/spec/integration/schema/inferrer/sqlite_spec.rb +2 -0
- data/spec/shared/database_setup.rb +10 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/helpers.rb +2 -2
- data/spec/unit/plugin/timestamp_spec.rb +77 -0
- data/spec/unit/types_spec.rb +1 -1
- metadata +37 -24
@@ -4,15 +4,50 @@ require 'rom/sql/association'
|
|
4
4
|
module ROM
|
5
5
|
module SQL
|
6
6
|
class Schema < ROM::Schema
|
7
|
+
# Additional schema DSL for definition SQL associations
|
8
|
+
#
|
9
|
+
# This DSL is exposed in `associations do .. end` blocks in schema defintions.
|
10
|
+
#
|
11
|
+
# @api public
|
7
12
|
class AssociationsDSL < BasicObject
|
8
|
-
|
13
|
+
# @!attribute [r] source
|
14
|
+
# @return [Relation::Name] The source relation
|
15
|
+
attr_reader :source
|
9
16
|
|
17
|
+
# @!attribute [r] registry
|
18
|
+
# @return [RelationRegistry] Relations registry from a rom container
|
19
|
+
attr_reader :registry
|
20
|
+
|
21
|
+
# @api private
|
10
22
|
def initialize(source, &block)
|
11
23
|
@source = source
|
12
24
|
@registry = {}
|
13
25
|
instance_exec(&block)
|
14
26
|
end
|
15
27
|
|
28
|
+
# Establish a one-to-many association
|
29
|
+
#
|
30
|
+
# @example using relation identifier
|
31
|
+
# has_many :tasks
|
32
|
+
#
|
33
|
+
# @example with a :through option
|
34
|
+
# # this establishes many-to-many association
|
35
|
+
# has_many :tasks, through: :users_tasks
|
36
|
+
#
|
37
|
+
# @example using aliased association with a custom view
|
38
|
+
# has_many :posts, as: :published_posts, view: :published
|
39
|
+
#
|
40
|
+
# @example using custom target relation
|
41
|
+
# has_many :user_posts, relation: :posts
|
42
|
+
#
|
43
|
+
# @param [Symbol] target The target relation identifier
|
44
|
+
# @param [Hash] options A hash with additional options
|
45
|
+
#
|
46
|
+
# @return [Associations::OneToMany]
|
47
|
+
#
|
48
|
+
# @see #many_to_many
|
49
|
+
#
|
50
|
+
# @api public
|
16
51
|
def one_to_many(target, options = {})
|
17
52
|
if options[:through]
|
18
53
|
many_to_many(target, options)
|
@@ -22,6 +57,22 @@ module ROM
|
|
22
57
|
end
|
23
58
|
alias_method :has_many, :one_to_many
|
24
59
|
|
60
|
+
# Establish a one-to-one association
|
61
|
+
#
|
62
|
+
# @example using relation identifier
|
63
|
+
# one_to_one :addresses, as: :address
|
64
|
+
#
|
65
|
+
# @example with an intermediate join relation
|
66
|
+
# one_to_one :tasks, as: :priority_task, through: :assignments
|
67
|
+
#
|
68
|
+
# @param [Symbol] target The target relation identifier
|
69
|
+
# @param [Hash] options A hash with additional options
|
70
|
+
#
|
71
|
+
# @return [Associations::OneToOne]
|
72
|
+
#
|
73
|
+
# @see #belongs_to
|
74
|
+
#
|
75
|
+
# @api public
|
25
76
|
def one_to_one(target, options = {})
|
26
77
|
if options[:through]
|
27
78
|
one_to_one_through(target, options)
|
@@ -30,36 +81,103 @@ module ROM
|
|
30
81
|
end
|
31
82
|
end
|
32
83
|
|
84
|
+
# Establish a one-to-one association with a :through option
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# one_to_one_through :users, as: :author, through: :users_posts
|
88
|
+
#
|
89
|
+
# @return [Associations::OneToOneThrough]
|
90
|
+
#
|
91
|
+
# @api public
|
33
92
|
def one_to_one_through(target, options = {})
|
34
93
|
add(Association::OneToOneThrough.new(source, target, options))
|
35
94
|
end
|
36
95
|
|
96
|
+
# Establish a many-to-many association
|
97
|
+
#
|
98
|
+
# @example using relation identifier
|
99
|
+
# many_to_many :tasks, through: :users_tasks
|
100
|
+
#
|
101
|
+
# @param [Symbol] target The target relation identifier
|
102
|
+
# @param [Hash] options A hash with additional options
|
103
|
+
#
|
104
|
+
# @return [Associations::OneToOne]
|
105
|
+
#
|
106
|
+
# @see #one_to_many
|
107
|
+
#
|
108
|
+
# @api public
|
37
109
|
def many_to_many(target, options = {})
|
38
110
|
add(Association::ManyToMany.new(source, target, options))
|
39
111
|
end
|
40
112
|
|
113
|
+
# Establish a many-to-one association
|
114
|
+
#
|
115
|
+
# @example using relation identifier
|
116
|
+
# many_to_one :users, as: :author
|
117
|
+
#
|
118
|
+
# @param [Symbol] target The target relation identifier
|
119
|
+
# @param [Hash] options A hash with additional options
|
120
|
+
#
|
121
|
+
# @return [Associations::OneToOne]
|
122
|
+
#
|
123
|
+
# @see #one_to_many
|
124
|
+
#
|
125
|
+
# @api public
|
41
126
|
def many_to_one(target, options = {})
|
42
127
|
add(Association::ManyToOne.new(source, target, options))
|
43
128
|
end
|
44
129
|
|
130
|
+
# Shortcut for many_to_one which sets alias automatically
|
131
|
+
#
|
132
|
+
# @example with an alias (relation identifier is inferred via pluralization)
|
133
|
+
# belongs_to :user
|
134
|
+
#
|
135
|
+
# @example with an explicit alias
|
136
|
+
# belongs_to :users, as: :author
|
137
|
+
#
|
138
|
+
# @see #many_to_one
|
139
|
+
#
|
140
|
+
# @return [Associations::ManyToOne]
|
141
|
+
#
|
142
|
+
# @api public
|
45
143
|
def belongs_to(name, options = {})
|
46
144
|
many_to_one(dataset_name(name), {as: name}.merge(options))
|
47
145
|
end
|
48
146
|
|
147
|
+
# Shortcut for one_to_one which sets alias automatically
|
148
|
+
#
|
149
|
+
# @example with an alias (relation identifier is inferred via pluralization)
|
150
|
+
# one_to_one :address
|
151
|
+
#
|
152
|
+
# @example with an explicit alias and a custom view
|
153
|
+
# one_to_one :posts, as: :priority_post, view: :prioritized
|
154
|
+
#
|
155
|
+
# @see #one_to_one
|
156
|
+
#
|
157
|
+
# @return [Associations::ManyToOne]
|
158
|
+
#
|
159
|
+
# @api public
|
49
160
|
def has_one(name, options = {})
|
50
161
|
one_to_one(dataset_name(name), {as: name}.merge(options))
|
51
162
|
end
|
52
163
|
|
164
|
+
# Return an association set for a schema
|
165
|
+
#
|
166
|
+
# @return [AssociationSet]
|
167
|
+
#
|
168
|
+
# @api private
|
53
169
|
def call
|
54
170
|
AssociationSet.new(registry)
|
55
171
|
end
|
56
172
|
|
57
173
|
private
|
58
174
|
|
175
|
+
# @api private
|
59
176
|
def add(association)
|
60
177
|
registry[association.name] = association
|
61
178
|
end
|
62
179
|
|
180
|
+
# @api private
|
63
181
|
def dataset_name(name)
|
64
182
|
::Dry::Core::Inflector.pluralize(name).to_sym
|
65
183
|
end
|
data/lib/rom/sql/schema/dsl.rb
CHANGED
@@ -1,23 +1,65 @@
|
|
1
|
-
require 'rom/sql/
|
1
|
+
require 'rom/sql/attribute'
|
2
2
|
require 'rom/sql/schema/inferrer'
|
3
3
|
require 'rom/sql/schema/associations_dsl'
|
4
4
|
|
5
5
|
module ROM
|
6
6
|
module SQL
|
7
7
|
class Schema < ROM::Schema
|
8
|
+
# Extended schema DSL
|
9
|
+
#
|
10
|
+
# @api private
|
8
11
|
class DSL < ROM::Schema::DSL
|
9
12
|
attr_reader :associations_dsl
|
10
13
|
|
14
|
+
# Define associations for a relation
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# class Users < ROM::Relation[:sql]
|
18
|
+
# schema(infer: true) do
|
19
|
+
# associations do
|
20
|
+
# has_many :tasks
|
21
|
+
# has_many :posts
|
22
|
+
# has_many :posts, as: :priority_posts, view: :prioritized
|
23
|
+
# belongs_to :account
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# class Posts < ROM::Relation[:sql]
|
29
|
+
# schema(infer: true) do
|
30
|
+
# associations do
|
31
|
+
# belongs_to :users, as: :author
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# view(:prioritized) do
|
36
|
+
# where { priority <= 3 }
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @return [AssociationDSL]
|
41
|
+
#
|
42
|
+
# @api public
|
11
43
|
def associations(&block)
|
12
44
|
@associations_dsl = AssociationsDSL.new(relation, &block)
|
13
45
|
end
|
14
46
|
|
47
|
+
# Return a schema
|
48
|
+
#
|
49
|
+
# @api private
|
15
50
|
def call
|
16
51
|
SQL::Schema.define(
|
17
|
-
relation, opts.merge(attributes: attributes.values,
|
52
|
+
relation, opts.merge(attributes: attributes.values, attr_class: SQL::Attribute)
|
18
53
|
)
|
19
54
|
end
|
20
55
|
|
56
|
+
private
|
57
|
+
|
58
|
+
# Return schema opts
|
59
|
+
#
|
60
|
+
# @return [Hash]
|
61
|
+
#
|
62
|
+
# @api private
|
21
63
|
def opts
|
22
64
|
opts = { inferrer: inferrer }
|
23
65
|
|
data/lib/rom/sql/version.rb
CHANGED
data/rom-sql.gemspec
CHANGED
@@ -20,9 +20,9 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_runtime_dependency 'sequel', '~> 4.42'
|
22
22
|
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
23
|
-
spec.add_runtime_dependency 'dry-types', '~> 0.9'
|
23
|
+
spec.add_runtime_dependency 'dry-types', '~> 0.9', '>= 0.9.4'
|
24
24
|
spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.3'
|
25
|
-
spec.add_runtime_dependency 'rom', '~> 3.0.0.
|
25
|
+
spec.add_runtime_dependency 'rom', '~> 3.0.0.rc'
|
26
26
|
|
27
27
|
spec.add_development_dependency 'bundler'
|
28
28
|
spec.add_development_dependency 'rake', '~> 10.0'
|
@@ -0,0 +1,11 @@
|
|
1
|
+
RSpec.describe 'ROM::SQL::Types' do
|
2
|
+
describe 'ROM::SQL::Types::SQLite::Object' do
|
3
|
+
let(:type) { ROM::SQL::Types::SQLite::Object }
|
4
|
+
|
5
|
+
it 'passes an object of any type' do
|
6
|
+
[Object.new, 1, true, BasicObject.new].each do |obj|
|
7
|
+
expect(type[obj]).to be obj
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -192,4 +192,83 @@ RSpec.describe 'Plugins / :associates' do
|
|
192
192
|
end
|
193
193
|
end
|
194
194
|
end
|
195
|
+
|
196
|
+
with_adapters :sqlite do
|
197
|
+
context 'with Update command' do
|
198
|
+
subject(:command) do
|
199
|
+
container.commands[:tasks][:update].with_association(:user).by_pk(jane_task[:id])
|
200
|
+
end
|
201
|
+
|
202
|
+
let(:john) do
|
203
|
+
container.commands[:users][:create].call(name: 'John')
|
204
|
+
end
|
205
|
+
|
206
|
+
let(:jane) do
|
207
|
+
container.commands[:users][:create].call(name: 'Jane')
|
208
|
+
end
|
209
|
+
|
210
|
+
let(:jane_task) do
|
211
|
+
container.commands[:tasks][:create].call(user_id: jane[:id], title: 'Jane Task')
|
212
|
+
end
|
213
|
+
|
214
|
+
let(:john_task) do
|
215
|
+
container.commands[:tasks][:create].call(user_id: john[:id], title: 'John Task')
|
216
|
+
end
|
217
|
+
|
218
|
+
before do
|
219
|
+
conf.relation_classes[1].class_eval do
|
220
|
+
schema(infer: true) do
|
221
|
+
associations do
|
222
|
+
belongs_to :user
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
conf.commands(:users) do
|
228
|
+
define(:create) do
|
229
|
+
result :one
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
conf.commands(:tasks) do
|
234
|
+
define(:create) do
|
235
|
+
result :one
|
236
|
+
end
|
237
|
+
|
238
|
+
define(:update) do
|
239
|
+
result :one
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'automatically sets FK prior execution' do
|
245
|
+
expect(command.curry(title: 'Another John task').call(john)).
|
246
|
+
to eql(id: jane_task[:id], user_id: john[:id], title: 'Another John task')
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
context 'misconfigured assocs', :sqlite do
|
252
|
+
subject(:command) do
|
253
|
+
container.commands[:users][:create]
|
254
|
+
end
|
255
|
+
|
256
|
+
context 'when keys are missing in class-level config' do
|
257
|
+
before do
|
258
|
+
conf.commands(:users) do
|
259
|
+
define(:create) do
|
260
|
+
associates :tasks
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'raises error' do
|
266
|
+
expect { command }.
|
267
|
+
to raise_error(
|
268
|
+
ROM::SQL::Plugin::Associates::MissingJoinKeysError,
|
269
|
+
':create command for :users relation is missing join keys configuration for :tasks association'
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
195
274
|
end
|
@@ -24,6 +24,7 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
|
|
24
24
|
column :hw_address, "macaddr"
|
25
25
|
rainbow :color
|
26
26
|
point :center
|
27
|
+
xml :page
|
27
28
|
end
|
28
29
|
end
|
29
30
|
|
@@ -69,7 +70,8 @@ RSpec.describe 'ROM::SQL::Schema::PostgresInferrer', :postgres do
|
|
69
70
|
name: :center,
|
70
71
|
source: source,
|
71
72
|
read: ROM::SQL::Types::PG::PointTR.optional
|
72
|
-
)
|
73
|
+
),
|
74
|
+
page: ROM::SQL::Types::String.optional.meta(name: :page, source: source)
|
73
75
|
)
|
74
76
|
end
|
75
77
|
end
|
@@ -7,6 +7,7 @@ RSpec.describe 'ROM::SQL::Schema::SqliteInferrer', :sqlite do
|
|
7
7
|
conn.create_table :test_inferrence do
|
8
8
|
tinyint :tiny
|
9
9
|
int8 :big
|
10
|
+
column :dummy, nil
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
@@ -31,6 +32,7 @@ RSpec.describe 'ROM::SQL::Schema::SqliteInferrer', :sqlite do
|
|
31
32
|
expect(schema.to_h).to eql(
|
32
33
|
tiny: ROM::SQL::Types::Int.optional.meta(name: :tiny, source: source),
|
33
34
|
big: ROM::SQL::Types::Int.optional.meta(name: :big, source: source),
|
35
|
+
dummy: ROM::SQL::Types::SQLite::Object.optional.meta(name: :dummy, source: source)
|
34
36
|
)
|
35
37
|
end
|
36
38
|
end
|
@@ -21,7 +21,7 @@ shared_context 'database setup' do
|
|
21
21
|
%i(task_tags users_tasks tasks tags
|
22
22
|
subscriptions cards accounts
|
23
23
|
posts users
|
24
|
-
rabbits carrots
|
24
|
+
rabbits carrots notes
|
25
25
|
puppies schema_migrations
|
26
26
|
).each do |name|
|
27
27
|
conn.drop_table?(name)
|
@@ -91,6 +91,15 @@ shared_context 'database setup' do
|
|
91
91
|
String :name, null: false
|
92
92
|
TrueClass :cute, null: false, default: true
|
93
93
|
end
|
94
|
+
|
95
|
+
conn.create_table :notes do
|
96
|
+
primary_key :id
|
97
|
+
String :text, null: false
|
98
|
+
DateTime :created_at, null: false
|
99
|
+
DateTime :updated_at, null: false
|
100
|
+
DateTime :completed_at
|
101
|
+
Date :written
|
102
|
+
end
|
94
103
|
end
|
95
104
|
|
96
105
|
after do
|
data/spec/spec_helper.rb
CHANGED
@@ -47,7 +47,7 @@ Dir[root.join('support/**/*')].each { |f| require f }
|
|
47
47
|
require 'dry/core/deprecations'
|
48
48
|
Dry::Core::Deprecations.set_logger!(root.join('../log/deprecations.log'))
|
49
49
|
|
50
|
-
ROM::SQL.load_extensions(:postgres)
|
50
|
+
ROM::SQL.load_extensions(:postgres, :sqlite)
|
51
51
|
|
52
52
|
require 'dry-types'
|
53
53
|
module Types
|
data/spec/support/helpers.rb
CHANGED
@@ -11,11 +11,11 @@ module Helpers
|
|
11
11
|
ROM::SQL::Schema.define(
|
12
12
|
name,
|
13
13
|
attributes: attrs.map { |key, value| value.meta(name: key, source: ROM::Relation::Name.new(name)) },
|
14
|
-
|
14
|
+
attr_class: ROM::SQL::Attribute
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
18
18
|
def define_type(name, id, **opts)
|
19
|
-
ROM::SQL::
|
19
|
+
ROM::SQL::Attribute.new(ROM::Types.const_get(id).meta(name: name, **opts))
|
20
20
|
end
|
21
21
|
end
|