kysely icon indicating copy to clipboard operation
kysely copied to clipboard

sqlite introspector missing generated/computed columns

Open zihaolam opened this issue 8 months ago • 1 comments

Hi 👋,

I've been using kysely for production at work. We're using kysely-codegen to generate type definitions. One thing that I've noticed is that generated/computed columns are excluded from introspection due to SqliteIntrospector#getTableMetadata method using pragma_table_info, which excludes hidden columns.

    const tableMetadata = await this.#db
      .with('table_list', (qb) => this.#tablesQuery(qb, options))
      .selectFrom([
        'table_list as tl',
        sql<PragmaTableInfo>`pragma_table_info(tl.name)`.as('p'),
      ])
      .select([
        'tl.name as table',
        'p.cid',
        'p.name',
        'p.type',
        'p.notnull',
        'p.dflt_value',
        'p.pk',
      ])
      .orderBy(['tl.name', 'p.cid'])
      .execute()

Instead of pragma_table_info, if pragma_table_xinfo was used, then we can filter by the hidden column where p.hidden <> 1.

pragma_table_xinfo: The output has the same columns as for PRAGMA table_info plus a column, "hidden", whose value signifies a normal column (0), a dynamic or stored generated column (2 or 3), or a hidden column in a virtual table (1).

zihaolam avatar Mar 21 '25 15:03 zihaolam

For those encountering this issue, here is a solution based on @zihaolam's approach.

Create a custom introspector that is the SqliteIntrospector with the following changes:

- // https://www.sqlite.org/pragma.html#pragma_table_info
- interface PragmaTableInfo {
+ // https://www.sqlite.org/pragma.html#pragma_table_xinfo
+ interface PragmaTableXInfo {
  cid: number;
  dflt_value: unknown;
  name: string;
  notnull: 0 | 1;
  pk: number;
  type: string;
+ hidden: number;
}

// ...

const tableMetadata = await this.#db
  .with('table_list', qb => this._tablesQuery(qb, options))
  .selectFrom([
    'table_list as tl',
-   sql<PragmaTableInfo>`pragma_table_info(tl.name)`.as('p'),
+   sql<PragmaTableXInfo>`pragma_table_xinfo(tl.name)`.as('p'),
  ])
  .select([
    'tl.name as table',
    'p.cid',
    'p.name',
    'p.type',
    'p.notnull',
    'p.dflt_value',
    'p.pk',
+   'p.hidden',
  ])
  .orderBy('tl.name')
  .orderBy('p.cid')
  .execute();

// ...

+ const GENERATED_COL_HIDDEN_TYPE = 2;

return {
  name: name,
  isView: type === 'view',
  columns: columns.map(col => ({
    name: col.name,
    dataType: col.type,
-   isNullable: !col.notnull,    
+   isNullable: !col.notnull && col.hidden !== GENERATED_COL_HIDDEN_TYPE,
    isAutoIncrementing: col.name === autoIncrementCol,
    hasDefaultValue: col.dflt_value != null,
    comment: undefined,
  })),
};

To properly type generated columns as read-only, use ColumnType<..., never, never>. If you're using kysely-codegen, this can be done with the overrides config option.

Although it's a small patch, it would be very neat if Kysley would do this out of the box. Generated columns are awesome, first-class support would be great. They might need a new internal representation because isNullable and hasDefaultValue might not apply the same way. A flag like isGenerated would allow type generators like kysely-codegen to generate the proper read-only type. (Just a suggestion, I'm not familiar with the internals of this library.)

petdomaa100 avatar Nov 12 '25 05:11 petdomaa100