(module query-lang-test mzscheme
(require (lib "cut.ss" "srfi" "26"))
(require (file "query-core.ss")
(prefix q: (file "query-lang.ss"))
(file "test-base.ss")
(file "test-data.ss")
(file "type.ss"))
(provide query-lang-tests)
(define-simple-check (check-alias regexp named)
(regexp-match regexp (symbol->string (named-alias named))))
(define query-lang-tests
(test-suite
"query-lang.ss"
(test-case
"appropriate auto-aliases generated"
(check-alias #rx"table[0-9]+" (q:table 'table) "table")
(check-alias #rx"course[0-9]+" (q:entity entity:course) "entity"))
(test-case
"join: ON fields have to be defined on tables / fields from the arguments"
(let ([table1 (q:table 'table1)]
[table2 (q:table 'table2)]
[table3 (q:table 'table3)])
(check-not-exn
(cut q:inner table1 table2
(q:= (q:field table1 'field1 type:text)
(q:field table2 'field2 type:text))))
(check-exn
exn:fail:snooze?
(cut q:inner table1 table2
(q:= (q:field table1 'field1 type:text)
(q:field table3 'field2 type:text))))))
(test-case
"select: WHAT fields have to be defined on tables from the FROM clause"
(let ([table1 (q:table 'table1)]
[table2 (q:table 'table2)])
(check-not-exn
(cut q:select
#:what (list (q:field table1 'field1 type:text))
#:from table1)
"check 1")
(check-not-exn
(cut q:select
#:what (list (q:field table1 'field1 type:text))
#:from (q:outer table1 table2))
"check 2")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list (q:field table1 'field1 type:text))
#:from table2)
"check 3")
(check-not-exn
(lambda ()
(let* ([field1 (q:field table1 'table1 type:integer/1)]
[agg1a (q:count field1)]
[agg1b (q:count* table1)]
[field2 (q:field table2 'field2 type:integer/1)]
[agg2a (q:count field2)]
[agg2b (q:count* table2)]
[subq (q:select #:what (list field2 agg2a agg2b)
#:from table2)])
(q:select #:what (list field1 agg1a agg1b field2 agg2a agg2b)
#:from (q:outer table1 subq))))
"check 4")))
(test-case
"select: single-item mode is equivalent to multi-item mode"
(let ([table1 (q:table 'table1)]
[entity1 (q:entity entity:course)])
(let ([select1a (q:select
#:what (list (q:field table1 'field1 type:text))
#:from table1)]
[select1b (q:select
#:what (q:field table1 'field1 type:text)
#:from table1)]
[select2a (q:select
#:what (list entity1)
#:from entity1)]
[select2b (q:select
#:what entity1
#:from entity1)])
(check-false (select-single-item? select1a) "select1a is in multi-item mode")
(check-true (select-single-item? select1b) "select1b is in single-item mode")
(check-false (select-single-item? select2a) "select2a is in multi-item mode")
(check-true (select-single-item? select2b) "select2b is in single-item mode")
(check-equal? (select-what select1a)
(select-what select1b)
"select1a and select1b have the same WHAT")
(check-equal? (select-what select2a)
(select-what select2b)
"select2a and select2b have the same WHAT")
(check-equal? (select-what-entities select1a)
(select-what-entities select1b)
"select1a and select1b have the same WHAT entities")
(check-equal? (select-what-entities select2a)
(select-what-entities select2b)
"select2a and select2b have the same WHAT entities"))))
(test-case
"select: omitting #:what causes it to default to #:from"
(let* ([course (q:entity entity:course)]
[table (q:table 'table)]
[select1 (q:select #:from course)])
(check-equal? (select-what-entities select1)
(list entity:course)
"select-what-entities matches #:from")
(check-exn exn:fail:contract?
(cut q:select #:from table)
"cannot omit #:what when #:from is not an entity")))
(test-case
"select: WHERE fields have to be from tables or subqueries in the FROM clause"
(let* ([table1 (q:table 'table1)]
[table2 (q:table 'table2)]
[field1-1 (q:field table1 'field1 type:text)]
[field1-2 (q:field table1 'field2 type:text)]
[field2-1 (q:field table2 'field1 type:text)]
[field2-2 (q:field table2 'field2 type:text)])
(check-not-exn
(cut q:select
#:what (list field1-1)
#:from table1
#:where (q:= field1-2 1))
"check 1")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list field1-1)
#:from table1
#:where (q:= field2-1 1))
"check 2")
(check-not-exn
(cut q:select
#:what (list field1-1 field2-1)
#:from (q:outer table1 (q:select #:what (list field2-1) #:from table2))
#:where (q:and (q:= field1-1 1)
(q:= field1-2 1)
(q:= field2-1 1)))
"check 3")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list field1-1 field2-1)
#:from (q:outer table1 (q:select #:what (list field2-1) #:from table2))
#:where (q:and (q:= field1-1 1)
(q:= field1-2 1)
(q:= field2-1 1)
(q:= field2-2 1)))
"check 4")))
(test-case
"select: WHERE aggregates have to be from subqueries in the FROM clause"
(let* ([table1 (q:table 'table1)]
[table2 (q:table 'table2)]
[field1-1 (q:field table1 'field1 type:text)]
[field1-2 (q:field table1 'field2 type:text)]
[field2-1 (q:field table2 'field1 type:text)]
[field2-2 (q:field table2 'field2 type:text)]
[aggregate1-1 (q:count field1-1)])
(check-not-exn
(cut q:select
#:what (list aggregate1-1)
#:from table1
#:where (q:= field1-1 1))
"check 1")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list aggregate1-1)
#:from table1
#:where (q:= aggregate1-1 1))
"check 2")
(check-not-exn
(cut q:select
#:what (list aggregate1-1)
#:from (q:select #:what (list aggregate1-1) #:from table1)
#:where (q:= aggregate1-1 1))
"check 3")))
(test-case
"select: ORDER fields have to be defined on tables or subqueries in the FROM clause"
(let* ([table1 (q:table 'table1)]
[table2 (q:table 'table2)]
[field1-1 (q:field table1 'field1 type:text)]
[field1-2 (q:field table1 'field2 type:text)]
[field2-1 (q:field table2 'field1 type:text)]
[field2-2 (q:field table2 'field2 type:text)])
(check-not-exn
(cut q:select
#:what (list field1-1)
#:from table1
#:order (list (q:asc field1-2)))
"check 1")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list field1-1)
#:from table1
#:order (list (q:asc field2-1)))
"check 2")
(check-not-exn
(cut q:select
#:what (list field1-1)
#:from (q:outer table1 (q:select #:what (list field2-1) #:from table2))
#:order (list (q:asc field1-2) (q:asc field2-1)))
"check 3")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list field1-1)
#:from (q:outer table1 (q:select #:what (list field2-1) #:from table2))
#:order (list (q:asc field1-2) (q:asc field2-1) (q:asc field2-2)))
"check 4")))
(test-case
"select: ORDER aggregates have to be from tables or subqueries in the FROM clause"
(let* ([table1 (q:table 'table1)]
[table2 (q:table 'table2)]
[field1-1 (q:field table1 'field1 type:text)]
[field1-2 (q:field table1 'field2 type:text)]
[field2-1 (q:field table2 'field1 type:text)]
[field2-2 (q:field table2 'field2 type:text)]
[aggregate1-1 (q:count field1-1)]
[aggregate2-1 (q:count field2-1)]
[aggregate2-2 (q:count field2-2)])
(check-not-exn
(cut q:select
#:what (list aggregate1-1)
#:from table1
#:order (list (q:asc aggregate1-1)))
"check 1")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list aggregate1-1)
#:from table1
#:order (list (q:asc aggregate2-1)))
"check 2")
(check-not-exn
(cut q:select
#:what (list aggregate1-1)
#:from (q:outer table1 (q:select #:what (list aggregate2-1) #:from table2))
#:order (list (q:asc aggregate2-1)))
"check 3")
(check-exn
exn:fail:snooze?
(cut q:select
#:what (list aggregate1-1)
#:from (q:outer table1 (q:select #:what (list aggregate2-1) #:from table2))
#:order (list (q:asc aggregate2-2)))
"check 4")))
(test-case
"select-what-types"
(let ([table1 (q:table 'table1)]
[entity1 (q:entity entity:programme)])
(check-equal? (select-what-types (q:select #:what (list (q:field 'f1 table1 'field1 type:id)
(q:field 'f2 table1 'field2 type:revision)
(q:field 'f3 table1 'field3 type:text)
(q:field 'f4 table1 'field4 type:integer/1)
(q:field 'f5 table1 'field5 type:real)
(q:field 'f6 table1 'field6 type:symbol)
(q:field 'f7 table1 'field7 type:boolean/t)
(q:field 'f8 table1 'field8 type:time-tai))
#:from table1))
(list type:id type:revision type:text type:integer/1 type:real type:symbol type:boolean/t type:time-tai)
"q:fields")
(check-equal? (select-what-types (q:select #:what (list entity1) #:from entity1))
(list type:id type:revision type:symbol type:text)
"q:entity")
(check-equal? (select-what-types (q:select #:what entity1 #:from entity1))
(list type:id type:revision type:symbol type:text)
"q:entity in single-item mode")
(check-equal? (select-what-types (q:select #:what (list (q:count (q:field 'f4 table1 'field4 type:integer/1))
(q:count* table1)
(q:max (q:field 'f4 table1 'field4 type:integer/1))
(q:min (q:field 'f4 table1 'field4 type:integer/1))
(q:max (q:field 'f5 table1 'field5 type:real/1))
(q:min (q:field 'f5 table1 'field5 type:real/1))
(q:average (q:field 'f4 table1 'field4 type:integer/1))
(q:average (q:field 'f5 table1 'field5 type:real/1)))
#:from table1))
(list type:integer type:integer type:integer type:integer type:real type:real type:real type:real)
"q:aggregates")))
(test-case
"attributes from subquery are recognised when used in enclosing query"
(let* ([a (q:entity entity:course)]
[b (q:entity entity:programme-structure)]
[subq (q:select #:what (list a)
#:from a
#:limit 100
#:offset 200)])
(check-not-exn
(lambda ()
(q:select #:what (list a b)
#:from (q:inner subq b (q:= (q:attr a 'code) (q:attr b 'course-code))))))
(check-exn
exn:fail:snooze?
(lambda ()
(q:select #:what (list a b)
#:from (q:inner subq b (q:= (q:attr a 'blah) (q:attr b 'course-code))))))))
))
)