rom-sql 0.7.0 → 0.8.0

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