ensql 0.6.0 → 0.6.5
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 +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
|