ensql 0.6.0 → 0.6.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +52 -0
- data/.github/workflows/specs.yml +59 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +32 -2
- data/Gemfile +8 -14
- data/Gemfile.lock +10 -3
- data/README.md +124 -64
- data/ensql.gemspec +17 -11
- data/gemfiles/maintained.gemfile +22 -0
- data/gemfiles/maintained.gemfile.lock +81 -0
- data/gemfiles/minimum.gemfile +26 -0
- data/gemfiles/minimum.gemfile.lock +76 -0
- data/lib/ensql.rb +5 -65
- data/lib/ensql/active_record_adapter.rb +77 -33
- data/lib/ensql/adapter.rb +50 -12
- data/lib/ensql/error.rb +6 -0
- data/lib/ensql/load_sql.rb +39 -0
- data/lib/ensql/pool_wrapper.rb +21 -0
- data/lib/ensql/postgres_adapter.rb +177 -0
- data/lib/ensql/sequel_adapter.rb +79 -30
- data/lib/ensql/sql.rb +14 -13
- data/lib/ensql/transaction.rb +57 -0
- data/lib/ensql/version.rb +7 -5
- data/perf/adapter_benchmark.rb +102 -0
- metadata +93 -5
data/lib/ensql/sql.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
3
|
+
require_relative "adapter"
|
4
|
+
require_relative "error"
|
4
5
|
|
5
6
|
module Ensql
|
6
7
|
#
|
7
|
-
# Encapsulates a plain-text SQL statement and optional parameters to interpolate. Interpolation is indicated by one
|
8
|
-
#
|
8
|
+
# Encapsulates a plain-text SQL statement and optional parameters to interpolate. Interpolation is indicated by one of
|
9
|
+
# the four placeholder formats:
|
9
10
|
#
|
10
11
|
# 1. **Literal:** `%{param}`
|
11
12
|
# - Interpolates `param` as a quoted string or a numeric literal depending on the class.
|
@@ -31,6 +32,7 @@ module Ensql
|
|
31
32
|
# - Allows composition of SQL via subqueries.
|
32
33
|
#
|
33
34
|
# Any placeholders in the SQL must be present in the params hash or a KeyError will be raised during interpolation.
|
35
|
+
# Interpolation occurs just before the SQL is executed.
|
34
36
|
#
|
35
37
|
# @example
|
36
38
|
# # Interpolate a literal
|
@@ -52,9 +54,8 @@ module Ensql
|
|
52
54
|
# # SELECT * FROM users ORDER BY name asc
|
53
55
|
#
|
54
56
|
class SQL
|
55
|
-
|
56
57
|
# @!visibility private
|
57
|
-
def initialize(sql, params={}, name=
|
58
|
+
def initialize(sql, params = {}, name = "SQL")
|
58
59
|
@sql = sql
|
59
60
|
@name = name.to_s
|
60
61
|
@params = params
|
@@ -94,6 +95,7 @@ module Ensql
|
|
94
95
|
# (see Adapter.fetch_each_row)
|
95
96
|
def each_row(&block)
|
96
97
|
adapter.fetch_each_row(to_sql, &block)
|
98
|
+
nil
|
97
99
|
end
|
98
100
|
|
99
101
|
# Interpolate the params into the SQL statement.
|
@@ -104,14 +106,14 @@ module Ensql
|
|
104
106
|
interpolate(sql, params)
|
105
107
|
end
|
106
108
|
|
107
|
-
|
109
|
+
private
|
108
110
|
|
109
111
|
attr_reader :sql, :params, :name
|
110
112
|
|
111
|
-
NESTED_LIST
|
112
|
-
LIST
|
113
|
+
NESTED_LIST = /%{(\w+)\((.+)\)}/m
|
114
|
+
LIST = /%{\((\w+)\)}/
|
113
115
|
SQL_FRAGMENT = /%{!(\w+)}/
|
114
|
-
LITERAL
|
116
|
+
LITERAL = /%{(\w+)}/
|
115
117
|
|
116
118
|
def interpolate(sql, params)
|
117
119
|
params = params.transform_keys(&:to_s)
|
@@ -130,13 +132,13 @@ module Ensql
|
|
130
132
|
Array(array)
|
131
133
|
.map { |attrs| interpolate(nested_sql, Hash(attrs)) }
|
132
134
|
.map { |sql| "(#{sql})" }
|
133
|
-
.join(
|
135
|
+
.join(", ")
|
134
136
|
end
|
135
137
|
|
136
138
|
def interpolate_list(array)
|
137
|
-
return
|
139
|
+
return "(NULL)" if Array(array).empty?
|
138
140
|
|
139
|
-
|
141
|
+
"(" + Array(array).map { |v| literalize v }.join(", ") + ")"
|
140
142
|
end
|
141
143
|
|
142
144
|
def interpolate_sql(sql)
|
@@ -156,6 +158,5 @@ module Ensql
|
|
156
158
|
def adapter
|
157
159
|
Ensql.adapter
|
158
160
|
end
|
159
|
-
|
160
161
|
end
|
161
162
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "error"
|
4
|
+
require_relative "adapter"
|
5
|
+
|
6
|
+
module Ensql
|
7
|
+
class << self
|
8
|
+
# Wrap a block with a transaction. Uses the well supported
|
9
|
+
# SQL-standard commands for controlling a transaction by default, however database
|
10
|
+
# specific statements can be supplied. Any exceptions inside the block will
|
11
|
+
# trigger a rollback and be reraised. Alternatively, you can call
|
12
|
+
# {rollback!} to immediately exit the block and rollback the transaction.
|
13
|
+
# Returns the result of the block. If the block returns `:rollback`, the
|
14
|
+
# transaction will also be rolled back.
|
15
|
+
#
|
16
|
+
# # If `do_thing1` or `do_thing2` raise an error, no statements are committed.
|
17
|
+
# Ensql.transaction { do_thing1; do_thing2 }
|
18
|
+
#
|
19
|
+
# # If `do_thing2` is falsey, `do_thing1` is rolled back and `do_thing3` is skipped.
|
20
|
+
# Ensql.transaction { do_thing1; do_thing2 or Ensql.rollback!; do_thing3 }
|
21
|
+
#
|
22
|
+
# # Nest transactions with savepoints.
|
23
|
+
# Ensql.transaction do
|
24
|
+
# do_thing1
|
25
|
+
# Ensql.transaction(start: 'SAVEPOINT my_savepoint', commit: 'RELEASE SAVEPOINT my_savepoint', rollback: 'ROLLBACK TO SAVEPOINT my_savepoint') do
|
26
|
+
# do_thing2
|
27
|
+
# do_thing3
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # Use database-specific transaction semantics.
|
32
|
+
# Ensql.transaction(start: 'BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE READ ONLY DEFERRABLE') { }
|
33
|
+
#
|
34
|
+
# @see rollback!
|
35
|
+
# @param start the SQL to begin the transaction.
|
36
|
+
# @param commit the SQL to commit the transaction if successful.
|
37
|
+
# @param rollback the SQL to rollback the transaction if an error is raised.
|
38
|
+
def transaction(start: "START TRANSACTION", commit: "COMMIT", rollback: "ROLLBACK", &block)
|
39
|
+
adapter.run(start)
|
40
|
+
result = catch(:rollback, &block)
|
41
|
+
adapter.run(result == :rollback ? rollback : commit)
|
42
|
+
result
|
43
|
+
# # We need to try rollback on _any_ exception. Since we reraise, rescuing this is safe.
|
44
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
45
|
+
adapter.run(rollback)
|
46
|
+
raise
|
47
|
+
end
|
48
|
+
|
49
|
+
# Immediately rollback and exit the current transaction block. See
|
50
|
+
# {transaction}.
|
51
|
+
def rollback!
|
52
|
+
throw :rollback, :rollback
|
53
|
+
rescue UncaughtThrowError
|
54
|
+
raise Error, "not in a transaction block, can't rollback"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/ensql/version.rb
CHANGED
@@ -2,9 +2,11 @@
|
|
2
2
|
|
3
3
|
module Ensql
|
4
4
|
# Gem version
|
5
|
-
VERSION = "0.6.
|
6
|
-
#
|
7
|
-
|
8
|
-
#
|
9
|
-
|
5
|
+
VERSION = "0.6.5"
|
6
|
+
# Versions of activerecord compatible with the {ActiveRecordAdapter}
|
7
|
+
SUPPORTED_ACTIVERECORD_VERSIONS = [">= 5.0", "< 6.2"].freeze
|
8
|
+
# Versions of sequel compatible with the {SequelAdapter}
|
9
|
+
SUPPORTED_SEQUEL_VERSIONS = "~> 5.9"
|
10
|
+
# Versions of pg compatibile with the {PostgresAdapter}
|
11
|
+
SUPPORTED_PG_VERSIONS = [">= 0.19", "< 2"].freeze
|
10
12
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
#
|
6
|
+
# Compare operations performed using each adapter
|
7
|
+
#
|
8
|
+
|
9
|
+
ENV["TZ"] = "UTC"
|
10
|
+
|
11
|
+
require "benchmark/ips"
|
12
|
+
|
13
|
+
require_relative "lib/ensql/active_record_adapter"
|
14
|
+
require_relative "lib/ensql/sequel_adapter"
|
15
|
+
require_relative "lib/ensql/postgres_adapter"
|
16
|
+
|
17
|
+
ActiveRecord::Base.establish_connection(adapter: "postgresql")
|
18
|
+
DB = Sequel.connect("postgresql:/")
|
19
|
+
DB.extension(:pg_json)
|
20
|
+
|
21
|
+
adapters = {
|
22
|
+
'pg ': Ensql::PostgresAdapter.new { PG::Connection.open },
|
23
|
+
'ar ': Ensql::ActiveRecordAdapter.new(ActiveRecord::Base.connection_pool),
|
24
|
+
'seq ': Ensql::SequelAdapter.new(DB),
|
25
|
+
'pg-ar ': Ensql::PostgresAdapter.new(Ensql::ActiveRecordAdapter.pool),
|
26
|
+
'pg-seq ': Ensql::PostgresAdapter.new(Ensql::SequelAdapter.pool(DB))
|
27
|
+
}
|
28
|
+
|
29
|
+
ADAPTER = adapters.values.first
|
30
|
+
|
31
|
+
ADAPTER.run("drop table if exists number_benchmark")
|
32
|
+
ADAPTER.run("create table number_benchmark as select generate_series(1,100) as number")
|
33
|
+
|
34
|
+
adapter_tests = {
|
35
|
+
'literalize (String)': [:literalize, "It's quoted"],
|
36
|
+
'literalize (Long String)': [:literalize, "It's quoted" * 1000],
|
37
|
+
'literalize (Time)': [:literalize, Time.now],
|
38
|
+
'literalize (Int)': [:literalize, 1234],
|
39
|
+
'literalize (bool)': [:literalize, true],
|
40
|
+
'run INSERT': [:run, "insert into number_benchmark values (999)"],
|
41
|
+
'run SET': [:run, "set time zone UTC"],
|
42
|
+
'run SELECT': [:run, "select generate_series(1,100)"],
|
43
|
+
'count UPDATE': [:fetch_count, "update number_benchmark set number = number + 1"],
|
44
|
+
'count SELECT': [:fetch_count, "select generate_series(1,100)"],
|
45
|
+
'first column': [:fetch_first_column, "select generate_series(1,100)"],
|
46
|
+
'first column (of many)': [:fetch_first_column, "select *, now() from generate_series(1,100) as number"],
|
47
|
+
'first field': [:fetch_first_field, "select 1"],
|
48
|
+
'first field with cast': [:fetch_first_field, "select cast('2021-01-01' as timestamp)"],
|
49
|
+
'first field (of many)': [:fetch_first_field, "select generate_series(1,100)"],
|
50
|
+
'first row': [:fetch_first_row, "select 1, 2, 3"],
|
51
|
+
'first row (cast)': [:fetch_first_row, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json)"],
|
52
|
+
'first row (of many)': [:fetch_first_row, "select generate_series(1, 100)"],
|
53
|
+
'rows (1)': [:fetch_rows, "select 1, 1"],
|
54
|
+
'rows (100)': [:fetch_rows, "select 1, 1, generate_series(1, 100)"],
|
55
|
+
'rows (100,cast)': [:fetch_rows, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json), generate_series(1, 100)"],
|
56
|
+
'rows (100000)': [:fetch_rows, "select 1, 1, generate_series(1, 100000)"]
|
57
|
+
}
|
58
|
+
|
59
|
+
fetch_each_row_tests = {
|
60
|
+
'each_row (1)': [:fetch_each_row, "select 1, 1"],
|
61
|
+
'each_row (100)': [:fetch_each_row, "select 1, 1, generate_series(1, 100)"],
|
62
|
+
'each_row (100,cast)': [:fetch_each_row, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json), generate_series(1, 100)"],
|
63
|
+
'each_row (100000)': [:fetch_each_row, "select 1, 1, generate_series(1, 100000)"]
|
64
|
+
}
|
65
|
+
|
66
|
+
# Verify results are the same
|
67
|
+
adapter_tests.each do |name, args|
|
68
|
+
results = adapters.map { |n, a| [n, a.send(*args)] }.uniq { |n, result| result }
|
69
|
+
next if results.count == 1
|
70
|
+
|
71
|
+
warn "Differing results for #{name}: #{args}"
|
72
|
+
results.each { |n, result| warn " #{n} => #{result.inspect[0..500]}" }
|
73
|
+
end
|
74
|
+
|
75
|
+
# Compare times
|
76
|
+
adapter_tests.each do |test_name, args|
|
77
|
+
puts args.map { |a| a.inspect[0..100] }.join(" ")
|
78
|
+
|
79
|
+
Benchmark.ips(quiet: true) do |x|
|
80
|
+
x.config(stats: :bootstrap, confidence: 95, warmup: 0.2, time: 0.5)
|
81
|
+
|
82
|
+
adapters.each do |name, adapter|
|
83
|
+
x.report("#{test_name} - #{name}") { adapter.send(*args) }
|
84
|
+
end
|
85
|
+
|
86
|
+
x.compare!
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
fetch_each_row_tests.each do |test_name, args|
|
91
|
+
Benchmark.ips(quiet: true) do |x|
|
92
|
+
x.config(stats: :bootstrap, confidence: 95, warmup: 0.2, time: 0.5)
|
93
|
+
|
94
|
+
adapters.each do |name, adapter|
|
95
|
+
x.report("#{test_name} - #{name}") { adapter.send(*args) { |r| r } }
|
96
|
+
end
|
97
|
+
|
98
|
+
x.compare!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
ADAPTER.run("drop table number_benchmark")
|
metadata
CHANGED
@@ -1,16 +1,92 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ensql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Fone
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
12
|
-
dependencies:
|
13
|
-
|
11
|
+
date: 2021-03-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: connection_pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.9.3
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.9.3
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '3'
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '13.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '13.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: simplecov
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.21.2
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 0.21.2
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: yard
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 0.9.26
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 0.9.26
|
89
|
+
description: Escape your ORM and embrace the power and simplicity of writing plain
|
14
90
|
SQL again.
|
15
91
|
email:
|
16
92
|
- daniel@fone.net.nz
|
@@ -18,6 +94,8 @@ executables: []
|
|
18
94
|
extensions: []
|
19
95
|
extra_rdoc_files: []
|
20
96
|
files:
|
97
|
+
- ".github/workflows/lint.yml"
|
98
|
+
- ".github/workflows/specs.yml"
|
21
99
|
- ".gitignore"
|
22
100
|
- ".rspec"
|
23
101
|
- ".ruby-version"
|
@@ -32,12 +110,22 @@ files:
|
|
32
110
|
- bin/rspec
|
33
111
|
- bin/setup
|
34
112
|
- ensql.gemspec
|
113
|
+
- gemfiles/maintained.gemfile
|
114
|
+
- gemfiles/maintained.gemfile.lock
|
115
|
+
- gemfiles/minimum.gemfile
|
116
|
+
- gemfiles/minimum.gemfile.lock
|
35
117
|
- lib/ensql.rb
|
36
118
|
- lib/ensql/active_record_adapter.rb
|
37
119
|
- lib/ensql/adapter.rb
|
120
|
+
- lib/ensql/error.rb
|
121
|
+
- lib/ensql/load_sql.rb
|
122
|
+
- lib/ensql/pool_wrapper.rb
|
123
|
+
- lib/ensql/postgres_adapter.rb
|
38
124
|
- lib/ensql/sequel_adapter.rb
|
39
125
|
- lib/ensql/sql.rb
|
126
|
+
- lib/ensql/transaction.rb
|
40
127
|
- lib/ensql/version.rb
|
128
|
+
- perf/adapter_benchmark.rb
|
41
129
|
homepage: https://github.com/danielfone/ensql
|
42
130
|
licenses:
|
43
131
|
- MIT
|
@@ -57,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
145
|
- !ruby/object:Gem::Version
|
58
146
|
version: '0'
|
59
147
|
requirements: []
|
60
|
-
rubygems_version: 3.2.
|
148
|
+
rubygems_version: 3.2.14
|
61
149
|
signing_key:
|
62
150
|
specification_version: 4
|
63
151
|
summary: Write SQL the safe and simple way
|