rom-sql 2.5.0 → 3.0.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
  SHA256:
3
- metadata.gz: a266477f912328221111569376f3dc3a007e2db851d251255279f25a7bbe7b5e
4
- data.tar.gz: 440673756558a62398d09882dd3a29e76ea2ea21bb6c50c5c5a006a64d22fdf2
3
+ metadata.gz: 8e459278c95be56ea5da82ea6c6fec9a5bbc443f0865832cea04d3b1b56fbf5a
4
+ data.tar.gz: fa89dd7f60db5748c69eb85f804a1f2a9076ccfb22828e7dd9c27a7611c5bc2a
5
5
  SHA512:
6
- metadata.gz: b9efce415899ce279306fd604cce634c06cd70c06705cef9826ec990dbcee7f3c9fb70adc2a7bdebda62a6a7e2544aab77b06d5c4e3be3bda9c8083fb724dddd
7
- data.tar.gz: c784be269b61cfda971a8cc11c8cd0c6f0e4780c21d35c6fc521e7fad38585d59507b77698c3aa8a0f932559704bfba833b54e8506bb0e3fa3fe4620034c4d66
6
+ metadata.gz: 9496cf5afbb38f14782b9104ce05519b2b35674a6fe1ad799f9f34a7f717ad67f4bb79063720818159ff723789bf15e823f55a3d350e7fb30ab6c93607fb8807
7
+ data.tar.gz: 7a39e507a4ee369e45693d835a47852b2ccad837251b4b426e41496f63650dccd05038eb4dd47126f88b023e1b49a5fae4a2e4ad7af3745f43fd69c6b31f5142
@@ -1,3 +1,62 @@
1
+ ## 3.0.0 2019-04-24
2
+
3
+ ### Added
4
+
5
+ * Join DSL so that you can use arbitrary conditions when joining relations (flash-gordon)
6
+ ```ruby
7
+ users.join(tasks) { |users:, tasks:|
8
+ tasks[:user_id].is(users[:id]) & users[:name].is('John')
9
+ }
10
+ ```
11
+ You also can use table aliases, however the setup is a bit hairy:
12
+ ```ruby
13
+ # self-join "users" with itself using "authors" as an alias
14
+ authors = users.as(:authors).qualified(:authors)
15
+ result = users.join(authors) { |users: |
16
+ users[:id].is(authors[:id])
17
+ }.select(:name)
18
+ ```
19
+ * Support for `CASE` expression (wmaciejak + flash-gordon)
20
+ ```ruby
21
+ # matching expression result
22
+ users.select_append { id.case(1 => string('one'), else: string('something else')).as(:one_or_else) }
23
+
24
+ # searching for `true` result
25
+ users.select_append { string::case(id.is(1) => 'one', else: 'else').as(:one_or_else) }
26
+ ```
27
+ * Relations can be accessed in DSLs with keyword arguments (flash-gordon)
28
+ ```ruby
29
+ users.join(posts).select_append { |posts: | posts[:title] }
30
+ ```
31
+ * Support for `.exists` in the projection DSL (flash-gordon)
32
+ ```ruby
33
+ users.select_append { |posts: |
34
+ exists(posts.where(posts[:user_id] => id)).as(:has_posts)
35
+ }
36
+ ```
37
+ * `Relation#unfiltered` returns an unrestricted relation (removes restrictions from `WHERE` and `HAVING`) (flash-gordon)
38
+ * Support for `WITHIN GROUP` in the function DSL has been enhanced with block syntax (flash-gordon)
39
+ ```ruby
40
+ # previously available version
41
+ households.project { float::percentile_cont(0.5).within_group(income).as(:percentile) }
42
+ # using the new syntax
43
+ households.project { float::percentile_cont(0.5).within_group { income }.as(:percentile) }
44
+ ```
45
+ * Support for migrator options ie `ROM::Configuration.new(:sql, migrator: { path: "my_migrations" })` (rawburt)
46
+ * `Relation#pluck` works with multiple args too (timriley)
47
+
48
+ ### Changed
49
+
50
+ * [BREAKING] Updated to work with `dry-types 1.0.0` (flash-gordon)
51
+ * [BREAKING] `Types::Int` is now `Types::Integer` (GustavoCaso)
52
+
53
+ ### Fixed
54
+
55
+ - Using `Relation#inner_join` with has-many-through produces correct query (issue #279) (doriantaylor + solnic)
56
+ - Aliased attributes are handled correctly in `Relation#where` (waiting-for-dev)
57
+
58
+ [Compare v2.5.0...v3.0.0](https://github.com/rom-rb/rom-sql/compare/v2.5.0...v3.0.0)
59
+
1
60
  ## v2.5.0 2018-06-08
2
61
 
3
62
  ### Added
@@ -7,7 +66,7 @@
7
66
  tasks = relations[:tasks]
8
67
  users = relations[:users]
9
68
  user_tasks = tasks.where(tasks[:user_id].is(users[:id])
10
- tasks_count = user_tasks.select { int::count(id) }
69
+ tasks_count = user_tasks.select { integer::count(id) }
11
70
  users.select_append(tasks_count.as(:tasks_count))
12
71
  ```
13
72
 
@@ -146,7 +205,7 @@
146
205
  * Support for window function calls
147
206
 
148
207
  ```ruby
149
- employees.select { [dep_no, salary, int::avg(salary).over(partition: dep_no, order: id).as(:avg_salary)] }
208
+ employees.select { [dep_no, salary, integer::avg(salary).over(partition: dep_no, order: id).as(:avg_salary)] }
150
209
  ```
151
210
 
152
211
  * Function result can be negated, also `ROM::SQL::Function#not` was added (flash-gordon)
data/README.md CHANGED
@@ -34,6 +34,37 @@ Or install it yourself as:
34
34
 
35
35
  $ gem install rom-sql
36
36
 
37
+ ## Docker
38
+
39
+ ### Development
40
+
41
+ In order to have reproducible environment for development, Docker can be used. Provided it's installed, in order to start developing, one can simply execute:
42
+
43
+ ```bash
44
+ docker-compose run --rm gem "bash"
45
+ ```
46
+
47
+ If this is the first time this command is executed, it will take some time to set up the dependencies and build the rom-sql container. This should happen only on first execution and in case dependency images are removed.
48
+
49
+ After dependencies are set container will be started in a bash shell.
50
+
51
+ ### Testing
52
+
53
+ In order to test the changes, execute:
54
+
55
+ ```bash
56
+ docker-compose build gem
57
+ docker-compose run --rm gem 'rspec'
58
+ ```
59
+
60
+ ### Stopping the dependencies
61
+
62
+ In order to stop the dependencies, execute:
63
+
64
+ ```bash
65
+ docker-compose down --remove-orphans --volumes
66
+ ```
67
+
37
68
  ## License
38
69
 
39
70
  See `LICENSE` file.
@@ -36,8 +36,14 @@ module ROM
36
36
  # @api public
37
37
  def join(type, source = self.source, target = self.target)
38
38
  through_assoc = source.associations[through]
39
+
40
+ # first we join source to intermediary
39
41
  joined = through_assoc.join(type, source)
40
- joined.__send__(type, target.name.dataset, join_keys).qualified
42
+
43
+ # then we join intermediary to target
44
+ target_ds = target.name.dataset
45
+ through_jk = through_assoc.target.associations[target_ds].join_keys
46
+ joined.__send__(type, target_ds, through_jk).qualified
41
47
  end
42
48
 
43
49
  # @api public
@@ -26,8 +26,6 @@ module ROM
26
26
  fetch_or_store(args) { new(*args) }
27
27
  end
28
28
 
29
- option :extensions, type: Types::Hash, default: -> { TypeExtensions[type] }
30
-
31
29
  # Return a new attribute with an alias
32
30
  #
33
31
  # @example
@@ -36,8 +34,10 @@ module ROM
36
34
  # @return [SQL::Attribute]
37
35
  #
38
36
  # @api public
39
- def aliased(name)
40
- super.meta(name: meta.fetch(:name, name), sql_expr: sql_expr.as(name))
37
+ def aliased(alias_name)
38
+ super.with(name: name || alias_name).meta(
39
+ sql_expr: sql_expr.as(alias_name)
40
+ )
41
41
  end
42
42
  alias_method :as, :aliased
43
43
 
@@ -46,7 +46,7 @@ module ROM
46
46
  # @api public
47
47
  def canonical
48
48
  if aliased?
49
- meta(alias: nil, sql_expr: nil)
49
+ with(alias: nil).meta(sql_expr: nil)
50
50
  else
51
51
  self
52
52
  end
@@ -62,11 +62,12 @@ module ROM
62
62
  # @api public
63
63
  def qualified(table_alias = nil)
64
64
  return self if qualified? && table_alias.nil?
65
+ return meta(qualified: false) unless qualifiable?
65
66
 
66
67
  case sql_expr
67
68
  when Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier
68
- type = meta(qualified: table_alias || true)
69
- type.meta(sql_expr: type.to_sql_name)
69
+ attr = meta(qualified: table_alias || true)
70
+ attr.meta(sql_expr: attr.to_sql_name)
70
71
  else
71
72
  raise QualifyError, "can't qualify #{name.inspect} (#{sql_expr.inspect})"
72
73
  end
@@ -114,6 +115,15 @@ module ROM
114
115
  meta[:qualified].equal?(true) || meta[:qualified].is_a?(Symbol)
115
116
  end
116
117
 
118
+ # Return if an attribute is qualifiable
119
+ #
120
+ # @return [Boolean]
121
+ #
122
+ # @api public
123
+ def qualifiable?
124
+ !source.nil?
125
+ end
126
+
117
127
  # Return a new attribute marked as a FK
118
128
  #
119
129
  # @return [SQL::Attribute]
@@ -239,7 +249,7 @@ module ROM
239
249
  # Create a function DSL from the attribute
240
250
  #
241
251
  # @example
242
- # users[:id].func { int::count(id).as(:count) }
252
+ # users[:id].func { integer::count(id).as(:count) }
243
253
  #
244
254
  # @return [SQL::Function]
245
255
  #
@@ -282,11 +292,11 @@ module ROM
282
292
  def to_sql_name
283
293
  @_to_sql_name ||=
284
294
  if qualified? && aliased?
285
- Sequel.qualify(table_name, name).as(meta[:alias])
295
+ Sequel.qualify(table_name, name).as(self.alias)
286
296
  elsif qualified?
287
297
  Sequel.qualify(table_name, name)
288
298
  elsif aliased?
289
- Sequel.as(name, meta[:alias])
299
+ Sequel.as(name, self.alias)
290
300
  else
291
301
  Sequel[name]
292
302
  end
@@ -305,7 +315,7 @@ module ROM
305
315
  end
306
316
 
307
317
  # @api private
308
- def meta_ast
318
+ def meta_options_ast
309
319
  meta = super
310
320
  meta[:index] = true if indexed?
311
321
  meta
@@ -321,6 +331,40 @@ module ROM
321
331
  self.class.new(type.with(meta: cleaned_meta), options)
322
332
  end
323
333
 
334
+ # Wrap a value with the type, it allows using attribute and type specific methods
335
+ # on literals and things like this
336
+ #
337
+ # @param [Object] value any SQL-serializable value
338
+ # @return [SQL::Attribute]
339
+ #
340
+ # @api public
341
+ def value(value)
342
+ meta(sql_expr: Sequel[value])
343
+ end
344
+
345
+ # Build a case expression based on attribute. See SQL::Function#case
346
+ # when you don't have a specific expression after the CASE keyword.
347
+ # Pass the :else keyword to provide the catch-all case, it's mandatory
348
+ # because of the Sequel's API used underneath.
349
+ #
350
+ # @example
351
+ # users.select_append { id.case(1 => `'first'`, else: `'other'`).as(:first_or_not) }
352
+ #
353
+ # @param [Hash] mapping mapping between SQL expressions
354
+ # @return [SQL::Attribute]
355
+ #
356
+ # @api public
357
+ def case(mapping)
358
+ mapping = mapping.dup
359
+ otherwise = mapping.delete(:else) do
360
+ raise ArgumentError, 'provide the default case using the :else keyword'
361
+ end
362
+
363
+ type = mapping.values[0].type
364
+
365
+ Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise, self))
366
+ end
367
+
324
368
  private
325
369
 
326
370
  # Return Sequel Expression object for an attribute
@@ -376,6 +420,11 @@ module ROM
376
420
  end
377
421
  end
378
422
 
423
+ # @api private
424
+ def extensions
425
+ TypeExtensions[type]
426
+ end
427
+
379
428
  memoize :joined, :to_sql_name, :table_name, :canonical
380
429
  end
381
430
  end
@@ -1,3 +1,4 @@
1
+ require 'concurrent/map'
1
2
  require 'rom/support/inflector'
2
3
  require 'rom/constants'
3
4
 
@@ -13,15 +14,20 @@ module ROM
13
14
  # @return [Hash, RelationRegistry]
14
15
  attr_reader :relations
15
16
 
17
+ # @!attribute [r] picked_relations
18
+ # @return [Concurrent::Map]
19
+ attr_reader :picked_relations
20
+
16
21
  # @api private
17
22
  def initialize(schema)
18
23
  @schema = schema
19
24
  @relations = schema.respond_to?(:relations) ? schema.relations : EMPTY_HASH
25
+ @picked_relations = ::Concurrent::Map.new
20
26
  end
21
27
 
22
28
  # @api private
23
29
  def call(&block)
24
- result = instance_exec(relations, &block)
30
+ result = instance_exec(select_relations(block.parameters), &block)
25
31
 
26
32
  if result.is_a?(::Array)
27
33
  result
@@ -41,6 +47,17 @@ module ROM
41
47
  ::Sequel.lit(value)
42
48
  end
43
49
 
50
+ # Returns a result of SQL EXISTS clause.
51
+ #
52
+ # @example
53
+ # users.where { exists(users.where(name: 'John')) }
54
+ # users.select_append { |r| exists(r[:posts].where(r[:posts][:user_id] => id)).as(:has_posts) }
55
+ #
56
+ # @api public
57
+ def exists(relation)
58
+ ::ROM::SQL::Attribute[Types::Bool].meta(sql_expr: relation.dataset.exists)
59
+ end
60
+
44
61
  # @api private
45
62
  def respond_to_missing?(name, include_private = false)
46
63
  super || schema.key?(name)
@@ -58,6 +75,19 @@ module ROM
58
75
  def types
59
76
  ::ROM::SQL::Types
60
77
  end
78
+
79
+ # @api private
80
+ def select_relations(parameters)
81
+ @picked_relations.fetch_or_store(parameters.hash) do
82
+ keys = parameters.select { |type, _| type == :keyreq }
83
+
84
+ if keys.empty?
85
+ relations
86
+ else
87
+ keys.each_with_object({}) { |(_, k), rs| rs[k] = relations[k] }
88
+ end
89
+ end
90
+ end
61
91
  end
62
92
  end
63
93
  end
@@ -66,7 +66,7 @@ module ROM
66
66
  # # @!method length
67
67
  # # Return array size
68
68
  # #
69
- # # @return [SQL::Attribute<Types::Int>]
69
+ # # @return [SQL::Attribute<Types::Integer>]
70
70
  # #
71
71
  # # @api public
72
72
  #
@@ -129,7 +129,7 @@ module ROM
129
129
  end
130
130
 
131
131
  def length(type, expr)
132
- Attribute[SQL::Types::Int].meta(sql_expr: expr.pg_array.length)
132
+ Attribute[SQL::Types::Integer].meta(sql_expr: expr.pg_array.length)
133
133
  end
134
134
 
135
135
  def overlaps(type, expr, other_array)
@@ -248,9 +248,9 @@ module ROM
248
248
 
249
249
  def build_array_query(query, array_type = 'lquery')
250
250
  case query
251
- when Array
251
+ when ::Array
252
252
  ROM::SQL::Types::PG::Array(array_type)[query]
253
- when String
253
+ when ::String
254
254
  ROM::SQL::Types::PG::Array(array_type)[query.split(',')]
255
255
  end
256
256
  end
@@ -31,13 +31,13 @@ module ROM
31
31
 
32
32
  @range_parsers = {
33
33
  int4range: Sequel::Postgres::PGRange::Parser.new(
34
- 'int4range', SQL::Types::Coercible::Int
34
+ 'int4range', SQL::Types::Coercible::Integer
35
35
  ),
36
36
  int8range: Sequel::Postgres::PGRange::Parser.new(
37
- 'int8range', SQL::Types::Coercible::Int
37
+ 'int8range', SQL::Types::Coercible::Integer
38
38
  ),
39
39
  numrange: Sequel::Postgres::PGRange::Parser.new(
40
- 'numrange', SQL::Types::Coercible::Int
40
+ 'numrange', SQL::Types::Coercible::Integer
41
41
  ),
42
42
  tsrange: Sequel::Postgres::PGRange::Parser.new(
43
43
  'tsrange', ::Time.method(:parse)
@@ -75,7 +75,7 @@ module ROM
75
75
  # @api private
76
76
  def self.range(name, read_type)
77
77
  Type(name) do
78
- type = SQL::Types.Definition(Values::Range).constructor do |range|
78
+ type = SQL::Types.Nominal(Values::Range).constructor do |range|
79
79
  format('%s%s,%s%s',
80
80
  range.exclude_begin? ? :'(' : :'[',
81
81
  range.lower,
@@ -48,7 +48,7 @@ module ROM
48
48
 
49
49
  # @api private
50
50
  def name
51
- meta[:alias] || super
51
+ self.alias || super
52
52
  end
53
53
 
54
54
  # @see Attribute#qualified
@@ -60,6 +60,13 @@ module ROM
60
60
  )
61
61
  end
62
62
 
63
+ # @see Attribute#qualified?
64
+ #
65
+ # @api private
66
+ def qualified?(table_alias = nil)
67
+ meta[:func].args.all?(&:qualified?)
68
+ end
69
+
63
70
  # @see ROM::SQL::Attribute#is
64
71
  #
65
72
  # @api public
@@ -80,8 +87,8 @@ module ROM
80
87
  # @see https://www.postgresql.org/docs/9.6/static/tutorial-window.html
81
88
  #
82
89
  # @example
83
- # users.select { [id, int::row_number().over(partition: name, order: id).as(:row_no)] }
84
- # users.select { [id, int::row_number().over(partition: [first_name, last_name], order: id).as(:row_no)] }
90
+ # users.select { [id, integer::row_number().over(partition: name, order: id).as(:row_no)] }
91
+ # users.select { [id, integer::row_number().over(partition: [first_name, last_name], order: id).as(:row_no)] }
85
92
  #
86
93
  # @example frame variants
87
94
  # # ROWS BETWEEN 3 PRECEDING AND CURRENT ROW
@@ -132,14 +139,34 @@ module ROM
132
139
  Attribute[type].meta(sql_expr: ::Sequel.cast(expr, db_type))
133
140
  end
134
141
 
142
+ # Add a CASE clause for handling if/then logic. This version of CASE search for the first
143
+ # branch which evaluates to `true`. See SQL::Attriubte#case if you're looking for the
144
+ # version that matches an expression result
145
+ #
146
+ # @example
147
+ # users.select { bool::case(status.is("active") => true, else: false).as(:activated) }
148
+ #
149
+ # @param [Hash] mapping mapping between boolean SQL expressions to arbitrary SQL expressions
150
+ # @return [ROM::SQL::Attribute]
151
+ #
152
+ # @api public
153
+ def case(mapping)
154
+ mapping = mapping.dup
155
+ otherwise = mapping.delete(:else) do
156
+ raise ArgumentError, 'provide the default case using the :else keyword'
157
+ end
158
+
159
+ Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise))
160
+ end
161
+
135
162
  # Add a FILTER clause to aggregate function (supported by PostgreSQL 9.4+)
136
163
  # @see https://www.postgresql.org/docs/current/static/sql-expressions.html
137
164
  #
138
165
  # Filter aggregate using the specified conditions
139
166
  #
140
167
  # @example
141
- # users.project { int::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
142
- # users.project { int::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
168
+ # users.project { integer::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
169
+ # users.project { integer::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
143
170
  #
144
171
  # @param [Hash,SQL::Attribute] Conditions
145
172
  # @yield [block] A block with restrictions
@@ -158,6 +185,30 @@ module ROM
158
185
  super(conditions)
159
186
  end
160
187
 
188
+ # Add a WITHIN GROUP clause to aggregate function (supported by PostgreSQL)
189
+ # @see https://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES
190
+ #
191
+ # Establishes an order for an ordered-set aggregate, see the docs for more details
192
+ #
193
+ # @example
194
+ # households.project { fload::percentile_cont(0.5).within_group(income).as(:percentile) }
195
+ #
196
+ # @param [Array] A list of expressions for sorting within a group
197
+ # @yield [block] A block for getting the expressions using the Order DSL
198
+ #
199
+ # @return [SQL::Function]
200
+ #
201
+ # @api public
202
+ def within_group(*args, &block)
203
+ if block
204
+ group = args + ::ROM::SQL::OrderDSL.new(schema).(&block)
205
+ else
206
+ group = args
207
+ end
208
+
209
+ super(*group)
210
+ end
211
+
161
212
  private
162
213
 
163
214
  # @api private
@@ -0,0 +1,9 @@
1
+ require 'rom/sql/restriction_dsl'
2
+
3
+ module ROM
4
+ module SQL
5
+ # @api private
6
+ class JoinDSL < RestrictionDSL
7
+ end
8
+ end
9
+ end
@@ -4,10 +4,10 @@ module ROM
4
4
  module SQL
5
5
  class MapperCompiler < ROM::MapperCompiler
6
6
  def visit_attribute(node)
7
- name, _, meta = node
7
+ name, _, meta_options = node
8
8
 
9
- if meta[:wrapped]
10
- [name, from: meta[:alias]]
9
+ if meta_options[:wrapped]
10
+ [name, from: meta_options[:alias]]
11
11
  else
12
12
  [name]
13
13
  end
@@ -98,7 +98,7 @@ module ROM
98
98
 
99
99
  # @api private
100
100
  def initialize(uri, options = EMPTY_HASH)
101
- @migrator = options.fetch(:migrator) { Migrator.new(connection) }
101
+ @migrator = create_migrator(options[:migrator])
102
102
 
103
103
  self.class.instance ||= self
104
104
  end
@@ -146,6 +146,21 @@ module ROM
146
146
 
147
147
  migrator.auto_migrate!(self, schemas, options)
148
148
  end
149
+
150
+ private
151
+
152
+ # Create a `Migrator`. If `migrator_option` is a `Hash`, use it as options to `Migrator.new`.
153
+ #
154
+ # @api private
155
+ def create_migrator(migrator_option)
156
+ return Migrator.new(connection) unless migrator_option
157
+
158
+ if migrator_option.is_a?(Hash)
159
+ Migrator.new(connection, migrator_option)
160
+ else
161
+ migrator_option
162
+ end
163
+ end
149
164
  end
150
165
  end
151
166
  end
@@ -20,7 +20,7 @@ module ROM
20
20
 
21
21
  param :connection
22
22
 
23
- option :path, type: ROM::Types.Definition(Pathname), default: -> { DEFAULT_PATH }
23
+ option :path, type: ROM::Types.Nominal(Pathname), default: -> { DEFAULT_PATH }
24
24
 
25
25
  option :inferrer, default: -> { DEFAULT_INFERRER }
26
26
 
@@ -75,8 +75,8 @@ module ROM
75
75
  attr.primary_key?
76
76
  end
77
77
 
78
- def unwrap(type)
79
- type.optional? ? SQL::Attribute[type.right].meta(type.meta) : type
78
+ def unwrap(attr)
79
+ attr.optional? ? SQL::Attribute[attr.right, attr.options].meta(attr.meta) : attr
80
80
  end
81
81
  end
82
82
 
@@ -146,8 +146,7 @@ module ROM
146
146
  def with_association(name, opts = EMPTY_HASH)
147
147
  self.class.build(
148
148
  relation,
149
- **options,
150
- associations: associations.merge(name => opts)
149
+ { **options, associations: associations.merge(name => opts) }
151
150
  )
152
151
  end
153
152
  end
@@ -72,6 +72,26 @@ module ROM
72
72
  (total / per_page.to_f).ceil
73
73
  end
74
74
 
75
+ # Return one-based index of first tuple in page
76
+ #
77
+ # @return [Integer]
78
+ #
79
+ # @api public
80
+ def first_in_page
81
+ ((current_page - 1) * per_page) + 1
82
+ end
83
+
84
+ # Return one-based index of last tuple in page
85
+ #
86
+ # @return [Integer]
87
+ #
88
+ # @api public
89
+ def last_in_page
90
+ return total if current_page == total_pages
91
+
92
+ current_page * per_page
93
+ end
94
+
75
95
  # @api private
76
96
  def at(dataset, current_page, per_page = self.per_page)
77
97
  current_page = current_page.to_i
@@ -34,8 +34,8 @@ module ROM
34
34
  # @return [Rom::SQL::Function]
35
35
  #
36
36
  # @api public
37
- def function(name, attr)
38
- ::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name, attr)
37
+ def function(name, *attrs)
38
+ ::ROM::SQL::Function.new(::ROM::Types::Any, schema: schema).public_send(name, *attrs)
39
39
  end
