Skip to content

Instantly share code, notes, and snippets.

@ingoogni
Last active January 26, 2025 21:16
Show Gist options
  • Save ingoogni/6601119e3cd82e90389e4efda05d3db5 to your computer and use it in GitHub Desktop.
Save ingoogni/6601119e3cd82e90389e4efda05d3db5 to your computer and use it in GitHub Desktop.
Table that keeps its own history, SQLite
/*
Database schema SQLite for a table that keeps its own history and
never deletes data.
On update, historic data is kept under a new id and references the original
creation id. The end of life timestamp (ts_eol) is set so a time line can be
built. The new data is used to update the data under the existing id.
On delete, nothing is deleted. Only the ts_eol timestamp is set to current.
*/
DROP TABLE IF EXISTS Trigger;
DROP TABLE IF EXISTS Keep;
DROP VIEW IF EXISTS view_Keep;
/*
The table that keeps its own history. ts_eol and hist_id are used to keep history plus
one column with a partial index.
*/
CREATE TABLE IF NOT EXISTS Keep(
id INTEGER PRIMARY KEY NOT NULL,
-- start data block
person TEXT NOT NULL, -- part of partial index pidx_person_eol
asset TEXT NOT NULL,
ts_from TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, -- is not required for keeping history
-- ....
-- ....
-- end datablock
ts_eol TEXT DEFAULT NULL,
hist_id INTEGER DEFAULT NULL REFERENCES Keep(id),
CONSTRAINT id_not_historic CHECK(id <> hist_id)
);
CREATE UNIQUE INDEX pidx_person_eol
ON Keep(person)
WHERE ts_eol IS NULL
;
--CREATE INDEX historic_idx
-- ON Keep(hist_id)
--;
/*
the view to run the updates and deletes against
*/
CREATE VIEW IF NOT EXISTS view_Keep
AS
SELECT id, person, asset, ts_from,ts_eol, hist_id -- *
FROM Keep
WHERE ts_eol IS NULL
;
-- the below has to be created on (writer) connection to the db.
/*
The temp table holds a flag that makes it possible to directy update the Keep table.
When FALSE direct updates are not possible.
*/
CREATE TEMP TABLE IF NOT EXISTS Trigger(
trigger_on TEXT NOT NULL UNIQUE,
state BOOL CHECK(state IN (FALSE, TRUE))
);
INSERT INTO Trigger(trigger_on, state)
VALUES ('insert_keep', FALSE)
;
/*
trigger to prevent updates directly to the Keep table
*/
CREATE TEMP TRIGGER IF NOT EXISTS before_update_Keep
BEFORE UPDATE
ON main.Keep
WHEN (
SELECT state
FROM Trigger
WHERE trigger_on = 'insert_keep'
) = FALSE
BEGIN
SELECT RAISE(FAIL,'ERROR! Cannot update directly to Keep. Update to view_Keep');
END
;
/*
trigger to update the Keep table. The insert_keep flag has to be set
to TRUE before the update or else the before_update_Keep trigger will kick in.
*/
CREATE TEMP TRIGGER IF NOT EXISTS update_view_Keep
INSTEAD OF UPDATE
ON view_Keep
BEGIN
UPDATE Trigger
SET state = TRUE
WHERE trigger_on = 'insert_keep'
;
UPDATE Keep
SET asset = new.asset, ts_from = CURRENT_TIMESTAMP
WHERE person = old.person AND old.ts_eol IS NULL
;
UPDATE Trigger
SET state = FALSE
WHERE trigger_on = 'insert_keep'
;
INSERT INTO Keep (person, asset, hist_id, ts_from, ts_eol)
SELECT old.person, old.asset, old.id, old.ts_from, CURRENT_TIMESTAMP
;
END
;
/*
trigger to prevent deletes directly to the Keep table
*/
CREATE TEMP TRIGGER IF NOT EXISTS before_delete_Keep
BEFORE DELETE
ON main.Keep
BEGIN
SELECT RAISE(FAIL,'ERROR! Cannot delete directly from Keep. Delete from view_Keep');
END
;
/*
trigger to "delete" from the Keep table. The insert_keep flag has to be set
to TRUE before the update or else the before_update_Keep trigger will kick in.
*/
CREATE TEMP TRIGGER IF NOT EXISTS delete_view_Keep
INSTEAD OF DELETE
ON view_Keep
BEGIN
UPDATE Trigger
SET state = TRUE
WHERE trigger_on = 'insert_keep'
;
UPDATE Keep
SET ts_eol = CURRENT_TIMESTAMP
WHERE id = old.id
AND ts_eol IS NULL
AND hist_id IS NULL
;
UPDATE Trigger
SET state = FALSE
WHERE trigger_on = 'insert_keep'
;
END
;
/*
Al die willen te kaap'ren varen
Moeten mannen met baarden zijn
Jan, Pier, Tjores en Corneel
Die hebben baarden, die hebben baarden
Jan, Pier, Tjores en Corneel
Die hebben baarden, zij varen mee
*/
-- writer queries
INSERT INTO Keep(person, asset)
VALUES ('jan', 'baard'), ('pier', 'baard'), ('tjoris', 'baard'), ('corneel', 'baard')
;
UPDATE view_Keep
SET asset = 'glad'
WHERE person = 'corneel'
;
UPDATE view_Keep
SET asset = 'bakkebaard'
WHERE person = 'jan'
;
UPDATE view_Keep
SET asset = 'glad'
WHERE person = 'pier'
;
UPDATE view_Keep
SET asset = 'soul patch'
WHERE person = 'corneel'
;
DELETE FROM view_Keep
WHERE person = 'corneel'
;
-- transaction to reinstate a deleted person
BEGIN TRANSACTION
INSERT INTO Keep (person, asset, hist_id, ts_from, ts_eol)
SELECT person, asset, id, ts_from, CURRENT_TIMESTAMP
FROM KEEP
WHERE person = 'corneel'
AND ts_eol IS NOT NULL
AND hist_id IS NULL
;
UPDATE Trigger
SET state = TRUE
WHERE trigger_on = 'insert_keep'
;
UPDATE Keep
SET ts_eol = NULL, ts_from = CURRENT_TIMESTAMP
WHERE person = 'corneel'
AND ts_eol IS NOT NULL
AND hist_id IS NULL
;
UPDATE Trigger
SET state = FALSE
WHERE trigger_on = 'insert_keep'
;
ROLLBACK;
-- Reader queries
SELECT *
FROM Keep
WHERE person = 'corneel'
-- AND hist_id IS NOT NULL
;
/*history length*/
SELECT person, count(id)
FROM Keep
WHERE person = 'corneel'
;
/*history since*/
SELECT person, count(id), min(ts_from) AS since
FROM Keep
WHERE person = 'corneel'
;
/*sql to get deleted id's*/
SELECT id, person, asset, ts_from, ts_eol
FROM Keep
-- a deleted item / item timeline, has a set ts_eol, but no hist_id.
WHERE ts_eol IS NOT NULL
AND hist_id IS NULL
;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment