summaryrefslogtreecommitdiff
path: root/tests/run.scm
diff options
context:
space:
mode:
Diffstat (limited to 'tests/run.scm')
-rw-r--r--tests/run.scm949
1 files changed, 949 insertions, 0 deletions
diff --git a/tests/run.scm b/tests/run.scm
new file mode 100644
index 0000000..0469929
--- /dev/null
+++ b/tests/run.scm
@@ -0,0 +1,949 @@
+(import (chicken blob)
+ (chicken condition)
+ (chicken string)
+ test
+ postgresql
+ sql-null
+ srfi-4)
+
+(define-syntax test-error*
+ (syntax-rules ()
+ ((_ ?msg (?error-type ...) ?expr)
+ (let-syntax ((expression:
+ (syntax-rules ()
+ ((_ ?expr)
+ (condition-case (begin ?expr "<no error thrown>")
+ ((?error-type ...) '(?error-type ...))
+ (exn () (##sys#slot exn 1)))))))
+ (test ?msg '(?error-type ...) (expression: ?expr))))
+ ((_ ?msg ?error-type ?expr)
+ (test-error* ?msg (?error-type) ?expr))
+ ((_ ?error-type ?expr)
+ (test-error* (sprintf "~S" '?expr) ?error-type ?expr))))
+
+;; Perform a quick sanity check before running the actual tests
+(condition-case (disconnect (connect '((dbname . test))))
+ (exn (exn postgresql connect)
+ (print "Could not connect to server. Please ensure that the UNIX "
+ "user running these tests is allowed to connect to a database "
+ "with the name \"test\". Error from Postgres:")
+ (print ((condition-property-accessor 'exn 'message) exn))
+ (test-exit)))
+
+(test-begin "postgresql")
+
+(test-group "connection management"
+ (test "connect returns a connected connection"
+ '(#t #t)
+ (let* ((conn (connect '((dbname . test))))
+ (isconn (connection? conn))
+ (connected (connected? conn)))
+ (disconnect conn)
+ (list isconn connected)))
+ (test "disconnect causes connection to be disconnected"
+ '(#t #f)
+ (let ((conn (connect '((dbname . test)))))
+ (disconnect conn)
+ (list (connection? conn) (connected? conn))))
+ (test-error "cannot connect with invalid credentials"
+ (connect '((dbname . does-not-exist)
+ (username . nobody))))
+ (test-error "disconnect invalidates the connection"
+ (let ((conn (connect '((dbname . test)))))
+ (disconnect conn)
+ (reset-connection conn)))
+ ;; It would be nice if we could test some more error cases here but
+ ;; that's hard to do
+ )
+
+;; From now on, just keep using the same connection
+(define conn (connect '((dbname . test))))
+
+;; In the tests, we assume that the client lib is at least equal to
+;; the server's version.
+(define-values (major-version minor-version micro-version)
+ (apply values
+ (map string->number
+ (string-split
+ (value-at (query conn "SHOW server_version")) "."))))
+
+(test-group "low-level interface"
+ (test-assert "query returns result"
+ (result? (query conn "SELECT 1")))
+ (test "Correct row count"
+ 2
+ (row-count (query conn "SELECT 1 UNION SELECT 2")))
+ (test "Correct column count"
+ 4
+ (column-count (query conn "SELECT 1, 2, 3, 4")))
+ (test "Correct column name"
+ 'one
+ (column-name
+ (query conn "SELECT 1 AS one, 2 AS two") 0))
+ (test "Correct column names"
+ '(one two)
+ (column-names
+ (query conn "SELECT 1 AS one, 2 AS two")))
+ (test-error* "Condition for nonexistant column index"
+ (exn postgresql bounds)
+ (column-name
+ (query conn "SELECT 1 AS one, 2 AS two") 3))
+ (test "Not false for nameless column"
+ #f ;; Could check for ?column?, but that's a bit too specific
+ (not (column-name
+ (query conn "SELECT 1, 2") 0)))
+ ;; Maybe add a few tests here for case folding/noncase folding variants?
+ ;; Perhaps column-index-ci vs column-index? That would be
+ ;; misleading though, since column-index-ci isn't really ci,
+ ;; it will not match columns that are explicitly uppercased in the query.
+ (test "Correct column index"
+ 0
+ (column-index
+ (query conn "SELECT 1 AS one, 2 AS two") 'one))
+ (test "False column index for nonexistant column name"
+ #f
+ (column-index
+ (query conn "SELECT 1 AS one, 2 AS two") 'foo))
+ (test "False oid for virtual table"
+ #f
+ (table-oid
+ (query conn "SELECT 1 AS one, 2 AS two") 0))
+ (test-assert "Number for nonvirtual table"
+ (number?
+ (table-oid
+ (query conn "SELECT typlen FROM pg_type") 0)))
+
+ (test-error* "Condition for column index out of bounds"
+ (exn postgresql bounds)
+ (table-oid
+ (query conn "SELECT typname FROM pg_type") 1))
+ (test "Table column number for real table"
+ 0
+ (table-column-index
+ (query conn "SELECT typname FROM pg_type") 0))
+ (test "Column format is text for normal data"
+ 'text
+ (column-format
+ (query conn "SELECT 'hello'") 0))
+
+ (test "Column format is binary for forced binary data"
+ 'binary
+ (column-format
+ (query* conn "SELECT 1" '() format: 'binary) 0))
+
+ (test "Column type OID ok"
+ 23 ;; from catalog/pg_type.h
+ (column-type
+ (query conn "SELECT 1::int4") 0))
+ (test "Column modifier false"
+ #f
+ (column-type-modifier
+ (query conn "SELECT 1") 0))
+ (test "Column modifier for bit ok"
+ 2
+ (column-type-modifier
+ (query conn "SELECT '10'::bit(2)") 0))
+ (test "Result value string for strings"
+ "test"
+ (value-at (query conn "SELECT 'test'")))
+ (test "Result value string for 'name' type (with no custom parser)"
+ "test"
+ (value-at (query conn "SELECT 'test'::name")))
+ (test "Result row values"
+ '("one" "two")
+ (row-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 0))
+ (test "Result row values for second row"
+ '("three" "four")
+ (row-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 1))
+ (test "Result row alist"
+ '((a . "one") (b . "two"))
+ (row-alist
+ (query conn "SELECT 'one' AS a, 'two' AS b UNION SELECT 'three', 'four'") 0))
+ (test "Result row alist for second row"
+ '((a . "three") (b . "four"))
+ (row-alist
+ (query conn "SELECT 'one' AS a, 'two' AS b UNION SELECT 'three', 'four'") 1))
+ (test "Result column values"
+ '("one" "three")
+ (column-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 0))
+ (test "Result column values for second column"
+ '("two" "four")
+ (column-values
+ (query conn "SELECT 'one', 'two' UNION SELECT 'three', 'four'") 1))
+ (test "Result value number for numbers"
+ 1
+ (value-at (query conn "SELECT 1")))
+ (test "Result value string for raw numbers"
+ "1"
+ (value-at (query conn "SELECT 1") 0 0 raw: #t))
+ ;; We are using two levels of escaping here because the ::bytea cast
+ ;; performs another string interpretation. Yes, this is kinda confusing...
+ (test "Result value for null-terminated byte array"
+ (blob->u8vector (string->blob "h\x00ello"))
+ (value-at (query conn "SELECT E'h\\\\000ello'::bytea")))
+
+ (test "Result value for raw null-terminated byte array"
+ (if (>= major-version 9)
+ "\\x6800656c6c6f"
+ "h\\000ello")
+ (value-at (query conn "SELECT E'h\\\\000ello'::bytea") 0 0 raw: #t))
+
+ (test "Result value blob for binary string"
+ (string->blob "hello")
+ (value-at (query* conn "SELECT 'hello'" '() format: 'binary)))
+
+ (test "Result value blob for binary integer"
+ (u8vector->blob (u8vector 0 0 0 1))
+ (value-at (query* conn "SELECT 1::int4" '() format: 'binary)))
+
+ (test "Result value for binary string with NUL bytes"
+ (string->blob "h\x00ello")
+ (value-at (query* conn "SELECT E'h\\\\000ello'::bytea" '() format: 'binary)))
+
+ (test "Result value for array of integers"
+ `#(1 ,(sql-null) 3) ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array[1, null, 3]")))
+
+ (test "Result value for nested array of ints"
+ `#(#(1 ,(sql-null) 3)
+ #(4 5 ,(sql-null))) ;; Not sure if comparing sql-null is kosher
+ (value-at
+ (query conn "SELECT array[array[1, null, 3], array[4, 5, NULL]]")))
+
+ (test "Result value for array of strings"
+ `#("a" ,(sql-null) "c") ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array['a', null, 'c']")))
+
+ (test "Result value for nested array of strings"
+ `#(#("a" ,(sql-null) "c")
+ #("NULL" "e" ,(sql-null))) ;; Not sure if comparing sql-null is kosher
+ (value-at (query conn "SELECT array[array['a', null, 'c'], array['NULL', 'e', NULL]]")))
+
+ (test "Result value at row 0, column 1"
+ 2
+ (value-at (query conn "SELECT 1, 2 UNION SELECT 3, 4") 1 0))
+ (test "Result value at row 1, column 0"
+ 3
+ (value-at (query conn "SELECT 1, 2 UNION SELECT 3, 4") 0 1))
+ (test-assert "Result value sql-null for NULL"
+ (sql-null? (value-at (query conn "SELECT NULL"))))
+ (test-error* "Result value error for out of bounds row"
+ (exn postgresql bounds)
+ (value-at (query conn "SELECT NULL") 0 1))
+ (test-error* "Result value error for out of bounds column"
+ (exn postgresql bounds)
+ (value-at (query conn "SELECT NULL") 1 0))
+
+ (query conn "PREPARE foo (int) AS (SELECT 1, 2, $1)")
+ (test-assert "query with prepared statement name returns result"
+ (result? (query conn 'foo 3)))
+ (test "result value ok for query with prepared statement name"
+ '(1 2 3)
+ (row-values (query conn 'foo 3)))
+ (test-error* "condition for missing parameter names"
+ (exn postgresql query)
+ (query conn 'foo))
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (test "Number of affected rows 1 with INSERT"
+ 1
+ (affected-rows
+ (query conn "INSERT INTO foo (bar) VALUES (1);")))
+ (query conn "COMMIT")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (query conn "INSERT INTO foo (bar) VALUES (100);")
+ (query conn "INSERT INTO foo (bar) VALUES (101);")
+ (test "Number of affected rows 2 with UPDATE of two rows"
+ 2
+ (affected-rows
+ (query conn "UPDATE foo SET bar=102;")))
+ (query conn "COMMIT")
+
+ (test "Inserted OID false on SELECT"
+ #f
+ (inserted-oid
+ (query conn "SELECT 1")))
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) ON COMMIT DROP")
+ (test "Inserted OID false on OID-less table"
+ #f
+ (inserted-oid
+ (query conn "INSERT INTO foo (bar) VALUES (1);")))
+ (query conn "COMMIT")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TEMP TABLE foo ( bar integer ) WITH (OIDS=true) ON COMMIT DROP")
+ (test-assert "Inserted OID number on table with OID"
+ (number?
+ (inserted-oid
+ (query conn "INSERT INTO foo (bar) VALUES (1)"))))
+ (query conn "COMMIT")
+
+ (test "regular parameters"
+ "hi"
+ (value-at (query conn "SELECT $1::text" "hi") 0 0))
+ (test-assert "NULL parameters"
+ (sql-null? (value-at
+ (query conn "SELECT $1::text" (sql-null)) 0 0)))
+ (test "blob parameters"
+ "hi"
+ (value-at (query conn "SELECT $1::text" (string->blob "hi")) 0 0))
+ (test "boolean parameters"
+ '(#t #f)
+ (row-values (query conn "SELECT $1::bool, $2::bool" #t #f)))
+ (test "integer array parameters"
+ `(#(1 2) #(,(sql-null) 4))
+ (row-values (query conn "SELECT $1::int[], $2::int[]"
+ `#(1 2) `#(,(sql-null) 4))))
+ (test "nested integer array parameters"
+ `(#(#(1 2) #(,(sql-null) 4)))
+ (row-values (query conn "SELECT $1::int[][]"
+ `#(#(1 2) #(,(sql-null) 4)))))
+ (test "string array parameters (including 'null')"
+ `(#("a" "b") #(,(sql-null) "null"))
+ (row-values (query conn "SELECT $1::text[], $2::text[]"
+ `#("a" "b") `#(,(sql-null) "null"))))
+ (test "string array parameters with meta-characters"
+ `(#("a\\b" "c\"d") #("{" "}"))
+ (row-values (query conn "SELECT $1::text[], $2::text[]"
+ '#("a\\b" "c\"d") '#("{" "}"))))
+ (test "nested string array parameters"
+ `(#(#("a" "b") #(,(sql-null) "null")))
+ (row-values (query conn "SELECT $1::text[][]"
+ `#(#("a" "b") #(,(sql-null) "null")))))
+ (test "array bounds are ignored"
+ `#(#(#(1 2 3) #(4 5 6)))
+ (value-at
+ (query conn
+ "SELECT '[1:1][-2:-1][3:5]={{{1,2,3},{4,5,6}}}'::int[] AS f1")))
+
+ ;; Basic domains seem to return just their base type
+ (query conn "BEGIN")
+ (query conn "CREATE DOMAIN mydom AS integer CHECK (VALUE > 2)")
+ (test "basic domains"
+ 3
+ (value-at (query conn "SELECT $1::mydom" 3)))
+ (query conn "ROLLBACK")
+
+ (query conn "BEGIN")
+ (query conn "CREATE TYPE foo AS ( a integer, b text )")
+ (test "basic composite type"
+ `(1 "one")
+ (value-at (query conn "SELECT $1::foo" `(1 "one"))))
+ (test "composite type with meta-characters"
+ `(123 "\\backslash\"quote")
+ (value-at (query conn "SELECT $1::foo" `(123 "\\backslash\"quote"))))
+ (query conn "CREATE TYPE bar AS ( x foo, y integer )")
+ (test "Nested composite type"
+ `((2 "two") 3)
+ (value-at (query conn "SELECT $1::bar" `((2 "two") 3))))
+ (query conn "CREATE DOMAIN mydom AS integer CHECK (VALUE > 1)")
+ (query conn "CREATE TYPE qux AS ( i integer, d mydom )")
+ (test "Composite type containing domain value"
+ `(1 2)
+ (value-at (query conn "SELECT $1::qux" `(1 2))))
+ (query conn "ROLLBACK")
+
+ (test "anonymous composite type ('record')"
+ '("one" "two")
+ (value-at (query conn "SELECT ROW('one', 'two')")))
+
+ (test "anonymous composite type ('record') with NULL"
+ `("one" ,(sql-null))
+ (value-at (query conn "SELECT ROW('one', NULL)")))
+
+ ;; ROW() is indistinguishable from ROW(NULL). The latter is more likely
+ ;; to occur than the former, so let's ensure that's how it's interpreted
+ ;; See also http://archives.postgresql.org/pgsql-hackers/2004-06/msg00159.php
+ (test "anonymous composite type ('record') with just one NULL"
+ `(,(sql-null))
+ (value-at (query conn "SELECT ROW(NULL)"))))
+
+(test-group "escaping and quotation"
+ (test-group "with standards conforming strings off"
+ (query conn "SET standard_conforming_strings='off'")
+ (test "String is escaped correctly"
+ "it''s \"this\" \\\\ safe!"
+ (escape-string conn "it's \"this\" \\ safe!"))
+ (test "Small string is escaped correctly"
+ "foo"
+ (escape-string conn "foo"))
+ (test "Bytea is escaped correctly (u8vector)"
+ (if (>= major-version 9)
+ "\\\\x576800617427730a75703f"
+ "Wh\\\\000at''s\\\\012up?")
+ (escape-bytea conn (blob->u8vector (string->blob "Wh\x00at's\nup?"))))
+ (test "Bytea is unescaped correctly (blob)"
+ (blob->u8vector (string->blob "What's\nup? "))
+ ;; The extra quote is dropped here because it wouldn't be returned
+ ;; by pgsql either.
+ (unescape-bytea "What's\\012up?\\ "))
+ (test "Identifier is escaped and quoted correctly"
+ ;; Backslashes have no special meaning in identifiers,
+ ;; standards conforming strings or no
+ "\"it's \"\"this\"\" \\ safe!\""
+ (quote-identifier conn "it's \"this\" \\ safe!")))
+ (test-group "with standards conforming strings on"
+ (query conn "SET standard_conforming_strings='on'")
+ (test "String is escaped correctly"
+ "it''s \"this\" \\ safe!"
+ (escape-string conn "it's \"this\" \\ safe!"))
+ (test "Bytea is escaped correctly"
+ (if (>= major-version 9)
+ "\\x576800617427730a75703f"
+ "Wh\\000at''s\\012up?")
+ (escape-bytea conn "Wh\x00at's\nup?"))
+ (test "Bytea is unescaped correctly"
+ (blob->u8vector (string->blob "What's\nup? "))
+ ;; The extra quote is dropped here because it wouldn't be returned
+ ;; by pgsql either.
+ (unescape-bytea "What's\\012up?\\ "))
+ (test "Identifier is escaped and quoted correctly"
+ ;; Backslashes have no special meaning in identifiers,
+ ;; standards conforming strings or no
+ "\"it's \"\"this\"\" \\ safe!\""
+ (quote-identifier conn "it's \"this\" \\ safe!"))))
+
+(test-group "query error handling"
+ (test-group "basic info: position and code"
+ (handle-exceptions exn
+ (begin (test "Class of real query error is available and set to expected value"
+ "42"
+ ((condition-property-accessor 'query 'error-class) exn))
+ (test "Code of real query error is available and set to expected value"
+ "42P01" ; undefined_table
+ ((condition-property-accessor 'query 'error-code) exn))
+ ;; We can't set locale w/o superuser rights, so we'll
+ ;; have to make do with an incomplete severity check.
+ (test-assert "Severity of real query error is a symbol"
+ (symbol? ((condition-property-accessor
+ 'query 'severity) exn)))
+ (test "Statement position in real query error is correct"
+ 15
+ ((condition-property-accessor
+ 'query 'statement-position) exn)))
+ (query conn "SELECT 1 FROM nonexistant_table")
+ (error "query call didn't fail")))
+ (test-group "message and code"
+ (handle-exceptions exn
+ (begin (test "Error class is available and set to expected value"
+ "42"
+ ((condition-property-accessor 'query 'error-class) exn))
+ (test "Error code is available and set to expected value"
+ "42P16"
+ ((condition-property-accessor 'query 'error-code) exn))
+ ;; We can't set locale w/o superuser rights, so we'll
+ ;; have to make do with some neutered checks.
+ (test-assert "Severity is a symbol"
+ (symbol? ((condition-property-accessor
+ 'query 'severity) exn)))
+ (test "Message primary is set to the expected value"
+ "testing, 1, 2, 3."
+ ((condition-property-accessor 'query 'message-primary) exn))
+ (test "Detail is set to the expected value"
+ "this is a test"
+ ((condition-property-accessor 'query 'message-detail) exn))
+ (test "Hint is set to the expected value"
+ "try again"
+ ((condition-property-accessor 'query 'message-hint) exn))
+ ;; XXX These two may be too version-specific
+ (test "source function is set to the correct value"
+ "exec_stmt_raise"
+ ((condition-property-accessor 'query 'source-function) exn))
+ (test "source file is set to the the correct value"
+ "pl_exec.c"
+ ((condition-property-accessor 'query 'source-file) exn))
+ (test-assert "source line is available and a number"
+ (number? ((condition-property-accessor
+ 'query 'source-line) exn))))
+ (query conn (conc "CREATE OR REPLACE FUNCTION cause_error() "
+ "RETURNS void AS $$\n"
+ " BEGIN\n"
+ " RAISE invalid_table_definition \n"
+ " USING MESSAGE = 'testing, 1, 2, 3.', \n"
+ " DETAIL = 'this is a test', \n"
+ " HINT = 'try again';\n"
+ " END;\n"
+ "$$ LANGUAGE plpgsql"))
+ (value-at (query conn "SELECT cause_error()"))
+ (error "query call didn't fail")))
+ (when (or (> major-version 9) (and (= major-version 9) (>= minor-version 3)))
+ (test-group "schema information"
+ (query conn "BEGIN")
+ (query conn (conc "CREATE DOMAIN posint AS int"
+ " CONSTRAINT must_be_positive CHECK (VALUE > 0)"))
+ (query conn (conc "CREATE TEMP TABLE foo (bar posint) ON COMMIT DROP"))
+ (handle-exceptions exn
+ (begin (test-assert "Schema name is set" ; It is pg_temp_2
+ ((condition-property-accessor
+ 'query 'schema-name) exn))
+ (test "Data type name is correct"
+ "posint"
+ ((condition-property-accessor 'query 'datatype-name) exn))
+ (test "Constraint name is correct"
+ "must_be_positive"
+ ((condition-property-accessor 'query 'constraint-name) exn)))
+ (query conn "INSERT INTO foo (bar) VALUES (-1)")
+ (error "query call didn't fail"))
+ (query conn "ROLLBACK")
+ (query conn "BEGIN")
+ (query conn (conc "CREATE TEMP TABLE foo (bar int NOT NULL) "
+ "ON COMMIT DROP"))
+ (handle-exceptions exn
+ (begin (test-assert "Schema name is set" ; It is pg_temp_2
+ ((condition-property-accessor
+ 'query 'schema-name) exn))
+ (test "Table name is correct"
+ "foo"
+ ((condition-property-accessor 'query 'table-name) exn))
+ (test "Column name is correct"
+ "bar"
+ ((condition-property-accessor 'query 'column-name) exn)))
+ (query conn "INSERT INTO foo (bar) VALUES (NULL)")
+ (error "query call didn't fail"))
+ (query conn "ROLLBACK"))))
+
+(test-group "COPY support"
+ (query conn "CREATE TEMP TABLE copy_table ( nr integer, s text )")
+ (test-group "low-level interface"
+ (test-error* "Cannot put copy data while no COPY in progress"
+ (exn postgresql i/o)
+ (put-copy-data conn "whatever"))
+ (query conn "COPY copy_table (s, nr) FROM STDIN")
+ (test-error* "Cannot initiate new query while waiting for COPY input"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (put-copy-data conn "one\t1\n")
+ (test-error* "Cannot initiate new query while COPY data in progress"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (put-copy-data conn "two\t2")
+ (put-copy-end conn)
+ (let ((res (query conn "SELECT * FROM copy_table")))
+ (test "Simple copy from STDIN works"
+ '((1 "one")
+ (2 "two"))
+ (list (row-values res 0) (row-values res 1))))
+ (test-error* "Cannot get copy data while no COPY in progress"
+ (exn postgresql i/o)
+ (get-copy-data conn))
+ (query conn "COPY copy_table (s, nr) TO STDOUT")
+ (test-error* "Cannot initiate new query while waiting for COPY output"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (test "Simple copy to STDOUT works, first record"
+ "one\t1\n"
+ (get-copy-data conn))
+ (test-error* "Cannot initiate new query while reading COPY data"
+ (exn postgresql i/o)
+ (query conn "SELECT 1"))
+ (test "Simple copy to STDOUT works, second record"
+ "two\t2\n"
+ (get-copy-data conn))
+ (test-assert "EOF is marked by a result object"
+ (result? (get-copy-data conn))))
+ (test-group "high-level interface"
+ (test "Mapping"
+ '(("one" "1")
+ ("two" "2"))
+ (copy-query-map string-split conn "COPY copy_table (s, nr) TO STDOUT"))
+ (test "Error while mapping gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-map (lambda _ (error "blah"))
+ conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Fold"
+ '(("one" "1")
+ ("two" "2"))
+ (reverse
+ (copy-query-fold
+ (lambda (data result)
+ (cons (string-split data) result))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Error while folding gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-fold (lambda _ (error "blah"))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "Fold-right"
+ '(("one" "1")
+ ("two" "2"))
+ (copy-query-fold-right
+ (lambda (data result)
+ (cons (string-split data) result))
+ '() conn "COPY copy_table (s, nr) TO STDOUT"))
+ (test "Error while folding right gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-fold-right (lambda _ (error "blah"))
+ '() conn "COPY copy_table (s, nr) TO STDOUT")))
+ (test "For-each"
+ '(("one" "1")
+ ("two" "2"))
+ (let ((res '()))
+ (copy-query-for-each (lambda (x)
+ (set! res (cons (string-split x) res)))
+ conn "COPY copy_table (s, nr) TO STDOUT")
+ (reverse res)))
+ (test "Error during for-each gets connection out of COPY state"
+ "okay"
+ (handle-exceptions exn
+ (value-at (query conn "SELECT 'okay'"))
+ (copy-query-for-each (lambda (x) (error "blah"))
+ conn "COPY copy_table (s, nr) TO STDOUT")))
+ (query conn "TRUNCATE copy_table")
+ (with-output-to-copy-query (lambda ()
+ (print "first\t1")
+ (print "second\t2"))
+ conn "COPY copy_table (s, nr) FROM STDIN")
+ (test "Port interface inserted data correctly"
+ '(("first" 1)
+ ("second" 2))
+ (let ((res (query conn "SELECT s, nr FROM copy_table")))
+ (list (row-values res 0) (row-values res 1))))
+ (query conn "TRUNCATE copy_table")
+ (handle-exceptions _
+ (void)
+ (with-output-to-copy-query (lambda ()
+ (print "first\t1")
+ (print "second\t2")
+ (error "blah"))
+ conn "COPY copy_table (s, nr) FROM STDIN"))
+ (test "Error inside with-output-to-copy caused abort of insert"
+ 0 (value-at (query conn "SELECT COUNT(*) FROM copy_table")))))
+
+(test-group "type parsers"
+ (test "Integer parsed correctly"
+ 1234
+ (numeric-parser "1234"))
+ (test "Float parsed correctly"
+ 123.456
+ (numeric-parser "123.456"))
+ (test-error* "Non-integer is an error"
+ (exn postgresql parse)
+ (numeric-parser "not an integer"))
+ (test "Boolean true parsed correctly"
+ #t
+ (bool-parser "t"))
+ (test "Boolean false parsed correctly"
+ #f
+ (bool-parser "f"))
+ (test "Byte array parsed correctly"
+ (blob->u8vector/shared (string->blob "abc\x01\x02\xffdef"))
+ (bytea-parser "abc\\001\\002\\377def"))
+ (test "Char parser"
+ #\x
+ (char-parser "x")))
+
+(test-group "type unparsers"
+ (test "Boolean true unparsed correctly"
+ "TRUE"
+ (bool-unparser conn #t))
+ (test "Boolean false unparsed correctly"
+ "FALSE"
+ (bool-unparser conn #f)))
+
+(test-group "high-level interface"
+ (test "row-fold"
+ '(("one" 2)
+ ("three" 4))
+ (reverse
+ (row-fold
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2))))
+ (test "column-fold"
+ '(("one" "three")
+ (2 4))
+ (reverse
+ (column-fold
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2))))
+ (test "row-fold-right"
+ '(("one" 2)
+ ("three" 4))
+ (row-fold-right
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2)))
+ (test "column-fold-right"
+ '(("one" "three")
+ (2 4))
+ (column-fold-right
+ cons '()
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4"
+ "one" 2)))
+ (test "row-for-each"
+ '(("one" 2)
+ ("three" 4))
+ (let ((res '()))
+ (row-for-each
+ (lambda (row) (set! res (cons row res)))
+ (query
+ conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))
+ (reverse res)))
+ (test "column-for-each"
+ '(("one" "three")
+ (2 4))
+ (let ((res '()))
+ (column-for-each
+ (lambda (col) (set! res (cons col res)))
+ (query
+ conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))
+ (reverse res)))
+ (test "row-map"
+ '(("one" 2)
+ ("three" 4))
+ (row-map
+ identity
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2)))
+ (test "column-map"
+ '(("one" "three")
+ (2 4))
+ (column-map
+ identity
+ (query conn
+ "SELECT $1::text, $2::integer UNION SELECT 'three', 4" "one" 2))))
+
+(test-group "transactions"
+ (query conn "CREATE TEMP TABLE foo ( bar integer )")
+
+ (test-group "simple transactions"
+ (test "Transaction inactive"
+ #f
+ (in-transaction? conn))
+ (test "Transaction active"
+ #t
+ (with-transaction conn
+ (lambda () (in-transaction? conn))))
+ (test "Successful transaction"
+ '(1)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Unsuccessful transaction"
+ #f
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ #f)))
+
+ (test "Empty table after unsuccessful transaction"
+ '()
+ (column-values (query conn "SELECT * FROM foo")))
+
+ (handle-exceptions exn
+ (void)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (error "oops!"))))
+
+ (test "Exception during transaction causes reset"
+ '()
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (test-group "nested transactions"
+ (test "Successful transaction"
+ '(1 2)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Unsuccessful main transaction"
+ '()
+ (and
+ (not
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))
+ #f)))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (test "Unsuccessful subtransaction"
+ '(1)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")
+ #f))
+ #t))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ ;; Test that errors do not kill the transaction. Apparently
+ ;; aborting transactions on errors is a psql(1) "feature", not a
+ ;; libpq one.
+ (test "Unsuccessful subtransaction with bad query"
+ '(1 2)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (handle-exceptions exn
+ #t
+ (with-transaction
+ conn (lambda ()
+ (query conn "INVALID QUERY"))))
+ (query conn "INSERT INTO foo (bar) VALUES (2)")))
+ (column-values (query conn "SELECT * FROM foo"))))
+
+ (query conn "TRUNCATE foo")
+
+ (test "Multiple subtransactions"
+ '(1 3)
+ (and
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (1)")
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (2)")
+ #f))
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO foo (bar) VALUES (3)")))))
+ (column-values (query conn "SELECT * FROM foo")))))
+
+ ;; Very annoying; read-only transactions can write to temporary tables,
+ ;; so we have to create a REAL table here.
+ (query conn "CREATE TABLE chicken_pgsql_test (bar int)")
+ ;; We'd better make damn sure it gets dropped again.
+ (on-exit (lambda () (query conn "DROP TABLE chicken_pgsql_test")))
+
+ (test-group "transaction access mode"
+ (test "Writes with explicit read/write access mode work"
+ '(1)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (1)")
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test "Reads with read-only access mode work"
+ '(1)
+ (with-transaction
+ conn (lambda ()
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read-only))
+
+ (test-error* "Writes with read-only access cause errors"
+ (exn postgresql query)
+ (with-transaction
+ conn (lambda ()
+ (query conn (conc "INSERT INTO chicken_pgsql_test (bar)"
+ "VALUES (1)")))
+ access: 'read-only))
+
+ (test "Switching read-write to read-only works"
+ '(1 2)
+ (with-transaction
+ conn (lambda ()
+ (with-transaction
+ conn (lambda ()
+ (query conn "SELECT * FROM chicken_pgsql_test"))
+ access: 'read-only)
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (2)")
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test "Switching read-write to read-only works with rollback"
+ '(1 2 3)
+ (with-transaction
+ conn (lambda ()
+ (handle-exceptions exn
+ (void)
+ (with-transaction
+ conn (lambda ()
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (3)"))
+ access: 'read-only))
+ (query conn "INSERT INTO chicken_pgsql_test (bar) VALUES (3)")
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read/write))
+
+ (test-error* "Switching read-only to read-write causes error"
+ (exn postgresql query)
+ (with-transaction
+ conn (lambda ()
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (with-transaction
+ conn (lambda ()
+ (query conn
+ (conc "INSERT INTO chicken_pgsql_test (bar)"
+ "VALUES (2)")))
+ access: 'read/write)
+ (column-values (query conn "SELECT * FROM chicken_pgsql_test")))
+ access: 'read-only))
+
+ ;; Run these "concurrent" tests with a second connection
+ (let ((conn2 (connect '((dbname . test)))))
+ (test "Read committed transaction does see concurrent changes"
+ '(1 2 3 4)
+ (with-transaction
+ conn (lambda ()
+ ;; MUST perform a query, since only after the first
+ ;; query will the isolation take effect.
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (query conn2 (conc "INSERT INTO chicken_pgsql_test (bar) "
+ "VALUES (4)"))
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ isolation: 'read-committed))
+
+ (test "Serializable transaction doesn't see concurrent changes"
+ '(1 2 3 4)
+ (with-transaction
+ conn (lambda ()
+ ;; MUST perform a query, since only after the first
+ ;; query will the isolation take effect.
+ (query conn "SELECT * FROM chicken_pgsql_test")
+ (query conn2 (conc "INSERT INTO chicken_pgsql_test (bar) "
+ "VALUES (5)"))
+ (column-values
+ (query conn "SELECT * FROM chicken_pgsql_test")))
+ isolation: 'serializable))
+ (disconnect conn2)))
+ )
+
+(test-end)
+
+(test-exit)