rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
130 lines (129 loc) • 6.56 kB
JavaScript
// NOTE(justinvdm, 9 Jun 2026): This file copies Kysely's SqliteIntrospector.
// Before trying to simplify it, read this comment.
//
// Problem: Cloudflare's DO SQLite adds internal tables (_cf_KV, _cf_METADATA)
// when storage.put/setAlarm is used. Kysely's SqliteIntrospector discovers
// these via sqlite_master, then runs PRAGMA table_info on each. Cloudflare's
// authorizer rejects PRAGMA on _cf_* tables with SQLITE_AUTH, breaking
// migrations and rendering the DO unusable.
//
// Why we copied instead of composed:
// - Kysely's SqliteIntrospector uses JS private fields (#db, #tablesQuery,
// #getTableMetadata). Private fields are truly private — subclasses cannot
// override them, and wrappers cannot intercept internal calls.
// - We considered a lighter approach: query sqlite_master with Kysely's
// builder, then run PRAGMA table_info per-table. This is lighter but
// requires inlining the table name into raw SQL. Even with escaping,
// any injection risk is unacceptable. The CTE approach below uses
// `pragma_table_info(tl.name)` where `tl.name` is a column reference,
// not an inlined value — zero injection surface.
// - We also considered Kysely plugins (AST rewriting, SQL string rewriting)
// but these are fragile: they depend on Kysely's internal AST shape and
// SQL formatting, both of which can change between releases.
//
// This is a copy of Kysely's SqliteIntrospector (MIT licensed) with one
// change: `.where('name', 'not like', '_cf_%')` to exclude Cloudflare tables.
//
// See: https://github.com/redwoodjs/sdk/issues/1219
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _DOSqliteIntrospector_instances, _DOSqliteIntrospector_db, _DOSqliteIntrospector_tablesQuery, _DOSqliteIntrospector_getTableMetadata;
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql, } from "kysely";
export class DOSqliteIntrospector {
constructor(db) {
_DOSqliteIntrospector_instances.add(this);
_DOSqliteIntrospector_db.set(this, void 0);
__classPrivateFieldSet(this, _DOSqliteIntrospector_db, db, "f");
}
async getSchemas() {
// Sqlite doesn't support schemas.
return [];
}
async getTables(options = { withInternalKyselyTables: false }) {
return await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_getTableMetadata).call(this, options);
}
async getMetadata(options) {
return {
tables: await this.getTables(options),
};
}
}
_DOSqliteIntrospector_db = new WeakMap(), _DOSqliteIntrospector_instances = new WeakSet(), _DOSqliteIntrospector_tablesQuery = function _DOSqliteIntrospector_tablesQuery(qb, options) {
let tablesQuery = qb
.selectFrom("sqlite_master")
.where("type", "in", ["table", "view"])
.where("name", "not like", "sqlite_%")
// context(justinvdm, 9 Jun 2026): Exclude Cloudflare internal tables.
// These are added by the DO runtime and cannot be introspected.
.where("name", "not like", "_cf_%")
.select(["name", "sql", "type"])
.orderBy("name");
if (!options.withInternalKyselyTables) {
tablesQuery = tablesQuery
.where("name", "!=", DEFAULT_MIGRATION_TABLE)
.where("name", "!=", DEFAULT_MIGRATION_LOCK_TABLE);
}
return tablesQuery;
}, _DOSqliteIntrospector_getTableMetadata = async function _DOSqliteIntrospector_getTableMetadata(options) {
const tablesResult = await __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f"), options).execute();
const tableMetadata = await __classPrivateFieldGet(this, _DOSqliteIntrospector_db, "f")
.with("table_list", (qb) => __classPrivateFieldGet(this, _DOSqliteIntrospector_instances, "m", _DOSqliteIntrospector_tablesQuery).call(this, qb, options))
.selectFrom([
"table_list as tl",
sql `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")
.orderBy("p.cid")
.execute();
const columnsByTable = {};
for (const row of tableMetadata) {
columnsByTable[row.table] ??= [];
columnsByTable[row.table].push(row);
}
return tablesResult.map(({ name, sql: tableSql, type }) => {
let autoIncrementCol = tableSql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
const columns = columnsByTable[name] ?? [];
if (!autoIncrementCol) {
const pkCols = columns.filter((r) => r.pk > 0);
if (pkCols.length === 1 &&
pkCols[0].type.toLowerCase() === "integer") {
autoIncrementCol = pkCols[0].name;
}
}
return {
name: name,
isView: type === "view",
columns: columns.map((col) => ({
name: col.name,
dataType: col.type,
isNullable: !col.notnull,
isAutoIncrementing: col.name === autoIncrementCol,
hasDefaultValue: col.dflt_value != null,
comment: undefined,
})),
};
});
};