SQLSourcery
Database backed structs for functional programmers. A SQLSourcery programmer is a sourcerer.
(require sql-sourcery) | package: sql-sourcery |
1 Motivation
Racket programs require lots of boilerplate for any sort of basic I/O persistency, and getting this I/O to fit into a functional style causes many additional problems. SQLSourcery attempts to allow a sourcerer to easily spin up state persistency of their structures that can fit into their coding paradigms with minimal adaptation.
2 Database Connection
SQLSourcery programs must first connect to a SQLite database. It is typical to set the database at the top of a file or module. Test modules typically use a different database than programs. When a database is changed, all loaded sourcery structures currently in existence are subject to error. Any operations besides sourcery structure declarations will throw an error if a database is not set.
procedure
(sourcery-db db-file-path) → void
db-file-path : string?
(sourcery-db "spells.db")
3 Sourcery Structures
3.1 Structure Definition
A sourcery-struct acts as a typical structure with added database persistence.
syntax
(sourcery-struct struct-name [(field-name type) (field-name type) ...])
struct-name : id?
field-name : id?
type : acceptable-struct-type?
type | = | STRING | ||
| | INTEGER | |||
| | BOOLEAN |
(sourcery-struct spell [(name STRING) (power INTEGER) (deadly? BOOLEAN)])
This will create a new structure source or connect to an existing structure source with an identical definition. If an existing sourcery-struct deinition with the same name already exists in the database but the definition is not identical, an error will be raised.
No field name identifier can start with the characters "__", nor can they be one of the following reserved fields:
update
create
map
unmapped
A sourcery-struct definition will create the following functions:
(spell-create "Summon Bees" 100 #true)
(spell "Summon Bees" 100 #true)
procedure
ref-struct : struct-name? field : any?
(define bees (spell-create "Summon Bees" 100 #true)) (define bees-new (spell-update bees "Summon Bees" 200 #true))
> (spell-power bees-new) 200
> (spell-power bees) 200
Here both bees and new-bees reference the same spell.
> (spell-name bees) "Summon Bees"
> (spell-power bees) 200
> (spell-deadly? bees) #t
3.2 Loading Structures
procedure
(sourcery-load struct-name) → (listof struct-name)
struct-name : id?
(spell-create "Expecto" 10 #false) (spell-create "Patronum" 100 #true) (sourcery-load spell)
(list (spell "Expecto" 10 #false) (spell "Patronum" 100 #true))
3.3 Deleting Structures
procedure
(sourcery-delete sourcery-struct) → void
sourcery-struct : sourcery-struct?
(define bees (spell-create "Summon Bees" 100 #true)) (sourcery-delete bees)
Attempting to operate on bees will result in an error.
(spell 'dead-reference)
procedure
(sourcery-filter-delete predicate refs)
→ (list-of sourcery-struct) predicate : (-> sourcery-struct? boolean?) refs : (list-of sourcery-struct)
(spell-create "Expecto" 10 #false) (spell-create "Patronum" 100 #true) (sourcery-filter-delete (λ (s) (spell-deadly? s)) (sourcery-load spell))
(list (spell "Patronum" 100 #true))
The example above deletes all spells that are not deadly.
4 Testing
SQLSourcery programs must test mutation and be able to easily write setup and teardown. While possible with racket, the library can be combersome to use. SQLSourcery comes with a testing library.
4.1 Testing Philosophy
Sourcery Test Suites uses the racket test-suite and adds the tests to a module level global lists of tests that can be run with a single command. At its core, these are most useful for their before and after clauses, which take in actions.
In order to take advantage of before and after clauses in test suites, varaibles must be avaiable in the before, during, and after stages, as well as being able to be modified. While this can be done with set!, the testing library allows easy declaration, setting, and clearing of testing varaibles through a simple API that clearly signifies their status for testing and will prevent accidental mutation of variables not used in testing.
Before and after clauses in test-suite are thunks that return void. To simplify writing these thunks, the testing library introduces the concept of actions that can perform multiple operations at once and can be composed together in an intuitive order for the context of testing.
Testing will use the currently set sourcery-db. It is customary to test by using a test module and using a testing database via a call to sourcery-db at the start of the module
4.2 Testing Variables
syntax
(declare-test-vars [var-id id?])
> (declare-test-vars a b c) > a #f
> b #f
> c #f
procedure
(set-test-var! var-id value) → void
var-id : id? value : any?
> (define d #f) > a #f
> b #f
> c #f
> d #f
> (set-test-var! a 1) > a 1
> (set-test-var! d 1) set-test-var!: Invalid test variable: d
procedure
(clear-test-vars! var-id ...) → void
var-id : id?
> (set-test-var! a 2) > (set-test-var! b 3) > (set-test-var! c 3) > a 2
> b 3
> c 3
> (clear-test-vars! a b) > a #f
> b #f
> c 3
4.3 Testing Actions
procedure
(action expr) → thunk?
expr : ...
> (define x 1) > ((action (set! x 2))) > x 2
> a #f
> ((action (set-test-var! a 3))) > a 3
syntax
(define-action [name id?] [expr any?] ...)
(define name (action expr ...))
> (define x 1) > (define-action action-1 (set! x 2)) > (action-1) > x 2
> a 3
> (define-action action-2 (set-test-var! a 4)) > (action-2) > a 4
procedure
(action-compose action ...) → thunk?
action : thunk?
> (declare-test-vars v1 v2) > (define-action v-action-1 (set-test-var! v1 1)) > (define-action v-action-2 (set-test-var! v2 2)) > (define-action v-action-3 (set-test-var! v1 3)) > (define v-action-4 (action-compose v-action-1 v-action-2 v-action-3)) > v1 #f
> v2 #f
> (v-action-4) > v1 3
> v2 2
syntax
(define-composed-action [name id?] [[action action?] ...])
> (define-composed-action v-action-4-easier [v-action-1 v-action-2 v-action-3]) > v1 #f
> v2 #f
> (v-action-4-easier) > v1 3
> v2 2
4.4 Testing Cleanup
procedure
(clear-sourcery-structs struct-name ...) → void
struct-name : struct-name?
> (define alakazam (spell-create "Alakazam" 100 #false)) > (clear-sourcery-structs spell) > (sourcery-load spell) '()
(spell 'dead-reference)
4.5 Sourcery Test Suites
procedure
(sourcery-test-suite name-expr maybe-before maybe-after test ...) → void name-expr : string? maybe-before : before-action maybe-after : after-action test : test?
(sourcery-test-suite "A sourcery-test-suite using fictional actions" #:before su-create-all #:after td-complete (check-equal? (spell-name (first sourcery-load-results)) "Summon Bees") (check-equal? (spell-name (second sourcery-load-results)) "Create Pig Tail On A Dursley"))
procedure
(run-sourcery-tests test-db-path end-db-path) → void test-db-path : string? end-db-path : string?
5 Programming with SQLSourcery
5.1 SQLSourcery, Side Effects, and Mutation
By nature, SQLSourcery is at odds with one of the often noted rules and advantages of functional programming - that mutation and side effects should be avoided when possible.
However, this does not mean that programmers should be forced to change the way they write their functional code, only that a few additional considerations must be made in order to get the advantages of database backing. This section is dedicated to explaining how programmers can think about these issues while still writing idiomatic code.
5.2 Under The Hood
At its core, SQLSourcery simply translates all operations with structures into SQL queries. It then introduces unique numeric idenifiers that are tied to each structure. Instances of structures are in fact simply references to these identifiers. Programming with sourcery structures is thus working with references, not values. While SQLSourcery allows for multiple references to the same value, it is designed so that there is no need use the feature, and one can write fully functional code, which then tracks its state with the connected database.
5.3 Dead References
Since sourcery structures at their core are references, and the values that a reference points to can be modified or deleted, it is then possible to have an invalid or "dead" reference. When sourcery-delete is used, any remaining references then become dead references. If a sourcerer chooses to use multiple references to the same value, it is the responsibility of the sourcerer to keep track of the status of references.