rom-sql 0.7.0 → 0.8.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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +12 -7
  4. data/CHANGELOG.md +28 -0
  5. data/Gemfile +6 -9
  6. data/README.md +5 -4
  7. data/circle.yml +10 -0
  8. data/lib/rom/plugins/relation/sql/auto_combine.rb +16 -3
  9. data/lib/rom/plugins/relation/sql/auto_wrap.rb +3 -2
  10. data/lib/rom/sql/association.rb +75 -0
  11. data/lib/rom/sql/association/many_to_many.rb +86 -0
  12. data/lib/rom/sql/association/many_to_one.rb +60 -0
  13. data/lib/rom/sql/association/name.rb +70 -0
  14. data/lib/rom/sql/association/one_to_many.rb +9 -0
  15. data/lib/rom/sql/association/one_to_one.rb +46 -0
  16. data/lib/rom/sql/association/one_to_one_through.rb +9 -0
  17. data/lib/rom/sql/commands.rb +2 -0
  18. data/lib/rom/sql/commands/create.rb +2 -2
  19. data/lib/rom/sql/commands/delete.rb +0 -1
  20. data/lib/rom/sql/commands/postgres.rb +76 -0
  21. data/lib/rom/sql/commands/update.rb +6 -3
  22. data/lib/rom/sql/commands_ext/postgres.rb +17 -0
  23. data/lib/rom/sql/gateway.rb +23 -15
  24. data/lib/rom/sql/header.rb +7 -1
  25. data/lib/rom/sql/plugin/assoc_macros.rb +3 -3
  26. data/lib/rom/sql/plugin/associates.rb +50 -9
  27. data/lib/rom/sql/qualified_attribute.rb +53 -0
  28. data/lib/rom/sql/relation.rb +76 -25
  29. data/lib/rom/sql/relation/reading.rb +138 -35
  30. data/lib/rom/sql/relation/writing.rb +21 -0
  31. data/lib/rom/sql/schema.rb +35 -0
  32. data/lib/rom/sql/schema/associations_dsl.rb +68 -0
  33. data/lib/rom/sql/schema/dsl.rb +27 -0
  34. data/lib/rom/sql/schema/inferrer.rb +80 -0
  35. data/lib/rom/sql/support/active_support_notifications.rb +27 -17
  36. data/lib/rom/sql/types.rb +11 -0
  37. data/lib/rom/sql/types/pg.rb +26 -0
  38. data/lib/rom/sql/version.rb +1 -1
  39. data/rom-sql.gemspec +4 -2
  40. data/spec/integration/association/many_to_many_spec.rb +137 -0
  41. data/spec/integration/association/many_to_one_spec.rb +110 -0
  42. data/spec/integration/association/one_to_many_spec.rb +58 -0
  43. data/spec/integration/association/one_to_one_spec.rb +57 -0
  44. data/spec/integration/association/one_to_one_through_spec.rb +90 -0
  45. data/spec/integration/combine_spec.rb +24 -24
  46. data/spec/integration/commands/create_spec.rb +215 -168
  47. data/spec/integration/commands/delete_spec.rb +88 -46
  48. data/spec/integration/commands/update_spec.rb +141 -60
  49. data/spec/integration/commands/upsert_spec.rb +83 -0
  50. data/spec/integration/gateway_spec.rb +9 -17
  51. data/spec/integration/migration_spec.rb +3 -5
  52. data/spec/integration/plugins/associates_spec.rb +168 -0
  53. data/spec/integration/plugins/auto_wrap_spec.rb +46 -0
  54. data/spec/integration/read_spec.rb +80 -77
  55. data/spec/integration/relation_schema_spec.rb +180 -0
  56. data/spec/integration/schema_inference_spec.rb +67 -0
  57. data/spec/integration/setup_spec.rb +22 -0
  58. data/spec/{support → integration/support}/active_support_notifications_spec.rb +0 -0
  59. data/spec/{support → integration/support}/rails_log_subscriber_spec.rb +0 -0
  60. data/spec/shared/database_setup.rb +46 -8
  61. data/spec/shared/relations.rb +8 -0
  62. data/spec/shared/users_and_accounts.rb +10 -0
  63. data/spec/shared/users_and_tasks.rb +20 -2
  64. data/spec/spec_helper.rb +64 -11
  65. data/spec/support/helpers.rb +9 -0
  66. data/spec/unit/association/many_to_many_spec.rb +89 -0
  67. data/spec/unit/association/many_to_one_spec.rb +81 -0
  68. data/spec/unit/association/name_spec.rb +68 -0
  69. data/spec/unit/association/one_to_many_spec.rb +62 -0
  70. data/spec/unit/association/one_to_one_spec.rb +62 -0
  71. data/spec/unit/association/one_to_one_through_spec.rb +69 -0
  72. data/spec/unit/association_errors_spec.rb +2 -4
  73. data/spec/unit/gateway_spec.rb +12 -3
  74. data/spec/unit/migration_tasks_spec.rb +3 -3
  75. data/spec/unit/migrator_spec.rb +2 -4
  76. data/spec/unit/{combined_associations_spec.rb → plugin/assoc_macros/combined_associations_spec.rb} +13 -19
  77. data/spec/unit/{many_to_many_spec.rb → plugin/assoc_macros/many_to_many_spec.rb} +9 -15
  78. data/spec/unit/{many_to_one_spec.rb → plugin/assoc_macros/many_to_one_spec.rb} +9 -14
  79. data/spec/unit/plugin/assoc_macros/one_to_many_spec.rb +78 -0
  80. data/spec/unit/plugin/base_view_spec.rb +11 -11
  81. data/spec/unit/plugin/pagination_spec.rb +62 -62
  82. data/spec/unit/relation_spec.rb +218 -146
  83. data/spec/unit/schema_spec.rb +15 -14
  84. data/spec/unit/types_spec.rb +40 -0
  85. metadata +105 -21
  86. data/.rubocop.yml +0 -74
  87. data/.rubocop_todo.yml +0 -21
  88. data/spec/unit/one_to_many_spec.rb +0 -83