40
40
  alias_method :f, :function
41
41
 
@@ -54,7 +54,11 @@ module ROM
54
54
  type = type(meth)
55
55
 
56
56
  if type
57
- ::ROM::SQL::Function.new(type, schema: schema)
57
+ if args.empty?
58
+ ::ROM::SQL::Function.new(type, schema: schema)
59
+ else
60
+ ::ROM::SQL::Attribute[type].value(args[0])
61
+ end
58
62
  else
59
63
  super
60
64
  end
@@ -1,4 +1,5 @@
1
1
  require 'rom/support/inflector'
2
+ require 'rom/sql/join_dsl'
2
3
 
3
4
  module ROM
4
5
  module SQL
@@ -147,15 +148,19 @@ module ROM
147
148
 
148
149
  # Pluck values from a specific column
149
150
  #
150
- # @example
151
+ # @example Single value
151
152
  # users.pluck(:id)
152
- # # [1, 2, 3]
153
+ # # [1, 2]
154
+ #
155
+ # @example Multiple values
156
+ # users.pluck(:id, :name)
157
+ # # [[1, "Jane"] [2, "Joe"]]
153
158
  #
154
159
  # @return [Array]
155
160
  #
156
161
  # @api public
157
- def pluck(name)
158
- select(name).map(name)
162
+ def pluck(*names)
163
+ select(*names).map(names.length == 1 ? names.first : names)
159
164
  end
