public commit

This commit is contained in:
dswbx
2024-11-16 12:01:47 +01:00
commit 90f80c4280
582 changed files with 49291 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
import {
type AliasableExpression,
type DatabaseIntrospector,
type Expression,
type Kysely,
type KyselyPlugin,
type RawBuilder,
type SelectQueryBuilder,
type SelectQueryNode,
type Simplify,
sql
} from "kysely";
export type QB = SelectQueryBuilder<any, any, any>;
export type IndexMetadata = {
name: string;
table: string;
isUnique: boolean;
columns: { name: string; order: number }[];
};
export interface ConnectionIntrospector extends DatabaseIntrospector {
getIndices(tbl_name?: string): Promise<IndexMetadata[]>;
}
export interface SelectQueryBuilderExpression<O> extends AliasableExpression<O> {
get isSelectQueryBuilder(): true;
toOperationNode(): SelectQueryNode;
}
export type DbFunctions = {
jsonObjectFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O> | null>;
jsonArrayFrom<O>(expr: SelectQueryBuilderExpression<O>): RawBuilder<Simplify<O>[]>;
jsonBuildObject<O extends Record<string, Expression<unknown>>>(
obj: O
): RawBuilder<
Simplify<{
[K in keyof O]: O[K] extends Expression<infer V> ? V : never;
}>
>;
};
export abstract class Connection {
kysely: Kysely<any>;
constructor(
kysely: Kysely<any>,
public fn: Partial<DbFunctions> = {},
protected plugins: KyselyPlugin[] = []
) {
this.kysely = kysely;
}
getIntrospector(): ConnectionIntrospector {
return this.kysely.introspection as ConnectionIntrospector;
}
supportsBatching(): boolean {
return false;
}
supportsIndices(): boolean {
return false;
}
async ping(): Promise<boolean> {
const res = await sql`SELECT 1`.execute(this.kysely);
return res.rows.length > 0;
}
protected async batch<Queries extends QB[]>(
queries: [...Queries]
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
throw new Error("Batching not supported");
}
async batchQuery<Queries extends QB[]>(
queries: [...Queries]
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
// bypass if no client support
if (!this.supportsBatching()) {
const data: any = [];
for (const q of queries) {
const result = await q.execute();
data.push(result);
}
return data;
}
return await this.batch(queries);
}
}

View File

@@ -0,0 +1,100 @@
import { type Client, type InStatement, createClient } from "@libsql/client/web";
import { LibsqlDialect } from "@libsql/kysely-libsql";
import { type DatabaseIntrospector, Kysely, ParseJSONResultsPlugin, sql } from "kysely";
import { FilterNumericKeysPlugin } from "../plugins/FilterNumericKeysPlugin";
import { KyselyPluginRunner } from "../plugins/KyselyPluginRunner";
import type { QB } from "./Connection";
import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector";
export const LIBSQL_PROTOCOLS = ["wss", "https", "libsql"] as const;
export type LibSqlCredentials = {
url: string;
authToken?: string;
protocol?: (typeof LIBSQL_PROTOCOLS)[number];
};
class CustomLibsqlDialect extends LibsqlDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, {
excludeTables: ["libsql_wasm_func_table"]
});
}
}
export class LibsqlConnection extends SqliteConnection {
private client: Client;
constructor(client: Client);
constructor(credentials: LibSqlCredentials);
constructor(clientOrCredentials: Client | LibSqlCredentials) {
const plugins = [new FilterNumericKeysPlugin(), new ParseJSONResultsPlugin()];
let client: Client;
if ("url" in clientOrCredentials) {
let { url, authToken, protocol } = clientOrCredentials;
if (protocol && LIBSQL_PROTOCOLS.includes(protocol)) {
console.log("changing protocol to", protocol);
const [, rest] = url.split("://");
url = `${protocol}://${rest}`;
}
//console.log("using", url, { protocol });
client = createClient({ url, authToken });
} else {
//console.log("-- client provided");
client = clientOrCredentials;
}
const kysely = new Kysely({
// @ts-expect-error libsql has type issues
dialect: new CustomLibsqlDialect({ client }),
plugins
//log: ["query"],
});
super(kysely, {}, plugins);
this.client = client;
}
override supportsBatching(): boolean {
return true;
}
override supportsIndices(): boolean {
return true;
}
getClient(): Client {
return this.client;
}
protected override async batch<Queries extends QB[]>(
queries: [...Queries]
): Promise<{
[K in keyof Queries]: Awaited<ReturnType<Queries[K]["execute"]>>;
}> {
const stms: InStatement[] = queries.map((q) => {
const compiled = q.compile();
//console.log("compiled", compiled.sql, compiled.parameters);
return {
sql: compiled.sql,
args: compiled.parameters as any[]
};
});
const res = await this.client.batch(stms);
// let it run through plugins
const kyselyPlugins = new KyselyPluginRunner(this.plugins);
const data: any = [];
for (const r of res) {
const rows = await kyselyPlugins.transformResultRows(r.rows);
data.push(rows);
}
//console.log("data", data);
return data;
}
}

View File