@@ -0,0 +1,53 @@
1
+ require 'rom/support/cache'
2
+
3
+ module ROM
4
+ module SQL
5
+ # Used as a pair table name + field name.
6
+ # Similar to Sequel::SQL::QualifiedIdentifier but we don't want
7
+ # Sequel types to leak into ROM
8
+ #
9
+ # @api private
10
+ class QualifiedAttribute
11
+ include Dry::Equalizer(:dataset, :attribute)
12
+
13
+ extend Cache
14
+
15
+ # Dataset (table) name
16
+ #
17
+ # @api private
18
+ attr_reader :dataset
19
+
20
+ # Attribute (field, column) name
21
+ #
22
+ # @api private
23
+ attr_reader :attribute
24
+
25
+ # @api private
26
+ def self.[](*args)
27
+ fetch_or_store(args) { new(*args) }
28
+ end
29
+
30
+ # @api private
31
+ def initialize(dataset, attribute)
32
+ @dataset = dataset
33
+ @attribute = attribute
34
+ end
35
+
36
+ # Used by Sequel for building SQL statements
37
+ #
38
+ # @api private
39
+ def sql_literal_append(ds, sql)
40
+ ds.qualified_identifier_sql_append(sql, dataset, attribute)
41
+ end
42
+
43
+ # Convinient interface for attribute names
44
+ #
45
+ # @return [Symbol]
46
+ #
47
+ # @api private
48
+ def to_sym
49
+ attribute
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,7 @@
1
1
  require 'rom/sql/header'
2
+ require 'rom/sql/types'
3
+
4
+ require 'rom/sql/schema'
2
5
 
3
6
  require 'rom/sql/relation/reading'
4
7
  require 'rom/sql/relation/writing'
@@ -9,11 +12,17 @@ require 'rom/plugins/relation/sql/base_view'
9
12
  require 'rom/plugins/relation/sql/auto_combine'
10
13
  require 'rom/plugins/relation/sql/auto_wrap'
11
14
 
15
+ require 'rom/support/deprecations'
16
+ require 'rom/support/constants'
17
+
12
18
  module ROM
13
19
  module SQL
14
20
  # Sequel-specific relation extensions
15
21
  #
22
+ # @api public
16
23
  class Relation < ROM::Relation
24
+ include SQL
25
+
17
26
  adapter :sql
18
27
 
19
28
  use :key_inference
@@ -25,14 +34,6 @@ module ROM
25
34
  include Writing
26
35
  include Reading
27
36
 
28
- # @attr_reader [Header] header Internal lazy-initialized header
29
- attr_reader :header
30
-
31
- # Name of the table used in FROM clause
32
- #
33
- # @attr_reader [Symbol] table
34
- attr_reader :table
35
-
36
37
  # Set default dataset for a relation sub-class
37
38
  #
38
39
  # @api private