160
165
 
161
166
  # Rename columns in a relation
@@ -217,7 +222,7 @@ module ROM
217
222
  # Project relation using column names and projection DSL
218
223
  #
219
224
  # @example using attributes
220
- # users.select(:id) { int::count(id).as(:count) }.group(:id).first
225
+ # users.select(:id) { integer::count(id).as(:count) }.group(:id).first
221
226
  # # {:id => 1, :count => 1}
222
227
  #
223
228
  # users.select { [id, name] }
@@ -387,7 +392,7 @@ module ROM
387
392
  # users.
388
393
  # qualified.
389
394
  # left_join(tasks).
390
- # select { [id, name, int::count(:tasks__id).as(:task_count)] }.
395
+ # select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
391
396
  # group(users[:id].qualified).
392
397
  # having(task_count: 2)
393
398
  # first
@@ -402,7 +407,7 @@ module ROM
402
407
  # users.
403
408
  # qualified.
404
409
  # left_join(tasks).
405
- # select { [id, name, int::count(:tasks__id).as(:task_count)] }.
410
+ # select { [id, name, integer::count(:tasks__id).as(:task_count)] }.
406
411
  # group(users[:id].qualified).
407
412
  # having { count(id.qualified) >= 1 }.
408
413
  # first
@@ -558,6 +563,16 @@ module ROM
558
563
  #
