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