@@ -40,38 +41,74 @@ module ROM
40
41
  super
41
42
 
42
43
  klass.class_eval do
44
+ schema_dsl SQL::Schema::DSL
45
+ schema_inferrer ROM::SQL::Schema::Inferrer
46
+
43
47
  dataset do
44
48
  table = opts[:from].first
45
49
 
46
50
  if db.table_exists?(table)
47
- # quick fix for dbs w/o primary_key inference
48
- #
49
- # TODO: add a way of setting a pk explicitly on a relation
50
- pk =
51
- if db.respond_to?(:primary_key)
52
- Array(db.primary_key(table))
53
- else
54
- [:id]
55
- end.map { |name| :"#{table}__#{name}" }
56
-
57
- select(*columns).order(*pk)
51
+ pk_header = klass.primary_key_header(db, table)
52
+ select(*columns).order(*pk_header.qualified)
58
53
  else
59
54
  self
60
55
  end
61
56
  end
57
+
58
+ # @!method by_pk(pk)
59
+ # Return a relation restricted by its primary key
60
+ # @param [Object] pk The primary key value
61
+ # @return [SQL::Relation]
62
+ # @api public
63
+ view(:by_pk, attributes[:base]) do |pk|
64
+ where(primary_key => pk)
65
+ end
62
66
  end
63
67
  end
64
68
 
69
+ # @api private
70
+ def self.associations
71
+ schema.associations
72
+ end
73
+
74
+ # @api private
75
+ def self.primary_key_header(db, table)
76
+ names =
77
+ if schema
78
+ schema.primary_key_names
79
+ elsif db.respond_to?(:primary_key)
80
+ Array(db.primary_key(table))
81
+ else
82
+ [:id]
83
+ end
84
+ Header.new(names, table)
85
+ end
86
+
87
+ # Set primary key
88
+ #
89
+ # @deprecated
90
+ #
91
+ # @api public
65
92
  def self.primary_key(value)
93
+ Deprecations.announce(
94
+ :primary_key, "use schema definition to configure primary key"
95
+ )
66
96
  option :primary_key, reader: true, default: value
67
97
  end
68
98
 
69
- primary_key :id
99
+ option :primary_key, reader: true, default: -> rel {
100
+ rel.schema? ? rel.schema.primary_key_name : :id
101
+ }
70
102
 
103
+ # Return table name from relation's sql statement
104
+ #
105
+ # This value is used by `header` for prefixing column names
106
+ #
107
+ # @return [Symbol]
108
+ #
71
109
  # @api private
72
- def initialize(dataset, registry = {})
73
- super
74
- @table = dataset.opts[:from].first
110
+ def table
111
+ @table ||= dataset.opts[:from].first
75
112
  end
76
113
 
77
114
  # Return a header for this relation
@@ -80,7 +117,7 @@ module ROM
80
117
  #
81
118
  # @api private
82
119
  def header
83
- @header ||= Header.new(dataset.opts[:select] || dataset.columns, table)
120
+ @header ||= Header.new(selected_columns, table)
84
121
  end
85
122
 
86
123
  # Return raw column names
@@ -89,7 +126,21 @@ module ROM
89
126
  #
90
127
  # @api private
91
128
  def columns
92
- dataset.columns
129
+ @columns ||= dataset.columns
130
+ end
131
+
132
+ protected
133
+
134
+ # Return a list of columns from *the sql select* statement or default to
135
+ # dataset columns
136
+ #
137
+ # This is used to construct relation's header
138
+ #
139
+ # @return [Array<Symbol>]
140
+ #
141
+ # @api private
142
+ def selected_columns
143
+ @selected_columns ||= dataset.opts.fetch(:select, columns)
93
144
  end
94
145
  end
95
146
  end
@@ -1,11 +1,30 @@
1
1
  module ROM
2
2
  module SQL
3
3
  class Relation < ROM::Relation
4
+ # Query API for SQL::Relation
5
+ #
6
+ # @api public
4
7
  module Reading
8
+ # Fetch a tuple identified by the pk
9
+ #
10
+ # @example
11
+ # users.fetch(1)
12
+ # # {:id => 1, name: "Jane"}
13
+ #
14
+ # @return [Relation]
15
+ #
16
+ # @raise [ROM::TupleCountMismatchError] When 0 or more than 1 tuples were found
17
+ #
18
+ # @api public
19
+ def fetch(pk)
20
+ by_pk(pk).one!
21
+ end
22
+
5
23
  # Return relation count
6
24
  #