559
564
  # @param [Relation] relation A relation for join
560
565
  #
566
+ # @overload join(relation, &block)
567
+ # Join with another relation using DSL
568
+ #
569
+ # @example
570
+ # users.join(tasks) { |users:, tasks:|
571
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
572
+ # }
573
+ #
574
+ # @param [Relation] relation A relation for join
575
+ #
561
576
  # @return [Relation]
562
577
  #
563
578
  # @api public
@@ -598,6 +613,16 @@ module ROM
598
613
  #
599
614
  # @param [Relation] relation A relation for left_join
600
615
  #
616
+ # @overload join(relation, &block)
617
+ # Join with another relation using DSL
618
+ #
619
+ # @example
620
+ # users.left_join(tasks) { |users:, tasks:|
621
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
622
+ # }
623
+ #
624
+ # @param [Relation] relation A relation for left_join
625
+ #
601
626
  # @return [Relation]
602
627
  #
603
628
  # @api public
@@ -637,6 +662,16 @@ module ROM
637
662
  #
638
663
  # @param [Relation] relation A relation for right_join
639
664
  #
665
+ # @overload join(relation, &block)
666
+ # Join with another relation using DSL
667
+ #
668
+ # @example
669
+ # users.right_join(tasks) { |users:, tasks:|
670
+ # tasks[:user_id].is(users[:id]) & users[:name].is('John')
671
+ # }
672
+ #
673
+ # @param [Relation] relation A relation for right_join
674
+ #
640
675
  # @return [Relation]
