ensql 0.6.2 → 0.6.3

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: 926a86bf4e2b069dfee856bc77567048cca9f5e621d48e255fe3d5f1c041a42d
4
- data.tar.gz: 294b707b6a8d8394ba12eafe20298bb800ab86584f354bacfe8cbdef66581531
3
+ metadata.gz: d67a62c833d6468ecbb753a54f592f2b9ecb295fb2db7eb943c25184c481e4c0
4
+ data.tar.gz: 32e9c5c68c6469bd1613e4b0881b22a7545616f63dffb08f4a6ba04d2ad28fb3
5
5
  SHA512:
6
- metadata.gz: fd0ed7157a3c0dd791a3635af47e7fb58ec54ab770e38eb1765e224e4674c4065278af952d74f142db42242b4ab7bb2cfb23f89eefbdb39ec3a5af44a3d7867a
7
- data.tar.gz: 34a692c5053ab53c916faf15522d218365e926fd56aa4bf814eac547110b554b996c5ef6bfed09b76136b43705b00e438ad67508e30814b9734bd6bb6b53d07f
6
+ metadata.gz: 563a4e08014aedec4c7a2321f78a22534e8250d6f1b4e5189ce1482b3348c5b8a25f411fcc059ece8984eece9cb53dcdc30d56691dde0856ef74aa14c61c18cc
7
+ data.tar.gz: 3e99489831703bb5aa978923fa2430bcab133ce8e2c4bcfbc84472a699aa09c70935f7d76945eee0cc5d90575c8ff2fe37a3268754e00f723143bdca2179c796
@@ -0,0 +1,55 @@
1
+ ---
2
+ ###########################
3
+ ###########################
4
+ ## Linter GitHub Actions ##
5
+ ###########################
6
+ ###########################
7
+ name: Lint Code Base
8
+
9
+ #
10
+ # Documentation:
11
+ # https://help.github.com/en/articles/workflow-syntax-for-github-actions
12
+ #
13
+
14
+ #############################
15
+ # Start the job on all push #
16
+ #############################
17
+ on:
18
+ push:
19
+ branches-ignore: [master]
20
+ # Remove the line above to run when pushing to master
21
+ pull_request:
22
+ branches: [master]
23
+
24
+ ###############
25
+ # Set the Job #
26
+ ###############
27
+ jobs:
28
+ build:
29
+ # Name the Job
30
+ name: Lint Code Base
31
+ # Set the agent to run on
32
+ runs-on: ubuntu-latest
33
+
34
+ ##################
35
+ # Load all steps #
36
+ ##################
37
+ steps:
38
+ ##########################
39
+ # Checkout the code base #
40
+ ##########################
41
+ - name: Checkout Code
42
+ uses: actions/checkout@v2
43
+ with:
44
+ # Full git history is needed to get a proper list of changed files within `super-linter`
45
+ fetch-depth: 0
46
+
47
+ ################################
48
+ # Run Linter against code base #
49
+ ################################
50
+ - name: Lint Code Base
51
+ uses: github/super-linter@v3
52
+ env:
53
+ VALIDATE_ALL_CODEBASE: false
54
+ DEFAULT_BRANCH: master
55
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
data/.yardopts CHANGED
@@ -1 +1,3 @@
1
1
  --markup markdown
2
+ -
3
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Change Log
2
2
 
