db_mod 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +46 -0
- data/.rspec +2 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +14 -0
- data/CODE_OF_CONDUCT.md +22 -0
- data/Gemfile +15 -0
- data/Guardfile +94 -0
- data/LICENSE +20 -0
- data/README.md +170 -0
- data/Rakefile +25 -0
- data/db_mod.gemspec +26 -0
- data/lib/db_mod/create.rb +51 -0
- data/lib/db_mod/exceptions/already_in_transaction.rb +11 -0
- data/lib/db_mod/exceptions/base.rb +7 -0
- data/lib/db_mod/exceptions/connection_not_set.rb +11 -0
- data/lib/db_mod/exceptions/duplicate_statement_name.rb +11 -0
- data/lib/db_mod/exceptions.rb +9 -0
- data/lib/db_mod/statements/params.rb +108 -0
- data/lib/db_mod/statements/prepared.rb +235 -0
- data/lib/db_mod/statements/statement.rb +29 -0
- data/lib/db_mod/statements.rb +13 -0
- data/lib/db_mod/transaction.rb +47 -0
- data/lib/db_mod/version.rb +5 -0
- data/lib/db_mod.rb +87 -0
- data/spec/db_mod/create_spec.rb +39 -0
- data/spec/db_mod/statements/prepared_spec.rb +155 -0
- data/spec/db_mod/transaction_spec.rb +69 -0
- data/spec/db_mod_spec.rb +90 -0
- data/spec/spec_helper.rb +15 -0
- metadata +177 -0
@@ -0,0 +1,235 @@
|
|
1
|
+
require_relative 'params'
|
2
|
+
|
3
|
+
module DbMod
|
4
|
+
module Statements
|
5
|
+
# Provides the +def_prepared+ function which allows
|
6
|
+
# {DbMod} modules to declare prepared SQL statements
|
7
|
+
# that will be added to the database connection when
|
8
|
+
# {DbMod#db_connect} is called.
|
9
|
+
#
|
10
|
+
# For statements that are not prepared ahead of execution,
|
11
|
+
# see +def_statement+ in {DbMod::Statements::Statement}.
|
12
|
+
#
|
13
|
+
# def_prepared
|
14
|
+
# ------------
|
15
|
+
#
|
16
|
+
# +def_prepared+ accepts two parameters:
|
17
|
+
# * `name` [Symbol]: The name that will be given to
|
18
|
+
# the prepared statement. A method will also be defined
|
19
|
+
# on the module with the same name which will call the
|
20
|
+
# statement and return the result.
|
21
|
+
# * `sql` [String]: The SQL statement to be prepared.
|
22
|
+
# Parameters may be declared using the $ symbol followed
|
23
|
+
# by a number ($1, $2, $3) or a name ($one, $two, $under_scores).
|
24
|
+
# The two styles may not be mixed in the same statement.
|
25
|
+
# The defined function can then be passed parameters
|
26
|
+
# that will be used when the statement is executed.
|
27
|
+
#
|
28
|
+
# ### example
|
29
|
+
#
|
30
|
+
# module MyModule
|
31
|
+
# include DbMod
|
32
|
+
#
|
33
|
+
# def_prepared :my_prepared, <<-SQL
|
34
|
+
# SELECT *
|
35
|
+
# FROM stuff
|
36
|
+
# WHERE a = $1 AND b = $2
|
37
|
+
# SQL
|
38
|
+
#
|
39
|
+
# def_prepared :my_named_prepared, <<-SQL
|
40
|
+
# SELECT *
|
41
|
+
# FROM stuff
|
42
|
+
# WHERE a = $a AND b = $b
|
43
|
+
# SQL
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# include MyModule
|
47
|
+
# db_connect db: 'mydb'
|
48
|
+
# my_prepared(1,2)
|
49
|
+
# my_named_prepared(a: 1, b: 2)
|
50
|
+
module Prepared
|
51
|
+
include Params
|
52
|
+
|
53
|
+
# Defines a module-specific +def_prepared+ function
|
54
|
+
# for a module that has just had {DbMod} included.
|
55
|
+
#
|
56
|
+
# @param mod [Module]
|
57
|
+
def self.setup(mod)
|
58
|
+
Prepared.define_def_prepared(mod)
|
59
|
+
Prepared.define_prepared_statements(mod)
|
60
|
+
Prepared.define_inherited_prepared_statements(mod)
|
61
|
+
Prepared.define_prepare_all_statements(mod)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Merge the prepared statements from a module
|
67
|
+
# into a given hash. Fails if there are any
|
68
|
+
# duplicates.
|
69
|
+
#
|
70
|
+
# @param statements [Hash] named list of prepared statements
|
71
|
+
# @param klass [Class,Module] ancestor (hopefully a DbMod module)
|
72
|
+
# to collect prepared statements from
|
73
|
+
def self.merge_statements(statements, klass)
|
74
|
+
return unless klass.respond_to? :prepared_statements
|
75
|
+
return if klass.prepared_statements.nil?
|
76
|
+
|
77
|
+
klass.prepared_statements.each do |name, sql|
|
78
|
+
fail DbMod::Exceptions::DuplicateStatementName if statements.key? name
|
79
|
+
|
80
|
+
statements[name] = sql
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Add a +def_prepared+ method definition to a module.
|
85
|
+
# This method allows modules to declare named SQL statements
|
86
|
+
# that will be prepared when the database connection is
|
87
|
+
# established, and that can be accessed via an instance
|
88
|
+
# method with the same name.
|
89
|
+
#
|
90
|
+
# @param mod [Module] a module with {DbMod} included
|
91
|
+
def self.define_def_prepared(mod)
|
92
|
+
mod.class.instance_eval do
|
93
|
+
define_method(:def_prepared) do |name, sql|
|
94
|
+
sql = sql.dup
|
95
|
+
name = name.to_sym
|
96
|
+
|
97
|
+
params = Params.parse_params! sql
|
98
|
+
prepared_statements[name] = sql
|
99
|
+
Prepared.define_prepared_method(mod, name, params)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Defines +prepare_all_statements+, a module method which
|
105
|
+
# accepts a connection object and will prepare on it all of
|
106
|
+
# the prepared statements that have been declared on the
|
107
|
+
# module or any of its included modules.
|
108
|
+
#
|
109
|
+
# @param mod [Module] module that has {DbMod} included
|
110
|
+
def self.define_prepare_all_statements(mod)
|
111
|
+
mod.class.instance_eval do
|
112
|
+
define_method(:prepare_all_statements) do |conn|
|
113
|
+
inherited_prepared_statements.each do |name, sql|
|
114
|
+
conn.prepare(name.to_s, sql)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Define a method in the module with the given name
|
121
|
+
# and parameters, that will call the prepared statement
|
122
|
+
# with the same name.
|
123
|
+
#
|
124
|
+
# @param mod [Module] module declaring the metho
|
125
|
+
# @param name [Symbol] method name
|
126
|
+
# @param params [Fixnum,Array<Symbol>]
|
127
|
+
# expected parameter count, or a list of argument names.
|
128
|
+
# An empty array produces a no-argument method.
|
129
|
+
def self.define_prepared_method(mod, name, params)
|
130
|
+
mod.expected_prepared_statement_parameters[name] = params
|
131
|
+
|
132
|
+
if params.is_a?(Array)
|
133
|
+
if params.empty?
|
134
|
+
define_no_args_prepared_method(mod, name)
|
135
|
+
else
|
136
|
+
define_named_args_prepared_method(mod, name, params)
|
137
|
+
end
|
138
|
+
else
|
139
|
+
define_fixed_args_prepared_method(mod, name, params)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Define a no-argument method with the given name
|
144
|
+
# that will call the prepared statement with the
|
145
|
+
# same name.
|
146
|
+
#
|
147
|
+
# @param mod [Module] {DbMod} enabled module
|
148
|
+
# where the method will be defined
|
149
|
+
# @param name [Symbol] name of the method to be defined
|
150
|
+
# and the prepared query to be called.
|
151
|
+
def self.define_no_args_prepared_method(mod, name)
|
152
|
+
mod.instance_eval do
|
153
|
+
define_method name, ->() { conn.exec_prepared(name.to_s) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Define a method with the given name that accepts the
|
158
|
+
# given set of named parameters, that will call the prepared
|
159
|
+
# statement with the same name.
|
160
|
+
#
|
161
|
+
# @param mod [Module] {DbMod} enabled module
|
162
|
+
# where the method will be defined
|
163
|
+
# @param name [Symbol] name of the method to be defined
|
164
|
+
# and the prepared query to be called.
|
165
|
+
# @param params [Array<Symbol>] list of parameter names
|
166
|
+
def self.define_named_args_prepared_method(mod, name, params)
|
167
|
+
method = lambda do |*args|
|
168
|
+
unless args.size == 1
|
169
|
+
fail ArgumentError, "unexpected arguments: #{args.inspect}"
|
170
|
+
end
|
171
|
+
args = Params.valid_named_args! params, args.first
|
172
|
+
conn.exec_prepared(name.to_s, args)
|
173
|
+
end
|
174
|
+
|
175
|
+
mod.instance_eval { define_method(name, method) }
|
176
|
+
end
|
177
|
+
|
178
|
+
# Define a method with the given name that accepts a fixed
|
179
|
+
# number of arguments, that will call the prepared statement
|
180
|
+
# with the same name.
|
181
|
+
#
|
182
|
+
# @param mod [Module] {DbMod} enabled module
|
183
|
+
# where the method will be defined
|
184
|
+
# @param name [Symbol] name of the method to be defined
|
185
|
+
# and the prepared query to be called.
|
186
|
+
# @param count [Fixnum] arity of the defined method,
|
187
|
+
# the number of parameters that the prepared statement
|
188
|
+
# requires
|
189
|
+
def self.define_fixed_args_prepared_method(mod, name, count)
|
190
|
+
method = lambda do |*args|
|
191
|
+
unless args.size == count
|
192
|
+
fail ArgumentError, "#{args.size} args given, #{count} expected"
|
193
|
+
end
|
194
|
+
|
195
|
+
conn.exec_prepared(name.to_s, args)
|
196
|
+
end
|
197
|
+
|
198
|
+
mod.instance_eval { define_method(name, method) }
|
199
|
+
end
|
200
|
+
|
201
|
+
# Adds +prepared_statements+ to a module. This list of named
|
202
|
+
# prepared statements will be added to the connection when
|
203
|
+
# {DbMod#db_connect} is called.
|
204
|
+
#
|
205
|
+
# @param mod [Module]
|
206
|
+
def self.define_prepared_statements(mod)
|
207
|
+
mod.class.instance_eval do
|
208
|
+
define_method(:prepared_statements) do
|
209
|
+
@prepared_statements ||= {}
|
210
|
+
end
|
211
|
+
|
212
|
+
define_method(:expected_prepared_statement_parameters) do
|
213
|
+
@expected_prepared_statement_parameters ||= {}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
# Adds +inherited_prepared_statements+ to a module. This list
|
219
|
+
# of named prepared statements declared on this module and all
|
220
|
+
# included modules will be added to the connection when
|
221
|
+
# {DbMod#db_connect} is called.
|
222
|
+
def self.define_inherited_prepared_statements(mod)
|
223
|
+
mod.class.instance_eval do
|
224
|
+
define_method(:inherited_prepared_statements) do
|
225
|
+
inherited = {}
|
226
|
+
ancestors.each do |klass|
|
227
|
+
Prepared.merge_statements(inherited, klass)
|
228
|
+
end
|
229
|
+
inherited
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module DbMod
|
2
|
+
module Statements
|
3
|
+
# Provides the +def_statement+ function which allows
|
4
|
+
# {DbMod} modules to declare SQL statements that can
|
5
|
+
# then be executed later using a specially defined
|
6
|
+
# instance method.
|
7
|
+
#
|
8
|
+
# To declare prepared statements, see +def_prepared+
|
9
|
+
# in {DbMod::Statements::Prepared}.
|
10
|
+
#
|
11
|
+
# def_statement
|
12
|
+
# -------------
|
13
|
+
#
|
14
|
+
# +def_statement+ accepts two parameters:
|
15
|
+
# * `name` [Symbol]: The name that will be given to the
|
16
|
+
# method that can be used to execute the SQL statement
|
17
|
+
# and return the result.
|
18
|
+
# * `sql` [String]: The SQL statement that shoul be executed
|
19
|
+
# when the method is called. Parameters may be declared
|
20
|
+
# using the $ symbol followed by a number ($1, $2, $3) or
|
21
|
+
# a name ($one, $two, $under_scores). The two styles may
|
22
|
+
# not be mixed in the same statement. The defined function
|
23
|
+
# can then be passed parameters that will be used to fill
|
24
|
+
# in the statement before execution.
|
25
|
+
module Statement
|
26
|
+
# Not yet implemented.
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative 'statements/statement'
|
2
|
+
require_relative 'statements/prepared'
|
3
|
+
|
4
|
+
module DbMod
|
5
|
+
# Functions allowing {DbMod} modules to declare
|
6
|
+
# SQL statements that can be called later via
|
7
|
+
# automatically declared instance methods.
|
8
|
+
#
|
9
|
+
# See {DbMod::Statements::Statement} for details on +def_statement+
|
10
|
+
# and {DbMod::Statements::Prepared} for details on +def_prepared+.
|
11
|
+
module Statements
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DbMod
|
2
|
+
# Module which provides transaction blocks for db_mod
|
3
|
+
# enabled classes.
|
4
|
+
module Transaction
|
5
|
+
protected
|
6
|
+
|
7
|
+
# Create a transaction on the db_mod database connection.
|
8
|
+
# Calls +BEGIN+ then yields to the given block. Calls
|
9
|
+
# +COMMIT+ once the block yields, or +ROLLBACK+ if the
|
10
|
+
# block raises an exception.
|
11
|
+
#
|
12
|
+
# Not thread safe. May not be called from inside another
|
13
|
+
# transaction.
|
14
|
+
# @return [Object] the result of +yield+
|
15
|
+
def transaction
|
16
|
+
start_transaction!
|
17
|
+
|
18
|
+
result = yield
|
19
|
+
|
20
|
+
query 'COMMIT'
|
21
|
+
|
22
|
+
result
|
23
|
+
rescue
|
24
|
+
query 'ROLLBACK'
|
25
|
+
raise
|
26
|
+
|
27
|
+
ensure
|
28
|
+
end_transaction!
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Start the database transaction, or fail if
|
34
|
+
# one is already open.
|
35
|
+
def start_transaction!
|
36
|
+
fail DbMod::Exceptions::AlreadyInTransaction if @in_transaction
|
37
|
+
@in_transaction = true
|
38
|
+
|
39
|
+
query 'BEGIN'
|
40
|
+
end
|
41
|
+
|
42
|
+
# End the database transaction
|
43
|
+
def end_transaction!
|
44
|
+
@in_transaction = false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/db_mod.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'pg'
|
2
|
+
require_relative 'db_mod/exceptions'
|
3
|
+
require_relative 'db_mod/transaction'
|
4
|
+
require_relative 'db_mod/create'
|
5
|
+
require_relative 'db_mod/statements'
|
6
|
+
|
7
|
+
# This is the foundation module for enabling db_mod
|
8
|
+
# support in an application. Including this module
|
9
|
+
# will give your class or object the protected methods
|
10
|
+
# {#db_connect} and {#conn=}, allowing the connection
|
11
|
+
# to be set or created, as well as the methods {#conn},
|
12
|
+
# {#query}, {#transaction}, and {#def_prepared}.
|
13
|
+
module DbMod
|
14
|
+
include Transaction
|
15
|
+
|
16
|
+
# When a module includes {DbMod}, we define some
|
17
|
+
# class-level functions specific to the module.
|
18
|
+
def self.included(mod)
|
19
|
+
DbMod::Create.setup(mod)
|
20
|
+
DbMod::Statements::Prepared.setup(mod)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
# Database object to be used for all database
|
26
|
+
# interactions in this module.
|
27
|
+
# Use {#db_connect} to initialize the object.
|
28
|
+
attr_accessor :conn
|
29
|
+
|
30
|
+
# Shorthand for +conn.query+
|
31
|
+
def query(sql)
|
32
|
+
unless @conn
|
33
|
+
fail DbMod::Exceptions::ConnectionNotSet, 'db_connect not called'
|
34
|
+
end
|
35
|
+
conn.query(sql)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create a new database connection to be used
|
39
|
+
# for all database interactions in this module.
|
40
|
+
#
|
41
|
+
# @param options [Hash] database connection options
|
42
|
+
# @option options [String] :db
|
43
|
+
# the name of the database to connect to
|
44
|
+
# @option options [String] :host
|
45
|
+
# the host server for the database. If not supplied a local
|
46
|
+
# posix socket connection will be attempted.
|
47
|
+
# @option options [Fixnum] :port
|
48
|
+
# port number the database server is listening on. Default is 5432.
|
49
|
+
# @option options [String] :user
|
50
|
+
# username for database authentication. If not supplied the
|
51
|
+
# name of the user running the script will be used (i.e. ENV['USER'])
|
52
|
+
# @option options [String] :pass
|
53
|
+
# password for database authentication. If not supplied then
|
54
|
+
# trusted authentication will be attempted.
|
55
|
+
def db_connect(options = {})
|
56
|
+
db_defaults! options
|
57
|
+
@conn = db_connect! options
|
58
|
+
self.class.prepare_all_statements(@conn)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Load any missing options from defaults
|
64
|
+
#
|
65
|
+
# @param options [Hash] see {#db_connect}
|
66
|
+
def db_defaults!(options)
|
67
|
+
fail ArgumentError, 'database name :db not supplied' unless options[:db]
|
68
|
+
options[:port] ||= 5432
|
69
|
+
options[:user] ||= ENV['USER']
|
70
|
+
options[:pass] ||= 'trusted?'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Create the database object itself.
|
74
|
+
#
|
75
|
+
# @param options [Hash] see {#db_connect}
|
76
|
+
def db_connect!(options)
|
77
|
+
PGconn.connect(
|
78
|
+
options[:host],
|
79
|
+
options[:port],
|
80
|
+
'',
|
81
|
+
'',
|
82
|
+
options[:db],
|
83
|
+
options[:user],
|
84
|
+
options[:pass]
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module CreateTest
|
4
|
+
include DbMod
|
5
|
+
|
6
|
+
def do_thing
|
7
|
+
query 'SELECT 1'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe DbMod::Create do
|
12
|
+
before do
|
13
|
+
@conn = instance_double 'PGconn'
|
14
|
+
allow(PGconn).to receive(:connect).and_return(@conn)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'creates module instances' do
|
18
|
+
expect(PGconn).to receive(:connect).with(
|
19
|
+
nil, 5432, '', '', 'testdb', ENV['USER'], 'trusted?'
|
20
|
+
)
|
21
|
+
|
22
|
+
test = CreateTest.create db: 'testdb'
|
23
|
+
|
24
|
+
expect(@conn).to receive(:query).with('SELECT 1')
|
25
|
+
|
26
|
+
test.do_thing
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can be supplied an existing connection' do
|
30
|
+
expect(PGconn).not_to receive(:connect)
|
31
|
+
expect(@conn).to receive(:is_a?).with(PGconn).and_return true
|
32
|
+
|
33
|
+
test = CreateTest.create @conn
|
34
|
+
|
35
|
+
expect(@conn).to receive(:query).with('SELECT 1')
|
36
|
+
|
37
|
+
test.do_thing
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DbMod::Statements::Prepared do
|
4
|
+
subject do
|
5
|
+
Module.new do
|
6
|
+
include DbMod
|
7
|
+
|
8
|
+
def_prepared :one, <<-SQL
|
9
|
+
SELECT *
|
10
|
+
FROM foo
|
11
|
+
WHERE a = $1 AND b = $2 AND c = $1
|
12
|
+
SQL
|
13
|
+
|
14
|
+
def_prepared :two, <<-SQL
|
15
|
+
SELECT *
|
16
|
+
FROM foo
|
17
|
+
WHERE a = $a AND b = $b AND c = $a
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
@conn = instance_double 'PGconn'
|
24
|
+
allow(PGconn).to receive(:connect).and_return(@conn)
|
25
|
+
allow(@conn).to receive(:prepare)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'adds statements when the connection is created' do
|
29
|
+
expect(@conn).to receive(:prepare) do |name, sql|
|
30
|
+
expect(%w(one two)).to include name
|
31
|
+
expect(sql.split(/\s+/m).join(' ').strip).to eq(
|
32
|
+
'SELECT * FROM foo WHERE a = $1 AND b = $2 AND c = $1'
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
subject.create db: 'testdb'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'executes statements with numbered params' do
|
40
|
+
db = subject.create db: 'testdb'
|
41
|
+
|
42
|
+
expect(@conn).to receive(:exec_prepared).with('one', [1, 'two'])
|
43
|
+
db.one(1, 'two')
|
44
|
+
|
45
|
+
expect { db.one 'too', 'many', 'args' }.to raise_exception ArgumentError
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'executes statements with named params' do
|
49
|
+
db = subject.create db: 'testdb'
|
50
|
+
|
51
|
+
expect(@conn).to receive(:exec_prepared).with('two', [2, 'three'])
|
52
|
+
db.two(b: 'three', a: 2)
|
53
|
+
|
54
|
+
expect { db.two bad: 'arg', b: 1, a: 1 }.to raise_exception ArgumentError
|
55
|
+
expect { db.two b: 'a missing' }.to raise_exception ArgumentError
|
56
|
+
expect { db.two 1, 2 }.to raise_exception ArgumentError
|
57
|
+
expect { db.two 1 }.to raise_exception ArgumentError
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'prepares and exposes inherited prepared statements' do
|
61
|
+
mod = subject
|
62
|
+
sub_module = Module.new do
|
63
|
+
include mod
|
64
|
+
|
65
|
+
def_prepared :three, <<-SQL
|
66
|
+
SELECT *
|
67
|
+
FROM foo
|
68
|
+
WHERE a = $1 OR b = $2 OR c = $1
|
69
|
+
SQL
|
70
|
+
end
|
71
|
+
|
72
|
+
expect(@conn).to receive(:prepare).exactly(3).times do |name, _|
|
73
|
+
expect(%w(one two three)).to include name
|
74
|
+
end
|
75
|
+
|
76
|
+
sub_module.create db: 'testdb'
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'does not allow mixed parameter types' do
|
80
|
+
expect do
|
81
|
+
Module.new do
|
82
|
+
include DbMod
|
83
|
+
|
84
|
+
def_prepared :numbers_and_names, <<-SQL
|
85
|
+
SELECT *
|
86
|
+
FROM foo
|
87
|
+
WHERE this = $1 AND wont = $work
|
88
|
+
SQL
|
89
|
+
end
|
90
|
+
end.to raise_exception ArgumentError
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'allows no parameters' do
|
94
|
+
expect do
|
95
|
+
mod = Module.new do
|
96
|
+
include DbMod
|
97
|
+
|
98
|
+
def_prepared :no_params, 'SELECT 1'
|
99
|
+
end
|
100
|
+
|
101
|
+
expect(@conn).to receive(:prepare).with('no_params', 'SELECT 1')
|
102
|
+
expect(@conn).to receive(:exec_prepared).with('no_params')
|
103
|
+
|
104
|
+
db = mod.create(db: 'testdb')
|
105
|
+
db.no_params
|
106
|
+
|
107
|
+
expect { db.no_params(1) }.to raise_exception ArgumentError
|
108
|
+
end.not_to raise_exception
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'does not allow invalid parameters' do
|
112
|
+
%w(CAPITALS numb3rs_and_l3tt3rs).each do |param|
|
113
|
+
expect do
|
114
|
+
Module.new do
|
115
|
+
include DbMod
|
116
|
+
|
117
|
+
def_prepared :bad_params, %(
|
118
|
+
SELECT * FROM foo where bad = $#{param}
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end.to raise_exception ArgumentError
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'does not allow duplicate statement names' do
|
126
|
+
mod = subject
|
127
|
+
sub_module = Module.new do
|
128
|
+
include mod
|
129
|
+
|
130
|
+
def_prepared :one, <<-SQL
|
131
|
+
SELECT not FROM gonna WHERE work = $1
|
132
|
+
SQL
|
133
|
+
end
|
134
|
+
|
135
|
+
expect { sub_module.create db: 'testdb' }.to raise_exception(
|
136
|
+
DbMod::Exceptions::DuplicateStatementName
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'validates numbered arguments' do
|
141
|
+
[
|
142
|
+
'a = $1 AND b = $2 AND c = $2 AND c = $4',
|
143
|
+
'a = $2 AND b = $2 AND c = $3',
|
144
|
+
'a = $1 AND b = $2 AND c = $4'
|
145
|
+
].each do |params|
|
146
|
+
expect do
|
147
|
+
Module.new do
|
148
|
+
include DbMod
|
149
|
+
|
150
|
+
def_prepared :bad_params, "SELECT * FROM foo WHERE #{params}"
|
151
|
+
end
|
152
|
+
end.to raise_exception ArgumentError
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TransactionTest
|
4
|
+
include DbMod
|
5
|
+
|
6
|
+
def connect
|
7
|
+
db_connect db: 'testdb'
|
8
|
+
end
|
9
|
+
|
10
|
+
def transaction!
|
11
|
+
transaction do
|
12
|
+
query 'SELECT 1'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def nested
|
17
|
+
transaction do
|
18
|
+
transaction do
|
19
|
+
# Not reached
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe DbMod::Transaction do
|
26
|
+
subject { TransactionTest.new }
|
27
|
+
|
28
|
+
before do
|
29
|
+
@conn = instance_double 'PGconn'
|
30
|
+
allow(@conn).to receive(:query)
|
31
|
+
allow(subject).to receive(:db_connect!).and_return @conn
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#transaction' do
|
35
|
+
it 'expects the connection to be set' do
|
36
|
+
expect { subject.transaction! }.to raise_exception(
|
37
|
+
DbMod::Exceptions::ConnectionNotSet
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'calls BEGIN and COMMIT' do
|
42
|
+
subject.connect
|
43
|
+
|
44
|
+
expect(@conn).to receive(:query).with 'BEGIN'
|
45
|
+
expect(@conn).to receive(:query).with 'SELECT 1'
|
46
|
+
expect(@conn).to receive(:query).with 'COMMIT'
|
47
|
+
|
48
|
+
subject.transaction!
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'calls ROLLBACK if there is a failure' do
|
52
|
+
subject.connect
|
53
|
+
|
54
|
+
expect(@conn).to receive(:query).with 'BEGIN'
|
55
|
+
expect(@conn).to receive(:query).with('SELECT 1').and_raise 'error'
|
56
|
+
expect(@conn).to receive(:query).with 'ROLLBACK'
|
57
|
+
|
58
|
+
expect { subject.transaction! }.to raise_exception 'error'
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'guards against concurrent transactions' do
|
62
|
+
subject.connect
|
63
|
+
|
64
|
+
expect { subject.nested }.to raise_exception(
|
65
|
+
DbMod::Exceptions::AlreadyInTransaction
|
66
|
+
)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|