641
676
  #
642
677
  # @api public
@@ -944,8 +979,8 @@ module ROM
944
979
  # @example adding number of user tasks
945
980
  # tasks = relations[:tasks]
946
981
  # users = relations[:users]
947
- # user_tasks = tasks.where(tasks[:user_id].is(users[:id])
948
- # tasks_count = user_tasks.select { int::count(id) }
982
+ # user_tasks = tasks.where(tasks[:user_id].is(users[:id]))
983
+ # tasks_count = user_tasks.select { integer::count(id) }
949
984
  # users.select_append(tasks_count.as(:tasks_count))
950
985
  #
951
986
  # @return [SQL::Attribute]
@@ -955,6 +990,18 @@ module ROM
955
990
  SQL::Attribute[attr.type].meta(sql_expr: subquery)
956
991
  end
957
992
 
993
+ # Discard restrictions in `WHERE` and `HAVING` clauses
994
+ #
995
+ # @example calling .by_pk has no effect
996
+ # users.by_pk(1).unfiltered
997
+ #
998
+ # @return [SQL::Relation]
999
+ #
1000
+ # @api public
1001
+ def unfiltered
1002
+ new(dataset.__send__(__method__))
1003
+ end
1004
+
958
1005
  private
959
1006
 
960
1007
  # Build a locking clause
