pg_exec_array_params 0.1.0 → 0.1.1

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: 487eaa0af43d806c4d5ead7fe7ba640a23f0f382611773ed79565c583740fb5c
4
- data.tar.gz: e6b1b0c0347546658a9786642a19f77acd173d1413c9174165e4ec12f312e105
3
+ metadata.gz: 1fdcb04ffde2b46c124f9165cfb0a383e15583d01ae52c830211e51894c44a78
4
+ data.tar.gz: 410af6e18d74d9feab49848f1796831ac79aefadfccd40614b40743abbacf727
5
5
  SHA512:
6
- metadata.gz: c7f70ef196f4353edf9195224598e83b82be4e5811ae2610b631bf416ccdceac89cdedf133aa96ad1c706e1e6994dbf074d171584fec9bbff91e61775b3c613a
7
- data.tar.gz: 4410043f791594bc5367ffc7c94d75ab56bfd5cfeaf84076b2745ae27854d56645aec7d6eea5a7bf1984f6c16d373b1da1ec5e25c13cf360d33ffd9c4bbe26f2
6
+ metadata.gz: d7f4877366134724817ff915786446d622657d40c820d2b3f1fb31dd3237862366b840c6c0ab483760ad91918b4c238d41db0c89db3301ac981381b032849867
7
+ data.tar.gz: ccf1a880c9442b5447cd43a43b262e507503b126d0854e21539ec79927a5fb17c5257f6f5d2995d9ac5cde74e3119c45fc8a9dfd1890d6825c7d368bffb02b97
@@ -1,3 +1,9 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Metrics/ClassLength:
5
+ Max: 150
6
+
1
7
  Metrics/AbcSize:
2
8
  Max: 30
3
9
 
data/Gemfile CHANGED
@@ -5,11 +5,15 @@ source 'https://rubygems.org'
5
5
  # Specify your gem's dependencies in pg_exec_array_params.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'rails'
9
+
8
10
  gem 'codecov', require: false
11
+ gem 'pry-byebug', '~> 3.9'
12
+ gem 'rake', '~> 12.0'
13
+ gem 'rspec-github', '~> 2.3'
14
+ gem 'rspec-its', '~> 1.3'
9
15
  gem 'rubocop', '~> 1.0'
10
16
  gem 'rubocop-rspec', '~> 2.0.0.pre'
11
- gem 'rspec-github', '~> 2.3'
12
- gem 'pry-byebug', '~> 3.9'
13
17
  gem 'simplecov', '~> 0.19'
14
18
 
15
19
  group :benchmark do
data/README.md CHANGED
@@ -1,21 +1,38 @@
1
1
  # PgExecArrayParams
2
2
 
