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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fae231a134bab168af4e2cc51bc9f525839165aa
4
- data.tar.gz: dcaa467ae85f00b2298d7a11ba24612bd1db17b6
3
+ metadata.gz: bbd3084b35810eb5773f42d14ae8c75de6d6de0f
4
+ data.tar.gz: 9dfa889eed6b3005a6c6385c70bceb78aa975fe5
5
5
  SHA512:
6
- metadata.gz: 4279201898c5ff53884bd2dd1d8743baea691965bd2e26c573a9453001deff5fe1f6e17b37ec05b1bf6c67907ea7f2e40ecc75301746921c310a72c6a29b4a9d
7
- data.tar.gz: 15878ffaebb4557cb9bd1b95fa3c1326d21a91017175f532cad871bb3cddeae039bf80f72e4139735ad932af589b3d6a05cf97c0269c2600c165e3d3c79f9914
6
+ metadata.gz: 9d2ac005bf0b9bd1ce789d9e34822581c66510c1861c3e5bb7d6b778bc8526ef287cfc77a3aa3e204eb5c8c2c744e88fb092417c66c3bb19d74734d022f70abb
7
+ data.tar.gz: f10f50b34e35e892014a394ae54e606d42bd8f8750c3640674e09116553c5b73a5846578a02e66c15011ceed60aa8d85d8e982a6bbef0ce482be18b66b28b4e2
@@ -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
@@ -4,6 +4,7 @@ gemspec
4
4
 
5
5
  group :test do
6
6
  gem 'byebug', platforms: :mri
7
+ gem 'anima'
7
8
  gem 'rom', github: 'rom-rb/rom', branch: 'master'
8
9
  gem 'virtus'
9
10
  gem 'activesupport'
@@ -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 DatabaseError < ROM::CommandError
8
+ class Error < StandardError
10
9
  attr_reader :original_exception
11
10
 
12
- def initialize(error, message)
13
- super(message)
14
- @original_exception = error
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
@@ -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 = Array([tuples]).flatten.map do |tuple|
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 *ERRORS => e
8
- raise ConstraintError, e.message
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, type: Hash, reader: true
21
+ option :original, reader: true
13
22
 
14
- alias_method :to, :call
15
- alias_method :set, :call
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
- private
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
@@ -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 :header, :table
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
- # Return a header for this relation
33
+ # Project a relation
28
34
  #
29
- # @return [Header]
35
+ # This method is intended to be used internally within a relation object
30
36
  #
31
- # @api private
32
- def header
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
- # @return [Array<Symbol>]
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, key, type, join_type, options)
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, key, type, join_type, options)
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!
@@ -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
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '0.4.3'.freeze
3
+ VERSION = '0.5.0'.freeze
4
4
  end
5
5
  end
@@ -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
- passed = false
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
- expect(result.value).to be(nil)
105
- expect(result.error).to_not be(nil)
106
- expect(passed).to be(false)
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 'handles not-null constraint violation error' do
142
- result = users.try { users.create.call(name: nil) }
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
- expect(result.error).to be_instance_of(ROM::SQL::ConstraintError)
145
- expect(result.error.message).to match(/not-null/)
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 'handles uniqueness constraint violation error' do
149
- result = users.try {
150
- users.create.call(name: 'Jane')
151
- } >-> user {
152
- users.try { users.create.call(name: user[:name]) }
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
- expect(result.error).to be_instance_of(ROM::SQL::ConstraintError)
156
- expect(result.error.message).to match(/unique/)
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
- it 'handles database errors' do
160
- Params.attribute :bogus_field
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
- result = users.try { users.create.call(name: 'some name', bogus_field: 23) }
224
+ result = command.call
163
225
 
164
- expect(result.error).to be_instance_of(ROM::SQL::DatabaseError)
165
- expect(result.error.original_exception).to be_instance_of(Sequel::DatabaseError)
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 'handles database errors' do
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(Sequel::DatabaseError)
59
+ expect(command.relation).to receive(:delete).and_raise(
60
+ Sequel::DatabaseError, 'totally wrong'
61
+ )
60
62
 
61
- result = users.try { command.call }
62
-
63
- expect(result.value).to be(nil)
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.commands.users }
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]).set(peter)
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]).set(peter)
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]).set(peter)
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).to(peter)
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([{ id: 1, name: 'Peter' }])
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).to(name: piotr[:name])
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 'handles database errors' do
79
- result = users.try do
80
- users.update.by_id(piotr[:id]).set(bogus_field: '#trollface')
81
- end
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
- [:users, :tasks, :tags, :task_tags, :rabbits, :carrots, :schema_migrations].each do |name|
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
- Integer :user_id
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.3
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-17 00:00:00.000000000 Z
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