rom-sql 0.4.3 → 0.5.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/CHANGELOG.md +21 -0
- data/Gemfile +1 -0
- data/lib/rom/sql.rb +32 -9
- data/lib/rom/sql/commands.rb +0 -11
- data/lib/rom/sql/commands/create.rb +25 -1
- data/lib/rom/sql/commands/delete.rb +10 -0
- data/lib/rom/sql/commands/error_wrapper.rb +8 -4
- data/lib/rom/sql/commands/update.rb +40 -6
- data/lib/rom/sql/plugin/associates.rb +82 -0
- data/lib/rom/sql/relation.rb +71 -17
- data/lib/rom/sql/relation/associations.rb +6 -4
- data/lib/rom/sql/relation/class_methods.rb +69 -0
- data/lib/rom/sql/repository.rb +9 -0
- data/lib/rom/sql/version.rb +1 -1
- data/spec/integration/commands/create_spec.rb +102 -28
- data/spec/integration/commands/delete_spec.rb +7 -7
- data/spec/integration/commands/update_spec.rb +20 -15
- data/spec/shared/database_setup.rb +3 -2
- data/spec/unit/many_to_one_spec.rb +6 -1
- data/spec/unit/one_to_many_spec.rb +4 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bbd3084b35810eb5773f42d14ae8c75de6d6de0f
|
4
|
+
data.tar.gz: 9dfa889eed6b3005a6c6385c70bceb78aa975fe5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d2ac005bf0b9bd1ce789d9e34822581c66510c1861c3e5bb7d6b778bc8526ef287cfc77a3aa3e204eb5c8c2c744e88fb092417c66c3bb19d74734d022f70abb
|
7
|
+
data.tar.gz: f10f50b34e35e892014a394ae54e606d42bd8f8750c3640674e09116553c5b73a5846578a02e66c15011ceed60aa8d85d8e982a6bbef0ce482be18b66b28b4e2
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## v0.5.0 2015-05-22
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Association macros support addition `:on` option (solnic)
|
6
|
+
* `associates` plugin for `Create` command (solnic)
|
7
|
+
* Support for NotNullConstraintError (solnic)
|
8
|
+
* Support for UniqueConstraintConstraintError (solnic)
|
9
|
+
* Support for ForeignKeyConstraintError (solnic)
|
10
|
+
* Support for CheckConstraintError (solnic)
|
11
|
+
* `Commands::Update#original` supports objects coercible to_h now (solnic)
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
|
15
|
+
* [BREAKING] Constraint errors are no longer command errors which means `try` and
|
16
|
+
`transaction` blocks will not catch them (solnic)
|
17
|
+
* `Commands::Update#set` has been deprecated (solnic)
|
18
|
+
* `Commands::Update#to` has been deprecated (solnic)
|
19
|
+
|
20
|
+
[Compare v0.4.3...0.5.0](https://github.com/rom-rb/rom-sql/compare/v0.4.2...0.5.0)
|
21
|
+
|
1
22
|
## v0.4.3 2015-05-17
|
2
23
|
|
3
24
|
### Fixed
|
data/Gemfile
CHANGED
data/lib/rom/sql.rb
CHANGED
@@ -4,16 +4,43 @@ require "rom"
|
|
4
4
|
module ROM
|
5
5
|
module SQL
|
6
6
|
NoAssociationError = Class.new(StandardError)
|
7
|
-
ConstraintError = Class.new(ROM::CommandError)
|
8
7
|
|
9
|
-
class
|
8
|
+
class Error < StandardError
|
10
9
|
attr_reader :original_exception
|
11
10
|
|
12
|
-
def initialize(
|
13
|
-
super(message)
|
14
|
-
@original_exception =
|
11
|
+
def initialize(original_exception)
|
12
|
+
super(original_exception.message)
|
13
|
+
@original_exception = original_exception
|
14
|
+
set_backtrace(original_exception.backtrace)
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
DatabaseError = Class.new(Error)
|
19
|
+
|
20
|
+
ConstraintError = Class.new(Error)
|
21
|
+
|
22
|
+
NotNullConstraintError = Class.new(ConstraintError)
|
23
|
+
UniqueConstraintError = Class.new(ConstraintError)
|
24
|
+
ForeignKeyConstraintError = Class.new(ConstraintError)
|
25
|
+
CheckConstraintError = Class.new(ConstraintError)
|
26
|
+
|
27
|
+
ERROR_MAP = {
|
28
|
+
Sequel::DatabaseError => DatabaseError,
|
29
|
+
Sequel::NotNullConstraintViolation => NotNullConstraintError,
|
30
|
+
Sequel::UniqueConstraintViolation => UniqueConstraintError,
|
31
|
+
Sequel::ForeignKeyConstraintViolation => ForeignKeyConstraintError,
|
32
|
+
Sequel::CheckConstraintViolation => CheckConstraintError
|
33
|
+
}.freeze
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require 'rom/sql/plugin/associates'
|
38
|
+
require 'rom/sql/plugin/pagination'
|
39
|
+
|
40
|
+
ROM.plugins do
|
41
|
+
adapter :sql do
|
42
|
+
register :pagination, ROM::SQL::Plugin::Pagination, type: :relation
|
43
|
+
register :associates, ROM::SQL::Plugin::Associates, type: :command
|
17
44
|
end
|
18
45
|
end
|
19
46
|
|
@@ -28,7 +55,3 @@ if defined?(Rails)
|
|
28
55
|
end
|
29
56
|
|
30
57
|
ROM.register_adapter(:sql, ROM::SQL)
|
31
|
-
|
32
|
-
ROM.plugins do
|
33
|
-
register :pagination, ROM::SQL::Plugin::Pagination, type: :relation
|
34
|
-
end
|
data/lib/rom/sql/commands.rb
CHANGED
@@ -1,16 +1,5 @@
|
|
1
1
|
require 'rom/commands'
|
2
2
|
|
3
|
-
module ROM
|
4
|
-
module SQL
|
5
|
-
module Commands
|
6
|
-
ERRORS = [
|
7
|
-
Sequel::UniqueConstraintViolation,
|
8
|
-
Sequel::NotNullConstraintViolation
|
9
|
-
].freeze
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
3
|
require 'rom/sql/commands/create'
|
15
4
|
require 'rom/sql/commands/update'
|
16
5
|
require 'rom/sql/commands/delete'
|
@@ -5,12 +5,22 @@ require 'rom/sql/commands/transaction'
|
|
5
5
|
module ROM
|
6
6
|
module SQL
|
7
7
|
module Commands
|
8
|
+
# SQL create command
|
9
|
+
#
|
10
|
+
# @api public
|
8
11
|
class Create < ROM::Commands::Create
|
12
|
+
adapter :sql
|
13
|
+
|
9
14
|
include Transaction
|
10
15
|
include ErrorWrapper
|
11
16
|
|
17
|
+
use :associates
|
18
|
+
|
19
|
+
# Inserts provided tuples into the database table
|
20
|
+
#
|
21
|
+
# @api public
|
12
22
|
def execute(tuples)
|
13
|
-
insert_tuples =
|
23
|
+
insert_tuples = with_input_tuples(tuples) do |tuple|
|
14
24
|
attributes = input[tuple]
|
15
25
|
validator.call(attributes)
|
16
26
|
attributes.to_h
|
@@ -19,10 +29,24 @@ module ROM
|
|
19
29
|
insert(insert_tuples)
|
20
30
|
end
|
21
31
|
|
32
|
+
private
|
33
|
+
|
34
|
+
# Executes insert statement and returns inserted tuples
|
35
|
+
#
|
36
|
+
# @api private
|
22
37
|
def insert(tuples)
|
23
38
|
pks = tuples.map { |tuple| relation.insert(tuple) }
|
24
39
|
relation.where(relation.primary_key => pks)
|
25
40
|
end
|
41
|
+
|
42
|
+
# Yields tuples for insertion or return an enumerator
|
43
|
+
#
|
44
|
+
# @api private
|
45
|
+
def with_input_tuples(tuples)
|
46
|
+
input_tuples = Array([tuples]).flatten.map
|
47
|
+
return input_tuples unless block_given?
|
48
|
+
input_tuples.each { |tuple| yield(tuple) }
|
49
|
+
end
|
26
50
|
end
|
27
51
|
end
|
28
52
|
end
|
@@ -5,10 +5,20 @@ require 'rom/sql/commands/transaction'
|
|
5
5
|
module ROM
|
6
6
|
module SQL
|
7
7
|
module Commands
|
8
|
+
# SQL delete command
|
9
|
+
#
|
10
|
+
# @api public
|
8
11
|
class Delete < ROM::Commands::Delete
|
12
|
+
adapter :sql
|
13
|
+
|
9
14
|
include Transaction
|
10
15
|
include ErrorWrapper
|
11
16
|
|
17
|
+
# Deletes tuples from a relation
|
18
|
+
#
|
19
|
+
# @return [Array<Hash>] deleted tuples
|
20
|
+
#
|
21
|
+
# @api public
|
12
22
|
def execute
|
13
23
|
deleted = target.to_a
|
14
24
|
target.delete
|
@@ -1,13 +1,17 @@
|
|
1
1
|
module ROM
|
2
2
|
module SQL
|
3
3
|
module Commands
|
4
|
+
# Shared error handler for all SQL commands
|
5
|
+
#
|
6
|
+
# @api private
|
4
7
|
module ErrorWrapper
|
8
|
+
# Handle Sequel errors and re-raise ROM-specific errors
|
9
|
+
#
|
10
|
+
# @api public
|
5
11
|
def call(*args)
|
6
12
|
super
|
7
|
-
rescue *
|
8
|
-
raise
|
9
|
-
rescue Sequel::DatabaseError => e
|
10
|
-
raise DatabaseError.new(e, e.message)
|
13
|
+
rescue *ERROR_MAP.keys => e
|
14
|
+
raise ERROR_MAP[e.class], e
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'rom/support/deprecations'
|
2
|
+
|
1
3
|
require 'rom/sql/commands'
|
2
4
|
require 'rom/sql/commands/error_wrapper'
|
3
5
|
require 'rom/sql/commands/transaction'
|
@@ -5,15 +7,27 @@ require 'rom/sql/commands/transaction'
|
|
5
7
|
module ROM
|
6
8
|
module SQL
|
7
9
|
module Commands
|
10
|
+
# Update command
|
11
|
+
#
|
12
|
+
# @api public
|
8
13
|
class Update < ROM::Commands::Update
|
14
|
+
adapter :sql
|
15
|
+
|
16
|
+
include Deprecations
|
17
|
+
|
9
18
|
include Transaction
|
10
19
|
include ErrorWrapper
|
11
20
|
|
12
|
-
option :original,
|
21
|
+
option :original, reader: true
|
13
22
|
|
14
|
-
|
15
|
-
|
23
|
+
deprecate :set, :call
|
24
|
+
deprecate :to, :call
|
16
25
|
|
26
|
+
# Updates existing tuple in a relation
|
27
|
+
#
|
28
|
+
# @return [Array<Hash>, Hash]
|
29
|
+
#
|
30
|
+
# @api public
|
17
31
|
def execute(tuple)
|
18
32
|
attributes = input[tuple]
|
19
33
|
validator.call(attributes)
|
@@ -27,10 +41,28 @@ module ROM
|
|
27
41
|
end
|
28
42
|
end
|
29
43
|
|
44
|
+
# Update existing tuple only when it changed
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# user = rom.relation(:users).one
|
48
|
+
# new_user = { name: 'Jane Doe' }
|
49
|
+
#
|
50
|
+
# rom.command(:users).change(user).call(new_user)
|
51
|
+
#
|
52
|
+
# @param [#to_h, Hash] original The original tuple
|
53
|
+
#
|
54
|
+
# @return [Command::Update]
|
55
|
+
#
|
56
|
+
# @api public
|
30
57
|
def change(original)
|
31
|
-
self.class.new(relation, options.merge(original: original))
|
58
|
+
self.class.new(relation, options.merge(original: original.to_h))
|
32
59
|
end
|
33
60
|
|
61
|
+
private
|
62
|
+
|
63
|
+
# Executes update statement for a given tuple
|
64
|
+
#
|
65
|
+
# @api private
|
34
66
|
def update(tuple)
|
35
67
|
pks = relation.map { |t| t[primary_key] }
|
36
68
|
dataset = relation.dataset
|
@@ -38,12 +70,14 @@ module ROM
|
|
38
70
|
dataset.unfiltered.where(primary_key => pks).to_a
|
39
71
|
end
|
40
72
|
|
73
|
+
# @api private
|
41
74
|
def primary_key
|
42
75
|
relation.primary_key
|
43
76
|
end
|
44
77
|
|
45
|
-
|
46
|
-
|
78
|
+
# Check if input tuple is different from the original one
|
79
|
+
#
|
80
|
+
# @api private
|
47
81
|
def diff(tuple)
|
48
82
|
if original
|
49
83
|
Hash[tuple.to_a - (tuple.to_a & original.to_a)]
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module ROM
|
2
|
+
module SQL
|
3
|
+
module Plugin
|
4
|
+
# Make a command that automaticaly sets FK attribute on input tuples
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
module Associates
|
8
|
+
# @api private
|
9
|
+
def self.included(klass)
|
10
|
+
klass.extend(ClassMethods)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
# Set fk on tuples from parent tuple
|
16
|
+
#
|
17
|
+
# @param [Array<Hash>, Hash] tuples The input tuple(s)
|
18
|
+
# @param [Hash] parent The parent tuple with its pk already set
|
19
|
+
#
|
20
|
+
# @return [Array<Hash>,Hash]
|
21
|
+
#
|
22
|
+
# @overload SQL::Commands::Create#execute
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
def execute(tuples, parent)
|
26
|
+
fk, pk = association[:key]
|
27
|
+
|
28
|
+
input_tuples = with_input_tuples(tuples).map { |tuple|
|
29
|
+
tuple.merge(fk => parent.fetch(pk))
|
30
|
+
}
|
31
|
+
|
32
|
+
super(input_tuples)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
# @api private
|
38
|
+
def inherited(klass)
|
39
|
+
klass.defines :associations
|
40
|
+
klass.associations []
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
# Set command to associate tuples with a parent tuple using provided keys
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# class CreateTask < ROM::Commands::Create[:sql]
|
48
|
+
# relation :tasks
|
49
|
+
# associates :user, [:user_id, :id]
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# create_user = rom.command(:user).create.with(name: 'Jane')
|
53
|
+
#
|
54
|
+
# create_tasks = rom.command(:tasks).create
|
55
|
+
# .with [{ title: 'One' }, { title: 'Two' } ]
|
56
|
+
#
|
57
|
+
# command = create_user >> create_tasks
|
58
|
+
# command.call
|
59
|
+
#
|
60
|
+
# @param [Symbol] name The name of associated table
|
61
|
+
# @param [Hash] options The options
|
62
|
+
# @option options [Array] :key The association keys
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def associates(name, options)
|
66
|
+
if associations.include?(name)
|
67
|
+
raise(
|
68
|
+
ArgumentError,
|
69
|
+
"#{name} association is already defined for #{self.class}"
|
70
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
option :association, reader: true, default: -> command { options }
|
74
|
+
include InstanceMethods
|
75
|
+
|
76
|
+
associations << name
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/rom/sql/relation.rb
CHANGED
@@ -4,19 +4,25 @@ require 'rom/sql/relation/class_methods'
|
|
4
4
|
require 'rom/sql/relation/inspection'
|
5
5
|
require 'rom/sql/relation/associations'
|
6
6
|
|
7
|
-
require 'rom/sql/plugin/pagination'
|
8
|
-
|
9
7
|
module ROM
|
10
8
|
module SQL
|
11
9
|
# Sequel-specific relation extensions
|
12
10
|
#
|
13
11
|
class Relation < ROM::Relation
|
12
|
+
adapter :sql
|
13
|
+
|
14
14
|
extend ClassMethods
|
15
15
|
|
16
16
|
include Inspection
|
17
17
|
include Associations
|
18
18
|
|
19
|
-
attr_reader
|
19
|
+
# @attr_reader [Header] header Internal lazy-initialized header
|
20
|
+
attr_reader :header
|
21
|
+
|
22
|
+
# Name of the table used in FROM clause
|
23
|
+
#
|
24
|
+
# @attr_reader [Symbol] table
|
25
|
+
attr_reader :table
|
20
26
|
|
21
27
|
# @api private
|
22
28
|
def initialize(dataset, registry = {})
|
@@ -24,44 +30,74 @@ module ROM
|
|
24
30
|
@table = dataset.opts[:from].first
|
25
31
|
end
|
26
32
|
|
27
|
-
#
|
33
|
+
# Project a relation
|
28
34
|
#
|
29
|
-
#
|
35
|
+
# This method is intended to be used internally within a relation object
|
30
36
|
#
|
31
|
-
# @
|
32
|
-
|
33
|
-
@header ||= Header.new(dataset.opts[:select] || dataset.columns, table)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Return raw column names
|
37
|
+
# @example
|
38
|
+
# rom.relation(:users) { |r| r.project(:id, :name) }
|
37
39
|
#
|
38
|
-
# @
|
40
|
+
# @param [Symbol] names A list of symbol column names
|
41
|
+
#
|
42
|
+
# @return [Relation]
|
39
43
|
#
|
40
|
-
# @api private
|
41
|
-
def columns
|
42
|
-
dataset.columns
|
43
|
-
end
|
44
|
-
|
45
44
|
# @api public
|
46
45
|
def project(*names)
|
47
46
|
select(*header.project(*names))
|
48
47
|
end
|
49
48
|
|
49
|
+
# Rename columns in a relation
|
50
|
+
#
|
51
|
+
# This method is intended to be used internally within a relation object
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# rom.relation(:users) { |r| r.rename(name: :user_name) }
|
55
|
+
#
|
56
|
+
# @param [Hash] options A name => new_name map
|
57
|
+
#
|
58
|
+
# @return [Relation]
|
59
|
+
#
|
50
60
|
# @api public
|
51
61
|
def rename(options)
|
52
62
|
select(*header.rename(options))
|
53
63
|
end
|
54
64
|
|
65
|
+
# Prefix all columns in a relation
|
66
|
+
#
|
67
|
+
# This method is intended to be used internally within a relation object
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# rom.relation(:users) { |r| r.prefix(:user) }
|
71
|
+
#
|
72
|
+
# @param [Symbol] name The prefix
|
73
|
+
#
|
74
|
+
# @return [Relation]
|
75
|
+
#
|
55
76
|
# @api public
|
56
77
|
def prefix(name = Inflector.singularize(table))
|
57
78
|
rename(header.prefix(name).to_h)
|
58
79
|
end
|
59
80
|
|
81
|
+
# Qualifies all columns in a relation
|
82
|
+
#
|
83
|
+
# This method is intended to be used internally within a relation object
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# rom.relation(:users) { |r| r.qualified }
|
87
|
+
#
|
88
|
+
# @return [Relation]
|
89
|
+
#
|
60
90
|
# @api public
|
61
91
|
def qualified
|
62
92
|
select(*qualified_columns)
|
63
93
|
end
|
64
94
|
|
95
|
+
# Return a list of qualified column names
|
96
|
+
#
|
97
|
+
# This method is intended to be used internally within a relation object
|
98
|
+
#
|
99
|
+
# @return [Relation]
|
100
|
+
#
|
65
101
|
# @api public
|
66
102
|
def qualified_columns
|
67
103
|
header.qualified.to_a
|
@@ -341,6 +377,24 @@ module ROM
|
|
341
377
|
def unique?(criteria)
|
342
378
|
where(criteria).count.zero?
|
343
379
|
end
|
380
|
+
|
381
|
+
# Return a header for this relation
|
382
|
+
#
|
383
|
+
# @return [Header]
|
384
|
+
#
|
385
|
+
# @api private
|
386
|
+
def header
|
387
|
+
@header ||= Header.new(dataset.opts[:select] || dataset.columns, table)
|
388
|
+
end
|
389
|
+
|
390
|
+
# Return raw column names
|
391
|
+
#
|
392
|
+
# @return [Array<Symbol>]
|
393
|
+
#
|
394
|
+
# @api private
|
395
|
+
def columns
|
396
|
+
dataset.columns
|
397
|
+
end
|
344
398
|
end
|
345
399
|
end
|
346
400
|
end
|
@@ -54,7 +54,6 @@ module ROM
|
|
54
54
|
"defined for relation #{name.inspect}"
|
55
55
|
end
|
56
56
|
|
57
|
-
key = assoc[:key]
|
58
57
|
type = assoc[:type]
|
59
58
|
table_name = assoc[:class].table_name
|
60
59
|
|
@@ -63,7 +62,7 @@ module ROM
|
|
63
62
|
select = options[:select] || {}
|
64
63
|
graph_join_many_to_many(table_name, assoc, select)
|
65
64
|
else
|
66
|
-
graph_join_other(table_name,
|
65
|
+
graph_join_other(table_name, assoc, type, join_type, options)
|
67
66
|
end
|
68
67
|
|
69
68
|
graph_rel = graph_rel.where(assoc[:conditions]) if assoc[:conditions]
|
@@ -97,13 +96,16 @@ module ROM
|
|
97
96
|
)
|
98
97
|
end
|
99
98
|
|
100
|
-
def graph_join_other(name,
|
99
|
+
def graph_join_other(name, assoc, type, join_type, options)
|
100
|
+
key = assoc[:key]
|
101
|
+
on_conditions = assoc[:on] || {}
|
102
|
+
|
101
103
|
join_keys =
|
102
104
|
if type == :many_to_one
|
103
105
|
{ primary_key => key }
|
104
106
|
else
|
105
107
|
{ key => primary_key }
|
106
|
-
end
|
108
|
+
end.merge(on_conditions)
|
107
109
|
|
108
110
|
graph(
|
109
111
|
name, join_keys,
|
@@ -1,7 +1,13 @@
|
|
1
1
|
module ROM
|
2
2
|
module SQL
|
3
3
|
class Relation < ROM::Relation
|
4
|
+
# Class DSL for SQL relations
|
5
|
+
#
|
6
|
+
# @api private
|
4
7
|
module ClassMethods
|
8
|
+
# Set up model and association ivars for descendant class
|
9
|
+
#
|
10
|
+
# @api private
|
5
11
|
def inherited(klass)
|
6
12
|
klass.class_eval do
|
7
13
|
class << self
|
@@ -13,20 +19,83 @@ module ROM
|
|
13
19
|
super
|
14
20
|
end
|
15
21
|
|
22
|
+
# Set up a one-to-many association
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# class Users < ROM::Relation[:sql]
|
26
|
+
# one_to_many :tasks, key: :user_id
|
27
|
+
#
|
28
|
+
# def with_tasks
|
29
|
+
# association_join(:tasks)
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# @param [Symbol] name The name of the association
|
34
|
+
# @param [Hash] options The options hash
|
35
|
+
# @option options [Symbol] :key Name of the key to join on
|
36
|
+
# @option options [Hash] :on Additional conditions for join
|
37
|
+
# @option options [Hash] :conditions Additional conditions for WHERE
|
38
|
+
#
|
39
|
+
# @api public
|
16
40
|
def one_to_many(name, options)
|
17
41
|
associations << [__method__, name, { relation: name }.merge(options)]
|
18
42
|
end
|
19
43
|
|
44
|
+
# Set up a many-to-many association
|
45
|
+
#
|
46
|
+
# @example
|
47
|
+
# class Tasks < ROM::Relation[:sql]
|
48
|
+
# many_to_many :tags,
|
49
|
+
# join_table: :task_tags,
|
50
|
+
# left_key: :task_id,
|
51
|
+
# right_key: :tag_id,
|
52
|
+
#
|
53
|
+
# def with_tags
|
54
|
+
# association_join(:tags)
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @param [Symbol] name The name of the association
|
59
|
+
# @param [Hash] options The options hash
|
60
|
+
# @option options [Symbol] :join_table Name of the join table
|
61
|
+
# @option options [Hash] :left_key Name of the left join key
|
62
|
+
# @option options [Hash] :right_key Name of the right join key
|
63
|
+
# @option options [Hash] :on Additional conditions for join
|
64
|
+
# @option options [Hash] :conditions Additional conditions for WHERE
|
65
|
+
#
|
66
|
+
# @api public
|
20
67
|
def many_to_many(name, options = {})
|
21
68
|
associations << [__method__, name, { relation: name }.merge(options)]
|
22
69
|
end
|
23
70
|
|
71
|
+
# Set up a many-to-one association
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# class Tasks < ROM::Relation[:sql]
|
75
|
+
# many_to_one :users, key: :user_id
|
76
|
+
#
|
77
|
+
# def with_users
|
78
|
+
# association_join(:users)
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# @param [Symbol] name The name of the association
|
83
|
+
# @param [Hash] options The options hash
|
84
|
+
# @option options [Symbol] :join_table Name of the join table
|
85
|
+
# @option options [Hash] :key Name of the join key
|
86
|
+
# @option options [Hash] :on Additional conditions for join
|
87
|
+
# @option options [Hash] :conditions Additional conditions for WHERE
|
88
|
+
#
|
89
|
+
# @api public
|
24
90
|
def many_to_one(name, options = {})
|
25
91
|
relation_name = Inflector.pluralize(name).to_sym
|
26
92
|
new_options = options.merge(relation: relation_name)
|
27
93
|
associations << [__method__, name, new_options]
|
28
94
|
end
|
29
95
|
|
96
|
+
# Finalize the relation by setting up its associations (if any)
|
97
|
+
#
|
98
|
+
# @api private
|
30
99
|
def finalize(relations, relation)
|
31
100
|
model.set_dataset(relation.dataset)
|
32
101
|
model.dataset.naked!
|
data/lib/rom/sql/repository.rb
CHANGED
@@ -6,6 +6,15 @@ require 'rom/sql/commands'
|
|
6
6
|
|
7
7
|
module ROM
|
8
8
|
module SQL
|
9
|
+
# SQL repository
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# db = Sequel.connect(:sqlite)
|
13
|
+
# repository = ROM::SQL::Repository.new(db)
|
14
|
+
#
|
15
|
+
# users = repository.dataset(:users)
|
16
|
+
#
|
17
|
+
# @api public
|
9
18
|
class Repository < ROM::Repository
|
10
19
|
include Options
|
11
20
|
include Migration
|
data/lib/rom/sql/version.rb
CHANGED
@@ -5,6 +5,7 @@ describe 'Commands / Create' do
|
|
5
5
|
include_context 'database setup'
|
6
6
|
|
7
7
|
subject(:users) { rom.commands.users }
|
8
|
+
subject(:tasks) { rom.commands.tasks }
|
8
9
|
|
9
10
|
before do
|
10
11
|
class Params
|
@@ -33,7 +34,12 @@ describe 'Commands / Create' do
|
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
37
|
+
setup.commands(:tasks) do
|
38
|
+
define(:create)
|
39
|
+
end
|
40
|
+
|
36
41
|
setup.relation(:users)
|
42
|
+
setup.relation(:tasks)
|
37
43
|
end
|
38
44
|
|
39
45
|
context '#transaction' do
|
@@ -92,18 +98,19 @@ describe 'Commands / Create' do
|
|
92
98
|
|
93
99
|
it 'creates nothing if constraint error was raised' do
|
94
100
|
expect {
|
95
|
-
|
96
|
-
|
97
|
-
result = users.create.transaction {
|
98
|
-
users.create.call(name: 'Jane')
|
99
|
-
users.create.call(name: 'Jane')
|
100
|
-
} >-> value {
|
101
|
-
passed = true
|
102
|
-
}
|
101
|
+
begin
|
102
|
+
passed = false
|
103
103
|
|
104
|
-
|
105
|
-
|
106
|
-
|
104
|
+
users.create.transaction {
|
105
|
+
users.create.call(name: 'Jane')
|
106
|
+
users.create.call(name: 'Jane')
|
107
|
+
} >-> value {
|
108
|
+
passed = true
|
109
|
+
}
|
110
|
+
rescue => error
|
111
|
+
expect(error).to be_instance_of(ROM::SQL::UniqueConstraintError)
|
112
|
+
expect(passed).to be(false)
|
113
|
+
end
|
107
114
|
}.to_not change { rom.relations.users.count }
|
108
115
|
end
|
109
116
|
|
@@ -138,30 +145,97 @@ describe 'Commands / Create' do
|
|
138
145
|
])
|
139
146
|
end
|
140
147
|
|
141
|
-
it '
|
142
|
-
|
148
|
+
it 're-raises not-null constraint violation error' do
|
149
|
+
expect {
|
150
|
+
users.try { users.create.call(name: nil) }
|
151
|
+
}.to raise_error(ROM::SQL::NotNullConstraintError, /not-null/)
|
152
|
+
end
|
143
153
|
|
144
|
-
|
145
|
-
expect
|
154
|
+
it 're-raises uniqueness constraint violation error' do
|
155
|
+
expect {
|
156
|
+
users.try {
|
157
|
+
users.create.call(name: 'Jane')
|
158
|
+
} >-> user {
|
159
|
+
users.try { users.create.call(name: user[:name]) }
|
160
|
+
}
|
161
|
+
}.to raise_error(ROM::SQL::UniqueConstraintError, /unique/)
|
146
162
|
end
|
147
163
|
|
148
|
-
it '
|
149
|
-
|
150
|
-
users.
|
151
|
-
|
152
|
-
|
153
|
-
}
|
164
|
+
it 're-raises check constraint violation error' do
|
165
|
+
expect {
|
166
|
+
users.try {
|
167
|
+
users.create.call(name: 'J')
|
168
|
+
}
|
169
|
+
}.to raise_error(ROM::SQL::CheckConstraintError, /name/)
|
170
|
+
end
|
154
171
|
|
155
|
-
|
156
|
-
expect
|
172
|
+
it 're-raises fk constraint violation error' do
|
173
|
+
expect {
|
174
|
+
tasks.try {
|
175
|
+
tasks.create.call(user_id: 918273645)
|
176
|
+
}
|
177
|
+
}.to raise_error(ROM::SQL::ForeignKeyConstraintError, /user_id/)
|
178
|
+
end
|
179
|
+
|
180
|
+
it 're-raises database errors' do
|
181
|
+
expect {
|
182
|
+
Params.attribute :bogus_field
|
183
|
+
users.try { users.create.call(name: 'some name', bogus_field: 23) }
|
184
|
+
}.to raise_error(ROM::SQL::DatabaseError)
|
157
185
|
end
|
158
186
|
|
159
|
-
|
160
|
-
|
187
|
+
describe '.associates' do
|
188
|
+
it 'sets foreign key prior execution for many tuples' do
|
189
|
+
setup.commands(:tasks) do
|
190
|
+
define(:create) do
|
191
|
+
associates :user, key: [:user_id, :id]
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
create_user = rom.command(:users).create.with(name: 'Jade')
|
196
|
+
|
197
|
+
create_task = rom.command(:tasks).create.with([
|
198
|
+
{ title: 'Task one' }, { title: 'Task two' }
|
199
|
+
])
|
200
|
+
|
201
|
+
command = create_user >> create_task
|
202
|
+
|
203
|
+
result = command.call
|
204
|
+
|
205
|
+
expect(result).to match_array([
|
206
|
+
{ id: 1, user_id: 1, title: 'Task one' },
|
207
|
+
{ id: 2, user_id: 1, title: 'Task two' }
|
208
|
+
])
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'sets foreign key prior execution for one tuple' do
|
212
|
+
setup.commands(:tasks) do
|
213
|
+
define(:create) do
|
214
|
+
result :one
|
215
|
+
associates :user, key: [:user_id, :id]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
create_user = rom.command(:users).create.with(name: 'Jade')
|
220
|
+
create_task = rom.command(:tasks).create.with(title: 'Task one')
|
221
|
+
|
222
|
+
command = create_user >> create_task
|
161
223
|
|
162
|
-
|
224
|
+
result = command.call
|
163
225
|
|
164
|
-
|
165
|
-
|
226
|
+
expect(result).to match_array(id: 1, user_id: 1, title: 'Task one')
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'raises when already defined' do
|
230
|
+
expect {
|
231
|
+
setup.commands(:tasks) do
|
232
|
+
define(:create) do
|
233
|
+
result :one
|
234
|
+
associates :user, key: [:user_id, :id]
|
235
|
+
associates :user, key: [:user_id, :id]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
}.to raise_error(ArgumentError, /user/)
|
239
|
+
end
|
166
240
|
end
|
167
241
|
end
|
@@ -53,15 +53,15 @@ describe 'Commands / Delete' do
|
|
53
53
|
expect(result.value).to eql(id: 2, name: 'Jane')
|
54
54
|
end
|
55
55
|
|
56
|
-
it '
|
56
|
+
it 're-raises database error' do
|
57
57
|
command = users.delete.by_name('Jane')
|
58
58
|
|
59
|
-
expect(command.relation).to receive(:delete).and_raise(
|
59
|
+
expect(command.relation).to receive(:delete).and_raise(
|
60
|
+
Sequel::DatabaseError, 'totally wrong'
|
61
|
+
)
|
60
62
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
expect(result.error).to be_a(ROM::SQL::DatabaseError)
|
65
|
-
expect(result.error.original_exception).to be_a(Sequel::DatabaseError)
|
63
|
+
expect {
|
64
|
+
users.try { command.call }
|
65
|
+
}.to raise_error(ROM::SQL::DatabaseError, /totally wrong/)
|
66
66
|
end
|
67
67
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'anima'
|
2
3
|
|
3
4
|
describe 'Commands / Update' do
|
4
5
|
include_context 'database setup'
|
5
6
|
|
6
|
-
subject(:users) { rom.
|
7
|
+
subject(:users) { rom.command(:users) }
|
7
8
|
|
8
9
|
let(:relation) { rom.relations.users }
|
9
10
|
let(:piotr) { relation.by_name('Piotr').first }
|
@@ -24,13 +25,21 @@ describe 'Commands / Update' do
|
|
24
25
|
define(:update)
|
25
26
|
end
|
26
27
|
|
28
|
+
User = Class.new { include Anima.new(:id, :name) }
|
29
|
+
|
30
|
+
setup.mappers do
|
31
|
+
register :users, entity: -> tuples { tuples.map { |tuple| User.new(tuple) } }
|
32
|
+
end
|
33
|
+
|
27
34
|
relation.insert(name: 'Piotr')
|
28
35
|
end
|
29
36
|
|
37
|
+
after { Object.send(:remove_const, :User) }
|
38
|
+
|
30
39
|
context '#transaction' do
|
31
40
|
it 'update record if there was no errors' do
|
32
41
|
result = users.update.transaction do
|
33
|
-
users.update.by_id(piotr[:id]).
|
42
|
+
users.update.by_id(piotr[:id]).call(peter)
|
34
43
|
end
|
35
44
|
|
36
45
|
expect(result.value).to eq([{ id: 1, name: 'Peter' }])
|
@@ -38,7 +47,7 @@ describe 'Commands / Update' do
|
|
38
47
|
|
39
48
|
it 'updates nothing if error was raised' do
|
40
49
|
users.update.transaction do
|
41
|
-
users.update.by_id(piotr[:id]).
|
50
|
+
users.update.by_id(piotr[:id]).call(peter)
|
42
51
|
raise ROM::SQL::Rollback
|
43
52
|
end
|
44
53
|
|
@@ -48,7 +57,7 @@ describe 'Commands / Update' do
|
|
48
57
|
|
49
58
|
it 'updates everything when there is no original tuple' do
|
50
59
|
result = users.try do
|
51
|
-
users.update.by_id(piotr[:id]).
|
60
|
+
users.update.by_id(piotr[:id]).call(peter)
|
52
61
|
end
|
53
62
|
|
54
63
|
expect(result.value.to_a).to match_array([{ id: 1, name: 'Peter' }])
|
@@ -56,10 +65,10 @@ describe 'Commands / Update' do
|
|
56
65
|
|
57
66
|
it 'updates when attributes changed' do
|
58
67
|
result = users.try do
|
59
|
-
users.update.by_id(piotr[:id]).change(piotr).
|
68
|
+
users.as(:entity).update.by_id(piotr[:id]).change(User.new(piotr)).call(peter)
|
60
69
|
end
|
61
70
|
|
62
|
-
expect(result.value.to_a).to match_array([
|
71
|
+
expect(result.value.to_a).to match_array([User.new(id: 1, name: 'Peter')])
|
63
72
|
end
|
64
73
|
|
65
74
|
it 'does not update when attributes did not change' do
|
@@ -69,19 +78,15 @@ describe 'Commands / Update' do
|
|
69
78
|
expect(piotr_rel).not_to receive(:update)
|
70
79
|
|
71
80
|
result = users.try do
|
72
|
-
users.update.by_id(piotr[:id]).change(piotr).
|
81
|
+
users.update.by_id(piotr[:id]).change(piotr).call(name: piotr[:name])
|
73
82
|
end
|
74
83
|
|
75
84
|
expect(result.value.to_a).to be_empty
|
76
85
|
end
|
77
86
|
|
78
|
-
it '
|
79
|
-
|
80
|
-
users.update.by_id(piotr[:id]).
|
81
|
-
|
82
|
-
|
83
|
-
expect(result.value).to be(nil)
|
84
|
-
expect(result.error).to be_a(ROM::SQL::DatabaseError)
|
85
|
-
expect(result.error.original_exception).to be_a(Sequel::DatabaseError)
|
87
|
+
it 're-reaises database errors' do
|
88
|
+
expect {
|
89
|
+
users.try { users.update.by_id(piotr[:id]).call(bogus_field: '#trollface') }
|
90
|
+
}.to raise_error(ROM::SQL::DatabaseError, /bogus_field/)
|
86
91
|
end
|
87
92
|
end
|
@@ -7,7 +7,7 @@ shared_context 'database setup' do
|
|
7
7
|
let(:setup) { ROM.setup(:sql, conn) }
|
8
8
|
|
9
9
|
def drop_tables
|
10
|
-
[:
|
10
|
+
[:tasks, :users, :tags, :task_tags, :rabbits, :carrots, :schema_migrations].each do |name|
|
11
11
|
conn.drop_table?(name)
|
12
12
|
end
|
13
13
|
end
|
@@ -21,11 +21,12 @@ shared_context 'database setup' do
|
|
21
21
|
primary_key :id
|
22
22
|
String :name, null: false
|
23
23
|
index :name, unique: true
|
24
|
+
check { char_length(name) > 2 }
|
24
25
|
end
|
25
26
|
|
26
27
|
conn.create_table :tasks do
|
27
28
|
primary_key :id
|
28
|
-
|
29
|
+
foreign_key :user_id, :users
|
29
30
|
String :title
|
30
31
|
end
|
31
32
|
|
@@ -3,9 +3,14 @@ require 'spec_helper'
|
|
3
3
|
describe 'Defining many-to-one association' do
|
4
4
|
include_context 'users and tasks'
|
5
5
|
|
6
|
+
before do
|
7
|
+
conn[:users].insert id: 2, name: 'Jane'
|
8
|
+
conn[:tasks].insert id: 2, user_id: 2, title: 'Task one'
|
9
|
+
end
|
10
|
+
|
6
11
|
it 'extends relation with association methods' do
|
7
12
|
setup.relation(:tasks) do
|
8
|
-
many_to_one :users, key: :user_id
|
13
|
+
many_to_one :users, key: :user_id, on: { name: 'Piotr' }
|
9
14
|
|
10
15
|
def all
|
11
16
|
select(:id, :title)
|
@@ -4,6 +4,9 @@ describe 'Defining one-to-many association' do
|
|
4
4
|
include_context 'users and tasks'
|
5
5
|
|
6
6
|
before do
|
7
|
+
conn[:users].insert id: 2, name: 'Jane'
|
8
|
+
conn[:tasks].insert id: 2, user_id: 2, title: 'Task one'
|
9
|
+
|
7
10
|
setup.mappers do
|
8
11
|
define(:users)
|
9
12
|
|
@@ -15,7 +18,7 @@ describe 'Defining one-to-many association' do
|
|
15
18
|
|
16
19
|
it 'extends relation with association methods' do
|
17
20
|
setup.relation(:users) do
|
18
|
-
one_to_many :tasks, key: :user_id
|
21
|
+
one_to_many :tasks, key: :user_id, on: { title: 'Finish ROM' }
|
19
22
|
|
20
23
|
def by_name(name)
|
21
24
|
where(name: name)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rom-sql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Piotr Solnica
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-05-
|
11
|
+
date: 2015-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -123,6 +123,7 @@ files:
|
|
123
123
|
- lib/rom/sql/migration.rb
|
124
124
|
- lib/rom/sql/migration/migrator.rb
|
125
125
|
- lib/rom/sql/migration/template.rb
|
126
|
+
- lib/rom/sql/plugin/associates.rb
|
126
127
|
- lib/rom/sql/plugin/pagination.rb
|
127
128
|
- lib/rom/sql/rake_task.rb
|
128
129
|
- lib/rom/sql/relation.rb
|