rom-sql 3.1.0 → 3.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +214 -101
  3. data/LICENSE +1 -1
  4. data/README.md +12 -54
  5. data/lib/rom-sql.rb +2 -0
  6. data/lib/rom/plugins/relation/sql/auto_restrictions.rb +2 -0
  7. data/lib/rom/plugins/relation/sql/instrumentation.rb +2 -0
  8. data/lib/rom/plugins/relation/sql/postgres/explain.rb +6 -7
  9. data/lib/rom/plugins/relation/sql/postgres/full_text_search.rb +53 -0
  10. data/lib/rom/plugins/relation/sql/postgres/streaming.rb +108 -0
  11. data/lib/rom/sql.rb +2 -0
  12. data/lib/rom/sql/associations.rb +2 -0
  13. data/lib/rom/sql/associations/core.rb +3 -1
  14. data/lib/rom/sql/associations/many_to_many.rb +3 -1
  15. data/lib/rom/sql/associations/many_to_one.rb +2 -0
  16. data/lib/rom/sql/associations/one_to_many.rb +2 -0
  17. data/lib/rom/sql/associations/one_to_one.rb +2 -0
  18. data/lib/rom/sql/associations/one_to_one_through.rb +2 -0
  19. data/lib/rom/sql/associations/self_ref.rb +2 -0
  20. data/lib/rom/sql/attribute.rb +26 -7
  21. data/lib/rom/sql/attribute_aliasing.rb +28 -0
  22. data/lib/rom/sql/commands.rb +2 -0
  23. data/lib/rom/sql/commands/create.rb +2 -0
  24. data/lib/rom/sql/commands/delete.rb +2 -0
  25. data/lib/rom/sql/commands/error_wrapper.rb +2 -0
  26. data/lib/rom/sql/commands/update.rb +2 -0
  27. data/lib/rom/sql/dsl.rb +11 -3
  28. data/lib/rom/sql/error.rb +2 -0
  29. data/lib/rom/sql/errors.rb +2 -0
  30. data/lib/rom/sql/extensions.rb +2 -0
  31. data/lib/rom/sql/extensions/active_support_notifications.rb +2 -0
  32. data/lib/rom/sql/extensions/mysql.rb +2 -0
  33. data/lib/rom/sql/extensions/mysql/type_builder.rb +2 -0
  34. data/lib/rom/sql/extensions/postgres.rb +3 -0
  35. data/lib/rom/sql/extensions/postgres/commands.rb +3 -1
  36. data/lib/rom/sql/extensions/postgres/type_builder.rb +6 -4
  37. data/lib/rom/sql/extensions/postgres/type_serializer.rb +2 -0
  38. data/lib/rom/sql/extensions/postgres/types.rb +2 -0
  39. data/lib/rom/sql/extensions/postgres/types/array.rb +6 -4
  40. data/lib/rom/sql/extensions/postgres/types/array_types.rb +3 -1
  41. data/lib/rom/sql/extensions/postgres/types/geometric.rb +2 -0
  42. data/lib/rom/sql/extensions/postgres/types/json.rb +13 -11
  43. data/lib/rom/sql/extensions/postgres/types/ltree.rb +25 -23
  44. data/lib/rom/sql/extensions/postgres/types/network.rb +2 -0
  45. data/lib/rom/sql/extensions/postgres/types/range.rb +2 -0
  46. data/lib/rom/sql/extensions/rails_log_subscriber.rb +2 -0
  47. data/lib/rom/sql/extensions/sqlite.rb +2 -0
  48. data/lib/rom/sql/extensions/sqlite/type_builder.rb +2 -0
  49. data/lib/rom/sql/extensions/sqlite/types.rb +2 -0
  50. data/lib/rom/sql/foreign_key.rb +3 -1
  51. data/lib/rom/sql/function.rb +14 -3
  52. data/lib/rom/sql/gateway.rb +6 -1
  53. data/lib/rom/sql/group_dsl.rb +2 -0
  54. data/lib/rom/sql/index.rb +2 -0
  55. data/lib/rom/sql/join_dsl.rb +2 -0
  56. data/lib/rom/sql/mapper_compiler.rb +2 -0
  57. data/lib/rom/sql/migration.rb +5 -3
  58. data/lib/rom/sql/migration/inline_runner.rb +2 -0
  59. data/lib/rom/sql/migration/migrator.rb +4 -2
  60. data/lib/rom/sql/migration/recorder.rb +2 -0
  61. data/lib/rom/sql/migration/runner.rb +4 -2
  62. data/lib/rom/sql/migration/schema_diff.rb +2 -0
  63. data/lib/rom/sql/migration/template.rb +2 -0
  64. data/lib/rom/sql/migration/writer.rb +12 -4
  65. data/lib/rom/sql/order_dsl.rb +2 -0
  66. data/lib/rom/sql/plugin/associates.rb +4 -2
  67. data/lib/rom/sql/plugin/nullify.rb +2 -0
  68. data/lib/rom/sql/plugin/pagination.rb +2 -0
  69. data/lib/rom/sql/plugins.rb +2 -0
  70. data/lib/rom/sql/projection_dsl.rb +3 -1
  71. data/lib/rom/sql/rake_task.rb +2 -0
  72. data/lib/rom/sql/relation.rb +3 -1
  73. data/lib/rom/sql/relation/reading.rb +6 -4
  74. data/lib/rom/sql/relation/writing.rb +2 -0
  75. data/lib/rom/sql/restriction_dsl.rb +9 -1
  76. data/lib/rom/sql/schema.rb +16 -2
  77. data/lib/rom/sql/schema/attributes_inferrer.rb +5 -3
  78. data/lib/rom/sql/schema/dsl.rb +3 -1
  79. data/lib/rom/sql/schema/index_dsl.rb +5 -2
  80. data/lib/rom/sql/schema/inferrer.rb +12 -8
  81. data/lib/rom/sql/schema/type_builder.rb +4 -2
  82. data/lib/rom/sql/spec/support.rb +5 -3
  83. data/lib/rom/sql/tasks/migration_tasks.rake +16 -11
  84. data/lib/rom/sql/transaction.rb +2 -0
  85. data/lib/rom/sql/type_dsl.rb +2 -0
  86. data/lib/rom/sql/type_extensions.rb +3 -1
  87. data/lib/rom/sql/type_serializer.rb +2 -0
  88. data/lib/rom/sql/types.rb +2 -0
  89. data/lib/rom/sql/version.rb +3 -1
  90. data/lib/rom/sql/wrap.rb +2 -0
  91. data/lib/rom/types/values.rb +2 -0
  92. metadata +25 -45
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
 
