vlcn-io / typed-sql

MIT License
101 stars 2 forks source link

Add runtime helper functions #4

Closed Azarattum closed 1 year ago

Azarattum commented 1 year ago

This PR introduces 3 new functions that are now available on sql template literal:

For example:

await sql`INSERT INTO ${sql.table("foo")} ${sql.values(["1", 1])}`;
// -> INSERT INTO "foo" VALUES (?,?)

await sql`INSERT INTO ${sql.table("foo")} ${sql.values({a: "1", b: 1})}`;
// -> INSERT INTO "foo" ("a","b") VALUES (?,?)

table and column are the same quote function under the hood. The difference is only in typing. table function will accept any valid table name from the schema as its argument. While column function accepts any column name from any table by default and can be narrowed using a generic.

// For schema: `foo: { a: string; b: number; }, bar: { c: number }`
sql.table(/* accepts "foo" | "bar" */);
sql.column(/* accepts "a" | "b" | "c" */);
sql.column<"foo">(/* accepts "a" | "b" */);

values can accepts arrays or objects as row values. It has variadic parameters to allow for multiple rows.

sql.values(["1", 1], ["2", 2]); // -> VALUES (?,?),(?,?)
sql.values({a: "1", b: 1}, {a: "2", b: 2}); // -> ("a","b") VALUES (?,?),(?,?)

sql.values<"foo">(/* accepts { a: string; b: number; } or (string | number)[] */);

We might generate these generics automatically in the future if this would be feasible to do. For now it is up to the user to specify those.

Azarattum commented 1 year ago

Actually I used prepared statements in the original implementation. The problem I've encountered is that we (as the library) cannot tell what the user intention is. If we eagerly try to prepare everything passed into the template function, we often end up trying to prepare incomplete sql snippets.

So the question comes down to what do we value more flexibility or ease of use? Is there any value in giving user a choice to cache or not to cache prepared statements or is it just objectively better to always cache them? I'll take a look at the article and think a bit more about it. This probably should be a separate PR anyway.

tantaman commented 1 year ago

Actually I used prepared statements in the original implementation. The problem I've encountered is that we (as the library) cannot tell what the user intention is. If we eagerly try to prepare everything passed into the template function, we often end up trying to prepare incomplete sql snippets.

We could also let the user decide when to prepare:

E.g.,

const query = sql`...`;

const prepared = query.prepare();
// or
const prepared = db.prepare(query);

const results = prepared.bind(values).then();

This probably should be a separate PR anyway.

Agreed

Azarattum commented 1 year ago

@tantaman, is this ready to be merged? I've added all the tests here.

tantaman commented 1 year ago

lgtm.

https://media4.giphy.com/media/3o7abB06u9bNzA8lu8/giphy.gif?cid=ecf05e47foarv3ojc52y4qtgxavo717djpp24c1oe4rqcwyg&ep=v1_gifs_search&rid=giphy.gif&ct=g

I'm also getting pretty close to having something workable for SELECT statements.