3
3
  ![](https://github.com/lunatic-cat/pg_exec_array_params/workflows/ci/badge.svg)
4
+ [![Gem Version](https://badge.fury.io/rb/pg_exec_array_params.svg)](https://badge.fury.io/rb/pg_exec_array_params)
5
+ [![codecov](https://codecov.io/gh/lunatic-cat/pg_exec_array_params/branch/master/graph/badge.svg?token=X5K67X3V0Z)](undefined)
4
6
 
5
7
  Use same parametized query and put `Array<T>` instead of any `T`
6
8
 
7
9
  ## Example
8
10
 
9
- ```ruby
10
- query = 'select * from t1 where a1 = $1 and a2 = $2 and a3 = $3 and a4 = $4'
11
- params = [1, [2, 3], 'foo', ['bar', 'baz']]
11
+ ### Inside `WHERE` part
12
12
 
13
- # PG::Connection.exec_params called with:
14
- # 'SELECT * FROM "t1" WHERE "a1" = $1 AND "a2" IN ($2, $3) AND "a3" = $4 AND "a4" IN ($5, $6)'
15
- # [1, 2, 3, "foo", "bar", "baz"]
13
+ ```ruby
14
+ # Instead of:
15
+ # PG::Connection.exec_params(
16
+ # 'SELECT * FROM "t1" WHERE "a1" = $1 AND "a3" IN ($4, $5, $6) AND "a2" IN ($2, $3)',
17
+ # [1, 2, 3, "foo", "bar", "baz"]
18
+ # )
19
+ query = 'select * from t1 where a1 = $1 and a3 = $3 and a2 = $2'
20
+ params = [1, [2, 3], ['foo', 'bar', 'baz']]
16
21
  PgExecArrayParams.exec_array_params(conn, query, params)
17
22
  ```
18
23
 
24
+ ### Inside `SELECT` part
25
+
26
+ ```ruby
27
+ # Instead of:
28
+ # PG::Connection.exec_params(
29
+ # 'SELECT ARRAY[$1, $2]'
30
+ # [1, 2]
31
+ # )
32
+ PgExecArrayParams.exec_array_params(conn, 'select $1', [[1, 2]])
33
+ => [{"array"=>"{1,2}"}]
34
+ ```
35
+
19
36
  ## Problem
20
37
 
21
38
  ```ruby
@@ -40,6 +57,16 @@ PgExecArrayParams.exec_array_params(conn, 'select * from users where id = $1', [
40
57
  => [{"id" => 1}, {"id" => 2}]
41
58
  ```
42
59
 
60
+ ## Batteries
61
+
62
+ This can also provide more info than plain `pg_query` gem:
63
+
64
+ ```ruby
65
+ sql = 'with y as (select * from s) select x1, y.y1, z.z as z1 from x join z on z.z = x join y on y.y = x'
66
+ PgExecArrayParams::Query.new(sql, []).columns.map(&:name)
67
+ => ['x1', 'y1', 'z1']
68
+ ```
69
+
43
70
  ## Integration with 'pg' gem
44
71
 
45
72
  ```ruby
@@ -1,10 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pg_exec_array_params/error'
4
+ require 'pg_exec_array_params/rewriters'
5
+ require 'pg_exec_array_params/rewriters/node'
6
+ require 'pg_exec_array_params/rewriters/res_target'
7
+ require 'pg_exec_array_params/rewriters/a_expr'
8
+ require 'pg_exec_array_params/sql_ref_index'
9
+ require 'pg_exec_array_params/column'
3
10
  require 'pg_exec_array_params/query'
4
11
  require 'pg_exec_array_params/version'
5
12
 
6
13
  module PgExecArrayParams
7
- class Error < StandardError; end
14
+ PARAM_REF = 'ParamRef'
15
+ REXPR = 'rexpr'
16
+ NUMBER = 'number'
17
+ LOCATION = 'location'
18
+
19
+ # AExpr['kind']
20
+ EQ_KIND = 0
21
+ IN_KIND = 7
22
+
23
+ class Optional; end
24
+
8
25
  module_function
9
26
 
10
27
  def exec_array_params(conn, sql, params, *args)
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ class Column
5
+ attr_reader :table, :column_name, :as_name
6
+
7
+ def initialize(table:, column_name:, as_name:)
8
+ @table = table
9
+ @column_name = column_name
10
+ @as_name = as_name
11
+ end
12
+
13
+ def name
14
+ @as_name || @column_name
15
+ end
16
+
17
+ def self.from_res_target(res_target)
18
+ return unless (column_ref = res_target.fetch('val', {})['ColumnRef'])
19
+
20
+ idents = column_ref['fields'].map { |field| field.fetch('String', {})['str'] }
21
+ if idents.size <= 1
22
+ column_name = idents.first
23
+ else
24
+ table, column_name, = idents
25
+ end
26
+
27
+ return unless column_name
28
+
29
+ new(table: table, column_name: column_name, as_name: res_target['name'])
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ class Error < StandardError
5
+ attr_accessor :query, :node
6
+
7
+ def initialize(message, query = nil, node = nil)
8
+ super(message)
9
+ @msg = message
10
+ @query = query
11
+ @node = node
12
+ end
13
+
14
+ def to_s
15
+ "#{@msg}\n#{@query}\n#{@node}"
16
+ end
17
+ end
18
+ end
@@ -4,16 +4,6 @@ require 'pg_query'
4
4
 
5
5
  module PgExecArrayParams
6
6
  class Query
7
- PARAM_REF = 'ParamRef'
8
- REXPR = 'rexpr'
9
- A_EXPR = 'A_Expr'
10
- KIND = 'kind'
11
- LOCATION = 'location'
12
- NUMBER = 'number'
13
-
14
- EQ_KIND = 0
15
- IN_KIND = 7
16
-
17
7
  attr_reader :query, :args
18
8
 
19
9
  def initialize(query, args = [])
@@ -26,109 +16,54 @@ module PgExecArrayParams
26
16
  end
27
17
 
28
18
  def sql
29
- return query unless should_rebuild?
30
-
31
- @sql || (rebuild_query! && @sql)
19
+ should_rebuild? ? (@sql || (rebuild_query! && @sql)) : query
32
20
  end
33
21
 
34
22
  def binds
35
- return args unless should_rebuild?
23
+ should_rebuild? ? (@binds ||= args.flatten(1)) : args
24
+ end
36
25
 
37
- @binds || (rebuild_query! && @binds)
26
+ def columns
27
+ @columns || (rebuild_query! && @columns)
38
28
  end
39
29
 
40
30
  private
41
31
 
42
32
  def should_rebuild?
43
- args.any? { |param| param.is_a?(Array) }
44
- end
45
-
46
- def rebuild_query!
47
- @param_idx = 0
48
- @ref_idx = 1
49
- @binds = []
50
- each_param_ref do |value|
51
- # puts({value_before: value}.inspect)
52
-
53
- if args[@param_idx].is_a? Array
54
- value[KIND] = IN_KIND
55
- value[REXPR] = []
56
- args[@param_idx].each do |param|
57
- raise Error, "Param: #{param.inspect} not primitive" if param.respond_to?(:each)
58
-
59
- value[REXPR] << { PARAM_REF => { NUMBER => @ref_idx } }
60
- @binds << param
61
- @ref_idx += 1
62
- end
63
- else
64
- value[REXPR][PARAM_REF][NUMBER] = @ref_idx
65
- @ref_idx += 1
66
-
67
- # nested_refs == 1 unwraps, wrap it back
68
- value[REXPR] = [value[REXPR]] if value[KIND] == IN_KIND
69
-
70
- @binds << args[@param_idx]
71
- end
72
-
73
- @param_idx += 1
74
- # puts({value_after_: value}.inspect)
33
+ args.any? do |param|
34
+ param.is_a?(Array) && (param.none? do |item|
35
+ item.respond_to?(:each) && raise(Error, "Param includes not primitive: #{item.inspect}")
36
+ end)
75
37
  end
76
- @sql = tree.deparse
77
- # puts({sql: @sql, binds: @binds}.inspect)
78
- true
79
38
  end
80
39
 
81
40
  def tree
82
41
  @tree ||= PgQuery.parse(query)
83
42
  end
84
43
 
85
- def each_param_ref
44
+ def rebuild_query!
45
+ @columns ||= []
86
46
  tree.send :treewalker!, tree.tree do |_expr, key, value, _location|
87
- if key == A_EXPR
88
- if assign_param_via_eq?(value)
89
- yield value
90
- elsif (nested_refs = assign_param_via_in?(value))
91
- if nested_refs == 1
92
- value[REXPR] = value[REXPR].first
93
- yield value
94
- else
95
- message = [
96
- 'Cannot splice multiple references, leave the only one:',
97
- query,
98
- refs_underline(value)
99
- ].join("\n")
100
- raise Error, message
101
- end
102
- end
47
+ case key
48
+ when 'targetList'
49
+ @columns += value.map do |node|
50
+ Column.from_res_target(node['ResTarget'])
51
+ end.compact
52
+ when 'ResTarget'
53
+ Rewriters::ResTarget.new(value, ref_idx).process
54
+ when 'A_Expr'
55
+ Rewriters::AExpr.new(value, ref_idx).process
103
56
  end
104
57
  end
58
+ @sql = tree.deparse
59
+ true
60
+ rescue Error => e
61
+ e.query = query
62
+ raise e
105
63
  end
106
64
 
107
- def refs_underline(value)
108
- from, size = refs_at(value)
109
- "#{'^'.rjust(from, ' ')}#{'-'.rjust(size, '-')}^"
110
- end
111
-
112
- def refs_at(value)
113
- first_ref = value[REXPR].find { |vexpr| vexpr.key?(PARAM_REF) } [PARAM_REF]
114
- last_ref = value[REXPR].reverse.find { |vexpr| vexpr.key?(PARAM_REF) } [PARAM_REF]
115
- started = first_ref[LOCATION] + 1
116
- ended = last_ref[LOCATION] + last_ref[NUMBER].to_s.size
117
- [started, ended - started]
118
- end
119
-
120
- # = $1
121
- # {"kind"=>0, "name"=>[{"String"=>{"str"=>"="}}],
122
- # "lexpr"=>{"ColumnRef"=>{"fields"=>[{"String"=>{"str"=>"companies"}}, {"String"=>{"str"=>"id"}}],
123
- # "location"=>1242}},
124
- # "rexpr"=>{"ParamRef"=>{"number"=>4, "location"=>1261}}, "location"=>1259}
125
- def assign_param_via_eq?(value)
126
- (value[KIND] == EQ_KIND) && value[REXPR].is_a?(Hash) && value[REXPR].key?(PARAM_REF)
127
- end
128
-
129
- # IN ($1), returns number of nested REFs
130
- def assign_param_via_in?(value)
131
- (value[KIND] == IN_KIND) && value[REXPR].is_a?(Array) && value[REXPR].count { |vexpr| vexpr.key?(PARAM_REF) }
65
+ def ref_idx
66
+ @ref_idx ||= SqlRefIndex.new(args)
132
67
  end
133
68
  end
134
69
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ module Rewriters
5
+ end
6
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ module Rewriters
5
+ class AExpr < Node
6
+ KIND = 'kind'
7
+
8
+ private
9
+
10
+ def should_rewrite?
11
+ return true if assign_param_via_eq?
12
+
13
+ if (nested_refs = assign_param_via_in?)
14
+ if nested_refs == 1
15
+ value[REXPR] = value[REXPR].first
16
+ return true
17
+ else
18
+ suggest_n = value[REXPR].first[PARAM_REF][NUMBER]
19
+ raise Error.new("Leave only `= $#{suggest_n}` and pass an array", nil, self)
20
+ end
21
+ end
22
+ false
23
+ end
24
+
25
+ def rewrite!
26
+ # puts({value_before: value}.inspect)
27
+ old_ref_idx = value[REXPR][PARAM_REF][NUMBER] - 1 # one based
28
+ unless (new_ref_idx = ref_idx[old_ref_idx])
29
+ raise Error.new("No parameter for $#{old_ref_idx + 1}", nil, self)
30
+ end
31
+
32
+ if new_ref_idx.is_a?(Array)
33
+ value[KIND] = IN_KIND
34
+ value[REXPR] = Range.new(*new_ref_idx).map do |param_ref_idx|
35
+ { PARAM_REF => { NUMBER => param_ref_idx } }
36
+ end
37
+ else
38
+ value[REXPR][PARAM_REF][NUMBER] = new_ref_idx
39
+ # nested_refs == 1 unwraps, wrap it back
40
+ value[REXPR] = [value[REXPR]] if value[KIND] == IN_KIND
41
+ end
42
+ # puts({value_after_: value}.inspect)
43
+ end
44
+
45
+ # = $1
46
+ # {"kind"=>0, "name"=>[{"String"=>{"str"=>"="}}],
47
+ # "lexpr"=>{"ColumnRef"=>{"fields"=>[{"String"=>{"str"=>"companies"}}, {"String"=>{"str"=>"id"}}],
48
+ # "location"=>1242}},
49
+ # "rexpr"=>{"ParamRef"=>{"number"=>4, "location"=>1261}}, "location"=>1259}
50
+ def assign_param_via_eq?
51
+ (value[KIND] == EQ_KIND) && value[REXPR].is_a?(Hash) && value[REXPR].key?(PARAM_REF)
52
+ end
53
+
54
+ # IN ($1), returns number of nested REFs
55
+ def assign_param_via_in?
56
+ (value[KIND] == IN_KIND) && value[REXPR].is_a?(Array) && value[REXPR].count { |vexpr| vexpr.key?(PARAM_REF) }
57
+ end
58
+
59
+ def refs_at
60
+ first_ref = wrap_array(value[REXPR]).find { |vexpr| vexpr.key?(PARAM_REF) }&.fetch(PARAM_REF, {})
61
+ last_ref = wrap_array(value[REXPR]).reverse.find { |vexpr| vexpr.key?(PARAM_REF) }&.fetch(PARAM_REF, {})
62
+ return unless (start_ref_loc = first_ref[LOCATION])
63
+
64
+ return unless (end_ref_loc = last_ref[LOCATION])
65
+
66
+ started = start_ref_loc + 1
67
+ ended = end_ref_loc + last_ref.fetch(NUMBER, '').to_s.size
68
+ [started, ended - started]
69
+ end
70
+
71
+ def wrap_array(object)
72
+ if object.respond_to?(:to_ary)
73
+ object.to_ary || [object]
74
+ else
75
+ [object]
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ module Rewriters
5
+ class Node
6
+ attr_reader :value, :ref_idx
7
+
8
+ def initialize(value, ref_idx)
9
+ @value = value
10
+ @ref_idx = ref_idx
11
+ end
12
+
13
+ def process
14
+ rewrite! if should_rewrite?
15
+ end
16
+
17
+ # used in exception rendering
18
+ def to_s
19
+ return '<unknown node position>' unless (from, size = refs_at)
20
+
21
+ "#{'^'.rjust(from, ' ')}#{'-'.rjust(size, '-')}^"
22
+ end
23
+
24
+ private
25
+
26
+ # returns start and end index of value string repr inside query
27
+ # [from, size]
28
+ def refs_at; end
29
+
30
+ def should_rewrite?; end
31
+
32
+ def rewrite!; end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ module Rewriters
5
+ class ResTarget < Node
6
+ VAL = 'val'
7
+
8
+ def should_rewrite?
9
+ plain_selection?
10
+ end
11
+
12
+ def rewrite!
13
+ # puts({value_before: value}.inspect)
14
+ old_ref_idx = value[VAL][PARAM_REF][NUMBER] - 1 # one based
15
+ unless (new_ref_idx = ref_idx[old_ref_idx])
16
+ raise Error.new("No parameter for $#{old_ref_idx + 1}", nil, self)
17
+ end
18
+
19
+ if new_ref_idx.is_a?(Array)
20
+ elements = Range.new(*new_ref_idx).map do |param_ref_idx|
21
+ { PARAM_REF => { NUMBER => param_ref_idx } }
22
+ end
23
+ value[VAL] = { 'A_ArrayExpr' => { 'elements' => elements } }
24
+ else
25
+ value[VAL][PARAM_REF][NUMBER] = new_ref_idx
26
+ end
27
+ # puts({value_after_: value, 'ref_idx' => ref_idx}.inspect)
28
+ end
29
+
30
+ # handle "select $1"
31
+ # {"val"=>{"ParamRef"=>{"number"=>1, "location"=>7}}, "location"=>7}
32
+ # AExpr handles "select $1 + 1"
33
+ def plain_selection?
34
+ value.key?(VAL) && value[VAL].is_a?(Hash) && value[VAL].key?(PARAM_REF)
35
+ end
36
+
37
+ def refs_at
38
+ [value[VAL][PARAM_REF][LOCATION] + 1, value[VAL][PARAM_REF][NUMBER].to_s.size]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgExecArrayParams
4
+ # Calculates inclusive bounds of each element in a flattened list
5
+ # Bounds are one-based (as sql ref indexes), single value for non-arrays
6
+ # [1, [2, 3], 4, [5, 6, 7]] => [1, [2, 3], 4, [5, 7]]
7
+ class SqlRefIndex
8
+ attr_reader :array
9
+
10
+ def initialize(array)
11
+ @array = array
12
+ @extra_items = 0
13
+ end
14
+
15
+ def [](key)
16
+ sql_ref_index[key]
17
+ end
18
+
19
+ def sql_ref_index
20
+ @sql_ref_index ||= array.map.with_index(1) do |item, idx|
21
+ if item.is_a?(Array)
22
+ add_extra_items = item.size
23
+ add_extra_items -= 1 if add_extra_items.positive?
24
+ [idx + @extra_items, idx + (@extra_items += add_extra_items)]
25
+ else
26
+ idx + @extra_items
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgExecArrayParams
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_exec_array_params
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vlad Bokov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-31 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pg_query
@@ -69,8 +69,16 @@ files:
69
69
  - Rakefile
70
70
  - benchmark.rb
71
71
  - lib/pg_exec_array_params.rb
72
+ - lib/pg_exec_array_params/column.rb
73
+ - lib/pg_exec_array_params/error.rb
72
74
  - lib/pg_exec_array_params/query.rb
75
+ - lib/pg_exec_array_params/rewriters.rb
76
+ - lib/pg_exec_array_params/rewriters/a_expr.rb
77
+ - lib/pg_exec_array_params/rewriters/node.rb
78
+ - lib/pg_exec_array_params/rewriters/res_target.rb
79
+ - lib/pg_exec_array_params/sql_ref_index.rb
73
80
  - lib/pg_exec_array_params/version.rb
81
+ - pg_exec_array_params-0.1.0.gem
74
82
  - pg_exec_array_params.gemspec
75
83
  homepage: https://github.com/lunatic-cat/pg_exec_array_params
76
84
  licenses:
@@ -93,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
101
  - !ruby/object:Gem::Version
94
102
  version: '0'
95
103
  requirements: []
96
- rubygems_version: 3.1.2
104
+ rubygems_version: 3.0.8
97
105
  signing_key:
98
106
  specification_version: 4
99
107
  summary: PG::Connection#exec_params with arrays