ensql 0.6.0 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
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