rom-sql 2.5.0 → 3.0.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
  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