7
25
  # @example
8
- # users.count # => 12
26
+ # users.count
27
+ # # => 12
9
28
  #
10
29
  # @return [Relation]
11
30
  #
@@ -18,8 +37,9 @@ module ROM
18
37
  #
19
38
  # @example
20
39
  # users.first
40
+ # # {:id => 1, :name => "Jane"}
21
41
  #
22
- # @return [Relation]
42
+ # @return [Hash]
23
43
  #
24
44
  # @api public
25
45
  def first
@@ -30,8 +50,9 @@ module ROM
30
50
  #
31
51
  # @example
32
52
  # users.last
53
+ # # {:id => 2, :name => "Joe"}
33
54
  #
34
- # @return [Relation]
55
+ # @return [Hash]
35
56
  #
36
57
  # @api public
37
58
  def last
@@ -43,7 +64,8 @@ module ROM
43
64
  # This method is intended to be used internally within a relation object
44
65
  #
45
66
  # @example
46
- # rom.relation(:users) { |r| r.prefix(:user) }
67
+ # users.prefix(:user).to_a
68
+ # # {:user_id => 1, :user_name => "Jane"}
47
69
  #
48
70
  # @param [Symbol] name The prefix
49
71
  #
@@ -59,7 +81,7 @@ module ROM
59
81
  # This method is intended to be used internally within a relation object
60
82
  #
61
83
  # @example
62
- # rom.relation(:users) { |r| r.qualified }
84
+ # users.qualified
63
85
  #
64
86
  # @return [Relation]
65
87
  #
@@ -72,39 +94,49 @@ module ROM
72
94
  #
73
95
  # This method is intended to be used internally within a relation object
74
96
  #
75
- # @return [Relation]
97
+ # @example
98
+ # users.qualified_columns
99
+ # # [:users__id, :users__name]
100
+ #
101
+ # @return [Array<Symbol>]
76
102
  #
77
103
  # @api public
78
104
  def qualified_columns
79
105
  header.qualified.to_a
80
106
  end
81
107
 
82
- # Return if a restricted relation has 0 tuples
108
+ # Map tuples from the relation
83
109
  #
84
110
  # @example
85
- # users.unique?(email: 'jane@doe.org') # true
111
+ # users.map { |user| user[:id] }
112
+ # # [1, 2, 3]
86
113
  #
87
- # users.insert(email: 'jane@doe.org')
114
+ # users.map(:id).to_a
115
+ # # [1, 2, 3]
88
116
  #
89
- # users.unique?(email: 'jane@doe.org') # false
90
- #
91
- # @param [Hash] criteria hash for the where clause
92
- #
93
- # @return [Relation]
117
+ # @param [Symbol] key An optional name of the key for extracting values
118
+ # from tuples
94
119
  #
95
120
  # @api public
96
- def unique?(criteria)
97
- where(criteria).count.zero?
121
+ def map(key = nil, &block)
122
+ if key
123
+ dataset.map(key, &block)
124
+ else
125
+ dataset.map(&block)
126
+ end
98
127
  end
99
128
 
100
- # Map tuples from the relation
129
+ # Pluck values from a specific column
101
130
  #
102
131
  # @example
103
- # users.map { |user| ... }
132
+ # users.pluck(:id)
133
+ # # [1, 2, 3]
134
+ #
135
+ # @return [Array]
104
136
  #
105
137
  # @api public
106
- def map(&block)
107
- dataset.map(&block)
138
+ def pluck(name)
139
+ map(name)
108
140
  end
109
141
 
110
142
  # Project a relation
@@ -112,9 +144,9 @@ module ROM
112
144
  # This method is intended to be used internally within a relation object
113
145
  #
114
146
  # @example
115
- # rom.relation(:users) { |r| r.project(:id, :name) }
147
+ # users.project(:id, :name) }
116
148
  #
117
- # @param [Symbol] names A list of symbol column names
149
+ # @param [Symbol] *names A list of symbol column names
118
150
  #
119
151
  # @return [Relation]
120
152
  #
@@ -128,9 +160,10 @@ module ROM
128
160
  # This method is intended to be used internally within a relation object
129
161
  #
130
162
  # @example
131
- # rom.relation(:users) { |r| r.rename(name: :user_name) }
163
+ # users.rename(name: :user_name).first
164
+ # # {:id => 1, :user_name => "Jane" }
132
165
  #
133
- # @param [Hash] options A name => new_name map
166
+ # @param [Hash<Symbol=>Symbol>] options A name => new_name map
134
167
  #
