rom-sql 3.2.0 → 3.4.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.
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015-2019 rom-rb team
3
+ Copyright (c) 2015-2020 rom-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,70 +1,28 @@
1
1
  [gem]: https://rubygems.org/gems/rom-sql
2
2
  [actions]: https://github.com/rom-rb/rom-sql/actions
3
- [codeclimate]: https://codeclimate.com/github/rom-rb/rom-sql
4
- [inchpages]: http://inch-ci.org/github/rom-rb/rom-sql
3
+ [codacy]: https://www.codacy.com/gh/rom-rb/rom-sql
5
4
  [chat]: https://rom-rb.zulipchat.com
5
+ [inchpages]: http://inch-ci.org/github/rom-rb/rom-sql
6
6
 
7
- # rom-sql [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-942283.svg)][chat]
7
+ # rom-sql [![Join the chat at https://rom-rb.zulipchat.com](https://img.shields.io/badge/rom--rb-join%20chat-%23346b7a.svg)][chat]
8
8
 
9
9
  [![Gem Version](https://badge.fury.io/rb/rom-sql.svg)][gem]
10
10
  [![CI Status](https://github.com/rom-rb/rom-sql/workflows/ci/badge.svg)][actions]
11
- [![Code Climate](https://codeclimate.com/github/rom-rb/rom-sql/badges/gpa.svg)][codeclimate]
12
- [![Test Coverage](https://codeclimate.com/github/rom-rb/rom-sql/badges/coverage.svg)][codeclimate]
11
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8e2cbaf78af44185876c8fa41540d7ea)][codacy]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/8e2cbaf78af44185876c8fa41540d7ea)][codacy]
13
13
  [![Inline docs](http://inch-ci.org/github/rom-rb/rom-sql.svg?branch=master)][inchpages]
14
14
 
15
- SQL support for [rom-rb](https://github.com/rom-rb/rom).
16
-
17
- Resources:
18
-
19
- - [User Documentation](http://rom-rb.org/learn/sql/)
20
- - [API Documentation](http://rubydoc.info/gems/rom-sql)
21
-
22
- ## Installation
23
-
24
- Add this line to your application's Gemfile:
25
-
26
- ```ruby
27
- gem 'rom-sql'
28
- ```
29
-
30
- And then execute:
31
-
32
- $ bundle
33
-
34
- Or install it yourself as:
35
-
36
- $ gem install rom-sql
37
-
38
- ## Docker
39
-
40
- ### Development
41
-
42
- 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:
43
-
44
- ```bash
45
- docker-compose run --rm gem "bash"
46
- ```
47
-
48
- 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.
49
-
50
- After dependencies are set container will be started in a bash shell.
51
-
52
- ### Testing
53
-
54
- In order to test the changes, execute:
15
+ ## Links
55
16
 
56
- ```bash
57
- docker-compose build gem
58
- bin/run-specs
59
- ```
17
+ * [User documentation](http://rom-rb.org/learn/sql)
18
+ * [API documentation](http://rubydoc.info/gems/rom-sql)
60
19
 
61
- ### Stopping the dependencies
20
+ ## Supported Ruby versions
62
21
 
63
- In order to stop the dependencies, execute:
22
+ This library officially supports the following Ruby versions:
64
23
 
65
- ```bash
66
- docker-compose down --remove-orphans --volumes
67
- ```
24
+ * MRI >= `2.5`
25
+ * jruby >= `9.2`
68
26
 
69
27
  ## License
70
28
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module Plugins
5
+ module Relation
6
+ module SQL
7
+ module Postgres
8
+ # PG-specific extensions which adds `Relation#full_text_search` method
9
+ #
10
+ # @api public
11
+ module FullTextSearch
12
+ # Run a full text search on PostgreSQL.
13
+ # By default, searching for the inclusion of any of the terms in any of the cols.
14
+ #
15
+ # @example
16
+ # posts.full_text_search([:title, :content], 'apples', language: 'english') # => Relation which match the 'apples' phrase
17
+ #
18
+ # @option :headline [String] Append a expression to the selected columns aliased to headline that contains an extract of the matched text.
19
+ #
20
+ # @option :language [String] The language to use for the search (default: 'simple')
21
+ #
22
+ # @option :plain [Boolean] Whether a plain search should be used (default: false). In this case, terms should be a single string, and it will do a search where cols contains all of the words in terms. This ignores search operators in terms.
23
+ #
24
+ # @option :phrase [Boolean] Similar to :plain, but also adding an ILIKE filter to ensure that returned rows also include the exact phrase used.
25
+ #
26
+ # @option :rank [Boolean] Set to true to order by the rank, so that closer matches are returned first.
27
+ #
28
+ # @option :to_tsquery [Symbol] Can be set to :plain or :phrase to specify the function to use to convert the terms to a ts_query.
29
+ #
30
+ # @option :tsquery [Boolean] Specifies the terms argument is already a valid SQL expression returning a tsquery, and can be used directly in the query.
31
+ #
32
+ # @option :tsvector [Boolean] Specifies the cols argument is already a valid SQL expression returning a tsvector, and can be used directly in the query.
33
+ #
34
+ # @return [Relation]
35
+ #
36
+ # @see https://www.postgresql.org/docs/current/textsearch.html PostgreSQL docs
37
+ #
38
+ # @api public
39
+ def full_text_search(*args, &block)
40
+ new dataset.__send__(__method__, *args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ ROM.plugins do
50
+ adapter :sql do
51
+ register :pg_full_text_search, ROM::Plugins::Relation::SQL::Postgres::FullTextSearch, type: :relation
52
+ end
53
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ module Plugins
5
+ module Relation
6
+ module SQL
7
+ module Postgres
8
+ # PG-specific extensions which adds `Relation#stream` method
9
+ #
10
+ # @api public
11
+ module Streaming
12
+ extend Notifications::Listener
13
+
14
+ class StreamingNotSupportedError < StandardError; end
15
+
16
+ subscribe("configuration.gateway.connected") do |opts|
17
+ conn = opts[:connection]
18
+
19
+ next unless conn.database_type.to_sym == :postgres
20
+
21
+ next if defined?(JRUBY_VERSION)
22
+
23
+ begin
24
+ require "sequel_pg"
25
+ rescue LoadError
26
+ raise StreamingNotSupportedError, "add sequel_pg to Gemfile to use pg_streaming"
27
+ end
28
+
29
+ unless Sequel::Postgres.supports_streaming?
30
+ raise StreamingNotSupportedError, "postgres version does not support streaming"
31
+ end
32
+
33
+ conn.extension(:pg_streaming)
34
+ end
35
+
36
+ def self.included(klass)
37
+ super
38
+ ROM::Relation::Graph.include(Combined)
39
+ ROM::Relation::Composite.include(Composite)
40
+ end
41
+
42
+ if defined?(JRUBY_VERSION)
43
+ # Allows you to stream returned rows one at a time, instead of
44
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
45
+ #
46
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
47
+ #
48
+ # @example
49
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
50
+ #
51
+ # @return [Relation]
52
+ #
53
+ # @api publicY_VERSION
54
+ def stream_each
55
+ raise StreamingNotSupportedError, "not supported on jruby"
56
+ end
57
+ else
58
+ # Allows you to stream returned rows one at a time, instead of
59
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
60
+ #
61
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
62
+ #
63
+ # @example
64
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
65
+ #
66
+ # @return [Relation]
67
+ #
68
+ # @api public
69
+ def stream_each
70
+ return to_enum unless block_given?
71
+
72
+ ds = dataset.stream
73
+
74
+ if auto_map?
75
+ ds.each { |tuple| yield(mapper.([output_schema[tuple]]).first) }
76
+ else
77
+ ds.each { |tuple| yield(output_schema[tuple]) }
78
+ end
79
+ end
80
+
81
+ module Combined
82
+ def stream_each
83
+ raise StreamingNotSupportedError, "not supported on combined relations"
84
+ end
85
+ end
86
+
87
+ module Composite
88
+ def stream_each
89
+ return to_enum unless block_given?
90
+
91
+ left.stream_each do |tuple|
92
+ yield right.call([tuple]).first
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ ROM.plugins do
105
+ adapter :sql do
106
+ register :pg_streaming, ROM::Plugins::Relation::SQL::Postgres::Streaming, type: :relation
107
+ end
108
+ end
@@ -20,7 +20,7 @@ module ROM
20
20
  if target != self.target
21
21
  target.schema.merge(join_schema)
22
22
  else
23
- left.schema.project(*columns)
23
+ left.schema.uniq.project(*columns)
24
24
  end
25
25
  else
26
26
  target_schema
@@ -67,6 +67,21 @@ module ROM
67
67
  end
68
68
  end
69
69
 
70
+ # Return a new attribute that is aliased and marked as qualified
71
+ #
72
+ # Intended to be used when passing attributes to `dataset#select`
73
+ #
74
+ # @return [SQL::Attribute]
75
+ #
76
+ # @api public
77
+ def qualified_projection(table_alias = nil)
78
+ if aliased?
79
+ qualified(table_alias).aliased(self.alias)
80
+ else
81
+ qualified(table_alias)
82
+ end
83
+ end
84
+
70
85
  # Return a new attribute marked as joined
71
86
  #
72
87
  # Whenever you join two schemas, the right schema's attribute
@@ -285,11 +300,11 @@ module ROM
285
300
  # @api private
286
301
  def to_sql_name
287
302
  @_to_sql_name ||=
288
- if qualified? && aliased?
303
+ if qualified? && aliased_projection?
289
304
  Sequel.qualify(table_name, name).as(self.alias)
290
305
  elsif qualified?
291
306
  Sequel.qualify(table_name, name)
292
- elsif aliased?
307
+ elsif aliased_projection?
293
308
  Sequel.as(name, self.alias)
294
309
  else
295
310
  Sequel[name]
@@ -21,6 +21,34 @@ module ROM
21
21
  end
22
22
  alias as aliased
23
23
 
24
+
25
+ # Return true if this attribute is an aliased projection
26
+ #
27
+ # @example
28
+ # class Tasks < ROM::Relation[:memory]
29
+ # schema do
30
+ # attribute :user_id, Types::Integer, alias: :id
31
+ # attribute :name, Types::String
32
+ # end
33
+ # end
34
+ #
35
+ # Users.schema[:user_id].aliased?
36
+ # # => true
37
+ # Users.schema[:user_id].aliased_projection?
38
+ # # => false
39
+ #
40
+ # Users.schema[:user_id].qualified_projection.aliased?
41
+ # # => true
42
+ # Users.schema[:user_id].qualified_projection.aliased_projection?
43
+ # # => true
44
+ #
45
+ # @return [TrueClass,FalseClass]
46
+ #
47
+ # @api private
48
+ def aliased_projection?
49
+ self.meta[:sql_expr].is_a?(Sequel::SQL::AliasedExpression)
50
+ end
51
+
24
52
  private
25
53
 
26
54
  # @api private
@@ -5,3 +5,4 @@ require 'rom/sql/extensions/postgres/types'
5
5
  require 'rom/sql/extensions/postgres/type_builder'
6
6
  require 'rom/sql/extensions/postgres/type_serializer'
7
7
  require 'rom/plugins/relation/sql/postgres/explain'
8
+ require 'rom/plugins/relation/sql/postgres/full_text_search'
@@ -17,7 +17,7 @@ module ROM
17
17
  #
18
18
  # @api private
19
19
  def returning_dataset
20
- relation.dataset.returning(*relation.qualified_columns)
20
+ relation.dataset.returning(*relation.schema.qualified_projection)
21
21
  end
22
22
  end
23
23
 
@@ -78,6 +78,15 @@ module ROM
78
78
  )
79
79
  end
80
80
 
81
+ # @see Attribute#qualified_projection
82
+ #
83
+ # @api private
84
+ def qualified_projection(table_alias = nil)
85
+ meta(
86
+ func: ::Sequel::SQL::Function.new(func.name, *func.args.map { |arg| arg.respond_to?(:qualified_projection) ? arg.qualified_projection(table_alias) : arg })
87
+ )
88
+ end
89
+
81
90
  # @see Attribute#qualified?
82
91
  #
83
92
  # @api private
@@ -183,10 +192,10 @@ module ROM
183
192
  # Filter aggregate using the specified conditions
184
193
  #
185
194
  # @example
186
- # users.project { integer::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
187
- # users.project { integer::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
195
+ # users.project { integer::count(:id).filter(name.is("Jack")).as(:jacks) }.unordered
196
+ # users.project { integer::count(:id).filter { name.is("John") }).as(:johns) }.ordered
188
197
  #
189
- # @param [Hash,SQL::Attribute] Conditions
198
+ # @param condition [Hash,SQL::Attribute] Conditions
190
199
  # @yield [block] A block with restrictions
191
200
  #
192
201
  # @return [SQL::Function]
@@ -211,7 +220,7 @@ module ROM
211
220
  # @example
212
221
  # households.project { fload::percentile_cont(0.5).within_group(income).as(:percentile) }
213
222
  #
214
- # @param [Array] A list of expressions for sorting within a group
223
+ # @param args [Array] A list of expressions for sorting within a group
215
224
  # @yield [block] A block for getting the expressions using the Order DSL
216
225
  #
217
226
  # @return [SQL::Function]
@@ -81,6 +81,7 @@ module ROM
81
81
  def initialize(uri, options = EMPTY_HASH)
82
82
  @connection = connect(uri, options)
83
83
  load_extensions(Array(options[:extensions]))
84
+ Notifications.trigger("configuration.gateway.connected", connection: @connection)
84
85
 
85
86
  @options = options
86
87
 
@@ -247,4 +248,6 @@ module ROM
247
248
  end
248
249
  end
249
250
  end
251
+
252
+ Configuration.register_event("configuration.gateway.connected")
250
253
  end
@@ -31,7 +31,7 @@ module ROM
31
31
  # users.select { function(:count, :id).as(:total) }
32
32
  #
33
33
  # @param [Symbol] name SQL function
34
- # @param [Symbol] attr
34
+ # @param [Symbol] attrs
35
35
  #
36
36
  # @return [Rom::SQL::Function]
37
37
  #
@@ -37,7 +37,7 @@ module ROM
37
37
  table = opts[:from].first
38
38
 
39
39
  if db.table_exists?(table)
40
- select(*schema.map(&:qualified)).order(*schema.project(*schema.primary_key_names).qualified)
40
+ select(*schema.qualified_projection).order(*schema.project(*schema.primary_key_names).qualified)
41
41
  else
42
42
  self
43
43
  end
@@ -481,6 +481,18 @@ module ROM
481
481
  end
482
482
  end
483
483
 
484
+ # Removes ordering for the relation
485
+ #
486
+ # @example
487
+ # users.unordered
488
+ #
489
+ # @return [Relation]
490
+ #
491
+ # @api public
492
+ def unordered
493
+ new(dataset.unordered)
494
+ end
495
+
484
496
  # Reverse the order of the relation
485
497
  #
486
498
  # @example
@@ -1094,7 +1106,7 @@ module ROM
1094
1106
  join_opts = EMPTY_HASH
1095
1107
  end
1096
1108
 
1097
- new(dataset.__send__(type, other.name.to_sym, join_cond, join_opts))
1109
+ new(dataset.__send__(type, other.name.dataset.to_sym, join_cond, join_opts))
1098
1110
  else
1099
1111
  associations[other.name.key].join(type, self, other)
1100
1112
  end