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.
data/lib/ensql/sql.rb CHANGED
@@ -1,11 +1,12 @@
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
  #
7
- # Encapsulates a plain-text SQL statement and optional parameters to interpolate. Interpolation is indicated by one
8
- # of the four placeholder formats:
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='SQL')
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
- private
109
+ private
108
110
 
109
111
  attr_reader :sql, :params, :name
110
112
 
111
- NESTED_LIST = /%{(\w+)\((.+)\)}/m
112
- LIST = /%{\((\w+)\)}/
113
+ NESTED_LIST = /%{(\w+)\((.+)\)}/m
114
+ LIST = /%{\((\w+)\)}/
113
115
  SQL_FRAGMENT = /%{!(\w+)}/
114
- LITERAL = /%{(\w+)}/
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 '(NULL)' if Array(array).empty?
139
+ return "(NULL)" if Array(array).empty?
138
140
 
139
- '(' + Array(array).map { |v| literalize v }.join(', ') + ')'
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.0"
6
- # Version of the activerecord gem required to use the {ActiveRecordAdapter}
7
- ACTIVERECORD_VERSION = ['>= 5.0', '< 6.2'].freeze
8
- # Version of the sequel gem required to use the {SequelAdapter}
9
- SEQUEL_VERSION = '~> 5.10'
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.0
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-02-23 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Ditch your ORM and embrace the power and simplicity of writing plain
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.9
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