pg_exec_array_params 0.1.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.
- checksums.yaml +7 -0
- data/.github/workflows/branch.yml +50 -0
- data/.github/workflows/ci.yml +55 -0
- data/.gitignore +6 -0
- data/.rubocop.yml +11 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.md +115 -0
- data/Rakefile +15 -0
- data/benchmark.rb +71 -0
- data/lib/pg_exec_array_params.rb +21 -0
- data/lib/pg_exec_array_params/query.rb +134 -0
- data/lib/pg_exec_array_params/version.rb +5 -0
- data/pg_exec_array_params.gemspec +29 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 487eaa0af43d806c4d5ead7fe7ba640a23f0f382611773ed79565c583740fb5c
|
4
|
+
data.tar.gz: e6b1b0c0347546658a9786642a19f77acd173d1413c9174165e4ec12f312e105
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c7f70ef196f4353edf9195224598e83b82be4e5811ae2610b631bf416ccdceac89cdedf133aa96ad1c706e1e6994dbf074d171584fec9bbff91e61775b3c613a
|
7
|
+
data.tar.gz: 4410043f791594bc5367ffc7c94d75ab56bfd5cfeaf84076b2745ae27854d56645aec7d6eea5a7bf1984f6c16d373b1da1ec5e25c13cf360d33ffd9c4bbe26f2
|
@@ -0,0 +1,50 @@
|
|
1
|
+
name: branch
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches-ignore:
|
5
|
+
- master
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
services:
|
10
|
+
postgres:
|
11
|
+
image: postgres:13
|
12
|
+
env:
|
13
|
+
POSTGRES_USER: postgres
|
14
|
+
POSTGRES_PASSWORD: postgres
|
15
|
+
POSTGRES_DB: pg_exec_array_params_test
|
16
|
+
ports: ['5432:5432']
|
17
|
+
options: >-
|
18
|
+
--health-cmd pg_isready
|
19
|
+
--health-interval 10s
|
20
|
+
--health-timeout 5s
|
21
|
+
--health-retries 5
|
22
|
+
strategy:
|
23
|
+
matrix:
|
24
|
+
ruby: [ '2.7' ]
|
25
|
+
pg: [ '>= 1' ]
|
26
|
+
name: Ruby ${{ matrix.ruby }}, pg ${{ matrix.pg }}
|
27
|
+
steps:
|
28
|
+
- uses: actions/checkout@v2
|
29
|
+
- uses: actions/setup-ruby@v1
|
30
|
+
with:
|
31
|
+
ruby-version: ${{ matrix.ruby }}
|
32
|
+
- uses: actions/cache@v2
|
33
|
+
with:
|
34
|
+
path: vendor/bundle
|
35
|
+
key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.pg }}-${{ hashFiles('**/Gemfile') }}
|
36
|
+
restore-keys: |
|
37
|
+
${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.pg }}
|
38
|
+
- name: Test
|
39
|
+
env:
|
40
|
+
POSTGRES_USER: postgres
|
41
|
+
POSTGRES_PASSWORD: postgres
|
42
|
+
POSTGRES_DB: pg_exec_array_params_test
|
43
|
+
PG: ${{ matrix.pg }}
|
44
|
+
PG_GEM_VERSION: ${{ matrix.pg }}
|
45
|
+
CI: true
|
46
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
47
|
+
run: |
|
48
|
+
bundle config path vendor/bundle
|
49
|
+
bundle install --jobs 4 --retry 3 --without benchmark
|
50
|
+
bundle exec rake ci
|
@@ -0,0 +1,55 @@
|
|
1
|
+
name: ci
|
2
|
+
on:
|
3
|
+
pull_request:
|
4
|
+
types:
|
5
|
+
- opened
|
6
|
+
- synchronize
|
7
|
+
- reopened
|
8
|
+
push:
|
9
|
+
branches:
|
10
|
+
- master
|
11
|
+
jobs:
|
12
|
+
build:
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
services:
|
15
|
+
postgres:
|
16
|
+
image: postgres:13
|
17
|
+
env:
|
18
|
+
POSTGRES_USER: postgres
|
19
|
+
POSTGRES_PASSWORD: postgres
|
20
|
+
POSTGRES_DB: pg_exec_array_params_test
|
21
|
+
ports: ['5432:5432']
|
22
|
+
options: >-
|
23
|
+
--health-cmd pg_isready
|
24
|
+
--health-interval 10s
|
25
|
+
--health-timeout 5s
|
26
|
+
--health-retries 5
|
27
|
+
strategy:
|
28
|
+
matrix:
|
29
|
+
ruby: [ '2.5', '2.6', '2.7' ]
|
30
|
+
pg: [ '< 1', '>= 1' ]
|
31
|
+
name: Ruby ${{ matrix.ruby }}, pg ${{ matrix.pg }}
|
32
|
+
steps:
|
33
|
+
- uses: actions/checkout@v2
|
34
|
+
- uses: actions/setup-ruby@v1
|
35
|
+
with:
|
36
|
+
ruby-version: ${{ matrix.ruby }}
|
37
|
+
- uses: actions/cache@v2
|
38
|
+
with:
|
39
|
+
path: vendor/bundle
|
40
|
+
key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.pg }}-${{ hashFiles('**/Gemfile') }}
|
41
|
+
restore-keys: |
|
42
|
+
${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.pg }}
|
43
|
+
- name: Test
|
44
|
+
env:
|
45
|
+
POSTGRES_USER: postgres
|
46
|
+
POSTGRES_PASSWORD: postgres
|
47
|
+
POSTGRES_DB: pg_exec_array_params_test
|
48
|
+
PG: ${{ matrix.pg }}
|
49
|
+
PG_GEM_VERSION: ${{ matrix.pg }}
|
50
|
+
CI: true
|
51
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
52
|
+
run: |
|
53
|
+
bundle config path vendor/bundle
|
54
|
+
bundle install --jobs 4 --retry 3 --without benchmark
|
55
|
+
bundle exec rake ci
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in pg_exec_array_params.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem 'codecov', require: false
|
9
|
+
gem 'rubocop', '~> 1.0'
|
10
|
+
gem 'rubocop-rspec', '~> 2.0.0.pre'
|
11
|
+
gem 'rspec-github', '~> 2.3'
|
12
|
+
gem 'pry-byebug', '~> 3.9'
|
13
|
+
gem 'simplecov', '~> 0.19'
|
14
|
+
|
15
|
+
group :benchmark do
|
16
|
+
gem 'activerecord', require: false
|
17
|
+
gem 'benchmark-ips', require: false
|
18
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2020 Vlad Bokov
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# PgExecArrayParams
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
Use same parametized query and put `Array<T>` instead of any `T`
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
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']]
|
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"]
|
16
|
+
PgExecArrayParams.exec_array_params(conn, query, params)
|
17
|
+
```
|
18
|
+
|
19
|
+
## Problem
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
conn.exec_params('select * from users where id IN ($1)', [1,2])
|
23
|
+
=> PG::IndeterminateDatatype: ERROR: could not determine data type of parameter $2
|
24
|
+
|
25
|
+
conn.exec_params('select * from users where id IN ($1)', [[1,2]])
|
26
|
+
=> PG::InvalidTextRepresentation: ERROR: invalid input syntax for integer: "[1, 2]"
|
27
|
+
```
|
28
|
+
|
29
|
+
Currently you would generate `$n` parts and flatten params.
|
30
|
+
Or you can inline and embed arrays into query. *Don't forget to escape them*
|
31
|
+
|
32
|
+
## Solution
|
33
|
+
|
34
|
+
This library encapsulates the first approach in a clean way:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# rewrite query under the hood to
|
38
|
+
# select * from users where id IN ($1, $2)
|
39
|
+
PgExecArrayParams.exec_array_params(conn, 'select * from users where id = $1', [[1,2]])
|
40
|
+
=> [{"id" => 1}, {"id" => 2}]
|
41
|
+
```
|
42
|
+
|
43
|
+
## Integration with 'pg' gem
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
PG::Connection.include(PgExecArrayParams) # once in initializer
|
47
|
+
|
48
|
+
conn.exec_array_params('select * from users where id = $1', [[1,2]])
|
49
|
+
=> [{"id" => 1}, {"id" => 2}]
|
50
|
+
```
|
51
|
+
|
52
|
+
## Rails note
|
53
|
+
|
54
|
+
`ActiveRecord` uses the second path (inline + escape).
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
User.where(age: ["1'; drop table users;", "2"]).to_sql
|
58
|
+
=> SELECT "users".* FROM "users" WHERE "users"."age" IN ('1''; drop table users;', '2')
|
59
|
+
```
|
60
|
+
|
61
|
+
It's solid and bulletproof, but
|
62
|
+
|
63
|
+
- it must support multiple databases, but non-trivial queries require raw sql chunks anyway
|
64
|
+
- it's clever, but not so fast as raw `pg`
|
65
|
+
- if you're using `AR::Relation#to_sql` just to handle arrays, consider using this
|
66
|
+
|
67
|
+
## Benchmark
|
68
|
+
|
69
|
+
```sh
|
70
|
+
BENCH_PG_URL='postgres://...' bundle exec ruby benchmark.rb
|
71
|
+
```
|
72
|
+
|
73
|
+
<details>
|
74
|
+
<summary>Benchmarking SQL generation</summary>
|
75
|
+
|
76
|
+
```
|
77
|
+
Warming up --------------------------------------
|
78
|
+
activerecord 1.070k i/100ms
|
79
|
+
exec_array_params 213.704k i/100ms
|
80
|
+
Calculating -------------------------------------
|
81
|
+
activerecord 11.359k (± 3.9%) i/s - 56.710k in 5.000406s
|
82
|
+
exec_array_params 2.151M (± 3.0%) i/s - 10.899M in 5.072579s
|
83
|
+
```
|
84
|
+
</details>
|
85
|
+
|
86
|
+
```
|
87
|
+
Comparison:
|
88
|
+
exec_array_params: 2150601.0 i/s
|
89
|
+
activerecord: 11359.0 i/s - 189.33x (± 0.00) slower
|
90
|
+
```
|
91
|
+
|
92
|
+
<details>
|
93
|
+
<summary>Benchmarking query</summary>
|
94
|
+
|
95
|
+
```
|
96
|
+
Warming up --------------------------------------
|
97
|
+
activerecord#to_a 1.000 i/100ms
|
98
|
+
activerecord#pluck 1.000 i/100ms
|
99
|
+
exec_array_params 2.000 i/100ms
|
100
|
+
pg 2.000 i/100ms
|
101
|
+
Calculating -------------------------------------
|
102
|
+
activerecord#to_a 4.429 (± 0.0%) i/s - 23.000 in 5.203405s
|
103
|
+
activerecord#pluck 18.889 (± 5.3%) i/s - 95.000 in 5.044102s
|
104
|
+
exec_array_params 25.093 (± 4.0%) i/s - 126.000 in 5.039405s
|
105
|
+
pg 23.632 (± 8.5%) i/s - 118.000 in 5.033961s
|
106
|
+
```
|
107
|
+
</details>
|
108
|
+
|
109
|
+
```
|
110
|
+
Comparison:
|
111
|
+
exec_array_params: 25.1 i/s
|
112
|
+
pg: 23.6 i/s - same-ish: difference falls within error
|
113
|
+
activerecord#pluck: 18.9 i/s - 1.33x (± 0.00) slower
|
114
|
+
activerecord#to_a: 4.4 i/s - 5.67x (± 0.00) slower
|
115
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
RuboCop::RakeTask.new
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec_github) do |t|
|
11
|
+
t.rspec_opts = '--format RSpec::Github::Formatter -f progress'
|
12
|
+
end
|
13
|
+
|
14
|
+
task ci: %i[rubocop spec_github]
|
15
|
+
task default: %i[rubocop spec]
|
data/benchmark.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'benchmark/ips'
|
4
|
+
require 'active_record'
|
5
|
+
require 'pg'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
8
|
+
require 'pg_exec_array_params'
|
9
|
+
|
10
|
+
class User < ActiveRecord::Base
|
11
|
+
end
|
12
|
+
|
13
|
+
def connect(url)
|
14
|
+
# ActiveRecord::Base.legacy_connection_handling = false
|
15
|
+
ActiveRecord::Base.logger = Logger.new(IO::NULL)
|
16
|
+
ActiveRecord::Base.configurations = { default_env: { adapter: 'postgresql', url: url, pool: 1 } }
|
17
|
+
ActiveRecord::Base.establish_connection
|
18
|
+
ActiveRecord::Migration.verbose = false
|
19
|
+
ActiveRecord::Schema.define(version: 1) do
|
20
|
+
create_table :users, if_not_exists: true do |t|
|
21
|
+
t.integer :age
|
22
|
+
end
|
23
|
+
add_index(:users, :age) unless index_exists?(:users, :age)
|
24
|
+
end
|
25
|
+
ActiveRecord::Base.connection.raw_connection
|
26
|
+
end
|
27
|
+
|
28
|
+
def sql
|
29
|
+
puts 'Benchmarking SQL generation'
|
30
|
+
|
31
|
+
connect(ENV.fetch('BENCH_PG_URL'))
|
32
|
+
params = ["1'; drop table users;", '2']
|
33
|
+
query = 'select * from users where age = $1'
|
34
|
+
|
35
|
+
Benchmark.ips do |x|
|
36
|
+
x.report('activerecord') { User.where(age: params).to_sql }
|
37
|
+
x.report('exec_array_params') { PgExecArrayParams::Query.new(query, params).sql }
|
38
|
+
x.compare!
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def query
|
43
|
+
puts 'Benchmarking query'
|
44
|
+
|
45
|
+
conn = connect(ENV.fetch('BENCH_PG_URL'))
|
46
|
+
PG::Connection.include(PgExecArrayParams)
|
47
|
+
if conn.exec('select count(*) from users').first['count'].to_i < 1_000_000
|
48
|
+
puts "Seed #{conn.exec('INSERT INTO users (age) SELECT generate_series(1,1500000) % 90;')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
query = 'select * from users where age IN ($1, $2, $3)'
|
52
|
+
params = [10, 20, 30]
|
53
|
+
query2 = 'select * from users where age = $1'
|
54
|
+
params2 = [[10, 20, 30]]
|
55
|
+
|
56
|
+
Benchmark.ips do |x|
|
57
|
+
x.report('activerecord#to_a') { User.where(age: params).to_a }
|
58
|
+
x.report('activerecord#pluck') { User.where(age: params).pluck(:id, :age) }
|
59
|
+
x.report('exec_array_params') { conn.exec_array_params(query2, params2).to_a }
|
60
|
+
x.report('pg') { conn.exec_params(query, params).to_a }
|
61
|
+
x.compare!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if __FILE__ == $PROGRAM_NAME
|
66
|
+
if (meth = ARGV.first)
|
67
|
+
send meth
|
68
|
+
else
|
69
|
+
sql && query
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pg_exec_array_params/query'
|
4
|
+
require 'pg_exec_array_params/version'
|
5
|
+
|
6
|
+
module PgExecArrayParams
|
7
|
+
class Error < StandardError; end
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def exec_array_params(conn, sql, params, *args)
|
11
|
+
Query.new(sql, params).exec_params(conn, *args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(base)
|
15
|
+
return unless base.name == 'PG::Connection'
|
16
|
+
|
17
|
+
base.define_method :exec_array_params do |sql, params, *args|
|
18
|
+
Query.new(sql, params).exec_params(self, *args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pg_query'
|
4
|
+
|
5
|
+
module PgExecArrayParams
|
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
|
+
attr_reader :query, :args
|
18
|
+
|
19
|
+
def initialize(query, args = [])
|
20
|
+
@query = query
|
21
|
+
@args = args
|
22
|
+
end
|
23
|
+
|
24
|
+
def exec_params(conn, *args)
|
25
|
+
conn.exec_params(sql, binds, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def sql
|
29
|
+
return query unless should_rebuild?
|
30
|
+
|
31
|
+
@sql || (rebuild_query! && @sql)
|
32
|
+
end
|
33
|
+
|
34
|
+
def binds
|
35
|
+
return args unless should_rebuild?
|
36
|
+
|
37
|
+
@binds || (rebuild_query! && @binds)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
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)
|
75
|
+
end
|
76
|
+
@sql = tree.deparse
|
77
|
+
# puts({sql: @sql, binds: @binds}.inspect)
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
def tree
|
82
|
+
@tree ||= PgQuery.parse(query)
|
83
|
+
end
|
84
|
+
|
85
|
+
def each_param_ref
|
86
|
+
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
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
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) }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/pg_exec_array_params/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'pg_exec_array_params'
|
7
|
+
spec.version = PgExecArrayParams::VERSION
|
8
|
+
spec.authors = ['Vlad Bokov']
|
9
|
+
spec.email = ['vlad@lunatic.cat']
|
10
|
+
spec.license = 'MIT'
|
11
|
+
|
12
|
+
spec.summary = 'PG::Connection#exec_params with arrays'
|
13
|
+
spec.description = 'Escape each array element inside PG::Connection#exec_params properly'
|
14
|
+
spec.homepage = 'https://github.com/lunatic-cat/pg_exec_array_params'
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
|
16
|
+
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/lunatic-cat/pg_exec_array_params'
|
19
|
+
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.require_paths = ['lib']
|
24
|
+
|
25
|
+
spec.add_dependency('pg_query', '~> 1.2')
|
26
|
+
|
27
|
+
spec.add_development_dependency('pg', ENV.fetch('PG_GEM_VERSION', '~> 0'))
|
28
|
+
spec.add_development_dependency('rspec', '~> 3.0')
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_exec_array_params
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vlad Bokov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-10-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: pg_query
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pg
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description: Escape each array element inside PG::Connection#exec_params properly
|
56
|
+
email:
|
57
|
+
- vlad@lunatic.cat
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".github/workflows/branch.yml"
|
63
|
+
- ".github/workflows/ci.yml"
|
64
|
+
- ".gitignore"
|
65
|
+
- ".rubocop.yml"
|
66
|
+
- Gemfile
|
67
|
+
- LICENSE
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- benchmark.rb
|
71
|
+
- lib/pg_exec_array_params.rb
|
72
|
+
- lib/pg_exec_array_params/query.rb
|
73
|
+
- lib/pg_exec_array_params/version.rb
|
74
|
+
- pg_exec_array_params.gemspec
|
75
|
+
homepage: https://github.com/lunatic-cat/pg_exec_array_params
|
76
|
+
licenses:
|
77
|
+
- MIT
|
78
|
+
metadata:
|
79
|
+
homepage_uri: https://github.com/lunatic-cat/pg_exec_array_params
|
80
|
+
source_code_uri: https://github.com/lunatic-cat/pg_exec_array_params
|
81
|
+
post_install_message:
|
82
|
+
rdoc_options: []
|
83
|
+
require_paths:
|
84
|
+
- lib
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 2.4.0
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
requirements: []
|
96
|
+
rubygems_version: 3.1.2
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: PG::Connection#exec_params with arrays
|
100
|
+
test_files: []
|