@@ -988,6 +1035,8 @@ module ROM
988
1035
  if k.is_a?(Symbol) && schema.canonical.key?(k)
989
1036
  type = schema.canonical[k]
990
1037
  h[k] = v.is_a?(Array) ? v.map { |e| type[e] } : type[v]
1038
+ elsif k.is_a?(ROM::SQL::Attribute)
1039
+ h[k.canonical] = v
991
1040
  else
992
1041
  h[k] = v
993
1042
  end
@@ -1008,7 +1057,19 @@ module ROM
1008
1057
  elsif other.is_a?(Sequel::SQL::AliasedExpression)
1009
1058
  new(dataset.__send__(type, other, join_cond, opts, &block))
1010
1059
  elsif other.respond_to?(:name) && other.name.is_a?(Relation::Name)
1011
- associations[other.name.key].join(type, self, other)
1060
+ if block
1061
+ join_cond = JoinDSL.new(schema).(&block)
1062
+
1063
+ if other.name.aliaz
1064
+ join_opts = { table_alias: other.name.aliaz }
1065
+ else
1066
+ join_opts = EMPTY_HASH
1067
+ end
1068
+
1069
+ new(dataset.__send__(type, other.name.to_sym, join_cond, join_opts))
1070
+ else
1071
+ associations[other.name.key].join(type, self, other)
1072
+ end
1012
1073
  else
