pg_exec_array_params 0.1.0 → 0.1.1

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