rom-sql 3.2.0 → 3.3.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,97 @@
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
+ end
40
+
41
+ if defined?(JRUBY_VERSION)
42
+ # Allows you to stream returned rows one at a time, instead of
43
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
44
+ #
45
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
46
+ #
47
+ # @example
48
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
49
+ #
50
+ # @return [Relation]
51
+ #
52
+ # @api publicY_VERSION
53
+ def stream_each
54
+ raise StreamingNotSupportedError, "not supported on jruby"
55
+ end
56
+ else
57
+ # Allows you to stream returned rows one at a time, instead of
58
+ # collecting the entire result set in memory. Requires the `sequel_pg` gem
59
+ #
60
+ # @see https://github.com/jeremyevans/sequel_pg#streaming- sequel_pg docs
61
+ #
62
+ # @example
63
+ # posts.steam_each { |post| puts CSV.generate_line(post) }
64
+ #
65
+ # @return [Relation]
66
+ #
67
+ # @api public
68
+ def stream_each
69
+ return to_enum unless block_given?
70
+
71
+ ds = dataset.stream
72
+
73
+ if auto_map?
74
+ ds.each { |tuple| yield(mapper.([output_schema[tuple]]).first) }
75
+ else
76
+ ds.each { |tuple| yield(output_schema[tuple]) }
77
+ end
78
+ end
79
+
80
+ module Combined
81
+ def stream_each
82
+ raise StreamingNotSupportedError, "not supported on combined relations"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ ROM.plugins do
94
+ adapter :sql do
95
+ register :pg_streaming, ROM::Plugins::Relation::SQL::Postgres::Streaming, type: :relation
96
+ end
97
+ 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,5 @@ 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'
9
+ require "rom/plugins/relation/sql/postgres/streaming"
@@ -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
@@ -186,7 +195,7 @@ module ROM
186
195
  # users.project { integer::count(:id).filter(name.is("Jack")).as(:jacks) }.order(nil)
187
196
  # users.project { integer::count(:id).filter { name.is("John") }).as(:johns) }.order(nil)
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
@@ -1094,7 +1094,7 @@ module ROM
1094
1094
  join_opts = EMPTY_HASH
1095
1095
  end
1096
1096
 
1097
- new(dataset.__send__(type, other.name.to_sym, join_cond, join_opts))
1097
+ new(dataset.__send__(type, other.name.dataset.to_sym, join_cond, join_opts))
1098
1098
  else
1099
1099
  associations[other.name.key].join(type, self, other)
1100
1100
  end
@@ -67,6 +67,18 @@ module ROM
67
67
  new(map { |attr| attr.qualified(table_alias) })
68
68
  end
69
69
 
70
+ # Return a new schema with attributes that are aliased
71
+ # and marked as qualified
72
+ #
73
+ # Intended to be used when passing attributes to `dataset#select`
74
+ #
75
+ # @return [Schema]
76
+ #
77
+ # @api public
78
+ def qualified_projection(table_alias = nil)
79
+ new(map { |attr| attr.qualified_projection(table_alias) })
80
+ end
81
+
70
82
  # Project a schema
71
83
  #
72
84
  # @see ROM::Schema#project
@@ -129,7 +141,7 @@ module ROM
129
141
  #
130
142
  # @api public
131
143
  def call(relation)
132
- relation.new(relation.dataset.select(*self), schema: self)
144
+ relation.new(relation.dataset.select(*self.qualified_projection), schema: self)
133
145
  end
134
146
 
135
147
  # Return an empty schema