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.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ spec/coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
@@ -0,0 +1,4 @@
1
+ --files=*.md
2
+ --protected
3
+ --list-undoc
4
+ --exclude lib/squirm/version
@@ -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.
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Squirm
2
+ VERSION = "0.0.2"
3
+ end
@@ -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
@@ -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"}
@@ -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
@@ -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