3
+ ## [0.6.3] - 2021-03-11
4
+
5
+ - Supports transaction flow control using any flavour SQL with `Ensql.transaction` and `Ensql.rollback!`.
6
+ - Eliminates cyclic dependencies for `Error` and `Ensql.adapter`.
7
+ - Tidies specs.
8
+ - Adopts [standardrb](https://github.com/testdouble/standard).
9
+
3
10
  ## [0.6.2] - 2021-03-09
4
11
 
5
12
  - Adds a specialised adapter for PostgreSQL.
data/Gemfile CHANGED
@@ -6,10 +6,10 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  group :adapters do
9
- require_relative 'lib/ensql/version'
9
+ require_relative "lib/ensql/version"
10
10
  gem "activerecord", Ensql::SUPPORTED_ACTIVERECORD_VERSIONS
11
- gem "sequel", Ensql::SUPPORTED_SEQUEL_VERSIONS
12
- gem "sqlite3", "~> 1.4"
13
- gem "pg", "~> 1.2"
11
+ gem "sequel", Ensql::SUPPORTED_SEQUEL_VERSIONS
12
+ gem "sqlite3", "~> 1.4"
13
+ gem "pg", "~> 1.2"
14
14
  gem "sequel_pg"
15
15
  end
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ensql (0.6.2)
4
+ ensql (0.6.3)
5
5
  connection_pool (>= 0.9.3, < 3)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -3,30 +3,31 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/ensql.svg)](https://badge.fury.io/rb/ensql)
4
4
  [![Ruby](https://github.com/danielfone/ensql/actions/workflows/specs.yml/badge.svg)](https://github.com/danielfone/ensql/actions/workflows/specs.yml)
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/a4ab07e1a03c4d1e8043/maintainability)](https://codeclimate.com/github/danielfone/ensql/maintainability)
6
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
6
7
 
7
- Ensql lets you write SQL for your application the safe and simple way. Ditch your ORM and embrace the power and
8
- simplicity of writing plain SQL again.
8
+ Ensql provides a light-weight wrapper over your existing database connections, letting you write plain SQL for your
9
+ application safely and simply. Ditch your ORM and embrace the power and ease of writing SQL again.
9
10
 
10
- * **Write exactly the SQL you want.** Don't limit your queries to what's in the Rails docs. Composable scopes and
11
- dynamic includes can cripple performance for non-trivial queries. Break through the ORM abstraction and unlock the
12
- power of your database with well-structured SQL and modern database features.
11
+ * **Write exactly the SQL you want.** Don't limit your queries to what's in the Rails docs. Composable scopes and
12
+ dynamic includes can cripple performance for non-trivial queries. Break through the ORM abstraction and unlock the
13
+ power of your database with well-structured SQL and modern database features.
13
14
 
14
- * **Keep your SQL in its own files.** Just like models or view templates, it makes sense to organise your SQL on its
15
- own terms. Storing the queries in their own files encourages better formatted, well commented, literate SQL. It also
16
- leverages the syntax highlighting and autocompletion available in your editor. Snippets of HTML scatter through .rb
17
- files is an awkward code smell, and SQL is no different.
15
+ * **Keep your SQL in its own files.** Just like models or view templates, it makes sense to organise your SQL on its
16
+ own terms. Storing the queries in their own files encourages better formatted, well commented, literate SQL. It also
17
+ leverages the syntax highlighting and autocompletion available in your editor. Snippets of HTML scattered through .rb
18
+ files is an awkward code smell, and SQL is no different.
18
19
 
19
- * **Do more with your database.** Having a place to organise clean and readable SQL encourages you to make the most of it.
20
- In every project I've worked on I've been able to replace useful amounts of imperative ruby logic with a declarative
21
- SQL query, improving performance and reducing the opportunity for type errors and untested branches.
20
+ * **Do more with your database.** Having a place to organise clean and readable SQL encourages you to make the most of
21
+ it. In every project I've worked on I've been able to replace substantial amounts of imperative Ruby logic with a
22
+ declarative SQL query, improving performance and reducing the opportunity for type errors and untested branches.
22
23
 
23
- * **Safely interpolate user-supplied data.** Every web developer knows the risks of SQL injection. Ensql takes a
24
- fail-safe approach to interpolation, leveraging the underlying database adapter to turn ruby objects into properly
25
- quoted SQL literals. As long as user-supplied input is passed as parameters, your queries will be safe and
26
- well-formed.
24
+ * **Safely interpolate user-supplied data.** Every web developer knows the risks of SQL injection. Ensql takes a
25
+ fail-safe approach to interpolation, leveraging the underlying database adapter to turn ruby objects into properly
26
+ quoted SQL literals. As long as user-supplied input is passed as parameters, your queries will be safe and
27
+ well-formed.
27
28
 
28
- * **Use your existing database connection.** Ensql works with ActiveRecord or Sequel so you don't need to manage a
29
- separate connection to the database.
29
+ * **Use your existing database connection.** As well as using PostrgeSQL connections directly, Ensql can work with
30
+ ActiveRecord or Sequel so you don't need to manage a separate connection to the database.
30
31
 
31
32
  ```ruby
32
33
  # Run adhoc statements
@@ -50,22 +51,24 @@ current_results = Ensql.load_sql('results/page', results: all_results, page: 2)
50
51
  total = Ensql.load_sql('count', subquery: all_results)
51
52
  result = { data: current_results.rows, total: total.first_field }
52
53
  ```
53
- ### Further Reading:
54
+ Links:
54
55
 
55
56
  * [Source Code](https://github.com/danielfone/ensql)
56
57
  * [API Documentation](https://rubydoc.info/gems/ensql/Ensql/SQL)
57
- * [Rubygem](https://rubygems.org/gems/ensql)
58
+ * [Ruby Gem](https://rubygems.org/gems/ensql)
58
59
 
59
60
  ## Installation
60
61
 
61
62
  Add this gem to your Gemfile by running:
62
63
 
63
- $ bundle add ensql
64
-
64
+ ```shell
65
+ bundle add ensql
66
+ ```
65
67
  Or install it manually with:
66
68
 
67
- $ gem install ensql
68
-
69
+ ```shell
70
+ gem install ensql
71
+ ```
69
72
  Ensql requires:
70
73
 
71
74
  * ruby >= 2.4.0
@@ -95,26 +98,27 @@ Ensql.adapter = Ensql::PostgresAdapter.pool { PG.connect ENV['DATABASE_URL'] }
95
98
  ```
96
99
  You can also supply your own adapter (see [the API docs](https://rubydoc.info/gems/ensql/Ensql/Adapter) for details of the interface).
97
100
 
98
-
99
101
  SQL can be supplied directly or read from a file. You're encouraged to organise all but the most trivial statements in
100
102
  their own *.sql files, for the reasons outlined above. You can organise them in whatever way makes most sense for your
101
103
  project, but I've found sorting them into directories based on their purpose works well. For example:
102
104
 
103
- app/sql
104
- ├── analytics
105
- │   └── results.sql
106
- ├── program_details
107
- │   ├── widget_query.sql
108
- │   ├── item_query.sql
109
- │   ├── organisation_query.sql
110
- │   └── test_query.sql
111
- ├── reports
112
- │   ├── csv_export.sql
113
- │   ├── filtered.sql
114
- │   └── index.sql
115
- ├── redaction.sql
116
- ├── count.sql
117
- └── set_timeout.sql
105
+ ```text
106
+ app/sql
107
+ ├── analytics
108
+ │   └── results.sql
109
+ ├── program_details
110
+ │   ├── widget_query.sql
111
+ │   ├── item_query.sql
112
+ │   ├── organisation_query.sql
113
+ │   └── test_query.sql
114
+ ├── reports
115
+ │   ├── csv_export.sql
116
+ │   ├── filtered.sql
117
+ │   └── index.sql
118
+ ├── redaction.sql
119
+ ├── count.sql
120
+ └── set_timeout.sql
121
+ ```
118
122
 
119
123
  ### Interpolation
120
124
 
@@ -184,6 +188,22 @@ Ensql.sql('TRUNCATE logs').run # => nil
184
188
  Ensql.run('TRUNCATE logs') # same thing
185
189
  ```
186
190
 
191
+ ### Transactions
192
+
193
+ Ensql encourages you to write pure unmediated SQL with very little procedural management. However, transaction blocks
194
+ are the exception to this rule. Any exceptions inside a transaction block will trigger a rollback, otherwise the block
195
+ will be committed. The block uses SQL-standard commands by default, but custom SQL can be supplied.
196
+
197
+ ```ruby
198
+ Ensql.transaction(start: 'BEGIN ISOLATION LEVEL SERIALIZABLE') do
199
+ do_thing1
200
+ result = check_thing2
201
+ Ensql.rollback! unless result
202
+ do_thing3
203
+ end
204
+ ```
205
+ See [the API docs](https://rubydoc.info/gems/ensql/Ensql#transaction-class_method) for details.
206
+
187
207
  ## Things To Improve
188
208
 
189
209
  - Interpolation syntax. I'd love to ground this in something more reasonable than ruby's custom sprintf format. Maybe we
@@ -204,6 +224,14 @@ experiment.
204
224
 
205
225
  To install this gem onto your local machine, run `bundle exec rake install`.
206
226
 
227
+ ### PR Checklist
228
+
229
+ - [ ] Confirm the code works locally
230
+ - [ ] Update any relevant documentation
231
+ - [ ] Try to break it
232
+ - [ ] Tests the described behaviour
233
+ - [ ] Add a changelog entry (with version bump if needed)
234
+
207
235
  ### Release Checklist
208
236
 
209
237
  - [ ] Review changes in master since last release, especially the public API.
@@ -215,7 +243,7 @@ To install this gem onto your local machine, run `bundle exec rake install`.
215
243
 
216
244
  ## Contributing
217
245
 
218
- Bug reports and pull requests are welcome on GitHub at https://github.com/danielfone/ensql.
246
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/danielfone/ensql>.
219
247
 
220
248
  ## License
221
249
 
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 = "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 = "Ditch 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"
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 = "Ditch 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,8 @@ 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 = "exe"
29
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
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
32
  spec.add_dependency "connection_pool", ">= 0.9.3", "<3"
@@ -35,5 +35,4 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency "rspec", "~> 3.0"
36
36
  spec.add_development_dependency "simplecov", "~> 0.21.2"
37
37
  spec.add_development_dependency "yard", "~> 0.9.26"
38
-
39
38
  end
@@ -6,16 +6,16 @@
6
6
 
7
7
  source "https://rubygems.org"
8
8
 
9
- ruby '~> 2.5.0'
9
+ ruby "~> 2.5.0"
10
10
 
11
11
  # Specify your gem's dependencies in ensql.gemspec
12
- gemspec path: '../'
12
+ gemspec path: "../"
13
13
 
14
14
  # Optional runtime dependencies
15
15
  group :adapters do
16
- require_relative '../lib/ensql/version'
16
+ require_relative "../lib/ensql/version"
17
17
  gem "activerecord", "~> 5.2.0"
18
- gem "sequel", "~> 5.9"
18
+ gem "sequel", "~> 5.9"
19
19
  gem "sqlite3"
20
20
  gem "pg"
21
21
  gem "sequel_pg"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- ensql (0.6.2)
4
+ ensql (0.6.3)
5
5
  connection_pool (>= 0.9.3, < 3)
6
6
 
7
7
  GEM
@@ -6,21 +6,21 @@
6
6
 
7
7
  source "https://rubygems.org"
8
8
 
9
- ruby '2.4.0'
9
+ ruby "2.4.0"
10
10
 
11
11
  # Specify your gem's dependencies in ensql.gemspec
12
- gemspec path: '../'
12
+ gemspec path: "../"
13
13
 
14
14
  # Downgrade simplecov for ruby 2.4 compat
15
- gem 'simplecov', '~> 0.18.5'
16
- gem 'connection_pool', '0.9.3'
15
+ gem "simplecov", "~> 0.18.5"
16
+ gem "connection_pool", "0.9.3"
17
17
 
18
18
  # Optional runtime dependencies
19
19
  group :adapters do
20
- require_relative '../lib/ensql/version'
20
+ require_relative "../lib/ensql/version"
21
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
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
25
  gem "sequel_pg"
26
26
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- ensql (0.6.2)
4
+ ensql (0.6.3)
5
5
  connection_pool (>= 0.9.3, < 3)
6
6
 
7
7
  GEM
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,48 +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
- # Get the current connection adapter. If not specified, it will try to
80
- # autoload an adapter based on the availability of Sequel or ActiveRecord,
81
- # in that order.
82
- #
83
- # @example
84
- # require 'sequel'
85
- # Ensql.adapter # => Ensql::SequelAdapter.new
86
- # Ensql.adapter = Ensql::ActiveRecordAdapter.new # override adapter
87
- # Ensql.adapter = my_tsql_adapter # supply your own adapter
88
- #
89
- def adapter
90
- Thread.current[:ensql_adapter] || Thread.main[:ensql_adapter] ||= autoload_adapter
91
- end
92
-
93
- # Set the connection adapter to use. Must implement the interface defined in
94
- # {Ensql::Adapter}. This uses a thread-local variable so adapters can be
95
- # switched safely in a multi-threaded web server.
96
- def adapter=(adapter)
97
- if adapter.is_a?(Module) && (adapter.name == 'Ensql::SequelAdapter' || adapter.name == 'Ensql::ActiveRecordAdapter')
98
- warn "Using `#{adapter}` as an adapter is deprecated, use `#{adapter}.new`.", uplevel: 1
99
- end
100
-
101
- Thread.current[:ensql_adapter] = adapter
102
- end
103
-
104
- private
105
-
106
- def autoload_adapter
107
- if defined? Sequel
108
- require_relative 'ensql/sequel_adapter'
109
- SequelAdapter.new
110
- elsif defined? ActiveRecord
111
- require_relative 'ensql/active_record_adapter'
112
- ActiveRecordAdapter.new
113
- else
114
- raise Error, "Couldn't autodetect an adapter, please specify manually."
115
- end
116
- end
117
-
118
48
  end
119
49
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'version'
4
- require_relative 'adapter'
5
- require_relative 'pool_wrapper'
3
+ require_relative "version"
4
+ require_relative "adapter"
5
+ require_relative "pool_wrapper"
6
6
 
7
7
  # Ensure our optional dependency has a compatible version
8
- gem 'activerecord', Ensql::SUPPORTED_ACTIVERECORD_VERSIONS
9
- require 'active_record'
8
+ gem "activerecord", Ensql::SUPPORTED_ACTIVERECORD_VERSIONS
9
+ require "active_record"
10
10
 
11
11
  module Ensql
12
12
  #
@@ -43,7 +43,7 @@ module Ensql
43
43
 
44
44
  # Support deprecated class method interface
45
45
  class << self
46
- require 'forwardable'
46
+ require "forwardable"
47
47
  extend Forwardable
48
48
 
49
49
  delegate [:literalize, :run, :fetch_count, :fetch_each_row, :fetch_rows, :fetch_first_column, :fetch_first_field, :fetch_first_row] => :new
@@ -61,7 +61,7 @@ module Ensql
61
61
 
62
62
  # @visibility private
63
63
  def fetch_each_row(sql, &block)
64
- return to_enum(:fetch_each_row, sql) unless block_given?
64
+ return to_enum(:fetch_each_row, sql) unless block
65
65
 
66
66
  result = with_connection { |c| c.exec_query(sql) }
67
67
  # AR populates `column_types` with the types of any columns that haven't
@@ -90,7 +90,7 @@ module Ensql
90
90
  with_connection { |c| c.quote(value) }
91
91
  end
92
92
 
93
- private
93
+ private
94
94
 
95
95
  def with_connection(&block)
96
96
  @base.connection_pool.with_connection(&block)
data/lib/ensql/adapter.rb CHANGED
@@ -1,8 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../ensql"
3
+ require_relative "error"
4
4
 
5
5
  module Ensql
6
+ class << self
7
+ # Get the current connection adapter. If not specified, it will try to
8
+ # autoload an adapter based on the availability of Sequel or ActiveRecord,
9
+ # in that order.
10
+ #
11
+ # @example
12
+ # require 'sequel'
13
+ # Ensql.adapter # => Ensql::SequelAdapter.new
14
+ # Ensql.adapter = Ensql::ActiveRecordAdapter.new # override adapter
15
+ # Ensql.adapter = my_tsql_adapter # supply your own adapter
16
+ #
17
+ def adapter
18
+ Thread.current[:ensql_adapter] || Thread.main[:ensql_adapter] ||= autoload_adapter
19
+ end
20
+
21
+ # Set the connection adapter to use. Must implement the interface defined in
22
+ # {Ensql::Adapter}. This uses a thread-local variable so adapters can be
23
+ # switched safely in a multi-threaded web server.
24
+ def adapter=(adapter)
25
+ if adapter.is_a?(Module) && (adapter.name == "Ensql::SequelAdapter" || adapter.name == "Ensql::ActiveRecordAdapter")
26
+ warn "Using `#{adapter}` as an adapter is deprecated, use `#{adapter}.new`.", uplevel: 1
27
+ end
28
+
29
+ Thread.current[:ensql_adapter] = adapter
30
+ end
31
+
32
+ private
33
+
34
+ def autoload_adapter
35
+ if defined? Sequel
36
+ require_relative "sequel_adapter"
37
+ SequelAdapter.new
38
+ elsif defined? ActiveRecord
39
+ require_relative "active_record_adapter"
40
+ ActiveRecordAdapter.new
41
+ else
42
+ raise Error, "Couldn't autodetect an adapter, please specify manually."
43
+ end
44
+ end
45
+ end
46
+
6
47
  #
7
48
  # @abstract Do not use this module directly.
8
49
  #
@@ -11,7 +52,6 @@ module Ensql
11
52
  # that can be improved in the adapters.
12
53
  #
13
54
  module Adapter
14
-
15
55
  # @!group 1. Interface Methods
16
56
 
17
57
  # @!method literalize(value)
@@ -65,7 +105,6 @@ module Ensql
65
105
 
66
106
  # @!group 2. Predefined Methods
67
107
 
68
-
69
108
  # Execute the query and return only the first row of the result.
70
109
  # @return <Hash>
71
110
  def fetch_first_row(sql)
@@ -82,6 +121,5 @@ module Ensql
82
121
  def fetch_first_field(sql)
83
122
  fetch_first_row(sql)&.values&.first
84
123
  end
85
-
86
124
  end
87
125
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ensql
4
+ # Wrapper for errors raised by Ensql
5
+ class Error < StandardError; end
6
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sql"
4
+
5
+ module Ensql
6
+ class << self
7
+ # Path to search for *.sql queries in, defaults to "sql/". For example, if
8
+ # {sql_path} is set to 'app/queries', `load_sql('users/active')` will read
9
+ # 'app/queries/users/active.sql'.
10
+ # @see .load_sql
11
+ #
12
+ # @example
13
+ # Ensql.sql_path = Rails.root.join('app/queries')
14
+ #
15
+ def sql_path
16
+ @sql_path ||= "sql"
17
+ end
18
+ attr_writer :sql_path
19
+
20
+ # Load SQL from a file within {sql_path}. This is the recommended way to
21
+ # manage SQL in a non-trivial project. For details of how to write
22
+ # interpolation placeholders, see {SQL}.
23
+ #
24
+ # @see .sql_path=
25
+ # @return [Ensql::SQL]
26
+ #
27
+ # @example
28
+ # Ensql.load_sql('users/activity', report_params)
29
+ # Ensql.load_sql(:upsert_users, imported_users_attrs)
30
+ #
31
+ def load_sql(name, params = {})
32
+ path = File.join(sql_path, "#{name}.sql")
33
+ SQL.new(File.read(path), params, name)
34
+ end
35
+ end
36
+ end
@@ -3,7 +3,6 @@
3
3
  module Ensql
4
4
  # Wrap a 3rd-party connection pool with a standard interface. Connections can be checked out by {with}
5
5
  class PoolWrapper
6
-
7
6
  # Wraps a block for accessing a connection from a pool.
8
7
  #
9
8
  # PoolWrapper.new do |client_block|
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'version'
4
- require_relative 'adapter'
3
+ require_relative "version"
4
+ require_relative "adapter"
5
5
 
6
- gem 'pg', Ensql::SUPPORTED_PG_VERSIONS
7
- require 'pg'
8
- require 'connection_pool'
6
+ gem "pg", Ensql::SUPPORTED_PG_VERSIONS
7
+ require "pg"
8
+ require "connection_pool"
9
9
 
10
10
  module Ensql
11
11
  # Wraps a pool of PG connections to implement the {Adapter} interface. The
@@ -61,7 +61,7 @@ module Ensql
61
61
  # @visibility private
62
62
  def literalize(value)
63
63
  case value
64
- when NilClass then 'NULL'
64
+ when NilClass then "NULL"
65
65
  when Numeric, TrueClass, FalseClass then value.to_s
66
66
  when String then @quoter.encode(value)
67
67
  else
@@ -92,7 +92,7 @@ module Ensql
92
92
 
93
93
  # @visibility private
94
94
  def fetch_each_row(sql, &block)
95
- return to_enum(:fetch_each_row, sql) unless block_given?
95
+ return to_enum(:fetch_each_row, sql) unless block
96
96
 
97
97
  fetch_result(sql) { |res| res.each(&block) }
98
98
  end
@@ -102,7 +102,7 @@ module Ensql
102
102
  fetch_result(sql, &:to_a)
103
103
  end
104
104
 
105
- private
105
+ private
106
106
 
107
107
  def fetch_result(sql)
108
108
  execute(sql) do |res|
@@ -117,7 +117,7 @@ module Ensql
117
117
 
118
118
  # Use PG's built-in type mapping to serialize objects into SQL strings.
119
119
  def serialize(value)
120
- coder = encoder_for(value) or raise TypeError, "No SQL serializer for #{value.class}"
120
+ (coder = encoder_for(value)) || raise(TypeError, "No SQL serializer for #{value.class}")
121
121
  coder.encode(value)
122
122
  end
123
123
 
@@ -143,17 +143,18 @@ module Ensql
143
143
  # :nocov:
144
144
  unless defined? PG::TextEncoder::Numeric
145
145
  class NumericDecoder < PG::SimpleDecoder
146
- def decode(string, tuple=nil, field=nil)
146
+ def decode(string, tuple = nil, field = nil)
147
147
  BigDecimal(string)
148
148
  end
149
149
  end
150
+
150
151
  class NumericEncoder < PG::SimpleEncoder
151
152
  def encode(decimal)
152
- decimal.to_s('F')
153
+ decimal.to_s("F")
153
154
  end
154
155
  end
155
156
  private_constant :NumericDecoder, :NumericEncoder
156
- PG::BasicTypeRegistry.register_type(0, 'numeric', NumericEncoder, NumericDecoder)
157
+ PG::BasicTypeRegistry.register_type(0, "numeric", NumericEncoder, NumericDecoder)
157
158
  end
158
159
  # :nocov:
159
160
  end
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'version'
4
- require_relative 'adapter'
5
- require_relative 'pool_wrapper'
3
+ require_relative "version"
4
+ require_relative "adapter"
5
+ require_relative "pool_wrapper"
6
+ require_relative "error"
6
7
 
7
8
  # Ensure our optional dependency has a compatible version
8
- gem 'sequel', Ensql::SUPPORTED_SEQUEL_VERSIONS
9
- require 'sequel'
9
+ gem "sequel", Ensql::SUPPORTED_SEQUEL_VERSIONS
10
+ require "sequel"
10
11
 
11
12
  module Ensql
12
13
  #
@@ -40,7 +41,7 @@ module Ensql
40
41
 
41
42
  # Support deprecated class method interface
42
43
  class << self
43
- require 'forwardable'
44
+ require "forwardable"
44
45
  extend Forwardable
45
46
 
46
47
  delegate [:literalize, :run, :fetch_count, :fetch_each_row, :fetch_rows, :fetch_first_column, :fetch_first_field, :fetch_first_row] => :new
@@ -93,13 +94,12 @@ module Ensql
93
94
  db.literal(value)
94
95
  end
95
96
 
96
- private
97
+ private
97
98
 
98
99
  attr_reader :db
99
100
 
100
101
  def first_configured_database
101
- Sequel::DATABASES.first or raise Error, "no database found in Sequel::DATABASES"
102
+ Sequel::DATABASES.first || raise(Error, "no database found in Sequel::DATABASES")
102
103
  end
103
-
104
104
  end
105
105
  end
data/lib/ensql/sql.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../ensql"
3
+ require_relative "adapter"
4
+ require_relative "error"
4
5
 
5
6
  module Ensql
6
7
  #
@@ -53,9 +54,8 @@ module Ensql
53
54
  # # SELECT * FROM users ORDER BY name asc
54
55
  #
55
56
  class SQL
56
-
57
57
  # @!visibility private
58
- def initialize(sql, params={}, name='SQL')
58
+ def initialize(sql, params = {}, name = "SQL")
59
59
  @sql = sql
60
60
  @name = name.to_s
61
61
  @params = params
@@ -106,14 +106,14 @@ module Ensql
106
106
  interpolate(sql, params)
107
107
  end
108
108
 
109
- private
109
+ private
110
110
 
111
111
  attr_reader :sql, :params, :name
112
112
 
113
- NESTED_LIST = /%{(\w+)\((.+)\)}/m
114
- LIST = /%{\((\w+)\)}/
113
+ NESTED_LIST = /%{(\w+)\((.+)\)}/m
114
+ LIST = /%{\((\w+)\)}/
115
115
  SQL_FRAGMENT = /%{!(\w+)}/
116
- LITERAL = /%{(\w+)}/
116
+ LITERAL = /%{(\w+)}/
117
117
 
118
118
  def interpolate(sql, params)
119
119
  params = params.transform_keys(&:to_s)
@@ -132,13 +132,13 @@ module Ensql
132
132
  Array(array)
133
133
  .map { |attrs| interpolate(nested_sql, Hash(attrs)) }
134
134
  .map { |sql| "(#{sql})" }
135
- .join(', ')
135
+ .join(", ")
136
136
  end
137
137
 
138
138
  def interpolate_list(array)
139
- return '(NULL)' if Array(array).empty?
139
+ return "(NULL)" if Array(array).empty?
140
140
 
141
- '(' + Array(array).map { |v| literalize v }.join(', ') + ')'
141
+ "(" + Array(array).map { |v| literalize v }.join(", ") + ")"
142
142
  end
143
143
 
144
144
  def interpolate_sql(sql)
@@ -158,6 +158,5 @@ module Ensql
158
158
  def adapter
159
159
  Ensql.adapter
160
160
  end
161
-
162
161
  end
163
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,11 +2,11 @@
2
2
 
3
3
  module Ensql
4
4
  # Gem version
5
- VERSION = "0.6.2"
5
+ VERSION = "0.6.3"
6
6
  # Versions of activerecord compatible with the {ActiveRecordAdapter}
7
- SUPPORTED_ACTIVERECORD_VERSIONS = ['>= 5.0', '< 6.2'].freeze
7
+ SUPPORTED_ACTIVERECORD_VERSIONS = [">= 5.0", "< 6.2"].freeze
8
8
  # Versions of sequel compatible with the {SequelAdapter}
9
- SUPPORTED_SEQUEL_VERSIONS = '~> 5.9'
9
+ SUPPORTED_SEQUEL_VERSIONS = "~> 5.9"
10
10
  # Versions of pg compatibile with the {PostgresAdapter}
11
- SUPPORTED_PG_VERSIONS = ['>= 0.19', '< 2'].freeze
11
+ SUPPORTED_PG_VERSIONS = [">= 0.19", "< 2"].freeze
12
12
  end
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
2
5
  #
3
6
  # Compare operations performed using each adapter
4
7
  #
5
8
 
6
- ENV['TZ'] = 'UTC'
9
+ ENV["TZ"] = "UTC"
7
10
 
8
- require 'benchmark/ips'
11
+ require "benchmark/ips"
9
12
 
10
- require_relative 'lib/ensql/active_record_adapter'
11
- require_relative 'lib/ensql/sequel_adapter'
12
- require_relative 'lib/ensql/postgres_adapter'
13
+ require_relative "lib/ensql/active_record_adapter"
14
+ require_relative "lib/ensql/sequel_adapter"
15
+ require_relative "lib/ensql/postgres_adapter"
13
16
 
14
17
  ActiveRecord::Base.establish_connection(adapter: "postgresql")
15
18
  DB = Sequel.connect("postgresql:/")
@@ -20,13 +23,13 @@ adapters = {
20
23
  'ar ': Ensql::ActiveRecordAdapter.new(ActiveRecord::Base.connection_pool),
21
24
  'seq ': Ensql::SequelAdapter.new(DB),
22
25
  'pg-ar ': Ensql::PostgresAdapter.new(Ensql::ActiveRecordAdapter.pool),
23
- 'pg-seq ': Ensql::PostgresAdapter.new(Ensql::SequelAdapter.pool(DB)),
26
+ 'pg-seq ': Ensql::PostgresAdapter.new(Ensql::SequelAdapter.pool(DB))
24
27
  }
25
28
 
26
29
  ADAPTER = adapters.values.first
27
30
 
28
- ADAPTER.run('drop table if exists number_benchmark')
29
- ADAPTER.run('create table number_benchmark as select generate_series(1,100) as number')
31
+ ADAPTER.run("drop table if exists number_benchmark")
32
+ ADAPTER.run("create table number_benchmark as select generate_series(1,100) as number")
30
33
 
31
34
  adapter_tests = {
32
35
  'literalize (String)': [:literalize, "It's quoted"],
@@ -34,30 +37,30 @@ adapter_tests = {
34
37
  'literalize (Time)': [:literalize, Time.now],
35
38
  'literalize (Int)': [:literalize, 1234],
36
39
  'literalize (bool)': [:literalize, true],
37
- 'run INSERT': [:run, 'insert into number_benchmark values (999)'],
40
+ 'run INSERT': [:run, "insert into number_benchmark values (999)"],
38
41
  'run SET': [:run, "set time zone UTC"],
39
- 'run SELECT': [:run, 'select generate_series(1,100)'],
40
- 'count UPDATE': [:fetch_count, 'update number_benchmark set number = number + 1'],
41
- 'count SELECT': [:fetch_count, 'select generate_series(1,100)'],
42
- 'first column': [:fetch_first_column, 'select generate_series(1,100)'],
43
- 'first column (of many)': [:fetch_first_column, 'select *, now() from generate_series(1,100) as number'],
44
- 'first field': [:fetch_first_field, 'select 1'],
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"],
45
48
  'first field with cast': [:fetch_first_field, "select cast('2021-01-01' as timestamp)"],
46
- 'first field (of many)': [:fetch_first_field, 'select generate_series(1,100)'],
49
+ 'first field (of many)': [:fetch_first_field, "select generate_series(1,100)"],
47
50
  'first row': [:fetch_first_row, "select 1, 2, 3"],
48
51
  'first row (cast)': [:fetch_first_row, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json)"],
49
52
  'first row (of many)': [:fetch_first_row, "select generate_series(1, 100)"],
50
53
  'rows (1)': [:fetch_rows, "select 1, 1"],
51
54
  'rows (100)': [:fetch_rows, "select 1, 1, generate_series(1, 100)"],
52
55
  'rows (100,cast)': [:fetch_rows, "select cast('2021-01-01' as timestamp), cast('[1,2,3]' as json), generate_series(1, 100)"],
53
- 'rows (100000)': [:fetch_rows, "select 1, 1, generate_series(1, 100000)"],
56
+ 'rows (100000)': [:fetch_rows, "select 1, 1, generate_series(1, 100000)"]
54
57
  }
55
58
 
56
59
  fetch_each_row_tests = {
57
- 'each_row (1)': [:fetch_each_row, "select 1, 1" ],
60
+ 'each_row (1)': [:fetch_each_row, "select 1, 1"],
58
61
  'each_row (100)': [:fetch_each_row, "select 1, 1, generate_series(1, 100)"],
59
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)"],
60
- 'each_row (100000)': [:fetch_each_row, "select 1, 1, generate_series(1, 100000)"],
63
+ 'each_row (100000)': [:fetch_each_row, "select 1, 1, generate_series(1, 100000)"]
61
64
  }
62
65
 
63
66
  # Verify results are the same
@@ -71,7 +74,7 @@ end
71
74
 
72
75
  # Compare times
73
76
  adapter_tests.each do |test_name, args|
74
- puts args.map { |a| a.inspect[0..100] }.join(' ')
77
+ puts args.map { |a| a.inspect[0..100] }.join(" ")
75
78
 
76
79
  Benchmark.ips(quiet: true) do |x|
77
80
  x.config(stats: :bootstrap, confidence: 95, warmup: 0.2, time: 0.5)
@@ -96,4 +99,4 @@ fetch_each_row_tests.each do |test_name, args|
96
99
  end
97
100
  end
98
101
 
99
- ADAPTER.run('drop table number_benchmark')
102
+ ADAPTER.run("drop table number_benchmark")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ensql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
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-03-09 00:00:00.000000000 Z
11
+ date: 2021-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool
@@ -94,6 +94,7 @@ executables: []
94
94
  extensions: []
95
95
  extra_rdoc_files: []
96
96
  files:
97
+ - ".github/workflows/lint.yml"
97
98
  - ".github/workflows/specs.yml"
98
99
  - ".gitignore"
99
100
  - ".rspec"
@@ -116,10 +117,13 @@ files:
116
117
  - lib/ensql.rb
117
118
  - lib/ensql/active_record_adapter.rb
118
119
  - lib/ensql/adapter.rb
120
+ - lib/ensql/error.rb
121
+ - lib/ensql/load_sql.rb
119
122
  - lib/ensql/pool_wrapper.rb
120
123
  - lib/ensql/postgres_adapter.rb
121
124
  - lib/ensql/sequel_adapter.rb
122
125
  - lib/ensql/sql.rb
126
+ - lib/ensql/transaction.rb
123
127
  - lib/ensql/version.rb
124
128
  - perf/adapter_benchmark.rb
125
129
  homepage: https://github.com/danielfone/ensql