rom-sql 3.2.0 → 3.3.0

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