6.1 plisqin-lib
(require plisqin-lib) | package: plisqin |
syntax
(from instance-id queryable-expr statement ...)
statement = (define (proc-id proc-arg ...) proc-body ...) | (define val-id val-expr) | (join join-id join-stuff ...) | clause-expr
queryable-expr :
(or/c symbol? table? query? Subquery?)
clause-expr :
(or/c void? QueryClause? (listof (or/c void? QueryClause?)))
The queryable identifies the thing that is being queried – usually a database table or view, sometimes a subquery, never a query?. When queryable-expr is not a query?, the value of queryable-expr becomes this query’s queryable. When queryable-expr is a query?, this query inherits its queryable from the existing query. It also inherits that query’s list of joins and list of clauses; an example of this will come later.
The instance-id is bound as an instance? within each statement. More specifically, it is bound as an instanceof the queryable.
The define subforms bind proc-id or val-id within each statement that follows the definition.
The join subform binds join-id as a join? within each statement that follows the join. It also immediately adds the join to this query, guaranteeing that the join will appear in the generated SQL even if the join-id is never used anywhere else.
A clause-expr that is void? is discarded.
> (define q1 (from x 'X (%%select x".one") (%%select x".two")))
> (define q2 (from x 'X (list (%%select x".one") (void) (%%select x".two")))) > (equal? q1 q2) #t
See also the documentation on QueryClause?.
> (define total (from x 'X (join y 'Y) (%%select x".one") (%%select y".two")))
> (define first-half (from x 'X (%%select x".one")))
> (define both-halves (from x first-half (join y 'Y) (%%select y".two"))) > (equal? total both-halves) #t
syntax
(join instance-id queryable-expr maybe-to statement ...)
maybe-to =
| #:to link-expr statement = (define (proc-id proc-arg ...) proc-body ...) | (define val-id val-expr) | (join join-stuff ...) | clause-expr
queryable-expr :
(or/c symbol? table? join? Subquery?)
link-expr : instance?
clause-expr :
(or/c void? JoinClause? (listof (or/c void? JoinClause?)))
(from a 'A (join b 'B) (%%where b".foo = "a".foo")) ; is nearly equivalent to (from a 'A (define b (join b 'B #:to a)) (%%where b".foo = "a".foo"))
> (from a 'A (join b 'B #:to a)) eval:20:0: join: The `#:to` option is not allowed here. This
join is automatically linked to the enclosing query or join.
in: (join b (quote B) #:to a)
(define/contract (Owner pet) (-> (instanceof Pet) (instanceof Person)) (join p Person #:to pet (join-on (.= (PersonId p) (OwnerId pet)))))
> (define (B-given-A a) (join b 'B #:to a (%%join-on b".Foo = "a".Foo")))
> (from a 'A (if (join? (B-given-A a)) (%%select "of course it is a join") (error "Inconceivable!"))) (from a/0 'A (select "of course it is a join"))
(from p Person ; Now `p` is an `instance?` and an `(instanceof Person)` (where (>= (Age p) (val 21))))
Additionally, every join? is also an instance.
procedure
(instanceof queryable) → procedure?
queryable : any/c
procedure
n : (or/c #f nonnegative-integer?)
> (aw:show-table (from pc ProductCategory (limit 2)))
Show TableShow SQL
select pc.*
from ProductCategory pc
limit 2
ProductCategoryID
Name
rowguid
ModifiedDate
1
Bikes
{CFBDA25C-DF71-47A7-B81B-64EE161AA37C}
2008-04-30 00:00:00
2
Components
{C657828D-D808-4ABA-91A3-AF2CE02300E9}
2008-04-30 00:00:00
> (aw:show-table (from pc ProductCategory (limit 2) (limit #f)))
Show TableShow SQL
select pc.*
from ProductCategory pc
ProductCategoryID
Name
rowguid
ModifiedDate
1
Bikes
{CFBDA25C-DF71-47A7-B81B-64EE161AA37C}
2008-04-30 00:00:00
2
Components
{C657828D-D808-4ABA-91A3-AF2CE02300E9}
2008-04-30 00:00:00
3
Clothing
{10A7C342-CA82-48D4-8A38-46A2EB089B74}
2008-04-30 00:00:00
4
Accessories
{2BE3BE36-D9A2-4EEE-B593-ED895D97C2A6}
2008-04-30 00:00:00
procedure
n : (or/c #f nonnegative-integer?)
> (aw:show-table (from p Product (order-by (ProductID p)) (offset 30) (limit 3)))
Show TableShow SQL
select p.*
from Product p
order by p.ProductID
limit 3
offset 30
ProductID
Name
ProductNumber
MakeFlag
FinishedGoodsFlag
Color
SafetyStockLevel
ReorderPoint
StandardCost
ListPrice
Size
SizeUnitMeasureCode
WeightUnitMeasureCode
Weight
DaysToManufacture
ProductLine
Class
Style
ProductSubcategoryID
ProductModelID
SellStartDate
SellEndDate
DiscontinuedDate
rowguid
ModifiedDate
350
Fork Crown
FC-3654
1
0
#<sql-null>
800
600
0
0.0
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
1
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
2008-04-30 00:00:00
#<sql-null>
#<sql-null>
{1CBFA85B-5C9B-4B58-9C17-95238215D926}
2014-02-08 10:01:36.827000000
351
Front Derailleur Cage
FC-3982
0
0
Silver
800
600
0
0.0
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
0
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
2008-04-30 00:00:00
#<sql-null>
#<sql-null>
{01C901E3-4323-48ED-AB9E-9BFDA28BDEF6}
2014-02-08 10:01:36.827000000
352
Front Derailleur Linkage
FL-2301
0
0
Silver
800
600
0
0.0
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
0
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
#<sql-null>
2008-04-30 00:00:00
#<sql-null>
#<sql-null>
{88ED2E08-E775-4915-B506-831600B773FD}
2014-02-08 10:01:36.827000000
The last offset clause overrides all the previous ones. A value of #f clears the offset.
> (aw:show-table (from p Product (select (Color p)) (distinct #t)))
Show TableShow SQL
select distinct p.Color as Color
from Product p
Color
#<sql-null>
Black
Silver
Red
White
Blue
Multi
Yellow
Grey
Silver/Black
If a join has multiple join-type clauses, the last one overrides all the previous ones.
(define/contract (get-category subcat) (-> (instanceof ProductSubcategory) (and/c join? (instanceof ProductCategory))) (join pc ProductCategory #:to subcat (join-on (.= (ProductCategoryID pc) (?? (ProductCategoryID subcat) /void))))) (from p Product ; The Product:ProductSubcategory relationship is inherently optional. ; That is, some Products truly do not have Subcategories. (join subcat ProductSubcategory (join-type 'left) (join-on (.= (ProductSubcategoryID subcat) (?? (ProductSubcategoryID p) /void)))) ; The ProductSubcategory:ProductCategory relationship is not ; inherently optional, but the left-ness of `subcat` propogates ; to the return value of (get-category subcat) (join cat (get-category subcat)) ....)
Reusable joins should never use the 'inner join type, even when they represent a non-optional relationship. In the previous example, get-subcategory represents the ProductSubcategory:ProductCategory relationship which is not optional; specifically, every ProductSubcategory has exactly 1 ProductCategory. But get-subcategory still uses the the default join type of #f instead of 'inner, because its subcat argument might be a left join.
syntax
(define-schema schema-id table-spec ...)
table-spec = (table table-id item-spec ...) item-spec = #:column column-spec ... | #:property proc-spec ... | #:has-one proc-spec ... | #:has-group proc-spec ... proc-spec = [proc-id proc-body] column-spec = proc-id | [proc-id as-name* type* nullability* dbname*] as-name* =
| #:as as-name type* =
| #:type type nullability* =
| #:null nullability dbname* =
| #:dbname dbname
Defines each table-id as a table?. Note that each table-id can also be used as a proc-id, meaning that some tables are also procedures.
> (define-schema test-schema (table A #:property [foo (%%scalar "foo-given-A")]) (table B #:property [foo (%%scalar "foo-given-B")]) (table C)) > (foo A) (scalar "foo-given-A")
> (foo B) (scalar "foo-given-B")
; foo is not defined for C > (foo C) foo: contract violation
expected: one of (B A)
given: C
Recall that each proc-id is a procedure that takes one argument. Within the corresponding proc-body, the special form this will be bound to that single argument. So this is always an instanceof the table that encloses it.
Currently, #:property, #:has-one, and #:has-group are treated identically; they are only for code readability at this time. You should use #:property for procedures that return a Scalar? (or one of its subtypes). You should use #:has-group for procedures that return grouped joins. You should use #:has-one for procedures that return joins such that "this:joined-table" is a "many:1" or "1:1" relationship. A future version of Plisqin may add warnings if it can detect that your procedures are returning something other than the keyword indicates.
This procedure works on datums, not syntax objects. If you have renamed any tables or procedures (e.g. with prefix-in), you still need to use the original ids with this procedure.
> (define-schema test-schema (table A #:column x y z) (table B #:column x z) (table C #:column y z)) ; --- 1 --- ; See which procedures accept a certain table. ; If I have an (instanceof B), I can use this to ; see all the procedures which will accept it: > (test-schema '(_ B)) '(x z)
; --- 2 --- ; See which tables a certain proc will accept. ; This lists the tables that `y` accepts: > (test-schema '(y _)) '(A C)
; --- 3 --- ; List all the tables: > (test-schema 'tables) '(A B C)
syntax
Within define-schema, this will always be an instanceof the table that encloses it.
> (define-schema test-schema (table Tbl #:column foo)) > (table? Tbl) #t
> ((instanceof Tbl) Tbl) #t
> (foo Tbl) (scalar Tbl (sql "." 'foo))
> (to-sql (from t Tbl ; Good - `t` is an instance created by `from` (select (foo t)) ; BAD! - don't use `Tbl` as an instance in real code (select (foo Tbl)))) cannot render Tbl
procedure
(>> token [ #:type Type #:as as-name #:null nullability #:fallback /fallback]) → Token? token : Token? Type : type? = <no-change> as-name : (or/c symbol? string?) = <no-change> nullability : nullability? = <no-change> /fallback : fallback? = <no-change>
Show TableShow SQL
select 'Hello!' as Greeting
from Product p
limit 1
Greeting
Hello!
Future versions of Plisqin may introduce "as-name propogation" in which certain token constructions preserve the as-name of a child token. For example, (coalesce foo (val 0)) might inherit the as-name of foo.
> (let ([raw (%%sql "foo")]) (list (nullability raw) (nullability (>> raw #:null no)))) (list maybe no)
The #:fallback option assigns a new fallback. It is more common to use ?? to assign fallbacks.
syntax
(define-statement (id arg ...) body ...+)
arg = plain-arg | kw-arg plain-arg = arg-id | [arg-id Type-expr] | [arg-id Type-expr default-expr] kw-arg = keyword plain-arg
Type-expr : type?
(define-statement (foo a [b Number?] [c String? "c-default"] #:d d #:e [e Number?] #:f [f String? "f-default"]) (list a b c d e f)) ; produces code similar to (define (foo a b [c (%%val "c-default")] #:d d #:e e #:f [f (%%val "f-default")]) (list a b c d e f)) (begin ; this part is illustrative, not real code: (module+ metadata-for-compile-statements (define meta:foo (foo (param a Scalar?) (param b Number?) (param c String?) #:d (param d Scalar?) #:e (param e Number?) #:f (param f String?)))))
In the code above, the hypothetical (param b Number?) expression creates a placeholder token having type Number? and representing an SQL parameter. The a and d parameters were assigned the Scalar? type, which is the default when Type-expr is absent.
syntax
(compile-statements #:module module-path #:dialect dialect-expr maybe-provide)
maybe-provide =
| #:provide? #t | #:provide? #f
dialect-expr : dialect?
; This module contains a `get-category` uncompiled statement:
> (compile-statements #:module plisqin-examples/adventure-works/statements #:dialect (sqlite) #:provide? #f) ; Now `get-category` has been compiled to an unbound statement: > (displayln get-category)
#<unbound-statement: get-category
select cat.ProductCategoryID as ProductCategoryID
, cat.Name as Name
, cat.ModifiedDate as ModifiedDate
from ProductCategory cat
where (?1 /*name*/ is not null and (cat.Name = ?1 /*name*/))>
; We just need to supply the parameter values and we can ; execute it against a real database! > (define conn (aw:connect-adventure-works)) > (db:query conn (get-category #:name "Bikes"))
(rows-result
'(((name . "ProductCategoryID") (decltype . "integer_id"))
((name . "Name") (decltype . "text"))
((name . "ModifiedDate") (decltype . "text")))
'(#(1 "Bikes" "2008-04-30 00:00:00")))
> (db:disconnect conn)
The SQL is generated only once, using the given dialect-expr.
Unless #:provide? is #f, each unbound statement will be provided. This option is really only intended for this documentation; your code probably has no need to ever use this option.
procedure
x : unsafe-content?
> (displayln (to-sql (from a 'Album (%%where a".ReleaseYear = 1973"))))
select a.*
from Album a
where a.ReleaseYear = 1973
See also the date math examples at date+.
procedure
(unsafe-content? x) → any/c
x : any/c
Equivalent to (or/c Token? query? join? instance? interval? string? number? symbol? (listof unsafe-content?))
6.1.1 Nullability
value
value
value
procedure
(nullability? x) → any/c
x : any/c
> (nullability? yes) #t
> (nullability? no) #t
> (nullability? maybe) #t
procedure
(nullability x) → nullability?
x : any/c
> (nullability (val 3)) no
> (nullability (val "hi")) no
> (nullability 3) no
> (nullability "case when 1=1 then null else null end") maybe
> (nullability "case when 1=1 then 'foo' else 'bar' end") maybe
> (define content (%%sql "foo = 1")) > (nullability (%%where (>> content #:null no))) no
> (nullability (%%where (>> content #:null yes))) yes
6.1.2 Token Types
(require plisqin-lib/types) | package: plisqin |
procedure
(type-supertypes t) → (listof type?)
t : type?
> (type-supertypes Datetime?) (list Scalar?)
#:supertypes []
#:supertypes [Token?]
#:supertypes [Scalar?]
> (displayln (to-sql (from x 'X (where (>> (val 0) #:cast Bit?)))))
select x.*
from X x
where 0 <> 0
> (displayln (to-sql (from x 'X (where (>> (val 1) #:cast Bit?)))))
select x.*
from X x
where 1 <> 0
#:supertypes [Boolish?]
See Nullability for an explanation of "the unknown boolean value" and how to avoid strange three-valued logic.
A null value represents the unknown boolean value.
A zero value represents false.
Any other value represents true.
See example on Boolish?.
#:supertypes [Boolish?]
> (define q (from x 'X (define a-bool (.= (val 42) (val 42))) (where a-bool) (select a-bool)))
> (parameterize ([current-dialect (mssql)]) (displayln (to-sql q)))
select cast(case when (42 = 42) then 1 else 0 end as bit)
from X x
where (42 = 42)
; non-MSSQL dialects are much easier: > (displayln (to-sql q))
select (42 = 42)
from X x
where (42 = 42)
#:supertypes [Scalar?]
#:supertypes [Scalar?]
#:supertypes [Scalar?]
#:supertypes [Token?]
#:supertypes [Token?]
value
#:supertypes [Clause?]
value
#:supertypes [Clause?]
> (andmap QueryClause? (list (%%where 1) (%%group-by 1) (%%having 1) (%%order-by 1) (%%select 1) (limit 1) (offset 1) (distinct #t))) #t
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause? QueryClause?]
#:supertypes [JoinClause?]
6.1.3 Dialects
(require plisqin-lib/dialect) | package: plisqin |
parameter
(current-dialect) → (or/c #f dialect?)
(current-dialect dialect) → void? dialect : (or/c #f dialect?)
> (define q (from x 'X (limit 3)))
> (parameterize ([current-dialect (postgres)]) (displayln (to-sql q)))
select x.*
from X x
limit 3
> (parameterize ([current-dialect (mssql)]) (displayln (to-sql q)))
select top 3 x.*
from X x