135
168
  # @return [Relation]
136
169
  #
@@ -142,7 +175,8 @@ module ROM
142
175
  # Select specific columns for select clause
143
176
  #
144
177
  # @example
145
- # users.select(:id, :name)
178
+ # users.select(:id, :name).first
179
+ # # {:id => 1, :name => "Jane" }
146
180
  #
147
181
  # @return [Relation]
148
182
  #
@@ -155,6 +189,9 @@ module ROM
155
189
  #
156
190
  # @example
157
191
  # users.select(:id, :name).select_append(:email)
192
+ # # {:id => 1, :name => "Jane", :email => "jane@doe.org"}
193
+ #
194
+ # @param [Array<Symbol>] *args A list with column names
158
195
  #
159
196
  # @return [Relation]
160
197
  #
@@ -168,6 +205,8 @@ module ROM
168
205
  # @example
169
206
  # users.distinct(:country)
170
207
  #
208
+ # @param [Array<Symbol>] *args A list with column names
209
+ #
171
210
  # @return [Relation]
172
211
  #
173
212
  # @api public
@@ -180,7 +219,9 @@ module ROM
180
219
  # @example
181
220
  # users.sum(:age)
182
221
  #
183
- # @return Number
222
+ # @param [Array<Symbol>] *args A list with column names
223
+ #
224
+ # @return [Integer]
184
225
  #
185
226
  # @api public
186
227
  def sum(*args)
@@ -192,6 +233,8 @@ module ROM
192
233
  # @example
193
234
  # users.min(:age)
194
235
  #
236
+ # @param [Array<Symbol>] *args A list with column names
237
+ #
195
238
  # @return Number
196
239
  #
197
240
  # @api public
@@ -204,6 +247,8 @@ module ROM
204
247
  # @example
205
248
  # users.max(:age)
206
249
  #
250
+ # @param [Array<Symbol>] *args A list with column names
251
+ #
207
252
  # @return Number
208
253
  #
209
254
  # @api public
@@ -216,6 +261,8 @@ module ROM
216
261
  # @example
217
262
  # users.avg(:age)
218
263
  #
264
+ # @param [Array<Symbol>] *args A list with column names
265
+ #
219
266
  # @return Number
220
267
  #
221
268
  # @api public
@@ -225,11 +272,20 @@ module ROM
225
272
 
226
273
  # Restrict a relation to match criteria
227
274
  #
275
+ # If block is passed it'll be executed in the context of a condition
276
+ # builder object.
277
+ #
228
278
  # @example
229
279
  # users.where(name: 'Jane')
230
280
  #
281
+ # users.where { age >= 18 }
282
+ #
283
+ # @param [Hash] *args An optional hash with conditions for WHERE clause
284
+ #
231
285
  # @return [Relation]
232
286
  #
287
+ # @see http://sequel.jeremyevans.net/rdoc/files/doc/dataset_filtering_rdoc.html
288
+ #
233
289
  # @api public
234
290
  def where(*args, &block)
235
291
  __new__(dataset.__send__(__method__, *args, &block))
@@ -240,6 +296,8 @@ module ROM
240
296
  # @example
241
297
  # users.exclude(name: 'Jane')
242
298
  #
299
+ # @param [Hash] *args A hash with conditions for exclusion
300
+ #
243
301
  # @return [Relation]
244
302
  #
245
303
  # @api public
@@ -247,7 +305,8 @@ module ROM
247
305
  __new__(dataset.__send__(__method__, *args, &block))
248
306
  end
249
307
 
250
- # Inverts a request
308
+ # Inverts the current WHERE and HAVING clauses. If there is neither a
309
+ # WHERE or HAVING clause, adds a WHERE clause that is always false.
251
310
  #
252
311
  # @example
253
312
  # users.exclude(name: 'Jane').invert
@@ -258,8 +317,8 @@ module ROM
258
317
  # @return [Relation]
259
318
  #
260
319
  # @api public
261
- def invert(*args, &block)
262
- __new__(dataset.__send__(__method__, *args, &block))
320
+ def invert
321
+ __new__(dataset.invert)
263
322
  end
264
323
 
265
324
  # Set order for the relation
@@ -267,6 +326,8 @@ module ROM
267
326
  # @example
268
327
  # users.order(:name)
269
328
  #
329
+ # @param [Array<Symbol>] *args A list with column names
330
+ #
270
331
  # @return [Relation]
271
332
  #
272
333
  # @api public
