db_mod 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|