squirm 0.0.2
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/.gitignore +18 -0
- data/.yardopts +4 -0
- data/MIT-LICENSE +19 -0
- data/README.md +77 -0
- data/Rakefile +18 -0
- data/lib/squirm.rb +65 -0
- data/lib/squirm/core.rb +86 -0
- data/lib/squirm/pool.rb +48 -0
- data/lib/squirm/procedure.rb +218 -0
- data/lib/squirm/procedure.sql +15 -0
- data/lib/squirm/version.rb +3 -0
- data/spec/core_spec.rb +98 -0
- data/spec/helper.rb +19 -0
- data/spec/pool_spec.rb +34 -0
- data/spec/procedure_spec.rb +137 -0
- data/squirm.gemspec +22 -0
- metadata +87 -0
data/.gitignore
ADDED
data/.yardopts
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Norman Clarke and Business Vision S.A.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Squirm
|
2
|
+
|
3
|
+
Squirm is a database library that facilitates working with Postgres and stored
|
4
|
+
procedures.
|
5
|
+
|
6
|
+
## About
|
7
|
+
|
8
|
+
Squirm is not stable yet. Feel free to play around with it, but unless you want
|
9
|
+
to contribute to its development, you probably shouldn't use it for anything
|
10
|
+
sensitive.
|
11
|
+
|
12
|
+
It currently provides:
|
13
|
+
|
14
|
+
* A basic connection pool
|
15
|
+
* A little syntactic sugar around the pg gem
|
16
|
+
* Class for accessing stored procedures as if they were Ruby procs
|
17
|
+
|
18
|
+
Here's a quick demo of how you might use it:
|
19
|
+
|
20
|
+
-- Your database
|
21
|
+
CREATE TABLE "users" (
|
22
|
+
"name" VARCHAR(256),
|
23
|
+
"email" VARCHAR(64) NOT NULL UNIQUE
|
24
|
+
);
|
25
|
+
|
26
|
+
CREATE SCHEMA "users";
|
27
|
+
|
28
|
+
CREATE FUNCTION "users"."create"(_email text, _name text) RETURNS integer AS $$
|
29
|
+
DECLARE
|
30
|
+
new_id integer;
|
31
|
+
BEGIN
|
32
|
+
INSERT INTO "users" (email, name) VALUES (_email, _name)
|
33
|
+
RETURNING id INTO new_id;
|
34
|
+
IF FOUND THEN
|
35
|
+
RETURN new_id;
|
36
|
+
END IF;
|
37
|
+
END;
|
38
|
+
$$ LANGUAGE 'plpgsql';
|
39
|
+
|
40
|
+
Squirm.connect dbname: "your_database"
|
41
|
+
create = Squirm::Procedure.new("create", schema: "users")
|
42
|
+
id = create.call(email: "johndoe@example.com", name: "John Doe")
|
43
|
+
|
44
|
+
In and of itself, Squirm offers very little, but is meant to be a basic building
|
45
|
+
block for other libraries.
|
46
|
+
|
47
|
+
One such library is Squirm Model, which is currently under development.
|
48
|
+
|
49
|
+
This library will offer an ActiveModel-compatible, ORM-like library that uses
|
50
|
+
stored procedures and SQL rather than generating the SQL for you. Stay tuned
|
51
|
+
for more details.
|
52
|
+
|
53
|
+
## Author
|
54
|
+
|
55
|
+
Norman Clarke <nclarke@bvision.com>
|
56
|
+
|
57
|
+
## License
|
58
|
+
|
59
|
+
Copyright (c) 2011 Norman Clarke and Business Vision S.A.
|
60
|
+
|
61
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
62
|
+
of this software and associated documentation files (the "Software"), to deal
|
63
|
+
in the Software without restriction, including without limitation the rights
|
64
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
65
|
+
copies of the Software, and to permit persons to whom the Software is
|
66
|
+
furnished to do so, subject to the following conditions:
|
67
|
+
|
68
|
+
The above copyright notice and this permission notice shall be included in all
|
69
|
+
copies or substantial portions of the Software.
|
70
|
+
|
71
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
72
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
73
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
74
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
75
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
76
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
77
|
+
SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rake/clean'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
CLEAN.include "pkg", "spec/coverage", "doc", "*.gem"
|
5
|
+
|
6
|
+
task default: :test
|
7
|
+
|
8
|
+
task :gem do
|
9
|
+
sh "gem build squirm.gemspec"
|
10
|
+
end
|
11
|
+
|
12
|
+
task :test do
|
13
|
+
Rake::TestTask.new do |t|
|
14
|
+
t.libs << "spec"
|
15
|
+
t.test_files = FileList["spec/*_spec.rb"]
|
16
|
+
t.verbose = false
|
17
|
+
end
|
18
|
+
end
|
data/lib/squirm.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require "squirm/core"
|
2
|
+
require "squirm/pool"
|
3
|
+
require "squirm/procedure"
|
4
|
+
|
5
|
+
=begin
|
6
|
+
Squirm is an experimental anti-ORM for database-loving programmers who want to
|
7
|
+
take full advantage of the advanced functionality offered by Postgres. With
|
8
|
+
Squirm, you write your entire database layer using the tools of the domain
|
9
|
+
experts: SQL and stored procedures. Muahahahahaha!
|
10
|
+
|
11
|
+
== Using it
|
12
|
+
|
13
|
+
First of all you should know that this is experimental, and in-progress. So you
|
14
|
+
might want to exercise lots of caution. Don't build your mission critical app on
|
15
|
+
top of Squirm now, or possibly ever.
|
16
|
+
|
17
|
+
=== Getting a connection
|
18
|
+
|
19
|
+
Squirm comes with a very simple, threadsafe connection pool.
|
20
|
+
|
21
|
+
Squirm.connect dbname: "postgres", pool_size: 5, timeout: 5
|
22
|
+
|
23
|
+
=== Performing queries
|
24
|
+
|
25
|
+
The `Squirm.use` method will check out a connection and yield it to the block
|
26
|
+
you pass. The connection is a vanilla instance of PGConn, so all of Postgres's
|
27
|
+
functionality is directly exposed to you without any sugar or intermediaries.
|
28
|
+
|
29
|
+
When the block returns, the connection is checked back into the pool.
|
30
|
+
|
31
|
+
# conn is a PGconn instance
|
32
|
+
Squirm.use do |conn|
|
33
|
+
conn.exec "SELECT * FROM users" do |result|
|
34
|
+
puts result.first
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# shorthand for above
|
39
|
+
Squirm.exec "SELECT * FROM users" do |result|
|
40
|
+
puts result.first
|
41
|
+
end
|
42
|
+
|
43
|
+
`Squirm.use` executes the block inside a new thread, and set the currently
|
44
|
+
checked out connection as a thread local variable, so that calls to Squirm.exec
|
45
|
+
inside the block will use the same connection. It will wait for the thread to
|
46
|
+
return and then return the block's return value.
|
47
|
+
|
48
|
+
=== Accessing a stored procedure
|
49
|
+
|
50
|
+
Accessing an API you create is simple:
|
51
|
+
|
52
|
+
procedure = Squirm::Procedure.new "create", schema: "users"
|
53
|
+
procedure.call email: "john@example.com", name: "John Doe"
|
54
|
+
|
55
|
+
You can also get easy access to the functions that ship with Postgres, if for
|
56
|
+
some reason you want to use them:
|
57
|
+
|
58
|
+
proc = Squirm::Procedure.new("date", schema: "pg_catalog", args: "abstime")
|
59
|
+
proc.call("Jan 1, 2011") #=> "2011-01-01"
|
60
|
+
|
61
|
+
=end
|
62
|
+
module Squirm
|
63
|
+
Rollback, Timeout = 2.times.map { Class.new RuntimeError }
|
64
|
+
extend Core
|
65
|
+
end
|
data/lib/squirm/core.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require "pg"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module Squirm
|
5
|
+
|
6
|
+
# The core DSL used by Squirm.
|
7
|
+
module Core
|
8
|
+
|
9
|
+
# Establishes a connection pool.
|
10
|
+
# @param [Hash] options The connection options
|
11
|
+
# @option options [String] :pool Use the given pool rather than Squirm's.
|
12
|
+
# @option options [Fixnum] :timeout The pool timeout.
|
13
|
+
# @option options [Fixnum] :pool_size The pool size.
|
14
|
+
def connect(options = {})
|
15
|
+
return @pool = options[:pool] if options[:pool]
|
16
|
+
options = options.dup
|
17
|
+
timeout = options.delete(:timeout) || 5
|
18
|
+
pool_size = options.delete(:pool_size) || 1
|
19
|
+
@pool = Squirm::Pool.new(timeout)
|
20
|
+
pool_size.times do
|
21
|
+
conn = PGconn.open(options)
|
22
|
+
yield conn if block_given?
|
23
|
+
@pool.checkin conn
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Disconnects all pool connections and sets the pool to nil.
|
28
|
+
def disconnect
|
29
|
+
return unless pool
|
30
|
+
pool.map(&:close)
|
31
|
+
@pool = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
# Executes the query and passes the result to the block you specify.
|
35
|
+
def exec(*args, &block)
|
36
|
+
if current = Thread.current[:squirm_connection]
|
37
|
+
current.exec(*args, &block)
|
38
|
+
else
|
39
|
+
use {|conn| conn.exec(*args, &block)}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Gets the connection pool.
|
44
|
+
# @return [Squirm::Pool] The connection pool.
|
45
|
+
def pool
|
46
|
+
@pool if defined? @pool
|
47
|
+
end
|
48
|
+
|
49
|
+
# Performs a #use inside a transaction.
|
50
|
+
def transaction
|
51
|
+
use do |connection|
|
52
|
+
connection.transaction do |conn|
|
53
|
+
begin
|
54
|
+
yield conn
|
55
|
+
rescue Rollback
|
56
|
+
return
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Rolls back from inside a #transaction block.
|
63
|
+
def rollback
|
64
|
+
raise Rollback
|
65
|
+
end
|
66
|
+
|
67
|
+
# Checks out a connection and uses it for all database access inside the
|
68
|
+
# block.
|
69
|
+
def use
|
70
|
+
conn = @pool.checkout
|
71
|
+
begin
|
72
|
+
Thread.current[:squirm_connection] = conn
|
73
|
+
yield conn
|
74
|
+
ensure
|
75
|
+
Thread.current[:squirm_connection] = nil
|
76
|
+
@pool.checkin conn
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Quotes an SQL identifier.
|
81
|
+
# @return [String] The identifier.
|
82
|
+
def quote_ident(*args)
|
83
|
+
PGconn.quote_ident(*args.map(&:to_s))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/lib/squirm/pool.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "monitor"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module Squirm
|
6
|
+
|
7
|
+
# A ridiculously simple object pool.
|
8
|
+
class Pool
|
9
|
+
extend Forwardable
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :connections
|
13
|
+
attr_accessor :timeout
|
14
|
+
|
15
|
+
def_delegator :@mutex, :synchronize
|
16
|
+
|
17
|
+
def initialize(timeout=5)
|
18
|
+
@mutex = Monitor.new
|
19
|
+
@timeout = timeout
|
20
|
+
@condition = @mutex.new_cond
|
21
|
+
@queue = []
|
22
|
+
@connections = Set.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# Synchronizes iterations provided by Enumerable.
|
26
|
+
def each(&block)
|
27
|
+
synchronize { @connections.each(&block) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check a connection back in.
|
31
|
+
def checkout
|
32
|
+
synchronize do
|
33
|
+
return @queue.shift unless @queue.empty?
|
34
|
+
@condition.wait(@timeout)
|
35
|
+
@queue.empty? ? raise(Timeout) : next
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check out a connection.
|
40
|
+
def checkin(conn)
|
41
|
+
synchronize do
|
42
|
+
@connections.add conn
|
43
|
+
@queue.push conn
|
44
|
+
@condition.signal
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require "pathname"
|
2
|
+
|
3
|
+
module Squirm
|
4
|
+
|
5
|
+
# This class wraps access to a Postgres stored procedure, exposing it to
|
6
|
+
# Ruby as if it were a Ruby Proc.
|
7
|
+
class Procedure
|
8
|
+
|
9
|
+
# The Postgres stored procedure's name.
|
10
|
+
# @return [String]
|
11
|
+
attr :name
|
12
|
+
|
13
|
+
# The schema which holds the stored procedure. Defaults to +public+.
|
14
|
+
# @return [String]
|
15
|
+
attr :schema
|
16
|
+
|
17
|
+
# An instance of {Arguments} encapsulating information about the
|
18
|
+
# arguments needed to invoke the procedure.
|
19
|
+
# @return [Squirm::Procedure::Arguments]
|
20
|
+
attr :arguments
|
21
|
+
|
22
|
+
# The procedure's Postgres return type
|
23
|
+
# @return [String]
|
24
|
+
attr :return_type
|
25
|
+
|
26
|
+
# The SQL query used to invoke the stored procedure.
|
27
|
+
# @return [String]
|
28
|
+
attr :query
|
29
|
+
|
30
|
+
# Raised when an overloaded stored procedure is specified, but no argument
|
31
|
+
# list is given.
|
32
|
+
#
|
33
|
+
# To avoid this error when using overloaded procedures, initialize the
|
34
|
+
# {Squirm::Procedure} with the `:args` option.
|
35
|
+
class TooManyChoices < RuntimeError
|
36
|
+
end
|
37
|
+
|
38
|
+
# Raised when the stored procedure can not be found.
|
39
|
+
class NotFound < RuntimeError
|
40
|
+
end
|
41
|
+
|
42
|
+
# The SQL query used to load meta info about the procedure.
|
43
|
+
INFO_SQL = Pathname(__FILE__).dirname.join("procedure.sql").read
|
44
|
+
|
45
|
+
# Creates a new stored procedure.
|
46
|
+
def initialize(name, options = {})
|
47
|
+
@name = name
|
48
|
+
@schema = options[:schema] || 'public'
|
49
|
+
@arguments = Arguments.new(options[:args]) if options[:args]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Loads meta info about the stored procedure.
|
53
|
+
#
|
54
|
+
# This action is not performed in the constructor to allow instances to
|
55
|
+
# be created before a database connection has been established.
|
56
|
+
#
|
57
|
+
# @return [Squirm::Procedure] The instance
|
58
|
+
def load
|
59
|
+
query = (arguments or self).info_sql
|
60
|
+
Squirm.exec(query, [name, schema]) do |result|
|
61
|
+
validate result
|
62
|
+
set_values_from result
|
63
|
+
end
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
# The SQL query used to get meta information about the procedure.
|
68
|
+
# @see INFO_SQL
|
69
|
+
# @return [String]
|
70
|
+
def info_sql
|
71
|
+
INFO_SQL
|
72
|
+
end
|
73
|
+
|
74
|
+
# Invokes the procedure.
|
75
|
+
def call(*args, &block)
|
76
|
+
Squirm.exec query, arguments.format(*args) do |result|
|
77
|
+
if block_given?
|
78
|
+
yield result
|
79
|
+
elsif return_type =~ /\ASETOF/
|
80
|
+
result.to_a
|
81
|
+
else
|
82
|
+
result.getvalue(0,0)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
alias [] call
|
88
|
+
|
89
|
+
# Checks the number of values returned when looking up meta info about
|
90
|
+
# the procedure.
|
91
|
+
# @see #load
|
92
|
+
# @see #info_sql
|
93
|
+
def validate(result)
|
94
|
+
if result.ntuples == 0
|
95
|
+
raise NotFound
|
96
|
+
elsif result.ntuples > 1
|
97
|
+
raise TooManyChoices
|
98
|
+
end
|
99
|
+
end
|
100
|
+
private :validate
|
101
|
+
|
102
|
+
# Processes the meta info query_result, setting variables needed by the
|
103
|
+
# instance.
|
104
|
+
def set_values_from(result)
|
105
|
+
@arguments = Arguments.new(result[0]['arguments'])
|
106
|
+
@return_type = result[0]['return_type']
|
107
|
+
@query = "SELECT * FROM %s.%s(%s)" % [
|
108
|
+
quoted_schema,
|
109
|
+
quoted_name,
|
110
|
+
@arguments.to_params
|
111
|
+
]
|
112
|
+
end
|
113
|
+
private :set_values_from
|
114
|
+
|
115
|
+
# The quoted procedure name.
|
116
|
+
# @return [String]
|
117
|
+
def quoted_name
|
118
|
+
Squirm.quote_ident name
|
119
|
+
end
|
120
|
+
|
121
|
+
# The quoted schema name.
|
122
|
+
# @return [String]
|
123
|
+
def quoted_schema
|
124
|
+
Squirm.quote_ident schema
|
125
|
+
end
|
126
|
+
|
127
|
+
# A collection of argument definitions for a stored procedure. This class
|
128
|
+
# delegates both to an internal hash and to its keys, so it has mixed
|
129
|
+
# Array/Hash-like behavior. This allows you to access arguments by offset
|
130
|
+
# or name.
|
131
|
+
#
|
132
|
+
# This may seem like an odd mix of behaviors but is intended to
|
133
|
+
# idiomatically translate Postgres's support for both named and unnamed
|
134
|
+
# stored procedure arguments.
|
135
|
+
class Arguments
|
136
|
+
attr :hash, :string
|
137
|
+
|
138
|
+
extend Forwardable
|
139
|
+
include Enumerable
|
140
|
+
|
141
|
+
def_delegator :hash, :keys
|
142
|
+
def_delegator :keys, :each
|
143
|
+
|
144
|
+
alias to_s string
|
145
|
+
|
146
|
+
# Gets an instance of Arguments from a string.
|
147
|
+
#
|
148
|
+
# This string can come from a lookup in the pg_proc catalog, or in the
|
149
|
+
# case of overloaded functions, will be specified explicitly by the
|
150
|
+
# programmer.
|
151
|
+
def initialize(string)
|
152
|
+
@string = string
|
153
|
+
@hash = self.class.hashify(string)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Formats arguments used to call the stored procedure.
|
157
|
+
#
|
158
|
+
# When given a anything other than a hash, the arguments are returned
|
159
|
+
# without modification.
|
160
|
+
#
|
161
|
+
# When given a hash, the return value is an array or arguments in the
|
162
|
+
# order needed when calling the procedure. Missing values are replaced by
|
163
|
+
# nil.
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# # Assume a stored procedure with a definition like the following:
|
167
|
+
# # print_greeting(greeting text, greeter text, language text)
|
168
|
+
# arguments.format(greeter: "John", greeting: "hello") #=> ["hello", "John", nil]
|
169
|
+
# @return Array
|
170
|
+
def format(*args)
|
171
|
+
args.first.kind_of?(Hash) ? map {|name| args[0][name]} : args
|
172
|
+
end
|
173
|
+
|
174
|
+
# Gets an argument's Postgres type by index or offset.
|
175
|
+
# @overload [](offset)
|
176
|
+
# @param [Fixnum] offset The argument's offset
|
177
|
+
# @overload [](key)
|
178
|
+
# @param [String] key The argument's name
|
179
|
+
# @example
|
180
|
+
# arguments[0] #=> "text"
|
181
|
+
# arguments["created_at"] #=> "timestamp with time zone"
|
182
|
+
def [](key)
|
183
|
+
(key.kind_of?(Fixnum) ? keys : hash)[key]
|
184
|
+
end
|
185
|
+
|
186
|
+
# Gets Postgres-formatted params for use in calling the procedure.
|
187
|
+
# @example
|
188
|
+
# arguments.to_params #=> "$1::text, $2::integer, $3::text"
|
189
|
+
# @return String
|
190
|
+
def to_params
|
191
|
+
@params ||= each_with_index.map do |key, index|
|
192
|
+
"$%s::%s" % [index.next, hash[key]]
|
193
|
+
end.join(", ")
|
194
|
+
end
|
195
|
+
|
196
|
+
# Gets an SQL query used to look up meta information about a stored
|
197
|
+
# procedure with a matching argument signature.
|
198
|
+
def info_sql
|
199
|
+
"#{INFO_SQL} AND pg_catalog.pg_get_function_arguments(p.oid) = '#{to_s}'"
|
200
|
+
end
|
201
|
+
|
202
|
+
# Converts an argument string to a hash whose keys are argument names
|
203
|
+
# and whose values are argument types.
|
204
|
+
def self.hashify(string)
|
205
|
+
hash = {}
|
206
|
+
string.split(",").map do |arg|
|
207
|
+
arg, type = arg.strip.split(/\s+/, 2)
|
208
|
+
type ||= arg
|
209
|
+
arg = arg.gsub(/\s+/, '_').gsub(/\A_/, '')
|
210
|
+
count = hash.keys.count {|elem| elem =~ /#{arg}[\d]?/}
|
211
|
+
key = count == 0 ? arg : arg + count.next.to_s
|
212
|
+
hash[key.to_sym] = type
|
213
|
+
end
|
214
|
+
hash
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
SELECT p.proname AS "name",
|
2
|
+
pg_catalog.pg_get_function_result(p.oid) AS "return_type",
|
3
|
+
pg_catalog.pg_get_function_arguments(p.oid) AS "arguments",
|
4
|
+
p.proargtypes AS "argtypes",
|
5
|
+
CASE
|
6
|
+
WHEN p.proisagg THEN 'agg'
|
7
|
+
WHEN p.proiswindow THEN 'window'
|
8
|
+
WHEN p.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN 'trigger'
|
9
|
+
ELSE 'normal'
|
10
|
+
END AS "procedure_type"
|
11
|
+
FROM pg_catalog.pg_proc p
|
12
|
+
LEFT JOIN pg_catalog.pg_namespace n
|
13
|
+
ON n.oid = p.pronamespace
|
14
|
+
WHERE p.proname = $1::text
|
15
|
+
AND n.nspname = $2::text
|
data/spec/core_spec.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
require "ostruct"
|
3
|
+
|
4
|
+
describe Squirm do
|
5
|
+
|
6
|
+
before { Squirm.disconnect }
|
7
|
+
after { Squirm.disconnect }
|
8
|
+
|
9
|
+
it "should quote identifiers" do
|
10
|
+
assert_equal '"table"', Squirm.quote_ident("table")
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#connect" do
|
14
|
+
it "should use a pool if given" do
|
15
|
+
pool = OpenStruct.new
|
16
|
+
Squirm.connect pool: pool
|
17
|
+
assert_equal pool, Squirm.pool
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should establish a connection pool" do
|
21
|
+
Squirm.connect $squirm_test_connection
|
22
|
+
assert !Squirm.pool.connections.empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should establish :pool_size connections" do
|
26
|
+
Squirm.connect $squirm_test_connection.merge pool_size: 2
|
27
|
+
assert_equal 2, Squirm.pool.connections.count
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should set :timeout to the pool's timeout" do
|
31
|
+
Squirm.connect $squirm_test_connection.merge timeout: 9999
|
32
|
+
assert_equal 9999, Squirm.pool.timeout
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#disconnect" do
|
37
|
+
it "should close all connections" do
|
38
|
+
mock = MiniTest::Mock.new
|
39
|
+
mock.expect :close, nil
|
40
|
+
Squirm.instance_variable_set :@pool, [mock]
|
41
|
+
Squirm.disconnect
|
42
|
+
mock.verify
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#use" do
|
47
|
+
it "should set a connection as a Thread local var only during yield" do
|
48
|
+
connection = OpenStruct.new
|
49
|
+
Squirm.instance_variable_set :@pool, Squirm::Pool.new
|
50
|
+
Squirm.pool.checkin connection
|
51
|
+
Squirm.use do |conn|
|
52
|
+
assert_equal conn, Thread.current[:squirm_connection]
|
53
|
+
end
|
54
|
+
assert_nil Thread.current[:squirm_connection]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#exec" do
|
59
|
+
it "should execute a query" do
|
60
|
+
Squirm.connect $squirm_test_connection
|
61
|
+
Squirm.exec "SELECT 'world' as hello" do |result|
|
62
|
+
assert_equal "world", result.getvalue(0,0)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should use the thread local connection if set" do
|
67
|
+
mock = MiniTest::Mock.new
|
68
|
+
mock.expect(:exec, true, [String])
|
69
|
+
begin
|
70
|
+
Thread.current[:squirm_connection] = mock
|
71
|
+
Squirm.exec "SELECT * FROM table"
|
72
|
+
mock.verify
|
73
|
+
ensure
|
74
|
+
Thread.current[:squirm_connection] = nil
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#transaction" do
|
80
|
+
it "should set the connection to a transaction state" do
|
81
|
+
Squirm.connect $squirm_test_connection
|
82
|
+
Squirm.transaction do |conn|
|
83
|
+
assert_equal PGconn::PQTRANS_INTRANS, conn.transaction_status
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "#rollback" do
|
89
|
+
it "should exit the block" do
|
90
|
+
Squirm.connect $squirm_test_connection
|
91
|
+
Squirm.transaction do |conn|
|
92
|
+
Squirm.rollback
|
93
|
+
assert false, "rollback should have exited the block"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
if ENV["COVERAGE"]
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start do
|
4
|
+
add_filter "/spec/"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
9
|
+
require "rubygems"
|
10
|
+
gem "minitest"
|
11
|
+
require "minitest/spec"
|
12
|
+
require 'minitest/autorun'
|
13
|
+
require 'pg'
|
14
|
+
|
15
|
+
$VERBOSE = true
|
16
|
+
|
17
|
+
require "squirm"
|
18
|
+
|
19
|
+
$squirm_test_connection = {dbname: "squirm_test"}
|
data/spec/pool_spec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
describe Squirm::Pool do
|
4
|
+
describe "#checkout" do
|
5
|
+
it "should wait until timeout if all connections busy" do
|
6
|
+
assert try_checkout(0.05, 0.02)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should raise error if pool checkout times out" do
|
10
|
+
assert_raises Squirm::Timeout do
|
11
|
+
try_checkout(0.02, 0.05)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def try_checkout(timeout, sleep_time)
|
18
|
+
pool = Squirm::Pool.new(timeout)
|
19
|
+
pool.checkin Object.new
|
20
|
+
t1 = Thread.new do
|
21
|
+
conn = pool.checkout
|
22
|
+
sleep(sleep_time)
|
23
|
+
pool.checkin conn
|
24
|
+
end
|
25
|
+
t2 = Thread.new {
|
26
|
+
sleep(0.01)
|
27
|
+
pool.checkout
|
28
|
+
}
|
29
|
+
[t1, t2].map(&:value)
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require_relative "helper"
|
2
|
+
|
3
|
+
describe Squirm::Procedure::Arguments do
|
4
|
+
|
5
|
+
it "should have a count" do
|
6
|
+
args = Squirm::Procedure::Arguments.new("text, text")
|
7
|
+
assert_equal 2, args.count
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should number duplicate names" do
|
11
|
+
args = Squirm::Procedure::Arguments.new("text, text, text")
|
12
|
+
assert_equal :text, args[0]
|
13
|
+
assert_equal :text2, args[1]
|
14
|
+
assert_equal :text3, args[2]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should detect named args" do
|
18
|
+
args = Squirm::Procedure::Arguments.new("hello text, world text")
|
19
|
+
assert_equal :hello, args[0]
|
20
|
+
assert_equal :world, args[1]
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should remove leading underscores from arg names" do
|
24
|
+
args = Squirm::Procedure::Arguments.new("_hello text, _world text")
|
25
|
+
assert_equal :hello, args[0]
|
26
|
+
assert_equal :world, args[1]
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#to_params" do
|
30
|
+
it "should give a list of numeric params" do
|
31
|
+
args = Squirm::Procedure::Arguments.new("text, text")
|
32
|
+
assert_equal "$1::text, $2::text", args.to_params
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# describe "#format" do
|
37
|
+
# it "should return an array of args in the proper order" do
|
38
|
+
# args = Squirm::Procedure::Arguments.new("hello text, world text")
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe Squirm::Procedure do
|
45
|
+
|
46
|
+
before { Squirm.disconnect }
|
47
|
+
after { Squirm.disconnect }
|
48
|
+
|
49
|
+
it "should have an info SQL statement" do
|
50
|
+
assert_match(/SELECT/, Squirm::Procedure.new("foo").info_sql)
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#initialize" do
|
54
|
+
it "should set a default schema if none is given" do
|
55
|
+
procedure = Squirm::Procedure.new("foo")
|
56
|
+
assert_equal 'public', procedure.schema
|
57
|
+
assert_equal "foo", procedure.name
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should set arguments if given" do
|
61
|
+
procedure = Squirm::Procedure.new("foo", :args => "bar text")
|
62
|
+
assert_equal 1, procedure.arguments.count
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#load" do
|
67
|
+
|
68
|
+
before { Squirm.connect $squirm_test_connection }
|
69
|
+
|
70
|
+
it "should set the procedure's arguments" do
|
71
|
+
Squirm.connect $squirm_test_connection
|
72
|
+
proc = Squirm::Procedure.new("regexp_matches", :args => "text, text",
|
73
|
+
:schema => "pg_catalog").load
|
74
|
+
assert_equal [:text, :text2], proc.arguments.to_a
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should raise an exception if no function is found" do
|
78
|
+
begin
|
79
|
+
Squirm::Procedure.new("xxxxxx").load
|
80
|
+
assert false, "should have raised error"
|
81
|
+
rescue Squirm::Procedure::NotFound
|
82
|
+
assert true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should raise an exception if overloaded functions are loaded with no args" do
|
87
|
+
begin
|
88
|
+
Squirm::Procedure.new("date", :schema => "pg_catalog").load
|
89
|
+
assert false, "should have raised error"
|
90
|
+
rescue Squirm::Procedure::TooManyChoices
|
91
|
+
assert true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should load an overloaded functions if instance was initialized with :args" do
|
96
|
+
assert Squirm::Procedure.new("date", :args => "abstime", :schema => "pg_catalog").load
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#call" do
|
102
|
+
|
103
|
+
before {Squirm.connect $squirm_test_connection}
|
104
|
+
|
105
|
+
it "should yield the result to a block if given" do
|
106
|
+
proc = Squirm::Procedure.new("date", :args => "abstime", :schema => "pg_catalog").load
|
107
|
+
proc.call("Jan 1, 2011") do |result|
|
108
|
+
assert_instance_of PGresult, result
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should return the value of a single-row result" do
|
113
|
+
proc = Squirm::Procedure.new("date", :args => "abstime", :schema => "pg_catalog").load
|
114
|
+
assert_equal "2011-01-01", proc.call("Jan 1, 2011")
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should return the an array of hashes for set results" do
|
118
|
+
Squirm.transaction do |conn|
|
119
|
+
conn.exec(<<-SQL)
|
120
|
+
CREATE TABLE temp_table (name varchar);
|
121
|
+
INSERT INTO temp_table VALUES ('joe');
|
122
|
+
INSERT INTO temp_table VALUES ('bob');
|
123
|
+
CREATE FUNCTION temp_func() RETURNS SETOF temp_table AS $$
|
124
|
+
BEGIN
|
125
|
+
RETURN QUERY SELECT * FROM temp_table;
|
126
|
+
END;
|
127
|
+
$$ LANGUAGE 'plpgsql';
|
128
|
+
SQL
|
129
|
+
proc = Squirm::Procedure.new("temp_func").load
|
130
|
+
result = proc.call
|
131
|
+
assert_instance_of Array, result
|
132
|
+
assert_instance_of Hash, result[0]
|
133
|
+
Squirm.rollback
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/squirm.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path("../lib/squirm/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "squirm"
|
5
|
+
s.version = Squirm::VERSION
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Norman Clarke"]
|
8
|
+
s.email = ["norman@njclarke.com"]
|
9
|
+
s.homepage = "http://github.com/norman/squirm"
|
10
|
+
s.summary = %q{"An anti-ORM for database-loving programmers"}
|
11
|
+
s.description = %q{"Squirm is an anti-ORM for database-loving programmers"}
|
12
|
+
s.bindir = "bin"
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.required_ruby_version = ">= 1.9"
|
18
|
+
|
19
|
+
s.add_development_dependency "minitest", ">= 2.6"
|
20
|
+
s.add_runtime_dependency "pg", ">= 0.11.0"
|
21
|
+
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squirm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Norman Clarke
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-19 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: minitest
|
16
|
+
requirement: &70156030613100 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '2.6'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70156030613100
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: pg
|
27
|
+
requirement: &70156030612340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.11.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70156030612340
|
36
|
+
description: ! '"Squirm is an anti-ORM for database-loving programmers"'
|
37
|
+
email:
|
38
|
+
- norman@njclarke.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- .yardopts
|
45
|
+
- MIT-LICENSE
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- lib/squirm.rb
|
49
|
+
- lib/squirm/core.rb
|
50
|
+
- lib/squirm/pool.rb
|
51
|
+
- lib/squirm/procedure.rb
|
52
|
+
- lib/squirm/procedure.sql
|
53
|
+
- lib/squirm/version.rb
|
54
|
+
- spec/core_spec.rb
|
55
|
+
- spec/helper.rb
|
56
|
+
- spec/pool_spec.rb
|
57
|
+
- spec/procedure_spec.rb
|
58
|
+
- squirm.gemspec
|
59
|
+
homepage: http://github.com/norman/squirm
|
60
|
+
licenses: []
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '1.9'
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 1.8.5
|
80
|
+
signing_key:
|
81
|
+
specification_version: 3
|
82
|
+
summary: ! '"An anti-ORM for database-loving programmers"'
|
83
|
+
test_files:
|
84
|
+
- spec/core_spec.rb
|
85
|
+
- spec/helper.rb
|
86
|
+
- spec/pool_spec.rb
|
87
|
+
- spec/procedure_spec.rb
|