@@ -291,6 +352,11 @@ module ROM
291
352
  # @example
292
353
  # users.limit(1)
293
354
  #
355
+ # users.limit(10, 2)
356
+ #
357
+ # @param [Integer] limit The limit value
358
+ # @param [Integer] offset An optional offset
359
+ #
294
360
  # @return [Relation]
295
361
  #
296
362
  # @api public
@@ -303,6 +369,8 @@ module ROM
303
369
  # @example
304
370
  # users.limit(10).offset(2)
305
371
  #
372
+ # @param [Integer] num The offset value
373
+ #
306
374
  # @return [Relation]
307
375
  #
308
376
  # @api public
@@ -310,7 +378,10 @@ module ROM
310
378
  __new__(dataset.__send__(__method__, *args, &block))
311
379
  end
312
380
 
313
- # Join other relation using inner join
381
+ # Join with another relation using INNER JOIN
382
+ #
383
+ # @example
384
+ # users.inner_join(:tasks, id: :user_id)
314
385
  #
315
386
  # @param [Symbol] relation name
316
387
  # @param [Hash] join keys
@@ -322,7 +393,10 @@ module ROM
322
393
  __new__(dataset.__send__(__method__, *args, &block))
323
394
  end
324
395
 
325
- # Join other relation using left outer join
396
+ # Join other relation using LEFT OUTER JOIN
397
+ #
398
+ # @example
399
+ # users.left_join(:tasks, id: :user_id)
326
400
  #
327
401
  # @param [Symbol] relation name
328
402
  # @param [Hash] join keys
@@ -339,6 +413,8 @@ module ROM
339
413
  # @example
340
414
  # tasks.group(:user_id)
341
415
  #
416
+ # @param [Array<Symbol>] *args A list of column names
417
+ #
342
418
  # @return [Relation]
343
419
  #
344
420
  # @api public
@@ -352,6 +428,8 @@ module ROM
352
428
  # tasks.group_and_count(:user_id)
353
429
  # # => [{ user_id: 1, count: 2 }, { user_id: 2, count: 3 }]
354
430
  #
431
+ # @param [Array<Symbol>] *args A list of column names
432
+ #
355
433
  # @return [Relation]
356
434
  #
357
435
  # @api public
@@ -365,6 +443,8 @@ module ROM
365
443
  # tasks.select_group(:user_id)
366
444
  # # => [{ user_id: 1 }, { user_id: 2 }]
367
445
  #
446
+ # @param [Array<Symbol>] *args A list of column names
447
+ #
368
448
  # @return [Relation]
369
449
  #
370
450
  # @api public
@@ -374,17 +454,40 @@ module ROM
374
454
 
375
455
  # Adds a UNION clause for relation dataset using second relation dataset
376
456
  #
377
- # @param [Relation] relation object
378
- #
379
457
  # @example
380
458
  # users.where(id: 1).union(users.where(id: 2))
381
459
  # # => [{ id: 1, name: 'Piotr' }, { id: 2, name: 'Jane' }]
382
460
  #
461
+ # @param [Relation] relation Another relation
462
+ #
463
+ # @param [Hash] options Options for union
464
+ # @option options [Symbol] :alias Use the given value as the #from_self alias
465
+ # @option options [TrueClass, FalseClass] :all Set to true to use UNION ALL instead of UNION, so duplicate rows can occur
466
+ # @option options [TrueClass, FalseClass] :from_self Set to false to not wrap the returned dataset in a #from_self, use with care.
467
+ #
383
468
  # @return [Relation]
384
469
  #
385
470
  # @api public
386
- def union(relation, *args, &block)
387
- __new__(dataset.__send__(__method__, relation.dataset, *args, &block))
471
+ def union(relation, options = EMPTY_HASH, &block)
472
+ __new__(dataset.__send__(__method__, relation.dataset, options, &block))
473
+ end
474
+
475
+ # Return if a restricted relation has 0 tuples
476
+ #
477
+ # @example
478
+ # users.unique?(email: 'jane@doe.org') # true
479
+ #
480
+ # users.insert(email: 'jane@doe.org')
481
+ #
482
+ # users.unique?(email: 'jane@doe.org') # false
483
+ #
484
+ # @param [Hash] criteria The condition hash for WHERE clause
485
+ #
486
+ # @return [TrueClass, FalseClass]
487
+ #
488
+ # @api public
489
+ def unique?(criteria)
490
+ where(criteria).count.zero?
388
491
  end
389
492
  end
390
493
  end