Tracking Sqlite Changes

grafanasink wants to track changes in the grafana sqlite database as they're being written.

I see two options: an outside observer trying to track changes, by comparing them with a known good data source, or sqlite somehow tracking inserts and updates itself.

Can Go Register A Callback?

go-sqlite3 lets you provide a callback function when a database mutation happens.

It didn't work for my needs. Following the code is tricky as it crosses the C boundary, but any mutations happening outside of the current connection didn't trigger the hook. The behaviour sounds like it wraps Sqlite's Update Hook.

This quote from the update_hook documentation seems fairly damning for this approach:

The sqlite3_update_hook() interface registers a callback function with the database connection identified by the first argument

Whomp whomp.

Plan B: sqlite somehow tracking inserts and updates itself

The truth is that I don't know much about sqlite, but the docs are pretty great and showed it shouldn't be bad to create a trigger that'll keep track of changes in the grafana database table.

Plan: start showing I know how to use a trigger. Then, see if the trigger can automatically insert some data from the original insert, so ultimately I can add a trigger to Grafana's sqlite db that will track the name of an updated dashboard and the content of the update.

Start Simple


sqlite> CREATE TABLE inputs(input text);
sqlite> CREATE TABLE triggered(tracking text);
sqlite> CREATE TRIGGER inputtrigger AFTER INSERT ON inputs BEGIN INSERT INTO triggered VALUES("trig"); END;
sqlite> INSERT INTO inputs VALUES("hi friend");
sqlite> select * from triggered;
trig
sqlite> 
                    

Insert Some Info From The Original Query

I realised at this point that I'm building an audit table. Once I put the name to the concept I found really good prior art" to guide me.

The mask field in there isn't something I need while I'm toying around, but looks valuable for when I implement this against grafana itself.


sqlite> CREATE TABLE _inputs_history ( _rowid INTEGER, id INTEGER, input TEXT, _version INTEGER, _updated INTEGER);
sqlite> CREATE TRIGGER input_insert_history
   ...> AFTER INSERT ON inputs
   ...> BEGIN 
   ...> INSERT INTO _inputs_history(_rowid, id, input, _version, _updated) 
   ...> VALUES (new.rowid, new.id, new.input, 1, unixepoch('subsec'));
   ...> END;
sqlite> INSERT INTO inputs(input) VALUES("hi friend");
Parse error: no such column: new.id

The first bug! I had thought I got a free ID column, but the Primary Key docs show that I have to explicitly request one with the PRIMARY KEY keywords.

I can't add a primary key after the fact to my inputs table; it's not supported. Time to delete and recreate:


sqlite> DROP TABLE inputs;
sqlite> CREATE TABLE inputs(
(x1...> id INTEGER PRIMARY KEY,
(x1...> input TEXT
(x1...> );
sqlite> INSERT INTO inputs(input) VALUES("hi friend");
sqlite> select * from _inputs_history;
//nothing here

Second learning, this one from the docsTriggers are automatically dropped when the table that they are associated with (the table-name table) is dropped.


sqlite> CREATE TRIGGER input_insert_history AFTER INSERT ON inputs BEGIN INSERT INTO _inputs_history(_rowid, id, input, _version, _updated)  VALUES (new.rowid, new.id, new.input, 1, unixepoch('subsec')); END;
sqlite> INSERT INTO inputs(input) VALUES("hi friend");
sqlite> select * from _inputs_history;
2|2|hi friend|1|1728051238.601

Okay! Enough for the proof of concept stuff. I can move on to investigating the grafana db now. That'll be for next time in this project.