1013
1074
  raise ArgumentError, "+other+ must be either a symbol or a relation, #{other.class} given"
1014
1075
  end
@@ -6,15 +6,7 @@ module ROM
6
6
  class RestrictionDSL < DSL
7
7
  # @api private
8
8
  def call(&block)
9
- instance_exec(relations, &block)
10
- end
11
-
12
- # Returns a result of SQL EXISTS clause.
13
- #
14
- # @example
15
- # users.where { exists(users.where(name: 'John')) }
16
- def exists(relation)
17
- ::ROM::SQL::Attribute[Types::Bool].meta(sql_expr: relation.dataset.exists)
9
+ instance_exec(select_relations(block.parameters), &block)
18
10
  end
19
11
 
20
12
  private
@@ -25,10 +25,10 @@ module ROM
25
25
  inferred = columns.map do |(name, definition)|
26
26
  type = type_builder.(definition)
27
27
 
28
- attr_class.new(type.meta(name: name, source: schema.name)) if type
28
+ attr_class.new(type.meta(source: schema.name), name: name) if type
29
29
  end.compact
30
30
 
31
- missing = columns.map(&:first) - inferred.map { |attr| attr.meta[:name] }
31
+ missing = columns.map(&:first) - inferred.map { |attr| attr.name }
32
32
 
33
33
  [inferred, missing]
34
34
  end
@@ -26,8 +26,10 @@ module ROM
26
26
  end
27
27
 
28
28
  # @api private
29
- def call(schema_name, types)
30
- attributes = types.map { |type| attr_class.new(type).meta(source: schema_name) }
29
+ def call(schema_name, attrs)
30
+ attributes = attrs.map do |attr|
31
+ attr_class.new(attr[:type], attr[:options] || {}).meta(source: schema_name)
32
+ end
31
33
 