@@ -0,0 +1,22 @@
import type { Kysely, KyselyPlugin } from "kysely";
import { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
import { Connection, type DbFunctions } from "./Connection";
export class SqliteConnection extends Connection {
constructor(kysely: Kysely<any>, fn: Partial<DbFunctions> = {}, plugins: KyselyPlugin[] = []) {
super(
kysely,
{
...fn,
jsonArrayFrom,
jsonObjectFrom,
jsonBuildObject
},
plugins
);
}
override supportsIndices(): boolean {
return true;
}
}

View File

@@ -0,0 +1,164 @@
import type {
DatabaseIntrospector,
DatabaseMetadata,
DatabaseMetadataOptions,
ExpressionBuilder,
Kysely,
SchemaMetadata,
TableMetadata,
} from "kysely";
import { DEFAULT_MIGRATION_LOCK_TABLE, DEFAULT_MIGRATION_TABLE, sql } from "kysely";
import type { ConnectionIntrospector, IndexMetadata } from "./Connection";
export type SqliteIntrospectorConfig = {
excludeTables?: string[];
};
export class SqliteIntrospector implements DatabaseIntrospector, ConnectionIntrospector {
readonly #db: Kysely<any>;
readonly _excludeTables: string[] = [];
constructor(db: Kysely<any>, config: SqliteIntrospectorConfig = {}) {
this.#db = db;
this._excludeTables = config.excludeTables ?? [];
}
async getSchemas(): Promise<SchemaMetadata[]> {
// Sqlite doesn't support schemas.
return [];
}
async getIndices(tbl_name?: string): Promise<IndexMetadata[]> {
const indices = await this.#db
.selectFrom("sqlite_master")
.where("type", "=", "index")
.$if(!!tbl_name, (eb) => eb.where("tbl_name", "=", tbl_name))
.select("name")
.$castTo<{ name: string }>()
.execute();
return Promise.all(indices.map(({ name }) => this.#getIndexMetadata(name)));
}
async #getIndexMetadata(index: string): Promise<IndexMetadata> {
const db = this.#db;
// Get the SQL that was used to create the index.
const indexDefinition = await db
.selectFrom("sqlite_master")
.where("name", "=", index)
.select(["sql", "tbl_name", "type"])
.$castTo<{ sql: string | undefined; tbl_name: string; type: string }>()
.executeTakeFirstOrThrow();
//console.log("--indexDefinition--", indexDefinition, index);
// check unique by looking for the word "unique" in the sql
const isUnique = indexDefinition.sql?.match(/unique/i) != null;
const columns = await db
.selectFrom(
sql<{
seqno: number;
cid: number;
name: string;
}>`pragma_index_info(${index})`.as("index_info"),
)
.select(["seqno", "cid", "name"])
.orderBy("cid")
.execute();
return {
name: index,
table: indexDefinition.tbl_name,
isUnique: isUnique,
columns: columns.map((col) => ({
name: col.name,
order: col.seqno,
})),
};
}
private excludeTables(tables: string[] = []) {
return (eb: ExpressionBuilder<any, any>) => {
const and = tables.map((t) => eb("name", "!=", t));
return eb.and(and);
};
}
async getTables(
options: DatabaseMetadataOptions = { withInternalKyselyTables: false },
): Promise<TableMetadata[]> {
let query = this.#db
.selectFrom("sqlite_master")
.where("type", "in", ["table", "view"])
.where("name", "not like", "sqlite_%")
.select("name")
.orderBy("name")
.$castTo<{ name: string }>();
if (!options.withInternalKyselyTables) {
query = query.where(
this.excludeTables([DEFAULT_MIGRATION_TABLE, DEFAULT_MIGRATION_LOCK_TABLE]),
);
}
if (this._excludeTables.length > 0) {
query = query.where(this.excludeTables(this._excludeTables));
}
const tables = await query.execute();
return Promise.all(tables.map(({ name }) => this.#getTableMetadata(name)));
}
async getMetadata(options?: DatabaseMetadataOptions): Promise<DatabaseMetadata> {
return {
tables: await this.getTables(options),
};
}
async #getTableMetadata(table: string): Promise<TableMetadata> {
const db = this.#db;
// Get the SQL that was used to create the table.
const tableDefinition = await db
.selectFrom("sqlite_master")
.where("name", "=", table)
.select(["sql", "type"])
.$castTo<{ sql: string | undefined; type: string }>()
.executeTakeFirstOrThrow();
// Try to find the name of the column that has `autoincrement` 🤦
const autoIncrementCol = tableDefinition.sql
?.split(/[\(\),]/)
?.find((it) => it.toLowerCase().includes("autoincrement"))
?.trimStart()
?.split(/\s+/)?.[0]
?.replace(/["`]/g, "");
const columns = await db
.selectFrom(
sql<{
name: string;
type: string;
notnull: 0 | 1;
dflt_value: any;
}>`pragma_table_info(${table})`.as("table_info"),
)
.select(["name", "type", "notnull", "dflt_value"])
.orderBy("cid")
.execute();
return {
name: table,
isView: tableDefinition.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,
})),
};
}
}

View File

@@ -0,0 +1,31 @@
import type { DatabaseIntrospector, SqliteDatabase } from "kysely";
import { Kysely, SqliteDialect } from "kysely";
import { DeserializeJsonValuesPlugin } from "../plugins/DeserializeJsonValuesPlugin";
import { SqliteConnection } from "./SqliteConnection";
import { SqliteIntrospector } from "./SqliteIntrospector";
class CustomSqliteDialect extends SqliteDialect {
override createIntrospector(db: Kysely<any>): DatabaseIntrospector {
return new SqliteIntrospector(db, {
excludeTables: ["test_table"]
});
}
}
export class SqliteLocalConnection extends SqliteConnection {
constructor(private database: SqliteDatabase) {
const plugins = [new DeserializeJsonValuesPlugin()];
const kysely = new Kysely({
dialect: new CustomSqliteDialect({ database }),
plugins
//log: ["query"],
});
super(kysely);
this.plugins = plugins;
}
override supportsIndices(): boolean {
return true;
}
}