Tell HN: SQLite3 does have something like stored procedures
It's not exactly the same, no, but you still get to have DMLs "stored" in the DB. For example, I'm not sure how to return results from an `INSTEAD OF` trigger (that is, if I write `INSERT INTO <view> (...) SELECT ... RETURNING ...;`, what will be returned? Apparently what's returned is the values of the NEW row, while one might think that if the view ends in a `SELECT` then maybe that would be used as the source for the `RETURNING` (but, no).
Even with limitations, `INSTEAD OF` triggers as an approximation of stored procedures is very powerful.
You'd do something like:
CREATE VIEW IF NOT EXISTS things AS ...;
--
-- INSERTs on `things` function like SPs
-- where values furnished for columns
-- function as SP input parameters.
CREATE TRIGGER IF NOT EXISTS create_thing
INSTEAD OF INSERT ON things
FOR EACH ROW
BEGIN
...
END;
and to invoke such a "stored procedure" you'd INSERT INTO things (...) VALUES (...), ...;
or INSERT .. SELECT ..;
This language is Turing-complete. You can also loop via recursive triggers. And you can use `WHERE` clauses (and `OR IGNORE`) on statements in the bodies of `INSTEAD OF` triggers to implement conditionals.Most of this is taken from a comment I just wrote, https://news.ycombinator.com/item?id=31912640 but I've written this sort of thing before elsewhere too, including on the old sqlite3-users mailing list years ago. I'm pretty sure lots of you know about this -- it's really quite obvious.
5 comments
[ 2.8 ms ] story [ 19.5 ms ] threadDo you know of a better way than those select statements (which I presume are slow) to get the IDs?
Second, yeah, `last_insert_rowid()` doesn't work in this case, but what you might do is a) have a timestamp column and/or maybe a transaction ID column as well that you set in the INSERTs in the trigger body, b) maybe have audit/log tables that you insert into in those same trigger bodies, where you might use `last_insert_rowid()`.
Third, I urge you to think about why you care about the inserted row IDs. If you're doing INSERTs w/o `OR IGNORE`, then if your INSERT into the view succeeds then you know you can SELECT to see what IDs you got, then COMMIT. If you're doing `OR IGNORE`, then maybe you don't care whether you got IDs assigned just now or in the past, but you still want to know what IDs those rows have, so you select for them.
Lastly, I'd think about whether you want row IDs at all. I mean, they can be useful. And when it comes to things like "users", you generally want some sort of internal identifier other than the name. Like, if you rename users or roles often (but, you really should not allow that!) then using row IDs in the foreign keys means you don't have to worry about cascading renames. But you might be able to get away w/o role IDs. Also, you might find it better to have a single table for all "numeric ID" assignments, and then you could make your users and roles tables not have an INTEGER PRIMARY KEY, just the name as the PRIMARY KEY and a numeric ID column as a foreign key referencing that one single table.
E.g.,
then in your INSTEAD OF INSERT trigger you'd like this: I think you really want `OR IGNORE` in all cases, as it makes this trigger idempotent.Want to know the numeric IDs assigned to the users and roles possibly created this way? Just SELECT for them!
I see what you mean about `OR IGNORE`. I do worry that this means I'm failing other checks like NOT NULL, but I could handle that in application code.
I really appreciate you taking the time to write all this out for me. While I enjoy it, I don't get to talk to many people about schema design, and I don't have a lot of experience with it. Could you recommend any reading I should do?
Also, PRIMARY KEY implies NOT NULL for the columns that make it up, except that SQLite3 has a bug in it that causes it to allow NULLs in PK columns (sadpanda.png).
> Could you recommend any reading I should do?
First of all, I would start with the O'Reilly pocket book on SQL in general.
Second, I recommend you spend some time reading through SQLite3's docs and wikis. There's a trove of generally-useful stuff there. Pay special attention to everything to do with the WITHOUT ROWID feature. Row IDs can be a crutch that slows things down. That said, sometimes you really want an INTEGER PRIMARY KEY, and in SQLite3 that functions like a row ID.
As for schema design, you should know about normalization. The general advice is to normalize to the max then denormalize till your queries are performant. Much schema design wisdom derives naturally from that. For example, I showed you how to avoid row IDs because they seem unnecessary and they lead to extra duplication. For example, your role_user, if it use row IDs for its references, then queries on it will have to join to the user and role tables to get the names, or, if you end up having both, row IDs and names in it to avoid that, then now you have extra duplication and you'll even need a second set of FOREIGN KEY references in your role_user table to keep those names up to date.
I don't really have a good resource for this kind of knowledge. I've absorbed a lot of it by osmosis over many years, and I've developed some of it on my own.
You could use reading about EAV (entity-attribute-value) schema designs, too. Many love to hate them. I don't love them myself. But EAV schemas trivially lend themselves to writing graph queries. For example, one system I work with has a notion of relationships and, interestingly, of relationship groups that are interesting for graph queries, and it represents relationships as an EAV table, so one can easily write a recursive SQL query that traverses all paths from one entity on one side of the graph to the another (or all reachable ones) on the other side without having to make reference to the names of the relationship types.
I am of the opinion that an RDBMS could provide both, a relational table metaschema, and an EAV equivalent, so that one could write very clear queries against the former, but also very simple and powerful graph queries against the latter. However, I've not seen that anywhere. I want this.
BTW, again, when it comes to security-relevant entities, I highly recommend that you a) not allow renames, b) don't allow deletions -- users can be tombstoned deleted/terminated/separated, but do not allow their names to be reused. The reason is that others will tend to write down usernames, group names, etc., and use those for authorization, and then name reuse can cause you grief. You might allow aliases so that users can get different canonical names if they insist on it, but don't allow their earlier names to be reused.