;;; Project: Scheme-PG ;;; Author: David J. Neu, djneu@acm.org ;;; Maintainer: David J. Neu, djneu@acm.org ;;; Project Home Page: http://scheme-pg.sourceforge.net ;;; Copyright: Copyright (c) 2004 Universal Technical Resource Services, Inc. ;;; License: MIT License, see license.txt ;;; CVS Id: $Id: sql.ss,v 1.9 2005/03/21 20:24:18 djneu Exp $ (module sql mzscheme (require (only (lib "13.ss" "srfi") string-drop-right string-drop) (lib "string.ss") (lib "s-pg.ss" "scheme-pg")) ; only needed for the esacpe-string call in format-value (define-syntax where-clause-and (syntax-rules () ((_) '()) ((_ e) (list e)) ((_ e1 e2 e3 ...) (append (list e1 "AND") (where-clause-and e2 e3 ...))))) (define-syntax where-clause-or (syntax-rules () ((_) '()) ((_ e) (list e)) ((_ e1 e2 e3 ...) (append (list e1 "OR") (where-clause-or e2 e3 ...))))) (define operator? (lambda (aoperator) (symbol? aoperator))) (define format-operator (lambda (aoperator) (if (operator? aoperator) (string-upcase (format "~a" aoperator)) (error 'format-operator "expects a symbol, given: ~a" aoperator)))) ;;; column-name?: column -> boolean ;;; Returns #t if acolumn is a symbol and #f otherwise (define column-name? (lambda (acolumn) (symbol? acolumn))) ;;; format-column-name: symbol -> string ;;; Accepts a symbol representing a column name and returns a string ;;; containing the symbol surrounded by double quotes. This allows ;;; programmers to use column names that for example contain spaces or ;;; dashes. (define format-column-name (lambda (acolumn) (format "\"~a\"" acolumn))) ;;; as-column-name?: column -> boolean ;;; Returns #t if acolumn is a length three proper list of symbols ;;; with the first symbol being AS, and the second and third ;;; satisfying column-name?, and #f otherwise. For example, (as first-name fn). (define as-column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 3) (eq? (car acolumn) 'as) (column-name? (cadr acolumn)) (column-name? (caddr acolumn))))) ;;; function?: function -> boolean ;;; Returns #t if afunction is a symbol that starts with a :. (define function? (lambda (afunction) (and (symbol? afunction) (eq? (string-ref (symbol->string afunction) 0) #\:)))) ;;; function/column-name?: column -> boolean ;;; Returns #t if acolumn is a length two proper list of symbols with ;;; the first symbol satisfying function?, and the second symbol ;;; satisfying column-name?, and #f otherwise. For example, (:avg ;;; age). (define function/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 2) (function? (car acolumn)) (column-name? (cadr acolumn))))) ;;; format-function/column-name: column -> string ;;; Accepts an argument acolumn that satifies function/column-name? ;;; and returns a string. For example, (:avg age) => avg("age"). ;;; Surroudding the column names by double quotes allows programmers ;;; to use column name that contain contain spaces or dashes. (define format-function/column-name (lambda (acolumn) (let ((lfunction (cadr (regexp-split ":" (symbol->string (car acolumn)))))) (format "~a(\"~a\")" lfunction (cadr acolumn))))) ;;; format-column-name: symbol -> string ;;; Accepts an argument acolumn that satifies as-column-name? and ;;; returns a string. For example, (as first-name fn) => "first-name" ;;; AS "fn". Surroudding the column names by double quotes allows ;;; programmers to use column name that contain contain spaces or ;;; dashes. (define format-as-column-name (lambda (acolumn) (format "\"~a\" AS \"~a\"" (cadr acolumn) (caddr acolumn)))) ;;; table-name/column-name?: column -> boolean ;;; Returns #t if acolumn is a length two proper list of symbols and ;;; #f otherwise. (define table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 2) (symbol? (car acolumn)) (symbol? (cadr acolumn))))) ;;; format-table-name/column-name: symbol -> string ;;; Accepts a length two proper list of symbols symbol, representing a ;;; table name and a column name and returns a string of the form ;;; "table"."column" where table is the table name and column is the ;;; column name. This allows programmers to use table and column names ;;; that for example contain spaces or dashes. (define format-table-name/column-name (lambda (acolumn) (format "\"~a\".\"~a\"" (car acolumn) (cadr acolumn)))) ;;; as-table-name/column-name?: column -> boolean ;;; Returns #t if acolumn is a length three proper list with the first ;;; symbol being AS, the second satisfying table-name/column-name? and ;;; the third satisfying column-name?, and #f otherwise. For example, ;;; (as (person first-name) fn). (define as-table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 3) (eq? (car acolumn) 'as) (table-name/column-name? (cadr acolumn)) (column-name? (caddr acolumn))))) ;;; format-as-table-name/column-name: symbol -> string ;;; Accepts an argument acolumn that satifies ;;; as-table-name/column-name? and returns a string. For example, (as ;;; (person first-name) fn) => "person.first-name" AS "fn". ;;; Surroudding the column names by double quotes allows programmers ;;; to use column name that contain contain spaces or dashes. (define format-as-table-name/column-name (lambda (acolumn) (format "~a AS \"~a\"" (format-table-name/column-name (cadr acolumn)) (caddr acolumn)))) ;;; function/table-name/column-name?: column -> boolean ;;; Returns #t if acolumn is a length two proper list with the first ;;; element satifying function? and the second satifying ;;; table-name/column-name?, and #f otherwise. (define function/table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 2) (function? (car acolumn)) (table-name/column-name? (cadr acolumn))))) ;;; format-function/table-name/column-name: symbol -> string ;;; Accepts an argument acolumn that satifies ;;; function-table-name/column-name? and returns a string. For ;;; example, (:avg (pers age)) => avg("pers"."age"). (define format-function/table-name/column-name (lambda (acolumn) (let ((lfunction (cadr (regexp-split ":" (symbol->string (car acolumn)))))) (format "~a(~a)" lfunction (format-table-name/column-name (cadr acolumn)))))) ;;; schema-name/column-name/table-name?: column -> boolean ;;; Returns #t if acolumn is a length three proper list of symbols and ;;; #f otherwise. (define schema-name/table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 3) (symbol? (car acolumn)) (symbol? (cadr acolumn)) (symbol? (caddr acolumn))))) ;;; format-scheme-name/table-name/column-name: symbol -> string ;;; Accepts a length three proper list of symbols symbol, representing ;;; a schema name, a table name and a column name and returns a string ;;; of the form "schema"."table"."column" where schema is the name of ;;; the schema, table is the table name and column is the column ;;; name. This allows programmers to use table and column names that ;;; for example contain spaces or dashes. (define format-schema-name/table-name/column-name (lambda (acolumn) (format "\"~a\".\"~a\".\"~a\"" (car acolumn) (cadr acolumn) (caddr acolumn)))) ;;; as-schema-name/table-name/column-name?: column -> boolean ;;; Returns #t if acolumn is a length three proper list with the first ;;; symbol being AS, the second satisfying table-name/column-name? and ;;; the third satisfying column-name?, and #f otherwise. For example, ;;; (as (myschema person first-name) fn). (define as-schema-name/table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 3) (eq? (car acolumn) 'as) (schema-name/table-name/column-name? (cadr acolumn)) (column-name? (caddr acolumn))))) ;;; format-as-schema-name/table-name/column-name: symbol -> string ;;; Accepts an argument acolumn that satifies ;;; as-schema-name/table-name/column-name? and returns a string. For ;;; example, (as (myschema person first-name) fn) => ;;; "myschema.person.first-name" AS "fn". Surroudding the column ;;; names by double quotes allows programmers to use column name that ;;; contain contain spaces or dashes. (define format-as-schema-name/table-name/column-name (lambda (acolumn) (format "~a AS \"~a\"" (format-schema-name/table-name/column-name (cadr acolumn)) (caddr acolumn)))) ;;; schema-name/column-name/table-name?: column -> boolean ;;; Returns #t if acolumn is a length two proper list with the first ;;; element satisfying function? and the second satifying ;;; schema-name/table-name/column-name?, and #f otherwise. (define function/schema-name/table-name/column-name? (lambda (acolumn) (and (list? acolumn) (= (length acolumn) 2) (function? (car acolumn)) (schema-name/table-name/column-name? (cadr acolumn))))) ;;; format-scheme-name/table-name/column-name: symbol -> string ;;; Accepts a length three proper list of symbols symbol, representing ;;; a schema name, a table name and a column name and returns a string ;;; of the form "schema"."table"."column" where schema is the name of ;;; the schema, table is the table name and column is the column ;;; name. This allows programmers to use table and column names that ;;; for example contain spaces or dashes. (define format-function/schema-name/table-name/column-name (lambda (acolumn) (let ((lfunction (cadr (regexp-split ":" (symbol->string (car acolumn)))))) (format "~a(~a)" lfunction (format-schema-name/table-name/column-name (cadr acolumn)))))) ;;; column?: column -> boolean ;;; Returns #t when acolumn is either a Scheme symbol representing a ;;; column name or a proper list of length 2 of symbols representing a ;;; table name and a column name and returns #f otherwise. (define column? (lambda (acolumn) (cond ((column-name? acolumn) #t) ((as-column-name? acolumn) #t) ((function/column-name? acolumn) #t) ((table-name/column-name? acolumn) #t) ((as-table-name/column-name? acolumn) #t) ((function/table-name/column-name? acolumn) #t) ((schema-name/table-name/column-name? acolumn) #t) ((as-schema-name/table-name/column-name? acolumn) #t) ((function/schema-name/table-name/column-name? acolumn) #t) (else #f)))) ;;; format-column: acolumn -> string ;;; Returns a string representing column when acolumn is in either of ;;; the two valid column formats. Otherwise it raises an exception. (define format-column (lambda (acolumn) (cond ((column-name? acolumn) (format-column-name acolumn)) ((as-column-name? acolumn) (format-as-column-name acolumn)) ((function/column-name? acolumn) (format-function/column-name acolumn)) ((table-name/column-name? acolumn) (format-table-name/column-name acolumn)) ((as-table-name/column-name? acolumn) (format-as-table-name/column-name acolumn)) ((function/table-name/column-name? acolumn) (format-function/table-name/column-name acolumn)) ((schema-name/table-name/column-name? acolumn) (format-schema-name/table-name/column-name acolumn)) ((as-schema-name/table-name/column-name? acolumn) (format-as-schema-name/table-name/column-name acolumn)) ((function/schema-name/table-name/column-name? acolumn) (format-function/schema-name/table-name/column-name acolumn)) (else (error 'format-column "expects a symbol, or a length two proper list of symbols, or a length two proper list of symbols given: ~a" acolumn))))) (define format-value-string "format-value: expects a symbol or a length two proper list of symbols to be used as a column or number or string to be used as a value, given: ~a") ;;; In procedure format-value a value can either be a valid Scheme-PG ;;; column (see column? for the definition), or it can be a valid ;;; Scheme-PG value. The first case occurs in a condition in which ;;; the values of two database columns are compared. For example, ;;; SELECT * FROM pers WHERE first-name = last-name ;;; and ;;; SELECT * FROM pers,addr WHERE pers.id = addr.id. ;;; The second case occurs in a condition in which the value of a ;;; database column is compared to a constant. For example, ;;; SELECT * FROM pers WHERE last-name LIKE 'Do%' ;;; and ;;; SELECT * FROM pers WHERE age < 22. ;;; If avalue is a valid column it is formatted as one by calling ;;; format-column. If avalue is not a valid column then there are two ;;; valid situtations: it is a string or it is a number. If avalue is ;;; a string, escape-value is applied and the result is surrounded by ;;; single quotes. If avalue is a number it returned as a string ;;; without any additional formatting. In all other cases an ;;; exception is raised. An example of an additional Scheme data type ;;; that could be supported is a list, which could be used in an SQL ;;; IN, e.g. SELECT * FROM addr WHERE state IN ('NH', 'NJ', 'NY'). (define format-value (lambda (avalue) (cond ((null-object? avalue) "NULL") ((column? avalue) (format-column avalue)) ((string? avalue) (format "'~a'" (escape-string avalue))) ((number? avalue) (format "~a" avalue)) (else (error 'format-value format-value-string avalue))))) (define-syntax where-clause (syntax-rules (and or not) ((where-clause (not x)) (format "NOT ~a" (where-clause x))) ;; this would add parentheses ;;((where-clause (not x)) (list "NOT" (where-clause x))) ((where-clause (and x)) (where-clause x)) ((where-clause (and x y ...)) (where-clause-and (where-clause x) (where-clause y) ...)) ((where-clause (or x)) (where-clause x)) ((where-clause (or x y ...)) (where-clause-or (where-clause x) (where-clause y) ...)) ((where-clause (operator column value)) ;; this would add parentheses ;;(list (format "~a ~a ~a" (format-column `column) (format-operator `operator) (format-value `value)))))) (format "~a ~a ~a" (format-column `column) (format-operator `operator) (format-value `value))))) (define-syntax where (syntax-rules () ((_ . expr) (let ((lwhere-clause (where-clause . expr))) (if (null? lwhere-clause) "" (let ((lwhere-string (format "~a" lwhere-clause))) ;; remove redundant outer parentheses (if (and (eq? (string-ref lwhere-string 0) #\() (eq? (string-ref lwhere-string 0) #\()) (format "WHERE ~a " (string-drop-right (string-drop lwhere-string 1) 1)) (format "WHERE ~a " lwhere-string)))))))) (define-syntax comma-separate (syntax-rules () ((_) "") ((_ e) e) ((_ e1 e2 e3 ...) (format "~a,~a" e1 (comma-separate e2 e3 ...))))) (define-syntax space-separate (syntax-rules () ((_) "") ((_ e) e) ((_ e1 e2 e3 ...) (format "~a ~a" e1 (space-separate e2 e3 ...))))) ;;; table-name?: table-name -> boolean ;;; Returns #t if atable is a symbol and #f otherwise (define table-name? (lambda (atable) (symbol? atable))) ;;; schema-name/table-name?: column -> boolean ;;; Returns #t if atable is a length two proper list of symbols and ;;; #f otherwise. (define schema-name/table-name? (lambda (atable) (and (list? atable) (= (length atable) 2) (symbol? (car atable)) (symbol? (cadr atable))))) ;;; format-table-name: symbol -> string ;;; Accepts a symbol representing a table name and returns a string ;;; containing the symbol surrounded by double quotes. This allows ;;; programmers to use table names that for example contain spaces or ;;; dashes. (define format-table-name (lambda (atable) (format "\"~a\"" atable))) ;;; format-schema-name/table-name: symbol -> string ;;; Accepts a length two proper list of symbols symbol, representing a ;;; scheme name and a table name and returns a string of the form ;;; "schema"."table" where schema is the schema name and table is the ;;; table name. This allows programmers to use schema and table names ;;; that for example contain spaces or dashes. (define format-schema-name/table-name (lambda (atable) (format "\"~a\".\"~a\"" (car atable) (cadr atable)))) ;;; format-table: atable -> string ;;; Returns a string representing table when atable is in either of ;;; the two valid table formats. Otherwise it raises an exception. (define format-table (lambda (atable) (cond ((table-name? atable) (format-table-name atable)) ((schema-name/table-name? atable) (format-schema-name/table-name atable)) (else (error 'format-table "expects a symbol or a length two proper list of symbols, given: ~a" atable))))) ;;; This macro contains two rules that generate a SELECT statement, ;;; they are matched depending on whether a list of columns is ;;; explicitly specified or the literal all is used. The macro ;;; expects a list of columns, a list of tables and zero or more ;;; strings containings where, order-by, or other clauses. (define-syntax select (syntax-rules (all) ((_ (column ...) (table ...) clause ...) (format "SELECT ~a FROM ~a ~a" (comma-separate (format-column `column) ...) (comma-separate (format-table `table) ...) (space-separate clause ...))) ((_ all (table ...) clause ...) (format "SELECT * FROM ~a ~a" (comma-separate (format-table `table) ...) (space-separate clause ...))))) ;;; The select-distinct macro is the same as the select macro except ;;; that it creates a SELECT DISTINCT rather than a SELECT. (define-syntax select-distinct (syntax-rules (all) ((_ (column ...) (table ...) clause ...) (format "SELECT DISTINCT ~a FROM ~a ~a" (comma-separate (format-column `column) ...) (comma-separate (format-table `table) ...) (space-separate clause ...))) ((_ all (table ...) clause ...) (format "SELECT DISTINCT * FROM ~a ~a" (comma-separate (format-table `table) ...) (space-separate clause ...))))) ;;; limit: non-negative-integer non-negative-integer -> string ;;; The limit macro creates a LIMIT clause for use in a SELECT ;;; statement. The offset argument is the number of rows to be ;;; skipped, so an offset of 0 means that the first row returned will ;;; be the first row of the result. The number argument is an upper ;;; bound on the number of rows to return. Less than number rows can ;;; be returned depending on the number of rows in the result and the ;;; value of offset. ;;; ;;; (limit 5 10) => OFFSET 5 LIMIT 10 (define-syntax limit (syntax-rules () ((_ offset number) (format "OFFSET ~a LIMIT ~a" `offset `number)))) (define format-asc/desc (lambda (x) (cond ((eq? x 'asc) "ASC") ((eq? x 'desc) "DESC") (else (error 'format-asc/desc "expects the symbol 'ASC or the symbol 'DESC, given: ~a" x))))) ;;; order-by: (listof (symbol 'asc or 'desc)) -> string ;;; The order-by macro creates an ORDER BY clause for use in a SELECT. ;;; It accepts a proper list of length two lists that consists of a ;;; symbol representing a column name and either the symbol asc (to ;;; indicate the result should be put in ascending order) or the ;;; symbol desc (to indicate the result should be put in descending ;;; order). ;;; ;;;(order-by ((first-name asc)(last-name DESC))) => ORDER BY "first-name" ASC, "last-name" DESC (define-syntax order-by (syntax-rules () ((_ ((column asc/desc) ...)) (format "ORDER BY ~a" (comma-separate (format "~a ~a" (format-column `column) (format-asc/desc `asc/desc)) ...))))) ;;; The insert macro has four rules that support the creation of ;;; INSERT statements. ;;; ;;; The following form constructs the INSERT statement shown: ;;; (insert pers (1 "John" "Doe" 20)) ; rule 3 ;;; => INSERT INTO "pers" VALUES (1,'John','Doe',20) ;;; ;;; The following three forms all construct the same INSERT statement: ;;; (insert pers (id first-name last-name age) (1 "John" "Doe" 20)) ; rule 4 ;;; (insert pers ((id . 1) (first-name . "John") (last-name . "Doe") (age . 20))) ; rule 2 ;;; (insert pers ((id 1) (first-name "John") (last-name "Doe") (age 20))) ; rule 1 ;;; => INSERT INTO "pers" ("id","first-name","last-name","age") VALUES (1,'John','Doe',20) ;;; ;;; The following examples demonstrates how to insert a NULL value: ;;; (insert pers (1 "John" null-object 20)) ;;; => INSERT INTO "pers" VALUES (1,'John',NULL,20) ;;; ;;; (define last-name 'null-object) ;;; (insert pers (1 "John" ,x 20)) ;;; => INSERT INTO "pers" VALUES (1,'John',NULL,20) (define-syntax insert (syntax-rules () ((_ table ((column value) ...)) ; rule 1 (format "INSERT INTO ~a (~a) VALUES (~a)" (format-table `table) (comma-separate (format-column `column) ...) (comma-separate (format-value `value) ...))) ((_ table ((column . value) ...)) ; rule 2 (format "INSERT INTO ~a (~a) VALUES (~a)" (format-table `table) (comma-separate (format-column `column) ...) (comma-separate (format-value `value) ...))) ((_ table (value ...)) ; rule 3 (format "INSERT INTO ~a VALUES (~a)" (format-table `table) (comma-separate (format-value `value) ...))) ((_ table (column ...) (value ...)) ; rule 4 (format "INSERT INTO ~a (~a) VALUES (~a)" (format-table `table) (comma-separate (format-column `column) ...) (comma-separate (format-value `value) ...))))) ;;; The delete macro has two rules that support the creation of DELETE ;;; statements as shown in the examples below: ;;; (delete pers) ;;; => DELETE FROM "pers" ;;; ;;; (delete pers (where (and (< age 45) (= state "NJ")))) ;;; => DELETE FROM "pers" WHERE "age" < 45 AND "state" = 'NJ' (define-syntax delete (syntax-rules (all) ((_ table) (format "DELETE FROM ~a" (format-table `table))) ((_ table where-clause) (format "DELETE FROM ~a ~a" (format-table `table) where-clause)))) ;;; The update macro has six rules that support the creation of ;;; UPDATE statements. ;;; ;;; The following three forms all construct the same UPDATE statement: ;;; (update pers ((first-name "John") (age 20))) ; rule 1 ;;; (update pers ((first-name . "John") (age . 20))) ; rule 2 ;;; (update pers ((first-name age) ("John" 20))) ; rule 3 - does not work ;;; => UPDATE pers SET "first-name"='John', "age"=20 ;;; ;;; The following three forms all construct the same UPDATE statement: ;;; (update pers ((first-name "John") (age 20)) (where (= last-name "Doe"))) ; rule 1a ;;; (update pers ((first-name . "John") (age . 20)) (where (= last-name "Doe"))) ; rule 2a ;;; (update pers ((first-name age) ("John" 20)) (where (= last-name "Doe"))) ; rule 3a - does not work ;;; => UPDATE pers SET "first-name"='John', "age"=20 WHERE "age" < 20 ;;; ;;; The following examples demonstrates how to update a NULL value: ;;; (update pers ((first-name "John") (age null-object))) ;;; => UPDATE pers SET "first-name"='John', "age"=NULL (define-syntax update (syntax-rules (where) ((_ table ((column value) ...)) ; rule 1 (format "UPDATE ~a SET ~a" (format-table `table) (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...))) ((_ table ((column value) ...) where-clause) ; rule 1a (format "UPDATE ~a SET ~a ~a" (format-table `table) (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...) where-clause)) ((_ table ((column . value) ...)) ; rule 2 (format "UPDATE ~a SET ~a" (format-table `table) (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...))) ((_ table ((column . value) ...) where-clause) ; rule 2a (format "UPDATE ~a SET ~a ~a" (format-table `table) (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...) where-clause)))) ; ((_ table (column ...) (value ...)) ; rule 3 ; (format "UPDATE ~a SET ~a" (format-table `table) ; (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...))) ; ((_ table (column ...) (value ...) where-clause) ; rule 3a ; (format "UPDATE ~a SET ~a ~a" (format-table `table) ; (comma-separate (format "~a=~a" (format-column `column) (format-value `value)) ...) ; where-clause)))) (provide where select select-distinct limit order-by insert update delete ))