data/lib/rom-sql.rb CHANGED
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/support/notifications'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module Plugins
3
5
  module Relation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module Plugins
3
5
  module Relation
@@ -24,14 +26,11 @@ module ROM
24
26
  #
25
27
  # @api public
26
28
  def explain(format: :text, **options)
27
- bool_options = options.map { |opt, value| "#{ opt.to_s.upcase } #{ !!value }" }
28
- format_option = "FORMAT #{ format.to_s.upcase }"
29
+ bool_options = options.map { |opt, value| "#{opt.to_s.upcase} #{!!value}" }
30
+ format_option = "FORMAT #{format.to_s.upcase}"
31
+ explain_value = [format_option, *bool_options].join(', ')
29
32
 
30
- query =
31
- "EXPLAIN (" <<
32
- [format_option, *bool_options].join(', ') <<
33
- ") " <<
34
- dataset.sql
33
+ query = "EXPLAIN (#{explain_value}) #{dataset.sql}"
35
34
 
36
35
  rows = dataset.with_sql(query).map(:'QUERY PLAN')
37
36
 
@@ -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
data/lib/rom/sql.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'dry/equalizer'
2
4
 
3
5
  require 'rom/core'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/many_to_many'
2
4
  require 'rom/sql/associations/many_to_one'
3
5
  require 'rom/sql/associations/one_to_many'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Associations
@@ -19,7 +21,7 @@ module ROM
19
21
  def wrapped
20
22
  new_target = view ? target.send(view) : target
21
23
  to_wrap = self.class.allocate
22
- to_wrap.send(:initialize, definition, options.merge(target: new_target))
24
+ to_wrap.send(:initialize, definition, **options, target: new_target)
23
25
  to_wrap.wrap
24
26
  end
25
27
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/many_to_many'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -18,7 +20,7 @@ module ROM
18
20
  if target != self.target
19
21
  target.schema.merge(join_schema)
20
22
  else
21
- left.schema.project(*columns)
23
+ left.schema.uniq.project(*columns)
22
24
  end
23
25
  else
24
26
  target_schema
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/many_to_one'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/associations/one_to_many'
2
4
  require 'rom/sql/associations/core'
3
5
  require 'rom/sql/associations/self_ref'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/one_to_many'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rom/sql/associations/many_to_many'
2
4
 
3
5
  module ROM
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ROM
2
4
  module SQL
3
5
  module Associations
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'sequel/core'
2
4
  require 'dry/core/cache'
3
5
 
@@ -19,16 +21,18 @@ module ROM
19
21
 
20
22
  OPERATORS = %i[>= <= > <].freeze
21
23
  NONSTANDARD_EQUALITY_VALUES = [true, false, nil].freeze
22
- META_KEYS = %i(index foreign_key target sql_expr qualified).freeze
24
+ META_KEYS = %i[index foreign_key target sql_expr qualified].freeze
23
25
 
24
26
  # Error raised when an attribute cannot be qualified
25
27
  QualifyError = Class.new(StandardError)
26
28
 
27
29
  extend Dry::Core::Cache
28
30
 
29
- # @api private
30
- def self.[](*args)
31
- fetch_or_store(args) { new(*args) }
31
+ class << self
32
+ # @api private
33
+ def [](type, options = EMPTY_HASH)
34
+ fetch_or_store([type, options]) { new(type, **options) }
35
+ end
32
36
  end
33
37
 
34
38
  # Return a new attribute in its canonical form
@@ -63,6 +67,21 @@ module ROM
63
67
  end
64
68
  end
65
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
+
66
85
  # Return a new attribute marked as joined
67
86
  #
68
87
  # Whenever you join two schemas, the right schema's attribute
@@ -281,11 +300,11 @@ module ROM
281
300
  # @api private
282
301
  def to_sql_name
283
302
  @_to_sql_name ||=
284
- if qualified? && aliased?
303
+ if qualified? && aliased_projection?
285
304
  Sequel.qualify(table_name, name).as(self.alias)
286
305
  elsif qualified?
287
306
  Sequel.qualify(table_name, name)
288
- elsif aliased?
307
+ elsif aliased_projection?
289
308
  Sequel.as(name, self.alias)
290
309
  else
291
310
  Sequel[name]
@@ -318,7 +337,7 @@ module ROM
318
337
  cleaned_meta = meta.reject { |k, _| META_KEYS.include?(k) }
319
338
  type = optional? ? right : self.type
320
339
 
321
- self.class.new(type.with(meta: cleaned_meta), options)
340
+ self.class.new(type.with(meta: cleaned_meta), **options)
322
341
  end
323
342
 
324
343
  # Wrap a value with the type, it allows using attribute and type specific methods