example.ss
; Snooze v3 example
; =================
; By  Dave Gurnell (dave at untyped dot com)
;
; This self-contained example shows off some of the core parts of Snooze v3:
;
;   - defining the Snooze interface
;   - defining a persistent structure type
;   - saving structures to the database
;   - querying the database for structures
;   - deleting structures from the database
;
; other features of Snooze not demonstrated here include:
;
;   - fancy queries:
;     - aggregate functions
;     - inner, outer, left and right joins
;     - sorting and grouping
;     - limiting and offsetting
;   - generator combinators
;     - "projecting" results for simpler iteration
;   - connecting to multiple databases
;   - the Unlib "check" library (for data validation and error reporting)
;
; Lots of things below are simplified and are not the "whole truth".
; Please feel free to contact the author if you have questions or for more
; information.
;
; Full documentation for Snooze is forthcoming.
(module example mzscheme
  
  (require (lib "snooze.ss" "snooze")
           (prefix postgresql8: (lib "postgresql8.ss" "snooze" "postgresql8")))
  
  ; Snooze interface -----------------------------
  
  ; The core Snooze interface is divided into two parts:
  ;
  ;   - a DBMS-independent part, required directly from "snooze.ss"
  ;
  ;   - a DBMS-dependent part, defined in the file "snooze-unit.ss",
  ;     which has to be linked against a particular database backend
  ;     with the define-snooze-interface macro below
  ;
  ; Snooze currently supports two DBMS backends:
  ;   - SQLite 3 (via Jay McCarthy's sqlite.plt package)
  ;
  ;     Make sure you compile SQLite with thread safety enabled if you want to use this
  ;     backend from multiple Scheme threads.
  ;
  ;   - PostgreSQL 8 (via Ryan Culpepper's spgsql.plt package)
  (define-snooze-interface postgresql8:db@)
  
  ;; main : -> void
  ;;
  ;; This procedure represents the main entry point for the application.
  ;;
  ;; It sets up a database connection, which is stored in a parameter and
  ;; used for the rest of the program.
  ;;
  ;; The rest of the program creates some students, lists them, deletes a
  ;; few and lists the remaining ones.
  (define (main)
    ; Enter your DB connection arguments here:
    (with-database (postgresql8:make-config "hostname" 5432 "database" "username" "password" #:ssl 'no)
      (create-students!)
      (print-students)
      (delete-students!)
      (print-students)))
  
  ; Data structures ------------------------------
  
  ; The next statement defines two things:
  ;   - a data structure, "student", that can be saved and loaded to/from the database
  ;   - a metadata structure, "entity:student", that is used to refer to the data type in queries
  
  ; The database table has the following structure (this can be generated automatically
  ; using the create-table procedure in snooze-unit.ss, but see below for further advice):
  ;
  ;     CREATE TABLE student (
  ;         id INTEGER PRIMARY KEY,
  ;         revision INTEGER DEFAULT 0,
  ;         "family-name" TEXT,
  ;         "given-names" TEXT,
  ;         "year" INTEGER);
  ;
  ; Snooze uses an "id" column as a primary key in every table. This cannot be changed.
  ; IDs are generated automatically as new structures are saved to the database.
  ;
  ; Similarly, Snooze uses the "revision" column to guard against some concurrent update
  ; problems. It is initialised automatically and checked and incremented on every save.
  ; You should never have to worry about it.
  ;
  ; While Snooze will generate the database table for you automatically, we recommend
  ; you tweak the SQL yourself. TEXT fields, for example, use a lot of storage space
  ; and query time, and can't be ORDERed on in SELECT queries. Any textual column
  ; type will do for a type:text attribute, including CHARACTERs and VARCHARs of
  ; any length. You can also use other tools like default values and constraints.
  ;
  ; Field types include:
  ;
  ;     type:integer - signed integers (NULL values represented using #f)
  ;     type:text - string values (NULL values represented using #f)
  ;     type:symbol - symbols stored as strings in the DB (NULL values represented using #f)
  ;     type:boolean - boolean values (NULL cannot be represented at the Scheme level)
  ;
  ; Snooze does not care about the order of columns in the database: it references everything
  ; by name. However, table and column names *must* be the same as the relevant Scheme
  ; identitifers: there is no name mapping at present. This can mean some awkwardness at the
  ; SQL level, although Snooze provides a Scheme-based abstraction of SQL so this isn't as
  ; cumbersome as it initially sounds.
  
  ;; student : (struct (U string #f)
  ;;                   (U string #f)
  ;;                   (U string #f)
  ;;                   (U integer #f))
  ;;
  ;; entity:student : entity
  ;;
  ;; student?            : any -> boolean
  ;; student-id          : student -> (U integer #f)
  ;; student-revision    : student -> (U integer #f)
  ;; student-family-name : student -> (U string #f)
  ;; ...
  ;; set-student-family-name! : student string -> void
  ;; ...
  (define-persistent-struct student 
    ([family-name        type:text]
     [given-names        type:text]
     [programme          type:text]
     [year               type:integer]))
  
  ; Procedures -----------------------------------
  
  ; The Snooze query language is introduced below. This is a simple abstraction of SQL that
  ; provides constructs for manipulating data at the struct/entity level.
  ;
  ; Snooze exports all of its query language with a "q:" prefix to avoid name clashes with
  ; R5RS Scheme and certain SRFIs. There is currently no way to customise this prefix, although
  ; you can always copy and modify the code in snooze.ss.
  
  ;; find-students : -> (list-of student)
  (define (find-students)
    ; q:student : q:entity
    ;
    ; Think of this as an alias for a particular student in the query below.
    ; You need this level of indirection to do queries like:
    ;
    ;    (q:select #:what (list q:student1 q:student2) ...)
    ;
    ; that simultaneously alias a table under two different names.
    (define q:student
      (q:entity entity:student))
    ; q:select creates a SELECT data structure that can be used and re-used
    ; to interrogate the database:
    ;
    ;     q:select : #:what (list-of (U q:attr q:aggregate q:entity))
    ;                #:from (U q:entity q:join)
    ;                [#:where q:expr]
    ;                [#:order (list-of q:order)]
    ;                [#:group (list-of (U q:attr q:entity))]
    ;                [#:limit integer]
    ;                [#:offset integer]
    ;             -> select
    ;
    ; find-gen is the basic query mechanism. It takes a select structure and returns a
    ; "generator procedure", that returns a row of data at a time until it reaches
    ; the end of the table, when it returns the unique value g:end:
    ;
    ;     find-gen : select -> (-> (U (list-of any) g:end))
    ;
    ; We use the gen-> type to conveniently refer to generators in contracts:
    ;
    ;     (gen-> a) == (-> (U a g:end))
    ;
    ;     find-gen : select -> (gen-> (list-of (U persistent-struct any)))
    ;
    ; Generators are a general mechanism provided as part of Unlib. Generators can be
    ; manipulated with combinators that perform analogous functions to the list
    ; combinators in SRFI1. See generator.ss and gen.ss in Unlib for more detail.
    ;
    ; The items returned by a result generator are packaged up according to the
    ; contents of the #:what clause of the select statement. For example:
    ;
    ;     (find-gen (q:select #:what q:student...))
    ;
    ; returns a generator that generates single students before reaching g:end:
    ;
    ;     (gen-> (U student #f))
    ;
    ; Other examples:
    ;
    ;     (find-gen (q:select #:what (list (q:field q:student 'family-name)
    ;                                      (q:field q:student 'year)) ...))
    ;
    ;       ==> (gen-> (list (U string #f) (U integer #f))
    ;
    ;     (find-gen (q:select #:what (list q:student1 q:student2) ...))
    ;
    ;       ==> (gen-> (list (U student #f) (U student #f)))
    ;
    ; The #:what argument is usually a (list-of (U q:entity q:attr q:aggregate)), but
    ; it can also be a single (U q:entity q:attr q:aggregate). Snooze works out whether
    ; it should generate lists or single results appropriately.
    ;
    ; Two generator combinators are of particular use: g:map creates a generator B whose return
    ; values are related to a source generator A by some function f:A -> b:
    ;
    ;     g:map : (a -> b) (gen-> a) -> (gen-> b)
    ;
    ; g:collect takes a generator and calls it repeatedly, collecting its non-g:end results
    ; into a list:
    ;
    ;     g:collect : (gen-> a) -> (list-of a)
    (g:collect (g:find (q:select #:what q:student
                                 #:from q:student))))
  
  ;; g:students/programme : string -> (list-of student)
  ;;
  ;; This procedure returns a generator rather than a list. This lets us iterate over
  ;; a large amount of data without storing it all in memory (although caching mechanisms
  ;; in Snooze do consume some memory to increase speed if a row is generated more than once).
  (define (g:students/programme programme)
    ; q:student : q:entity
    (define q:student
      (q:entity entity:student))
    ; Return (gen-> student)
    (g:find (q:select #:what  q:student
                      #:from  q:student
                      #:where (q:= (q:attr q:student 'programme) programme))))
  
  ;; create-students! : -> void
  ;;
  ;; Newly created structures have an id and revision of #f
  ;; until they have been saves to the database.
  ;;
  ;; The save! procedure saves a structure and mutates its
  ;; id and revision fields appropriately.
  (define (create-students!)
    (save! (make-student "Gurnell" "Dave" "Programming" 1))
    (save! (make-student "Welsh"   "Noel" "Programming" 2))
    (save! (make-student "Jadud"   "Matt" "Programming" 3))
    (save! (make-student "Bloggs"  "Fred" "Mathematics" 1))
    (save! (make-student "Bloggs"  "Sue"  "Mathematics" 2))
    (save! (make-student "Doe"     "John" "Mathematics" 3)))
  
  ;; delete-students! : -> void
  ;;
  ;; The delete! procedure deletes a structure from the database
  ;; and sets its id and revision back to #f.
  ;;
  ;; Here we are using the g:for-each combinator to apply the
  ;; delete! function to each result generated by g:students/programme.
  (define (delete-students!)
    (g:for-each delete! (g:students/programme "Mathematics")))
  
  ;; print-students : -> void
  ;;
  ;; This procedure uses conventional for-each to iterate through the results
  ;; from find-students. We could use a generator and g:map or g:for-each to
  ;; do a similar sort of thing.
  (define (print-students)
    (printf "Printing students:~n")
    (for-each (lambda (student)
                (printf "   ~a~n" student))
              (find-students)))

  )