rom-sql 0.4.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|