8.18
top
← prev up next →

SQLSourceryπŸ”— i

Adrian Kant and Taylor Murphy

Database backed structs for functional programmers. A SQLSourcery programmer is a sourcerer.

Github Repo

1MotivationπŸ”— i

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.

2Database ConnectionπŸ”— i

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?
Creates a connection to a SQLite database at the given path location to be used for all SQLSourcery operations until the database is changed. Ensure tables exist for all previously defined structures in the program.

Example:
(sourcery-db "spells.db")

3Sourcery StructuresπŸ”— i

3.1Structure DefinitionπŸ”— i

A sourcery-struct acts as a typical structure with added database persistence.

syntax

( sourcery-struct struct-name
[(field-nametype)(field-nametype)...])
struct-name : id?
field-name : id?
type : acceptable-struct-type?
type = STRING
| INTEGER
| BOOLEAN

Example:
(sourcery-struct spell[(nameSTRING)(powerINTEGER)(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:

procedure

( struct-name?x)boolean?

x:any?
A predicate for a structure. Will return true only when its argument is of the type struct-name.

procedure

( struct-name-createfield...)struct-name?

field:any?
Creates a new instance of the structure in both the program and the database. The number of arguments and the type of each argument must match the sourcery-struct definition.

Example:
(spell-create"SummonBees"100#true)
Result:

(spell"Summon Bees"100#true)

procedure

( struct-name-updateref-structfield...)struct-name?

ref-struct:struct-name?
field:any?
Updates the given structure in the database to have the given field values and return the newly created structure in the program. All references to the given structure will also be changed.

Example:
(definebees(spell-create"Summon Bees"100#true))
(definebees-new(spell-updatebees"Summon Bees"200#true))

> (spell-powerbees-new)

200

> (spell-powerbees)

200

Here both bees and new-bees reference the same spell.

procedure

( struct-name-fields)any?

s:struct-name?
Creates accessors for each field are generated as the racket struct construct does.

Examples:
> (spell-namebees)

"Summon Bees"

> (spell-powerbees)

200

> (spell-deadly?bees)

#t

3.2Loading StructuresπŸ”— i

procedure

( sourcery-load struct-name)(listofstruct-name)

struct-name:id?
Return a list of all of the structures currently in existence in the current database.

Example:
(spell-create"Expecto"10#false)
(spell-create"Patronum"100#true)
(sourcery-load spell)

Result:

(list(spell"Expecto"10#false)(spell"Patronum"100#true))

3.3Deleting StructuresπŸ”— i

procedure

( sourcery-delete sourcery-struct)void

sourcery-struct:sourcery-struct?
Delete the given sourcery-struct from the database. Any existing references to the sourcery-struct will become dead references.

Example:
(definebees(spell-create"Summon Bees"100#true))

Attempting to operate on bees will result in an error.

Attempting to display bees will result in:

(spell'dead-reference)

procedure

( sourcery-filter-delete predicaterefs)

(list-ofsourcery-struct )
predicate:(->sourcery-struct?boolean?)
refs:(list-ofsourcery-struct )
Return all sourcery-struct’s which match the predicate and sourcery-delete the structures that fail the predicate.

Example:
(spell-create"Expecto"10#false)
(spell-create"Patronum"100#true)
(sourcery-filter-delete (λ(s)(spell-deadly?s))(sourcery-load spell))

Result:

(list(spell"Patronum"100#true))

The example above deletes all spells that are not deadly.

4TestingπŸ”— i

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.1Testing PhilosophyπŸ”— i

There are a few main concepts that come with SQLSourcery’s testing library:

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.2Testing VariablesπŸ”— i

syntax

( declare-test-vars [var-idid?])

Define the given ids to be test variables with the initial value of #f using the racket define.

Example:
> a

#f

> b

#f

> c

#f

procedure

( set-test-var! var-idvalue)void

var-id:id?
value:any?
Set the given test varaible to the given value. Will error if the given id is not a test varaible.

Example:
> (defined#f)
> a

#f

> b

#f

> c

#f

> d

#f

> a

1

set-test-var!: Invalid test variable: d

procedure

( clear-test-vars! var-id...)void

var-id:id?
Set the given test variable ids to #false. Error if given an invalid test id. If error occurs, all ids listed before the invalid id will be set.

Example:
> a

2

> b

3

> c

3

> a

#f

> b

#f

> c

3

4.3Testing ActionsπŸ”— i

procedure

( action expr)thunk?

expr:...
Create an action (thunk) that runs all expressions inside a begin and then returns void.

Example using set!:
> (definex1)
> ((action (set!x2)))
> x

2

Example using test variables:
> a

#f

> ((action (set-test-var! a3)))
> a

3

syntax

( define-action [nameid?][exprany?]...)

Define an action with the given name.

Shortcut for:

(definename(action expr...))

Example using set!:
> (definex1)
> (define-action action-1(set!x2))
> (action-1)
> x

2

Example using test variables:
> a

3

> (define-action action-2(set-test-var! a4))
> (action-2)
> a

4

procedure

( action-compose action...)thunk?

action:thunk?
Create a single action that executes the given actions from left to right.

Example:
> (define-action v-action-1(set-test-var! v11))
> (define-action v-action-2(set-test-var! v22))
> (define-action v-action-3(set-test-var! v13))
> (definev-action-4(action-compose v-action-1v-action-2v-action-3))
> v1

#f

> v2

#f

> (v-action-4)
> v1

3

> v2

2

syntax

( define-composed-action [nameid?][[actionaction?]...])

Define an action with the given name by composing the given actions.

:Example
> (define-composed-action v-action-4-easier[v-action-1v-action-2v-action-3])
> v1

#f

> v2

#f

> (v-action-4-easier)
> v1

3

> v2

2

4.4Testing CleanupπŸ”— i

procedure

( clear-sourcery-structs struct-name...)void

struct-name:struct-name?
sourcery-delete all existing sourcery-structs of the type of the given names. Will possibly create dead references.

:Example
> (definealakazam(spell-create"Alakazam"100#false))
> (sourcery-load spell)

'()

Accessesing alakazam will result in:

(spell'dead-reference)

4.5Sourcery Test SuitesπŸ”— i

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?
Acts the same as test-suite except that it will add the suite to the global tests.

Example:
"A sourcery-test-suite using fictional actions"
#:beforesu-create-all
#:aftertd-complete
(check-equal? (spell-name(firstsourcery-load-results))"Summon Bees")
(check-equal? (spell-name(secondsourcery-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?
Set the database using sourcery-db to the given test-db-path and run all of the sourcery-test-suites defined before the call using run-tests , setting the database using sourcery-db to the given end-db-path after completion.

5Programming with SQLSourceryπŸ”— i

5.1SQLSourcery, Side Effects, and MutationπŸ”— i

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.2Under The HoodπŸ”— i

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.3Dead ReferencesπŸ”— i

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.

top
← prev up next →

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /