├── .github
└── workflows
│ └── deploy.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── docs
├── index.md
└── test.md
├── doctestify.py
├── expected
└── test.out
├── mkdocs.yml
├── pgfsm--0.0.1.sql
├── pgfsm.control
├── sql
└── test.sql
└── test.sh
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: CI & Deploy Docs
2 |
3 | permissions:
4 | contents: write
5 | pages: write
6 |
7 | on:
8 | push:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build-and-deploy:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout repository
18 | uses: actions/checkout@v3
19 |
20 | - name: Run tests & build docs
21 | run: |
22 | chmod +x ./test.sh
23 | ./test.sh
24 |
25 | - name: Deploy to GitHub Pages
26 | uses: peaceiris/actions-gh-pages@v3
27 | with:
28 | github_token: ${{ secrets.GITHUB_TOKEN }}
29 | publish_branch: gh-pages
30 | publish_dir: ./site
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:17
2 |
3 | RUN apt-get update \
4 | && apt-get install -y --no-install-recommends \
5 | mkdocs \
6 | build-essential \
7 | postgresql-server-dev-17 \
8 | && rm -rf /var/lib/apt/lists/*
9 |
10 | ARG HOST_UID
11 | ARG HOST_GID
12 | RUN groupadd -g $HOST_GID hostgroup \
13 | && useradd -u $HOST_UID -g hostgroup hostuser
14 |
15 | USER hostuser
16 | COPY . /usr/src/demo
17 |
18 | USER root
19 | WORKDIR /usr/src/demo
20 | RUN make install
21 |
22 | USER postgres
23 | WORKDIR /
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | EXTENSION = pgfsm
2 | DATA = pgfsm--0.0.1.sql
3 |
4 | OUTS := $(wildcard expected/*.out)
5 | MDS := $(patsubst expected/%.out,docs/%.md,$(OUTS))
6 |
7 | .PHONY: doctest
8 |
9 | doctest: $(MDS)
10 |
11 | docs:
12 | mkdir -p docs
13 |
14 | docs/%.md: expected/%.out | docs
15 | python3 doctestify.py $< $@
16 |
17 | PG_CONFIG = pg_config
18 | PGXS := $(shell $(PG_CONFIG) --pgxs)
19 | REGRESS := $(patsubst sql/%.sql,%,$(wildcard sql/*.sql))
20 | TESTS := $(REGRESS)
21 | include $(PGXS)
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pgfsm
2 | Simple SQL finite state machine for Postgres
3 |
4 | This is some example code on how to store a simple state machine in
5 | SQL.
6 |
7 | There are two tables, fsm.machine and fsm.transition. The machine
8 | table has insert and update triggers to ensure that every row is in a
9 | valid state. The transition table is where transition between states
10 | are defined.
11 |
12 | There is a pgtap test in test.sql that illustrates the technique.
13 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Postgres Finite State Machines
2 |
--------------------------------------------------------------------------------
/docs/test.md:
--------------------------------------------------------------------------------
1 | ``` postgres-console
2 | CREATE EXTENSION IF NOT EXISTS pgfsm;
3 | ```
4 | Insert FSM definition of turnstile.
5 | ``` postgres-console
6 | INSERT INTO transition (name, from_state, transition, to_state)
7 | VALUES
8 | ('turnstile', 'locked', 'coin', 'unlocked'),
9 | ('turnstile', 'unlocked', 'push', 'locked');
10 | ```
11 | Insert FSM definition for door.
12 | ``` postgres-console
13 | INSERT INTO transition (name, from_state, transition, to_state)
14 | VALUES
15 | ('door', 'opened', 'close', 'closing'),
16 | ('door', 'closed', 'open', 'opening'),
17 | ('door', 'opening', 'is_opened', 'opened'),
18 | ('door', 'closing', 'is_closed', 'closed');
19 | ```
20 | insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
21 | ``` postgres-console
22 | INSERT INTO machine (name, state)
23 | VALUES
24 | ('turnstile', 'bar');
25 | ERROR: insert or update on table "machine" violates foreign key constraint "machine_name_state_fkey"
26 | DETAIL: Key (name, state)=(turnstile, bar) is not present in table "transition".
27 | ```
28 | insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
29 | ``` postgres-console
30 | INSERT INTO machine (name, state)
31 | VALUES
32 | ('fork', 'opened');
33 | ERROR: insert or update on table "machine" violates foreign key constraint "machine_name_state_fkey"
34 | DETAIL: Key (name, state)=(fork, opened) is not present in table "transition".
35 | ```
36 | Insert some machines in some valid states.
37 | ``` postgres-console
38 | INSERT INTO machine (id, name, state)
39 | VALUES
40 | (1, 'door', 'opened'),
41 | (2, 'door', 'closed'),
42 | (3, 'turnstile', 'locked'),
43 | (4, 'turnstile', 'unlocked');
44 | ```
45 | door 1 closing
46 | ``` postgres-console
47 | UPDATE machine SET state = 'closing' WHERE id = 1;
48 | ```
49 | door 1 closed
50 | ``` postgres-console
51 | UPDATE machine SET state = 'closed' WHERE id = 1;
52 | ```
53 | door 1 cant go from closed to closing
54 | ``` postgres-console
55 | UPDATE machine SET state = 'closing' WHERE id = 2;
56 | ERROR: Invalid transition closing
57 | CONTEXT: PL/pgSQL function check_valid_state_update() line 4 at RAISE
58 | ```
59 | Door 2 goes from closed to opening
60 | ``` postgres-console
61 | SELECT * FROM do_transition(2, 'open');
62 | id | name | state
63 | ----+------+---------
64 | 2 | door | opening
65 | (1 row)
66 | select state = 'opening' FROM machine WHERE id = 2;
67 | ?column?
68 | ----------
69 | t
70 | (1 row)
71 | ```
72 | Door 2 can go from opening to opened
73 | ``` postgres-console
74 | SELECT * FROM do_transition(2, 'is_opened');
75 | id | name | state
76 | ----+------+--------
77 | 2 | door | opened
78 | (1 row)
79 | select state = 'opened' FROM machine WHERE id = 2;
80 | ?column?
81 | ----------
82 | t
83 | (1 row)
84 | ```
85 | Door 2 cant go from opened to opening
86 | ``` postgres-console
87 | SELECT * FROM do_transition(2, 'is_opened');
88 | ERROR: No valid transition for 2 named is_opened
89 | CONTEXT: PL/pgSQL function do_transition(bigint,text) line 11 at RAISE
90 | select state = 'opened' FROM machine WHERE id = 2;
91 | ?column?
92 | ----------
93 | t
94 | (1 row)
95 | ```
--------------------------------------------------------------------------------
/doctestify.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | def doctestify(test):
4 | '''Convert pg_revert output to markdown.
5 |
6 | Text line blocks that start with `--` are stripped of that prefix
7 | to turn them into markdown. Line blocks that do not start with
8 | `--` are turned into markdown code blocks. A line can be hidden
9 | by putting `-- pragma:hide` somewhere in it, this will omit it
10 | from the output and useful for hiding psql metacommands also
11 | supported by pg_revert.
12 |
13 | >>> print(doctestify("""
14 | ... -- # Header
15 | ... --
16 | ... -- This is a *paragraph*.
17 | ... \pset something -- pragma:hide
18 | ...
19 | ... select version();
20 | ... version
21 | ... ----------------------------------------------------------------------------------------------------------
22 | ... PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
23 | ... (1 row)
24 | ...
25 | ... -- ## Subheader
26 | ... --
27 | ... -- - a list
28 | ... -- - _bold_
29 | ... select print('int32(4:4)'::matrix);
30 | ... ┌────────────────────┐
31 | ... │ print │
32 | ... ├────────────────────┤
33 | ... │ 0 1 2 3 ↵│
34 | ... │ ──────────── ↵│
35 | ... │ 0│ ↵│
36 | ... │ 1│ ↵│
37 | ... │ 2│ ↵│
38 | ... │ 3│ ↵│
39 | ... │ │
40 | ... └────────────────────┘
41 | ... (1 row)
42 | ... """))
43 | # Header
44 |
45 | This is a *paragraph*.
46 | ``` postgres-console
47 | select version();
48 | version
49 | ----------------------------------------------------------------------------------------------------------
50 | PostgreSQL 18devel on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
51 | (1 row)
52 | ```
53 | ## Subheader
54 |
55 | - a list
56 | - _bold_
57 | ``` postgres-console
58 | select print('int32(4:4)'::matrix);
59 | ┌────────────────────┐
60 | │ print │
61 | ├────────────────────┤
62 | │ 0 1 2 3 ↵│
63 | │ ──────────── ↵│
64 | │ 0│ ↵│
65 | │ 1│ ↵│
66 | │ 2│ ↵│
67 | │ 3│ ↵│
68 | │ │
69 | └────────────────────┘
70 | (1 row)
71 | ```
72 | '''
73 | lines = test.splitlines()
74 | markdown_lines = []
75 | code_block = False
76 |
77 | for line in lines:
78 | if not line or line.startswith("\\") or "-- pragma:hide" in line:
79 | continue
80 |
81 | line = line.replace("\\u", "")
82 | if line.startswith("--") and not line.startswith("---"):
83 | if code_block:
84 | markdown_lines.append("```")
85 | code_block = False
86 | markdown_lines.append(line[3:])
87 | else:
88 | if not code_block:
89 | markdown_lines.append("``` postgres-console")
90 | code_block = True
91 | markdown_lines.append(line)
92 |
93 | if code_block:
94 | markdown_lines.append("```")
95 |
96 | return "\n".join(markdown_lines)
97 |
98 | if __name__ == '__main__':
99 | import sys
100 | inpath = Path(sys.argv[1])
101 | infile = open(inpath, 'r')
102 | if len(sys.argv) == 3:
103 | outpath = Path(sys.argv[2])
104 | else:
105 | outpath = Path(*infile.with_suffix('.md'))
106 | outfile = open(outpath, 'w+')
107 | outfile.write(doctestify(infile.read()))
108 |
--------------------------------------------------------------------------------
/expected/test.out:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pgfsm;
2 | -- Insert FSM definition of turnstile.
3 | INSERT INTO transition (name, from_state, transition, to_state)
4 | VALUES
5 | ('turnstile', 'locked', 'coin', 'unlocked'),
6 | ('turnstile', 'unlocked', 'push', 'locked');
7 | -- Insert FSM definition for door.
8 | INSERT INTO transition (name, from_state, transition, to_state)
9 | VALUES
10 | ('door', 'opened', 'close', 'closing'),
11 | ('door', 'closed', 'open', 'opening'),
12 | ('door', 'opening', 'is_opened', 'opened'),
13 | ('door', 'closing', 'is_closed', 'closed');
14 | -- insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
15 | INSERT INTO machine (name, state)
16 | VALUES
17 | ('turnstile', 'bar');
18 | ERROR: insert or update on table "machine" violates foreign key constraint "machine_name_state_fkey"
19 | DETAIL: Key (name, state)=(turnstile, bar) is not present in table "transition".
20 | -- insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
21 | INSERT INTO machine (name, state)
22 | VALUES
23 | ('fork', 'opened');
24 | ERROR: insert or update on table "machine" violates foreign key constraint "machine_name_state_fkey"
25 | DETAIL: Key (name, state)=(fork, opened) is not present in table "transition".
26 | -- Insert some machines in some valid states.
27 | INSERT INTO machine (id, name, state)
28 | VALUES
29 | (1, 'door', 'opened'),
30 | (2, 'door', 'closed'),
31 | (3, 'turnstile', 'locked'),
32 | (4, 'turnstile', 'unlocked');
33 | -- door 1 closing
34 | UPDATE machine SET state = 'closing' WHERE id = 1;
35 | -- door 1 closed
36 | UPDATE machine SET state = 'closed' WHERE id = 1;
37 | -- door 1 cant go from closed to closing
38 | UPDATE machine SET state = 'closing' WHERE id = 2;
39 | ERROR: Invalid transition closing
40 | CONTEXT: PL/pgSQL function check_valid_state_update() line 4 at RAISE
41 | -- Door 2 goes from closed to opening
42 | SELECT * FROM do_transition(2, 'open');
43 | id | name | state
44 | ----+------+---------
45 | 2 | door | opening
46 | (1 row)
47 |
48 | select state = 'opening' FROM machine WHERE id = 2;
49 | ?column?
50 | ----------
51 | t
52 | (1 row)
53 |
54 | -- Door 2 can go from opening to opened
55 | SELECT * FROM do_transition(2, 'is_opened');
56 | id | name | state
57 | ----+------+--------
58 | 2 | door | opened
59 | (1 row)
60 |
61 | select state = 'opened' FROM machine WHERE id = 2;
62 | ?column?
63 | ----------
64 | t
65 | (1 row)
66 |
67 | -- Door 2 cant go from opened to opening
68 | SELECT * FROM do_transition(2, 'is_opened');
69 | ERROR: No valid transition for 2 named is_opened
70 | CONTEXT: PL/pgSQL function do_transition(bigint,text) line 11 at RAISE
71 | select state = 'opened' FROM machine WHERE id = 2;
72 | ?column?
73 | ----------
74 | t
75 | (1 row)
76 |
77 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Postgres Finite State Machines
2 |
3 | nav:
4 | - Home: index.md
5 | - Test: test.md
6 |
7 | theme:
8 | name: mkdocs
9 | features:
10 | - navigation.expand
11 | # - navigation.tabs
12 | # - navigation.sections
13 |
--------------------------------------------------------------------------------
/pgfsm--0.0.1.sql:
--------------------------------------------------------------------------------
1 | \echo Use "CREATE EXTENSION pgfsm" to load this file. \quit
2 |
3 |
4 | CREATE TABLE transition (
5 | name text NOT NULL,
6 | from_state text NOT NULL,
7 | transition text,
8 | to_state text,
9 | PRIMARY KEY (name, from_state)
10 | );
11 |
12 |
13 | CREATE TABLE machine (
14 | id BIGSERIAL PRIMARY KEY,
15 | name text NOT NULL,
16 | state text NOT NULL,
17 | FOREIGN KEY (name, state)
18 | REFERENCES transition (name, from_state)
19 | );
20 |
21 |
22 | CREATE FUNCTION transitions_for(bigint) RETURNS SETOF transition AS $$
23 | SELECT t.* FROM transition t, machine m
24 | WHERE
25 | m.id = $1 AND
26 | t.name = m.name AND
27 | t.from_state = m.state;
28 | $$ LANGUAGE sql;
29 |
30 |
31 | CREATE FUNCTION states_for(text) RETURNS SETOF text AS $$
32 | SELECT from_state FROM transition WHERE name = $1;
33 | $$ LANGUAGE sql;
34 |
35 |
36 | CREATE FUNCTION do_transition(bigint, text) RETURNS SETOF machine AS $$
37 | BEGIN
38 | UPDATE machine m SET state = t.to_state
39 | FROM transition t
40 | WHERE
41 | m.id = $1 AND
42 | m.name = t.name AND
43 | t.from_state = m.state AND
44 | t.transition = $2;
45 | IF NOT FOUND THEN
46 | RAISE EXCEPTION 'No valid transition for % named %', $1, $2;
47 | END IF;
48 | RETURN QUERY SELECT * FROM machine WHERE id = $1;
49 | END;
50 | $$ LANGUAGE plpgsql;
51 |
52 |
53 | CREATE FUNCTION check_valid_state_update() RETURNS trigger AS $$
54 | BEGIN
55 | IF NEW.state NOT IN (SELECT to_state FROM transitions_for(NEW.id)) THEN
56 | RAISE EXCEPTION 'Invalid transition %', NEW.state;
57 | END IF;
58 | RETURN NEW;
59 | END;
60 | $$ LANGUAGE plpgsql;
61 |
62 |
63 | CREATE TRIGGER fsm_machine_check_valid_update_trigger
64 | BEFORE UPDATE OF state ON machine
65 | FOR EACH ROW
66 | EXECUTE PROCEDURE check_valid_state_update();
67 |
68 |
69 | CREATE FUNCTION check_valid_state_insert() RETURNS trigger AS $$
70 | BEGIN
71 | IF NOT EXISTS (SELECT * FROM states_for(NEW.name)) THEN
72 | RAISE EXCEPTION 'Invalid initial state %', NEW.state;
73 | END IF;
74 | RETURN NEW;
75 | END;
76 | $$ LANGUAGE plpgsql;
77 |
78 |
79 | CREATE CONSTRAINT TRIGGER fsm_machine_check_valid_insert_trigger
80 | AFTER INSERT ON machine
81 | FOR EACH ROW
82 | EXECUTE PROCEDURE check_valid_state_insert();
83 |
--------------------------------------------------------------------------------
/pgfsm.control:
--------------------------------------------------------------------------------
1 | # pgfsm extension
2 | comment = 'Finite State Machine enforcement with triggers and util functions.'
3 | default_version = '0.0.1'
4 | relocatable = true
5 |
--------------------------------------------------------------------------------
/sql/test.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pgfsm;
2 |
3 | -- Insert FSM definition of turnstile.
4 | INSERT INTO transition (name, from_state, transition, to_state)
5 | VALUES
6 | ('turnstile', 'locked', 'coin', 'unlocked'),
7 | ('turnstile', 'unlocked', 'push', 'locked');
8 |
9 | -- Insert FSM definition for door.
10 | INSERT INTO transition (name, from_state, transition, to_state)
11 | VALUES
12 | ('door', 'opened', 'close', 'closing'),
13 | ('door', 'closed', 'open', 'opening'),
14 | ('door', 'opening', 'is_opened', 'opened'),
15 | ('door', 'closing', 'is_closed', 'closed');
16 |
17 | -- insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
18 | INSERT INTO machine (name, state)
19 | VALUES
20 | ('turnstile', 'bar');
21 |
22 | -- insert or update on table "machine" violates foreign key constraint "machine_name_fkey"
23 | INSERT INTO machine (name, state)
24 | VALUES
25 | ('fork', 'opened');
26 |
27 | -- Insert some machines in some valid states.
28 | INSERT INTO machine (id, name, state)
29 | VALUES
30 | (1, 'door', 'opened'),
31 | (2, 'door', 'closed'),
32 | (3, 'turnstile', 'locked'),
33 | (4, 'turnstile', 'unlocked');
34 |
35 | -- door 1 closing
36 | UPDATE machine SET state = 'closing' WHERE id = 1;
37 |
38 | -- door 1 closed
39 | UPDATE machine SET state = 'closed' WHERE id = 1;
40 |
41 | -- door 1 cant go from closed to closing
42 | UPDATE machine SET state = 'closing' WHERE id = 2;
43 |
44 | -- Door 2 goes from closed to opening
45 | SELECT * FROM do_transition(2, 'open');
46 | select state = 'opening' FROM machine WHERE id = 2;
47 |
48 | -- Door 2 can go from opening to opened
49 | SELECT * FROM do_transition(2, 'is_opened');
50 |
51 | select state = 'opened' FROM machine WHERE id = 2;
52 |
53 | -- Door 2 cant go from opened to opening
54 | SELECT * FROM do_transition(2, 'is_opened');
55 |
56 | select state = 'opened' FROM machine WHERE id = 2;
57 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | docker rm -f pg-pgfsm-ext
2 | docker build \
3 | --build-arg HOST_UID="$(id -u)" \
4 | --build-arg HOST_GID="$(id -g)" \
5 | -t pg-pgfsm-ext . \
6 | && docker run -d \
7 | --name pg-pgfsm-ext \
8 | -e POSTGRES_PASSWORD=pass123 \
9 | -v "$PWD":/usr/src/pgfsm \
10 | pg-pgfsm-ext \
11 | && docker exec -u "$(id -u):$(id -g)" pg-pgfsm-ext bash -c "\
12 | until pg_isready -U postgres; do sleep 3; done; \
13 | cd /usr/src/pgfsm; \
14 | PGUSER=postgres make installcheck; \
15 | PGUSER=postgres make doctest; \
16 | mkdocs build; \
17 | " \
18 | docker rm -f pg-pgfsm-ext
19 |
--------------------------------------------------------------------------------