32
34
  registry.map { |attr_names, options|
33
35
  build_index(attributes, attr_names, options)
@@ -22,7 +22,7 @@ module ROM
22
22
  DECIMAL_REGEX = /(?:decimal|numeric)\((\d+)(?:,\s*(\d+))?\)/.freeze
23
23
 
24
24
  ruby_type_mapping(
25
- integer: Types::Int,
25
+ integer: Types::Integer,
26
26
  string: Types::String,
27
27
  time: Types::Time,
28
28
  date: Types::Date,
@@ -37,7 +37,7 @@ module ROM
37
37
 
38
38
  def call(primary_key:, db_type:, type:, allow_null:, **rest)
39
39
  if primary_key
40
- map_pk_type(type, db_type, **rest)
40
+ map_pk_type(type, db_type, rest)
41
41
  else
42
42
  mapped_type = map_type(type, db_type, rest)
43
43
 
@@ -11,7 +11,7 @@ module ROM
11
11
  if value_type.class < ::Dry::Types::Type
12
12
  @definition = value_type
13
13
  else
14
- @definition = ::ROM::SQL::Types.Definition(value_type)
14
+ @definition = ::ROM::SQL::Types.Nominal(value_type)
15
15
  end
16
16
  end
17
17
 
@@ -21,7 +21,7 @@ module ROM
21
21
  defines :mapping
22
22
 
23
23
  mapping(
24
- Types::Int => 'integer',
24
+ Types::Integer => 'integer',
25
25
  Types::String => 'varchar',
26
26
  Types::Time => 'timestamp',
27
27
  Types::Date => 'date',
@@ -17,10 +17,10 @@ module ROM
17
17
  # @example with a custom type
18
18
  # attribute :user_id, Types.ForeignKey(:users, Types::UUID)
19
19
  #
20
- # @return [Dry::Types::Definition]
20
+ # @return [Dry::Types::Nominal]
21
21
  #
22
22
  # @api public
23
- def self.ForeignKey(relation, type = Types::Int.meta(index: true))
23
+ def self.ForeignKey(relation, type = Types::Integer.meta(index: true))
24
24
  super
25
25
  end
26
26
 
@@ -32,14 +32,14 @@ module ROM
32
32
  # output { Types::Coercible::Hash }
33
33
  # end
34
34
  #
35
- # @return [Dry::Types::Definition]
35
+ # @return [Dry::Types::Nominal]
36
36
  #
37
37
  # @api public
38
38
  def self.define(value_type, &block)
39
39
  TypeDSL.new(value_type).call(&block)
40
40
  end
41
41
 
42
- Serial = Int.meta(primary_key: true)
42
+ Serial = Integer.meta(primary_key: true)
43
43
 
44
44
  Blob = Constructor(Sequel::SQL::Blob, &Sequel::SQL::Blob.method(:new))
45
45
 
@@ -1,5 +1,5 @@
1
1
  module ROM
2
2
  module SQL
3
- VERSION = '2.5.0'.freeze
3
+ VERSION = '3.0.0'.freeze
4
4
  end
5
5
  end
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: 2.5.0
4
+ version: 3.0.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: 2018-06-08 00:00:00.000000000 Z
11
+ date: 2019-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -44,48 +44,48 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.12'
48
- - - ">="
49
- - !ruby/object:Gem::Version
50
- version: 0.12.1
47
+ version: '1.0'
51
48
  type: :runtime
52
49
  prerelease: false
53
50
  version_requirements: !ruby/object:Gem::Requirement
54
51
  requirements:
55
52
  - - "~>"
56
53
  - !ruby/object:Gem::Version
57
- version: '0.12'
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: 0.12.1
54
+ version: '1.0'
61
55
  - !ruby/object:Gem::Dependency
62
56
  name: dry-core
63
57
  requirement: !ruby/object:Gem::Requirement
64
58
  requirements:
65
59
  - - "~>"
66
60
  - !ruby/object:Gem::Version
67
- version: '0.3'
61
+ version: '0.4'
68
62
  type: :runtime
69
63
  prerelease: false
70
64
  version_requirements: !ruby/object:Gem::Requirement
71
65
  requirements:
72
66
  - - "~>"
73
67
  - !ruby/object:Gem::Version
74
- version: '0.3'
68
+ version: '0.4'
75
69
  - !ruby/object:Gem::Dependency
76
70
  name: rom-core
77
71
  requirement: !ruby/object:Gem::Requirement
78
72
  requirements:
79
73
  - - "~>"
80
74
  - !ruby/object:Gem::Version
81
- version: '4.1'
75
+ version: '5.0'
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 5.0.1
82
79
  type: :runtime
83
80
  prerelease: false
84
81
  version_requirements: !ruby/object:Gem::Requirement
85
82
  requirements:
86
83
  - - "~>"
87
84
  - !ruby/object:Gem::Version
88
- version: '4.1'
85
+ version: '5.0'
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 5.0.1
89
89
  - !ruby/object:Gem::Dependency
90
90
  name: bundler
91
91
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +184,7 @@ files:
184
184
  - lib/rom/sql/gateway.rb
185
185
  - lib/rom/sql/group_dsl.rb
186
186
  - lib/rom/sql/index.rb
187
+ - lib/rom/sql/join_dsl.rb
187
188
  - lib/rom/sql/mapper_compiler.rb
188
189
  - lib/rom/sql/migration.rb
189
190
  - lib/rom/sql/migration/inline_runner.rb
@@ -231,15 +232,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
231
232
  requirements:
232
233
  - - ">="
233
234
  - !ruby/object:Gem::Version
234
- version: 2.3.0
235
+ version: 2.4.0
235
236
  required_rubygems_version: !ruby/object:Gem::Requirement
236
237
  requirements:
237
238
  - - ">="
238
239
  - !ruby/object:Gem::Version
239
240
  version: '0'
240
241
  requirements: []
241
- rubyforge_project:
242
- rubygems_version: 2.7.6
242
+ rubygems_version: 3.0.3
243
243
  signing_key:
244
244
  specification_version: 4
245
245
  summary: SQL databases support for ROM