kanel icon indicating copy to clipboard operation
kanel copied to clipboard

Support Knex enums (CHECK constraints)

Open AudunWA opened this issue 1 year ago • 4 comments

When building tables using the enu function in Knex, the default behaviour is to add a CHECK constraint on the column instead of creating a pg enum. Example:

await knex.schema.table("foo", (tableBuilder) => {
    tableBuilder.enu("bar", ["a", "b", "c"]).defaultTo("a").notNullable();
});
create table public.foo
(
    bar    text                     default 'a'::text     not null
        constraint foo_bar_check
            check (extracted_by = ANY (ARRAY ['a'::text, 'b'::text, 'c'::text]))
);

Ideally, kanel should be able to generate

interface Foo {
    bar: "a" | "b" | "c";
}

from this table definition.

AudunWA avatar Jan 01 '24 19:01 AudunWA

Aha, interesting. This would have to rely on a heuristic though, which isn't really how anything works in Kanel (at the moment). I think I could enable it as a hook though, I'll look into that!

kristiandupont avatar Jan 02 '24 06:01 kristiandupont

@AudunWA added some basic check support https://github.com/kristiandupont/extract-pg-schema/pull/472

The problem with CHECK clause syntax is that it's extracted "as is" and you have to parse it yourself. I'm planning to contribute to kanel-zod and implement kanel-effect-schema codegen out of CHECK constraints and column comments.

@kristiandupont prefers ramda, and I do prefer fp-ts with parser-ts. Kristian could you please clarify what parser-combinator PEG-parser would be preferable to parse the CHECK constraints ? I'd, personally, go for parzec, but it doesn't really matter for me.

yuriy-yarosh avatar Jan 09 '24 01:01 yuriy-yarosh

@yuriy-yarosh I used peggy for tagged-comment-parser, but that's not a requirement of any kind.

I want to stress that anyone can create "extensions" for Kanel and just publish them on NPM themselves. I am happy to link to them from the documentation. If you want it to be a part of this repository, I would like to retain some level of consistency but if you don't care for Ramda (I personally have lost some affection for it because of the incomplete types), you don't have to use it 😊

kristiandupont avatar Jan 09 '24 07:01 kristiandupont

With @yuriy-yarosh's changes in https://github.com/kristiandupont/extract-pg-schema/pull/472, I was able to generate the types I wanted (although a bit quick and dirty, but not an issue in my case).

Example implementation:

getPropertyMetadata: (property, _details, generateFor) => {
        return {
            typeOverride: getTypeFromCheck(
                _details.name,
                property.name,
                _details.checks
            ),
        };
    }
function getTypeFromCheck(tableName, columnName, checks) {
    const check = checks.find(
        (check) =>
            check.clause.startsWith(`${columnName} = ANY (ARRAY[`) ||
            check.clause.startsWith(`${columnName} = '`)
    );
    if (check == null) {
        return undefined;
    }
    if (check.clause.startsWith(`${columnName} = ANY (ARRAY[`)) {
        const values = check.clause
            .match(/ARRAY\[(.*)]/)[1]
            .split(",")
            .map((it) =>
                it.trim().replaceAll("'", "").replaceAll("::text", "")
            );
        return values.map((it) => `"${it}"`).join(" | ");
    } else if (check.clause.startsWith(`${columnName} = '`)) {
        const value = check.clause.match(/'(.*)'::text/)[1];
        return `"${value}"`;
    } else {
        return undefined;
    }
}

AudunWA avatar Jan 10 '24 08:01 AudunWA