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/ensql.gemspec
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
require_relative "lib/ensql/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.authors
|
9
|
-
spec.email
|
10
|
-
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
6
|
+
spec.name = "ensql"
|
7
|
+
spec.version = Ensql::VERSION
|
8
|
+
spec.authors = ["Daniel Fone"]
|
9
|
+
spec.email = ["daniel@fone.net.nz"]
|
10
|
+
|
11
|
+
spec.summary = "Write SQL the safe and simple way"
|
12
|
+
spec.description = "Escape your ORM and embrace the power and simplicity of writing plain SQL again."
|
13
|
+
spec.homepage = "https://github.com/danielfone/ensql"
|
14
|
+
spec.license = "MIT"
|
15
15
|
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
16
16
|
|
17
17
|
# spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
@@ -25,8 +25,14 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
26
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
27
27
|
end
|
28
|
-
spec.bindir
|
29
|
-
spec.executables
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
30
|
spec.require_paths = ["lib"]
|
31
31
|
|
32
|
+
spec.add_dependency "connection_pool", ">= 0.9.3", "<3"
|
33
|
+
|
34
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
35
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
36
|
+
spec.add_development_dependency "simplecov", "~> 0.21.2"
|
37
|
+
spec.add_development_dependency "yard", "~> 0.9.26"
|
32
38
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This specifies the oldest maintained versions of our dependencies
|
5
|
+
#
|
6
|
+
|
7
|
+
source "https://rubygems.org"
|
8
|
+
|
9
|
+
ruby "~> 2.5.0"
|
10
|
+
|
11
|
+
# Specify your gem's dependencies in ensql.gemspec
|
12
|
+
gemspec path: "../"
|
13
|
+
|
14
|
+
# Optional runtime dependencies
|
15
|
+
group :adapters do
|
16
|
+
require_relative "../lib/ensql/version"
|
17
|
+
gem "activerecord", "~> 5.2.0"
|
18
|
+
gem "sequel", "~> 5.9"
|
19
|
+
gem "sqlite3"
|
20
|
+
gem "pg"
|
21
|
+
gem "sequel_pg"
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
ensql (0.6.5)
|
5
|
+
connection_pool (>= 0.9.3, < 3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (5.2.4.5)
|
11
|
+
activesupport (= 5.2.4.5)
|
12
|
+
activerecord (5.2.4.5)
|
13
|
+
activemodel (= 5.2.4.5)
|
14
|
+
activesupport (= 5.2.4.5)
|
15
|
+
arel (>= 9.0)
|
16
|
+
activesupport (5.2.4.5)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (>= 0.7, < 2)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
arel (9.0.0)
|
22
|
+
concurrent-ruby (1.1.8)
|
23
|
+
connection_pool (2.2.3)
|
24
|
+
diff-lcs (1.4.4)
|
25
|
+
docile (1.3.5)
|
26
|
+
i18n (1.8.9)
|
27
|
+
concurrent-ruby (~> 1.0)
|
28
|
+
minitest (5.14.4)
|
29
|
+
pg (1.2.3)
|
30
|
+
rake (13.0.3)
|
31
|
+
rspec (3.10.0)
|
32
|
+
rspec-core (~> 3.10.0)
|
33
|
+
rspec-expectations (~> 3.10.0)
|
34
|
+
rspec-mocks (~> 3.10.0)
|
35
|
+
rspec-core (3.10.1)
|
36
|
+
rspec-support (~> 3.10.0)
|
37
|
+
rspec-expectations (3.10.1)
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
+
rspec-support (~> 3.10.0)
|
40
|
+
rspec-mocks (3.10.2)
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
+
rspec-support (~> 3.10.0)
|
43
|
+
rspec-support (3.10.2)
|
44
|
+
sequel (5.42.0)
|
45
|
+
sequel_pg (1.14.0)
|
46
|
+
pg (>= 0.18.0, != 1.2.0)
|
47
|
+
sequel (>= 4.38.0)
|
48
|
+
simplecov (0.21.2)
|
49
|
+
docile (~> 1.1)
|
50
|
+
simplecov-html (~> 0.11)
|
51
|
+
simplecov_json_formatter (~> 0.1)
|
52
|
+
simplecov-html (0.12.3)
|
53
|
+
simplecov_json_formatter (0.1.2)
|
54
|
+
sqlite3 (1.4.2)
|
55
|
+
thread_safe (0.3.6)
|
56
|
+
tzinfo (1.2.9)
|
57
|
+
thread_safe (~> 0.1)
|
58
|
+
yard (0.9.26)
|
59
|
+
|
60
|
+
PLATFORMS
|
61
|
+
ruby
|
62
|
+
x86_64-darwin-18
|
63
|
+
x86_64-linux
|
64
|
+
|
65
|
+
DEPENDENCIES
|
66
|
+
activerecord (~> 5.2.0)
|
67
|
+
ensql!
|
68
|
+
pg
|
69
|
+
rake (~> 13.0)
|
70
|
+
rspec (~> 3.0)
|
71
|
+
sequel (~> 5.9)
|
72
|
+
sequel_pg
|
73
|
+
simplecov (~> 0.21.2)
|
74
|
+
sqlite3
|
75
|
+
yard (~> 0.9.26)
|
76
|
+
|
77
|
+
RUBY VERSION
|
78
|
+
ruby 2.5.8p224
|
79
|
+
|
80
|
+
BUNDLED WITH
|
81
|
+
2.2.9
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# This specifies the oldest compatible versions of our dependencies
|
5
|
+
#
|
6
|
+
|
7
|
+
source "https://rubygems.org"
|
8
|
+
|
9
|
+
ruby "2.4.0"
|
10
|
+
|
11
|
+
# Specify your gem's dependencies in ensql.gemspec
|
12
|
+
gemspec path: "../"
|
13
|
+
|
14
|
+
# Downgrade simplecov for ruby 2.4 compat
|
15
|
+
gem "simplecov", "~> 0.18.5"
|
16
|
+
gem "connection_pool", "0.9.3"
|
17
|
+
|
18
|
+
# Optional runtime dependencies
|
19
|
+
group :adapters do
|
20
|
+
require_relative "../lib/ensql/version"
|
21
|
+
gem "activerecord", Ensql::SUPPORTED_ACTIVERECORD_VERSIONS.to_s.scan(/\d+.\d+/).first
|
22
|
+
gem "sequel", Ensql::SUPPORTED_SEQUEL_VERSIONS.to_s.scan(/\d+.\d+/).first
|
23
|
+
gem "sqlite3", "~> 1.3.6" # AR version constraint
|
24
|
+
gem "pg", Ensql::SUPPORTED_PG_VERSIONS.to_s.scan(/\d+.\d+/).first
|
25
|
+
gem "sequel_pg"
|
26
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
ensql (0.6.5)
|
5
|
+
connection_pool (>= 0.9.3, < 3)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activemodel (5.0.0)
|
11
|
+
activesupport (= 5.0.0)
|
12
|
+
activerecord (5.0.0)
|
13
|
+
activemodel (= 5.0.0)
|
14
|
+
activesupport (= 5.0.0)
|
15
|
+
arel (~> 7.0)
|
16
|
+
activesupport (5.0.0)
|
17
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
18
|
+
i18n (~> 0.7)
|
19
|
+
minitest (~> 5.1)
|
20
|
+
tzinfo (~> 1.1)
|
21
|
+
arel (7.1.4)
|
22
|
+
concurrent-ruby (1.1.8)
|
23
|
+
connection_pool (0.9.3)
|
24
|
+
diff-lcs (1.4.4)
|
25
|
+
docile (1.3.5)
|
26
|
+
i18n (0.9.5)
|
27
|
+
concurrent-ruby (~> 1.0)
|
28
|
+
minitest (5.14.4)
|
29
|
+
pg (0.19.0)
|
30
|
+
rake (13.0.3)
|
31
|
+
rspec (3.10.0)
|
32
|
+
rspec-core (~> 3.10.0)
|
33
|
+
rspec-expectations (~> 3.10.0)
|
34
|
+
rspec-mocks (~> 3.10.0)
|
35
|
+
rspec-core (3.10.1)
|
36
|
+
rspec-support (~> 3.10.0)
|
37
|
+
rspec-expectations (3.10.1)
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
39
|
+
rspec-support (~> 3.10.0)
|
40
|
+
rspec-mocks (3.10.2)
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
42
|
+
rspec-support (~> 3.10.0)
|
43
|
+
rspec-support (3.10.2)
|
44
|
+
sequel (5.9.0)
|
45
|
+
sequel_pg (1.14.0)
|
46
|
+
pg (>= 0.18.0, != 1.2.0)
|
47
|
+
sequel (>= 4.38.0)
|
48
|
+
simplecov (0.18.5)
|
49
|
+
docile (~> 1.1)
|
50
|
+
simplecov-html (~> 0.11)
|
51
|
+
simplecov-html (0.12.3)
|
52
|
+
sqlite3 (1.3.13)
|
53
|
+
thread_safe (0.3.6)
|
54
|
+
tzinfo (1.2.9)
|
55
|
+
thread_safe (~> 0.1)
|
56
|
+
yard (0.9.26)
|
57
|
+
|
58
|
+
PLATFORMS
|
59
|
+
x86_64-darwin-18
|
60
|
+
x86_64-linux
|
61
|
+
|
62
|
+
DEPENDENCIES
|
63
|
+
activerecord (= 5.0)
|
64
|
+
connection_pool (= 0.9.3)
|
65
|
+
ensql!
|
66
|
+
pg (= 0.19)
|
67
|
+
rake (~> 13.0)
|
68
|
+
rspec (~> 3.0)
|
69
|
+
sequel (= 5.9)
|
70
|
+
sequel_pg
|
71
|
+
simplecov (~> 0.18.5)
|
72
|
+
sqlite3 (~> 1.3.6)
|
73
|
+
yard (~> 0.9.26)
|
74
|
+
|
75
|
+
BUNDLED WITH
|
76
|
+
2.2.9
|
data/lib/ensql.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "ensql/version"
|
4
|
+
require_relative "ensql/adapter"
|
4
5
|
require_relative "ensql/sql"
|
6
|
+
require_relative "ensql/transaction"
|
7
|
+
require_relative "ensql/load_sql"
|
5
8
|
|
6
9
|
#
|
7
10
|
# Primary interface for loading, interpolating and executing SQL statements
|
@@ -24,46 +27,13 @@ require_relative "ensql/sql"
|
|
24
27
|
# Ensql.sql('select * from users where id = %{id}', id: 1).first_row # => { "id" => 1, "email" => "test@example.com" }
|
25
28
|
#
|
26
29
|
module Ensql
|
27
|
-
# Wrapper for errors raised by Ensql
|
28
|
-
class Error < StandardError; end
|
29
|
-
|
30
30
|
class << self
|
31
|
-
|
32
31
|
# (see SQL)
|
33
32
|
# @return [Ensql::SQL] SQL statement
|
34
|
-
def sql(sql, params={})
|
33
|
+
def sql(sql, params = {})
|
35
34
|
SQL.new(sql, params)
|
36
35
|
end
|
37
36
|
|
38
|
-
# Path to search for *.sql queries in, defaults to "sql/". For example, if
|
39
|
-
# {sql_path} is set to 'app/queries', `load_sql('users/active')` will read
|
40
|
-
# 'app/queries/users/active.sql'.
|
41
|
-
# @see .load_sql
|
42
|
-
#
|
43
|
-
# @example
|
44
|
-
# Ensql.sql_path = Rails.root.join('app/queries')
|
45
|
-
#
|
46
|
-
def sql_path
|
47
|
-
@sql_path ||= 'sql'
|
48
|
-
end
|
49
|
-
attr_writer :sql_path
|
50
|
-
|
51
|
-
# Load SQL from a file within {sql_path}. This is the recommended way to
|
52
|
-
# manage SQL in a non-trivial project. For details of how to write
|
53
|
-
# interpolation placeholders, see {SQL}.
|
54
|
-
#
|
55
|
-
# @see .sql_path=
|
56
|
-
# @return [Ensql::SQL]
|
57
|
-
#
|
58
|
-
# @example
|
59
|
-
# Ensql.load_sql('users/activity', report_params)
|
60
|
-
# Ensql.load_sql(:upsert_users, imported_users_attrs)
|
61
|
-
#
|
62
|
-
def load_sql(name, params={})
|
63
|
-
path = File.join(sql_path, "#{name}.sql")
|
64
|
-
SQL.new(File.read(path), params, name)
|
65
|
-
end
|
66
|
-
|
67
37
|
# Convenience method to interpolate and run the supplied SQL on the current
|
68
38
|
# adapter.
|
69
39
|
# @return [void]
|
@@ -72,38 +42,8 @@ module Ensql
|
|
72
42
|
# Ensql.run("DELETE FROM users WHERE id = %{id}", id: user.id)
|
73
43
|
# Ensql.run("ALTER TABLE test RENAME TO old_test")
|
74
44
|
#
|
75
|
-
def run(sql, params={})
|
45
|
+
def run(sql, params = {})
|
76
46
|
SQL.new(sql, params).run
|
77
47
|
end
|
78
|
-
|
79
|
-
# Connection adapter to use. Must implement the interface defined in
|
80
|
-
# {Ensql::Adapter}. If not specified, it will try to autoload an adapter
|
81
|
-
# based on the availability of Sequel or ActiveRecord, in that order.
|
82
|
-
#
|
83
|
-
# @example
|
84
|
-
# require 'sequel'
|
85
|
-
# Ensql.adapter # => Ensql::SequelAdapter
|
86
|
-
# Ensql.adapter = Ensql::ActiveRecordAdapter # override adapter
|
87
|
-
# Ensql.adapter = CustomMSSQLAdapater # supply your own adapter
|
88
|
-
#
|
89
|
-
def adapter
|
90
|
-
@adapter ||= autoload_adapter
|
91
|
-
end
|
92
|
-
attr_writer :adapter
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def autoload_adapter
|
97
|
-
if defined? Sequel
|
98
|
-
require_relative 'ensql/sequel_adapter'
|
99
|
-
SequelAdapter
|
100
|
-
elsif defined? ActiveRecord
|
101
|
-
require_relative 'ensql/active_record_adapter'
|
102
|
-
ActiveRecordAdapter
|
103
|
-
else
|
104
|
-
raise Error, "Couldn't autodetect an adapter, please specify manually."
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
48
|
end
|
109
49
|
end
|
@@ -1,60 +1,104 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
3
|
+
require_relative "version"
|
4
|
+
require_relative "adapter"
|
5
|
+
require_relative "pool_wrapper"
|
5
6
|
|
6
7
|
# Ensure our optional dependency has a compatible version
|
7
|
-
gem
|
8
|
-
require
|
8
|
+
gem "activerecord", Ensql::SUPPORTED_ACTIVERECORD_VERSIONS
|
9
|
+
require "active_record"
|
9
10
|
|
10
11
|
module Ensql
|
11
12
|
#
|
12
|
-
#
|
13
|
-
# ActiveRecord connection to be configured and
|
14
|
-
#
|
13
|
+
# Wraps an ActiveRecord connection pool to implement the {Adapter} interface
|
14
|
+
# for ActiveRecord. Requires an ActiveRecord connection to be configured and
|
15
|
+
# established. By default, uses the connection pool on ActiveRecord::Base.
|
16
|
+
# Other pools can be passed to the constructor.
|
15
17
|
#
|
16
18
|
# @example
|
17
19
|
# require 'active_record'
|
18
20
|
# ActiveRecord::Base.establish_connection(adapter: 'postgresql', database: 'mydb')
|
19
|
-
# Ensql.adapter = Ensql::ActiveRecordAdapter
|
21
|
+
# Ensql.adapter = Ensql::ActiveRecordAdapter.new
|
22
|
+
# # Use database configuration for the Widget model instead
|
23
|
+
# Ensql.adapter = Ensql::ActiveRecordAdapter.new(Widget)
|
20
24
|
#
|
21
|
-
# @see
|
22
|
-
# @see ACTIVERECORD_VERSION Required gem version
|
25
|
+
# @see SUPPORTED_ACTIVERECORD_VERSIONS
|
23
26
|
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
27
|
+
class ActiveRecordAdapter
|
28
|
+
include Adapter
|
29
|
+
|
30
|
+
# Wrap the raw connections from an Active Record connection pool. This
|
31
|
+
# allows us to safely checkout the underlying database connection for use in
|
32
|
+
# a database specific adapter.
|
33
|
+
#
|
34
|
+
# Ensql.adapter = MySqliteAdapter.new(ActiveRecordAdapter.pool)
|
35
|
+
#
|
36
|
+
# @param base [Class] an ActiveRecord class to source connections from
|
37
|
+
# @return [PoolWrapper] a pool adapter for raw connections
|
38
|
+
def self.pool(base = ActiveRecord::Base)
|
39
|
+
PoolWrapper.new do |client_block|
|
40
|
+
base.connection_pool.with_connection { |connection| client_block.call connection.raw_connection }
|
35
41
|
end
|
36
42
|
end
|
37
43
|
|
38
|
-
#
|
39
|
-
|
40
|
-
|
44
|
+
# Support deprecated class method interface
|
45
|
+
class << self
|
46
|
+
require "forwardable"
|
47
|
+
extend Forwardable
|
48
|
+
|
49
|
+
delegate [:literalize, :run, :fetch_count, :fetch_each_row, :fetch_rows, :fetch_first_column, :fetch_first_field, :fetch_first_row] => :new
|
41
50
|
end
|
42
51
|
|
43
|
-
#
|
44
|
-
def
|
45
|
-
|
52
|
+
# @param base [Class] an ActiveRecord class to source connections from
|
53
|
+
def initialize(base = ActiveRecord::Base)
|
54
|
+
@base = base
|
46
55
|
end
|
47
56
|
|
48
|
-
#
|
49
|
-
def
|
50
|
-
|
57
|
+
# @visibility private
|
58
|
+
def fetch_rows(sql)
|
59
|
+
fetch_each_row(sql).to_a
|
51
60
|
end
|
52
61
|
|
53
|
-
|
54
|
-
|
62
|
+
# @visibility private
|
63
|
+
def fetch_each_row(sql, &block)
|
64
|
+
return to_enum(:fetch_each_row, sql) unless block
|
65
|
+
|
66
|
+
result = with_connection { |c| c.exec_query(sql) }
|
67
|
+
# AR populates `column_types` with the types of any columns that haven't
|
68
|
+
# already been type casted by pg decoders. If present, we need to
|
69
|
+
# deserialize them now.
|
70
|
+
if result.column_types.any?
|
71
|
+
result.each { |row| yield deserialize_types(row, result.column_types) }
|
72
|
+
else
|
73
|
+
result.each(&block)
|
74
|
+
end
|
55
75
|
end
|
56
76
|
|
57
|
-
|
77
|
+
# @visibility private
|
78
|
+
def run(sql)
|
79
|
+
with_connection { |c| c.execute(sql) }
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
|
83
|
+
# @visibility private
|
84
|
+
def fetch_count(sql)
|
85
|
+
with_connection { |c| c.exec_update(sql) }
|
86
|
+
end
|
58
87
|
|
88
|
+
# @visibility private
|
89
|
+
def literalize(value)
|
90
|
+
with_connection { |c| c.quote(value) }
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def with_connection(&block)
|
96
|
+
@base.connection_pool.with_connection(&block)
|
97
|
+
end
|
98
|
+
|
99
|
+
def deserialize_types(row, column_types)
|
100
|
+
column_types.each { |column, type| row[column] = type.deserialize(row[column]) }
|
101
|
+
row
|
102
|
+
end
|
59
103
|
end
|
60
104
|
end
|