Appearance
@polyspec/fsm-db
The DB store of fsm and its command line, fsm-db. A fsm project can keep its definitions and its lock in six tables of an orm database instead of files (docs/spec/fsm.md, B-2; docs/plans/fsm-v1.md, C-6). The package is private and is not published: it is used from this repository.
What it is
- The store (
src/db-store.mjs) converts the rows of the six tables into the definitions and the lock that the core of fsm checks and writes. The core validates; the store only converts and writes, in one transaction. fsm-dbruns the core commands against this store:install,check,lockandcopy.- A project uses one store, named in its
fsm.config.json:json,mermaid(the file stores offsm-gen) ordb.fsm-gendoes not handledb;fsm-dbhandles onlydb.
Set up
Install the packages from the repository root with
make db-deps. It installs@polyspec/ormv0.0.18 and@polyspec/ordered-jsonv0.0.9 from their release tarballs, pinned inpackage-lock.json.Create
fsm.config.jsonin the project root. The only accepted form for the database is:json{"store":"db","dsnEnv":"FSM_DSN"}dsnEnvnames the environment variable that holds the DSN. The DSN itself is never written in the file.Set that variable to the DSN of the database (see DSN).
Put the command
fsm-dbon the PATH:npm install -g ./packages/dbfrom the repository root. npm links the package of the clone, so the command runs the code of the clone.Create the six tables:
fsm-db install.
--project <dir> names the project root, which defaults to the current directory.
The six tables
The tables are the document set of schema/fsm.dbs (dbspec fsm). The first four hold the definitions, the last two the lock.
| table | columns | keys |
|---|---|---|
machine | name | PK(name) |
state | machine, key, code, position, initial, final | PK(machine, key); unique(machine, code); unique(machine, position); FK machine |
event | machine, key, position | PK(machine, key); FK machine |
edge | machine, event, from, to, position | PK(machine, event, from); FK to event; FK from and to to state |
lock_code | machine, code, key | PK(machine, code) |
lock_reserved | machine, code | PK(machine, code) |
The lock tables have no foreign key to machine, so the lock of a machine that was removed from the definitions stays. orm also creates its own table orm__row_lock in the database (observed with SQLite); fsm-db does not read or write it.
The db store has no committed record, so its baseline is the stored lock rows that load reads before a command writes them (docs/spec/fsm.md, B-3). A change that a command could write is refused by the stored-lock rules (E_CODE_REUSED, E_CODE_KEY_CHANGED, E_CODE_CHANGED), so E_LOCK_REGRESSED does not arise through a command here. Rows changed or deleted outside fsm (in SQL) leave no record that a command could compare, so they are not detected.
DSN
The DSN is the value of the environment variable that dsnEnv names. fsm-db reads no DSN from any other place. The DSN forms that orm v0.0.14 documents (docs/config.md of the orm repository) are:
text
mysql://user:password@host:3306/orm_example
mysql://user@localhost/orm_example?socket=/tmp/mysql.sock
postgres://user:password@host:5432/orm_example?sslmode=disable
postgres:///orm_example?host=/tmp
sqlite:///var/lib/orm_example.sqlite?_pragma=busy_timeout(5000)- The scheme selects MySQL, PostgreSQL or SQLite.
- A SQLite path must be absolute:
sqlite://followed by the absolute path, sosqlite:///var/...for/var/.... A relative path is rejected by orm withCONFIG; for exampleFSM_DSN=sqlite://fsm.sqliteprints[fsm-db] OrmError: CONFIG: sqlite DSN must include an absolute database path. - A SQLite connection waits up to 5000 milliseconds for a lock that another connection holds; the parameter
_pragma=busy_timeout(ms)changes that wait. - The tests of this package run on SQLite (
make db-test). Its store tests (db-store.test.mjsandstore-contract-all.test.mjs) also run on PostgreSQL and MySQL:make db-test-postgresandmake db-test-mysqlread the DSN fromFSM_TEST_PG_DSNandFSM_TEST_MYSQL_DSN, and fail when it is not set. The CLI and command tests run on the same dialects: each case gets an empty database, which on PostgreSQL and MySQL the admin DSN ofFSM_TEST_PG_ADMIN_DSNorFSM_TEST_MYSQL_ADMIN_DSNcreates and the file drops (plan C-6).
sh
export FSM_DSN=sqlite:///var/lib/fsm/order.sqliteWhen the variable is not set, every command stops before it touches a database:
sh
$ fsm-db check --project ./order-machines
[fsm-db] E_STORE_NOT_CONFIGURED - $.dsnEnv: the environment variable FSM_DSN is not setCommands
Every command takes --project <dir>. An E_... finding prints as [fsm-db] <code> <machine> <path>: <message> on stderr (- in place of a missing machine).
install
Installs the six tables, and the table of orm itself, through orm utils().schema().install. The behaviour, as observed with orm v0.0.14 and tested in tests/cli.test.mjs, depends on the database:
- No table exists: the tables are created and the command prints
[fsm-db] install: ok, exit 0. - All six tables exist: nothing is changed, the command prints the same line, exit 0. Running
installagain is safe. - Only some of the tables exist: orm rejects the install with
CONFIG. The command prints the error with its stack, and exits 1. Nothing is repaired automatically: there is no fallback.
sh
$ fsm-db install --project ./order-machines
[fsm-db] install: okcheck
Checks the stored definitions and lock with the core rules of docs/spec/fsm.md (B-5). On a database without the tables the command fails with orm's error, printed with its stack, and exits 1.
sh
$ fsm-db check --project ./order-machines
[fsm-db] check: okOn a database that has no table yet, check fails:
sh
$ fsm-db check --project ./new-machines
[fsm-db] OrmError: DRIVER: sqlite: no such table: machine
at ...lock
Runs the core lock command on the database (docs/spec/fsm.md, B-3). --rename machine.old=new renames a state key before the lock is written; the renamed state keeps its code. Names contain no . and no =. The lock is judged by the stored rows (see the tables section): a database has no commit, so E_LOCK_REGRESSED does not arise here.
sh
$ fsm-db lock --project ./order-machines --rename order.draft=created
[fsm-db] lock: okA rename of an unknown state is a finding:
sh
$ fsm-db lock --project ./order-machines --rename order.nope=x
[fsm-db] E_UNKNOWN_STATE_REF order $.states: state nope is not a state of ordercopy
Moves the definitions and the lock between the database and a file store. One of --to or --from names the file store, json or mermaid; --root names its directory.
copy --to json|mermaid --root <dir>: the database is the source,<dir>receives the files.copy --from json|mermaid --root <dir>: the database is the target. It must be empty (no rows in the six tables); a non-empty target fails withE_STORE_NOT_EMPTYand writes nothing.
sh
$ fsm-db copy --from json --root ./order-machines --project ./order-machines
[fsm-db] copy: ok
$ fsm-db copy --from json --root ./order-machines --project ./order-machines
[fsm-db] E_STORE_NOT_EMPTY - $: the target store is not empty, so copy writes nothing
$ fsm-db copy --to json --root ./exported --project ./order-machines
[fsm-db] copy: ok
$ fsm-db copy --to mermaid --root ./exported-mmd --project ./order-machines
[fsm-db] copy: okExit codes
| code | meaning |
|---|---|
| 0 | the command succeeded; stdout holds [fsm-db] <command>: ok |
| 1 | a finding (on stderr), or an unexpected rejection of orm or of the store, printed with its stack |
| 2 | a usage error: an unknown command, an option that the command does not take, or a missing or invalid value; the usage lines are on stderr |
A copy without --root, or without --to or --from, is a usage error. install takes only --project.
Moving an existing json or mermaid project to the database
fsm-db does not change fsm.config.json for you; the change is yours to make after the copy is checked.
- Check the file project:
fsm-gen check --project ./order-machinesprints[fsm-gen] check: ok. Commit the project, so the files stay in history. - Change
fsm.config.jsonto{"store":"db","dsnEnv":"FSM_DSN"}and setFSM_DSN. - Create the tables:
fsm-db install --project ./order-machines. - Copy the files into the database:
fsm-db copy --from json --root ./order-machines --project ./order-machines(usemermaidfor a*.fsm.mmdproject). The target must be empty; a second copy fails withE_STORE_NOT_EMPTY. - Check the database:
fsm-db check --project ./order-machines. - Remove the
machines/files andfsm.lock.jsonwhen the database is the source of truth. Until then they are not read.
While the configuration names db, fsm-gen refuses the project with E_STORE_NOT_CONFIGURED. To generate code from a database, export it into a directory whose fsm.config.json names the file store, then run fsm-gen there:
sh
$ fsm-db copy --to json --root ./export --project ./order-machines
[fsm-db] copy: ok
$ echo '{"store":"json"}' > ./export/fsm.config.json
$ fsm-gen check --project ./export
[fsm-gen] check: okChecks
make db-test runs the tests of this package through kit's test runner (run-tests of @polyspec/kit); it needs make db-deps first. Everything runs on SQLite; the store, CLI and command tests also run on PostgreSQL and MySQL (make db-test-postgres, make db-test-mysql) with the DSNs of FSM_TEST_PG_DSN and FSM_TEST_MYSQL_DSN and the admin DSNs of FSM_TEST_PG_ADMIN_DSN and FSM_TEST_MYSQL_ADMIN_DSN, which create the empty databases of the CLI tests.
tests/db-store.test.mjschecks the store: json → db → json and db → json → db give the same files and rows, a 64-character key round-trips unchanged, a failed write rolls back every row that it deleted and wrote,copyinto a non-empty database isE_STORE_NOT_EMPTYand writes nothing, and the generated models are the ones that orm-gen writes from the schema.tests/cli.test.mjsrunsfsm-dbas a process: the usage errors (exit 2), a missing configuration and a DSN variable that is not set (E_STORE_NOT_CONFIGURED),copyin both directions,lockrepairing a stored lock, andinstallon a new database, on an installed one and on a partly installed one.tests/command.test.mjsinstalls the package offline into a temporary npm prefix and runsfsm-dbfrom there, so the command that is tested is the installed one.
The core rules that the store relies on are tested in the compiler (make compiler-test), and the store contract is shared with the json and mermaid stores there.