#lang scheme/base
(require mzlib/etc
(only-in srfi/1 unzip2)
srfi/19
srfi/26
(file "../persistent-struct-syntax.ss")
(file "../test-base.ss")
(file "../test-data.ss")
(file "../era/era.ss")
(file "sql-alias.ss")
(prefix-in sql: (file "sql-lang.ss"))
(file "sql-struct.ss"))
(define sql-lang-tests
(test-suite "sql-lang.ss"
(test-case "alias"
(begin-with-definitions
(define p (make-entity-alias 'p entity:person))
(define q (make-query null #f p #f null null #f #f #f null null null))
(check-equal? (sql:alias 'p entity:person) p "entity")
(check-equal? (sql:alias 'q q)
(make-query-alias 'q q)
"query")
(check-equal? (sql:alias 'l (make-literal 1))
(make-expression-alias 'l (make-literal 1))
"expression")
(check-exn exn:fail:contract?
(cut sql:alias 'm (make-expression-alias 'l (make-literal 1)))
"expression-alias")
(check-equal? (sql:alias p attr:person-name)
(make-attribute-alias p attr:person-name)
"attribute")
(check-exn exn:fail:contract?
(cut sql:alias 'p-name p attr:pet-name)
"incorrect attribute")))
(test-case "expressions : function arities and self-quoting literals"
(begin-with-definitions
(define-values (sql:ops args)
(unzip2 (list (list sql:and (list #t #f #t))
(list sql:or (list #t #f #t))
(list sql:not (list #t))
(list sql:+ (list 1 2.5 3))
(list sql:- (list 1 2.5 3))
(list sql:* (list 1 2.5 3))
(list sql:/ (list 1 2.5))
(list sql:append (list 'a "b" 'c))
(list sql:replace (list "mat" "a" "e"))
(list sql:= (list #t 'a))
(list sql:<> (list #t 'a))
(list sql:< (list 1 2.5))
(list sql:> (list 'a "b"))
(list sql:<= (list (current-time time-tai) (current-time time-utc)))
(list sql:>= (list (current-time time-tai) (current-time time-utc)))
(list sql:like (list "a" 'b))
(list sql:regexp-match (list "a" 'b))
(list sql:regexp-match-ci (list "a" 'b))
(list sql:regexp-replace (list "a" 'b "c"))
(list sql:regexp-replace-ci (list "a" 'b "c"))
(list sql:regexp-replace* (list "a" 'b "c"))
(list sql:regexp-replace*-ci (list "a" 'b "c"))
(list sql:null? (list #t)))))
(map (lambda (sql:op args)
(define literals (map sql:literal args))
(check-equal? (apply sql:op args)
(apply sql:op literals)
(format "~s" (cons sql:op args))))
sql:ops
args)))
(test-case "sql:in"
(begin-with-definitions
(define-alias p person)
(check-not-exn
(cut sql:in 123 '(123 234 345))
"valid list")
(check-exn exn:fail:contract?
(cut sql:in 123 '(#f 234 345))
"invalid list (list items of different types)")
(check-exn exn:fail:contract?
(cut sql:in "123" '(123 234 345))
"invalid list (arguments of different types)")
(check-not-exn
(cut sql:in 123 (sql:select #:what p-id #:from p))
"valid subquery")
(check-exn exn:fail:contract?
(cut sql:in 123 (sql:select #:from p))
"invalid subquery (incorrect arity)")
(check-exn exn:fail:contract?
(cut sql:in "123" (sql:select #:what p-id #:from p))
"invalid subquery (arguments of different types)")))
(test-case "sql:if and sql:cond"
(check-equal? (sql:cond [#t "a"] [#f "b"] [else "c"])
(sql:if #t "a" (sql:if #f "b" "c"))
"else")
(check-equal? (sql:cond [#t "a"] [#f "b"])
(sql:if #t "a" (sql:if #f "b"))
"no else")
(check-true (integer-type? (expression-type (sql:cond [#t 1] [#f 2.0]))) "single type cond")
(check-true (real-type? (expression-type (sql:cond [#t 1] [#f 2.1]))) "multiple type cond"))
(test-case "joins: terms in join condition must be part of join operands"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define-alias p3 person)
(check-not-exn
(cut sql:inner p1 p2 (sql:= p1-name p2-name))
"valid")
(check-exn exn:fail:contract?
(cut sql:inner p1 p3 (sql:= p1-name p2-name))
"invalid")
(check-exn exn:fail:contract?
(cut sql:inner p1 p3 (sql:= p1-name p2-name))
"tricky")))
(test-case "select : single-item versus multi-item"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define select1 (sql:select #:what (list p1-name) #:from p1))
(define select2 (sql:select #:what p1-name #:from p1))
(check-equal? (query-what select1) (query-what select2) "what")
(check-equal? (query-extract-info select1) (list (query-extract-info select2)) "expand-info")))
(test-case "select : no #:what"
(begin-with-definitions
(define p1 (sql:alias 'p1 entity:person))
(define p2 (sql:alias 'p2 entity:pet))
(define select1 (sql:select #:from p1))
(check-equal? (query-extract-info (sql:select #:from p1))
entity:person
"entity")
(check-equal? (query-extract-info (sql:select #:from (sql:outer p1 p2)))
(list entity:person entity:pet)
"join")))
(test-case "select : queries in #:from get quoted"
(begin-with-definitions
(define p1 (sql:alias 'p1 entity:person))
(check-pred query-alias?
(query-from (sql:select #:from (sql:select #:from p1)))
"query")))
(test-case "select : members of #:what must be defined in #:from"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define-alias p1-count (sql:count* p1))
(check-not-exn
(cut sql:select #:what p1-name #:from p1)
"correct entity")
(check-not-exn
(cut sql:select #:what p1-name #:from (sql:outer p1 p2))
"join")
(check-exn exn:fail:contract?
(cut sql:select #:what p1-name #:from p2)
"incorrect entity")
(check-not-exn
(cut sql:select #:what p1-name #:from (sql:select #:what p1-name #:from p1))
"subquery with correct #:what")
(check-exn exn:fail:contract?
(cut sql:select #:what p1-name #:from (sql:select #:what p1-id #:from p1))
"subquery with incorrect #:what")
(check-not-exn
(cut sql:select #:what p1-count #:from (sql:select #:what p1-count #:from p1))
"redeclared expression")))
(test-case "select : members of #:distinct must be defined in #:from"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define-alias p1-count (sql:count* p1))
(check-not-exn
(cut sql:select #:distinct #t #:from p1)
"no entity")
(check-not-exn
(cut sql:select #:distinct (sql:= p1-name "x") #:from p1)
"single entity")
(check-not-exn
(cut sql:select #:distinct (sql:= p1-name p2-name) #:from (sql:outer p1 p2))
"join")
(check-exn exn:fail:contract?
(cut sql:select #:distinct (sql:= p1-name "x") #:from p2)
"incorrect entity")
(check-not-exn
(cut sql:select #:distinct (sql:= p1-name "x") #:from (sql:select #:what p1-name #:from p1))
"subquery with correct #:what")
(check-exn exn:fail:contract?
(cut sql:select #:distinct (sql:= p1-name "x") #:from (sql:select #:what p1-id #:from p1))
"subquery with incorrect #:what")
(check-not-exn
(cut sql:select #:distinct (sql:= p1-count 1) #:from (sql:select #:what p1-count #:from p1))
"redeclared expression")))
(test-case "select : members of #:where must be defined in #:from"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define p1-max-id (sql:alias 'p1-max-id (sql:max p1-id)))
(check-not-exn
(cut sql:select #:what p1-id #:from p1 #:where (sql:= p1-name "Dave"))
"entity")
(check-not-exn
(cut sql:select #:what p1-id #:from (sql:outer p1 p2) #:where (sql:= p1-name "Dave"))
"join")
(check-not-exn
(cut sql:select #:what p1-id #:from (sql:select #:from p1) #:where (sql:= p1-name "Dave"))
"subquery")
(check-not-exn
(cut sql:select
#:what p2-id
#:from (sql:outer (sql:select #:what p1-max-id #:from p1) p2)
#:where (sql:= p2-id p1-max-id))
"expression alias")
(check-exn exn:fail:contract?
(cut sql:select #:from p2 #:where (sql:= p1-name "Dave"))
"incorrect entity")))
(test-case "select : members of #:order must be defined in #:what or #:from"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define-alias expr1 (sql:+ p1-id 1))
(check-not-exn
(cut sql:select #:what p1-id #:from p1 #:order (list (sql:asc p1-name)))
"entity")
(check-exn exn:fail:contract?
(cut sql:select #:what p1-id #:from p1 #:order (list (sql:asc p2-name)))
"incorrect entity")
(check-not-exn
(cut sql:select
#:what p1-id
#:from (sql:outer p1 p2)
#:order (list (sql:asc p1-name)))
"join")
(check-not-exn
(cut sql:select
#:what p1-id
#:from (sql:select #:from p1)
#:order (list (sql:asc p1-name)))
"subquery")
(check-exn exn:fail:contract?
(cut sql:select
#:what p2-name
#:from (sql:select #:from p2)
#:order (list (sql:asc p1-name)))
"incorrect subquery")
(check-not-exn
(cut sql:select
#:what (list p1-id expr1)
#:from (sql:select #:from (sql:outer p1 p2))
#:order (list (sql:asc expr1) (sql:desc p1-id) (sql:asc p2-id)))
"expressions and attributes in #:what")))
(test-case "select : extract-info"
(begin-with-definitions
(define-alias p1 person)
(define-alias p2 person)
(define p1-count-id (sql:count p1-id))
(define p1-count (sql:count* p1))
(define p1-max-id (sql:max p1-id))
(define p1-min-id (sql:min p1-id))
(define p1-average-id (sql:average p1-id))
(check-equal? (query-extract-info (sql:select #:what p1-id #:from p1))
type:integer
"single attribute")
(check-equal? (query-extract-info (sql:select #:what (list p1-id p1-revision p1-name) #:from p1))
(list type:id type:revision type:string)
"list of attributes")
(check-equal? (query-extract-info (sql:select #:from p1))
entity:person
"single entity")
(check-equal? (query-extract-info (sql:select #:from (sql:outer p1 p2)))
(list entity:person entity:person)
"multiple entities")
(check-equal? (query-extract-info (sql:select #:what (list p1-count-id
p1-count
p1-max-id
p1-min-id
p1-average-id)
#:from p1))
(list type:integer type:integer type:integer type:integer type:real)
"aggregates")
(check-equal? (query-extract-info (sql:select #:what (list (sql:alias 'column1 (sql:+ p1-id p1-revision))
(sql:alias 'column2 (sql:+ p1-count-id 1.5))
(sql:alias 'column3 (sql:append p1-name " rocks!"))
(sql:alias 'column4 (sql:> p1-id 123)))
#:from p1))
(list type:integer type:real type:string type:boolean)
"expressions")))))
(provide sql-lang-tests)