squirm 0.0.2

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