@happyvertical/sql 0.102.4 → 0.102.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/chunks/{duckdb-LMyfKZGi.js → duckdb-c4vdDzPL.js} +5 -4
- package/dist/chunks/{duckdb-LMyfKZGi.js.map → duckdb-c4vdDzPL.js.map} +1 -1
- package/dist/chunks/duckdb-resources-DBlvosDo.js +44 -0
- package/dist/chunks/duckdb-resources-DBlvosDo.js.map +1 -0
- package/dist/chunks/{postgres-dialect-Bfswr4Vs.js → postgres-dialect-CGv6DxSP.js} +3 -775
- package/dist/chunks/postgres-dialect-CGv6DxSP.js.map +1 -0
- package/dist/chunks/query-RzXJ4B9s.js +353 -0
- package/dist/chunks/query-RzXJ4B9s.js.map +1 -0
- package/dist/chunks/schema-manager-cxsySSDA.js +777 -0
- package/dist/chunks/schema-manager-cxsySSDA.js.map +1 -0
- package/dist/chunks/{sqlite-connection-cache-ChWav9iY.js → sqlite-connection-cache-CYThh9l5.js} +2 -2
- package/dist/chunks/{sqlite-connection-cache-ChWav9iY.js.map → sqlite-connection-cache-CYThh9l5.js.map} +1 -1
- package/dist/chunks/{sqlite-native--z4BnolL.js → sqlite-native-CsJ_BLqN.js} +5 -4
- package/dist/chunks/{sqlite-native--z4BnolL.js.map → sqlite-native-CsJ_BLqN.js.map} +1 -1
- package/dist/chunks/{sqlite-0PMv1va2.js → sqlite-rILMdW7Z.js} +6 -5
- package/dist/chunks/{sqlite-0PMv1va2.js.map → sqlite-rILMdW7Z.js.map} +1 -1
- package/dist/index.d.ts +3 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -238
- package/dist/index.js.map +1 -1
- package/dist/pglite.js +2 -1
- package/dist/pglite.js.map +1 -1
- package/dist/query.d.ts +61 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +3 -0
- package/package.json +6 -2
- package/dist/chunks/duckdb-resources-Cci6Lo6H.js +0 -156
- package/dist/chunks/duckdb-resources-Cci6Lo6H.js.map +0 -1
- package/dist/chunks/postgres-dialect-Bfswr4Vs.js.map +0 -1
package/dist/pglite.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as DatabaseSchemaManager } from "./chunks/schema-manager-cxsySSDA.js";
|
|
2
|
+
import { a as createVectorCapabilities, c as createTransactionLock, d as validateDatabaseCacheOptions, i as createTransactionErrorTracker, l as ConnectionCache, n as createGetTableSchema, r as createPostgresDialect, t as createAlterTable } from "./chunks/postgres-dialect-CGv6DxSP.js";
|
|
2
3
|
import { DatabaseError } from "@happyvertical/utils";
|
|
3
4
|
//#region src/pglite.ts
|
|
4
5
|
/**
|
package/dist/pglite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pglite.js","names":[],"sources":["../src/pglite.ts"],"sourcesContent":["/**\n * PGlite adapter: real PostgreSQL compiled to WebAssembly, in the browser and\n * in Node.\n *\n * `@electric-sql/pglite` is an optional peer dependency, imported only when\n * this adapter creates a database. The module's import graph is browser-safe —\n * no `node:` built-ins and no `pg` — so it can be bundled for a page. Import it\n * from `@happyvertical/sql/pglite` there; the package root also reaches\n * Node-only adapters.\n *\n * It speaks the same Postgres dialect as the `pg` adapter (native UUID ids and\n * foreign keys, BIGINT, migrations): both run the statement logic in\n * `./shared/postgres-dialect`.\n *\n * @packageDocumentation\n */\n\nimport { DatabaseError } from '@happyvertical/utils';\nimport { DatabaseSchemaManager } from './schema-manager';\nimport {\n ConnectionCache,\n validateDatabaseCacheOptions,\n} from './shared/connection-cache';\nimport {\n createAlterTable,\n createGetTableSchema,\n createPostgresDialect,\n createTransactionErrorTracker,\n createVectorCapabilities,\n type PostgresQueryExecutor,\n} from './shared/postgres-dialect';\nimport { createTransactionLock } from './shared/transaction-lock';\nimport type {\n DatabaseCacheOptions,\n DatabaseInterface,\n SchemaInitializationOptions,\n SchemasOption,\n TransactionHandle,\n} from './shared/types';\n\n/** Package an install hint should name. */\nconst PGLITE_PACKAGE = '@electric-sql/pglite';\n\n/**\n * Thrown when the optional `@electric-sql/pglite` peer cannot be imported.\n */\nexport class PGlitePeerMissingError extends DatabaseError {\n constructor(cause?: unknown) {\n super(\n `The pglite database type needs the optional peer dependency ${PGLITE_PACKAGE}. Install it with \\`pnpm add ${PGLITE_PACKAGE}\\` (or the npm/yarn equivalent).`,\n { adapter: 'pglite', peer: PGLITE_PACKAGE },\n cause,\n );\n this.name = 'PGlitePeerMissingError';\n }\n}\n\n/**\n * The part of a PGlite query result this adapter reads.\n */\ninterface PGliteResultLike {\n rows: any[];\n affectedRows?: number;\n rowCount?: number;\n command?: string;\n}\n\n/**\n * The part of a PGlite transaction object this adapter drives.\n */\ninterface PGliteTransactionLike {\n query(sql: string, params?: any[]): Promise<PGliteResultLike>;\n exec(sql: string): Promise<PGliteResultLike[]>;\n rollback(): Promise<void>;\n readonly closed: boolean;\n}\n\n/**\n * The part of a PGlite instance this adapter drives. A `PGlite` and a\n * `PGliteWorker` both satisfy it.\n */\nexport interface PGliteLike {\n query(sql: string, params?: any[]): Promise<PGliteResultLike>;\n exec(sql: string): Promise<PGliteResultLike[]>;\n transaction<T>(\n callback: (tx: PGliteTransactionLike) => Promise<T>,\n ): Promise<T>;\n close(): Promise<void>;\n}\n\n/**\n * Configuration options for PGlite databases\n */\nexport interface PGliteOptions extends DatabaseCacheOptions {\n /**\n * Database type identifier\n */\n type?: 'pglite';\n\n /**\n * Where the database lives.\n *\n * - `memory://` or omitted: in memory, gone when the instance closes\n * - `idb://<name>`: IndexedDB (browser)\n * - `opfs-ahp://<name>`: Origin Private File System (browser, in a worker)\n * - any other value: a filesystem directory (Node, Bun, Deno)\n *\n * `url` is accepted as an alias so `HAVE_SQL_URL` selects a location.\n */\n dataDir?: string;\n\n /**\n * Alias for {@link dataDir}.\n */\n url?: string;\n\n /**\n * PGlite extensions to load, keyed by the name `CREATE EXTENSION` uses.\n * Forwarded as-is to PGlite, so pass the extension objects the PGlite\n * packages export, for example:\n *\n * ```typescript\n * import { vector } from '@electric-sql/pglite-pgvector';\n * await getDatabase({ type: 'pglite', extensions: { vector } });\n * ```\n */\n extensions?: Record<string, unknown>;\n\n /**\n * Further options forwarded to PGlite's constructor (`relaxedDurability`,\n * `initialMemory`, `loadDataDir`, `fs`, `parsers`, ...). `dataDir` and\n * `extensions` are taken from the options above.\n */\n pglite?: Record<string, unknown>;\n\n /**\n * Use a PGlite instance you created yourself, such as a `PGliteWorker` that\n * keeps the database off the main thread. The adapter never closes it and\n * never caches the database it builds around it.\n */\n client?: PGliteLike;\n\n /**\n * Opaque caller-owned cache identity. Calls with the same `dbid` share one\n * database. Without one, a persistent `dataDir` is its own identity — two\n * PGlite instances on the same storage would corrupt it — while an in-memory\n * database is private to the call that made it.\n */\n dbid?: string;\n\n /**\n * How long a queued transaction — or a statement issued while a transaction\n * is open — waits for the connection, in milliseconds. PGlite runs one\n * connection, so these run one at a time. Must be positive and finite.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n\n /**\n * Schema definitions for tables.\n *\n * **PGlite ignores this option** — like PostgreSQL, tables are managed by\n * migrations. It exists so callers can pass schemas uniformly to all\n * adapters.\n */\n schemas?: SchemasOption;\n}\n\nconst connectionCache = new ConnectionCache<DatabaseInterface>();\n\n/**\n * Clears the PGlite connection cache, closing every cached database.\n * Useful for test isolation.\n */\nexport async function clearPGliteConnectionCache(): Promise<void> {\n await connectionCache.clear(async (db) => {\n await db.close?.();\n });\n}\n\ninterface PGliteModule {\n // biome-ignore lint/style/useNamingConvention: mirrors the package's export\n PGlite: {\n create(\n dataDir?: string,\n options?: Record<string, unknown>,\n ): Promise<PGliteLike>;\n };\n // biome-ignore lint/style/useNamingConvention: mirrors the package's export\n messages: { DatabaseError: new (...args: any[]) => Error };\n}\n\n/**\n * Imports the optional peer. Isolated so a missing install becomes a typed\n * {@link PGlitePeerMissingError} instead of a bare module-resolution failure.\n */\nexport async function importPGlite(): Promise<PGliteModule> {\n try {\n const loaded = (await import('@electric-sql/pglite')) as unknown as\n | (PGliteModule & { default?: PGliteModule })\n | undefined;\n const module =\n typeof loaded?.PGlite?.create === 'function' ? loaded : loaded?.default;\n if (typeof module?.PGlite?.create !== 'function') {\n throw new Error(`${PGLITE_PACKAGE} did not export PGlite`);\n }\n return module;\n } catch (error) {\n throw new PGlitePeerMissingError(error);\n }\n}\n\nconst IN_MEMORY = 'memory://';\n\nconst isInMemory = (dataDir: string): boolean =>\n dataDir === '' || dataDir.startsWith(IN_MEMORY);\n\n/**\n * Creates a PGlite database adapter.\n *\n * @param options - PGlite options\n * @returns Database interface for PGlite\n * @throws {PGlitePeerMissingError} When `@electric-sql/pglite` is not installed\n */\nexport async function getDatabase(\n options: PGliteOptions = {},\n): Promise<DatabaseInterface> {\n validateDatabaseCacheOptions(options);\n const dataDir = options.dataDir ?? options.url ?? IN_MEMORY;\n const key = options.client\n ? undefined\n : options.dbid\n ? `pglite:id:${options.dbid}`\n : isInMemory(dataDir)\n ? undefined\n : `pglite:dir:${dataDir}`;\n return connectionCache.getOrCreate(\n key,\n options,\n () => createDatabase(options, dataDir),\n async (db) => {\n await db.close?.();\n },\n );\n}\n\n/**\n * Internal: creates the database. Loads the peer, so it never runs for a\n * cached hit.\n */\nasync function createDatabase(\n options: PGliteOptions,\n dataDir: string,\n): Promise<DatabaseInterface> {\n const pglite = await importPGlite();\n const isStatementError = (error: unknown): boolean =>\n error instanceof pglite.messages.DatabaseError;\n\n const ownsClient = !options.client;\n const pg: PGliteLike =\n options.client ??\n (await pglite.PGlite.create(dataDir, {\n ...options.pglite,\n ...(options.extensions ? { extensions: options.extensions } : {}),\n }));\n\n // One connection: transactions take turns, and so do top-level statements\n // while a transaction is open. PGlite queues them itself, but without a\n // deadline — a transaction handle that is never ended, or a top-level call\n // made from inside a transaction callback, would hang forever rather than\n // fail with an explanation.\n const lock = createTransactionLock('pglite', options.transactionQueueTimeout);\n\n let db: DatabaseInterface;\n let closePromise: Promise<void> | undefined;\n\n type Target = Pick<PGliteTransactionLike, 'query' | 'exec'>;\n\n /**\n * Runs one statement. Without parameters PostgreSQL's simple protocol is\n * used, like `pg`, so a multi-statement script works through `query()`.\n */\n const run = async (\n target: Target,\n sql: string,\n values?: any[],\n ): Promise<{ rows: any[]; rowCount: number | null }> => {\n const result =\n values && values.length > 0\n ? await target.query(sql, values)\n : (await target.exec(sql)).at(-1);\n if (!result) return { rows: [], rowCount: 0 };\n return {\n rows: result.rows,\n rowCount: result.rowCount ?? result.affectedRows ?? result.rows.length,\n };\n };\n\n // Statements outside a transaction.\n const client: PostgresQueryExecutor = {\n query: (sql, values) => lock.run(() => run(pg, sql, values)),\n };\n\n /**\n * Binds the statement-error tracking to one PGlite transaction object.\n * See `createTransactionErrorTracker` for why a statement failure has to\n * outlive the `catch` that swallowed it.\n */\n const createTransactionState = (pgTx: PGliteTransactionLike) => {\n const tracker = createTransactionErrorTracker(isStatementError);\n const executor: PostgresQueryExecutor = {\n query: (sql, values) => tracker.track(run(pgTx, sql, values), sql),\n };\n // `tx.client` is PGlite's own surface; route it through the tracker too so\n // raw use cannot hide a failure from the commit check.\n const rawClient = {\n query: (sql: string, params?: any[]) =>\n tracker.track(pgTx.query(sql, params), sql),\n exec: (sql: string) => tracker.track(pgTx.exec(sql), sql),\n };\n\n /**\n * Fails if the transaction can no longer commit. PostgreSQL answers COMMIT\n * on an aborted transaction with a ROLLBACK tag and no error, and PGlite\n * issues that COMMIT itself, so the aborted state is probed for here while\n * there is still an error to throw.\n */\n const assertCommittable = async (): Promise<void> => {\n await tracker.drain();\n const statementError = tracker.getError();\n if (statementError !== undefined) throw statementError;\n try {\n await pgTx.query('SELECT 1');\n } catch (error) {\n throw tracker.getError() ?? tracker.getUnconfirmedError() ?? error;\n }\n const unconfirmed = tracker.getUnconfirmedError();\n if (unconfirmed !== undefined) throw unconfirmed;\n };\n\n return { tracker, executor, rawClient, assertCommittable };\n };\n\n const dialect = createPostgresDialect({\n client,\n // One connection, so a probe cannot be answered from a second one.\n probeExecutor: (executor) => executor,\n runInTransaction: (work) =>\n lock.run(() =>\n pg.transaction(async (pgTx) => {\n const state = createTransactionState(pgTx);\n const result = await work(state.executor);\n await state.assertCommittable();\n return result;\n }),\n ),\n });\n const { serializeRecord, createClientMethods, createNestedTransaction } =\n dialect;\n\n const {\n insert,\n get,\n list,\n update,\n upsert,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n } = createClientMethods(client, false, serializeRecord);\n const getTableSchema = createGetTableSchema({ many, tableExists });\n const alterTable = createAlterTable(client);\n\n /**\n * Builds the interface handed to a transaction callback.\n *\n * There is no `AsyncLocalStorage` in a browser, so nested scopes are not\n * tracked ambiently: each nested callback receives a handle bound to its own\n * savepoint. Nested work must call the handle it was given.\n */\n const createScopedDatabase = (\n pgTx: PGliteTransactionLike,\n extra: (scoped: DatabaseInterface) => Record<string, unknown> = () => ({}),\n ) => {\n const state = createTransactionState(pgTx);\n let scoped!: DatabaseInterface;\n const nested = createNestedTransaction(\n state.executor,\n (override) => (override ? { ...scoped, transaction: override } : scoped),\n state.tracker.clearError,\n );\n scoped = {\n url: dataDir,\n client: state.rawClient,\n ...createClientMethods(\n state.executor,\n true,\n serializeRecord,\n state.tracker.clearError,\n ),\n transaction: nested,\n } as DatabaseInterface;\n Object.assign(scoped, extra(scoped));\n return { state, nested, scoped };\n };\n\n /**\n * Executes a callback within a database transaction. Commits on success and\n * rolls back when the callback throws.\n */\n const transaction = async <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ): Promise<T> => {\n const release = await lock.acquire();\n try {\n return await pg.transaction(async (pgTx) => {\n const { state, nested, scoped } = createScopedDatabase(pgTx);\n try {\n const result = await callback(scoped);\n await nested.drain();\n await state.assertCommittable();\n return result;\n } catch (error) {\n // A rejected Promise.all can leave queued savepoint work running;\n // PGlite rolls back as soon as this callback settles.\n await nested.drain();\n await state.tracker.drain();\n throw error;\n }\n });\n } finally {\n release();\n }\n };\n\n /**\n * Begins a transaction and returns a handle for manual control.\n *\n * PGlite only offers a callback form, so the callback is held open until the\n * handle is committed or rolled back.\n */\n const beginTransaction = async (): Promise<TransactionHandle> => {\n const release = await lock.acquire();\n try {\n let settle!: (outcome: 'commit' | 'rollback') => void;\n const outcome = new Promise<'commit' | 'rollback'>((resolve) => {\n settle = resolve;\n });\n let begun!: (tx: PGliteTransactionLike) => void;\n let failedToBegin!: (error: unknown) => void;\n const started = new Promise<PGliteTransactionLike>((resolve, reject) => {\n begun = resolve;\n failedToBegin = reject;\n });\n const finished = pg.transaction(async (pgTx) => {\n begun(pgTx);\n if ((await outcome) === 'rollback') await pgTx.rollback();\n });\n // BEGIN failing rejects `finished` before the callback ever runs.\n finished.catch(failedToBegin);\n const pgTx = await started;\n\n let active = true;\n const { state, nested, scoped } = createScopedDatabase(pgTx, () => ({\n commit: () => end('COMMIT'),\n rollback: () => end('ROLLBACK'),\n isActive: () => active,\n }));\n\n // The transaction is over however this ends, so the connection is\n // returned whether COMMIT succeeds, throws, or was never reachable.\n const end = async (command: 'COMMIT' | 'ROLLBACK'): Promise<void> => {\n if (!active) {\n throw new DatabaseError('Transaction already ended', {});\n }\n active = false;\n let failure: { error: unknown } | undefined;\n try {\n await nested.drain();\n await state.tracker.drain();\n if (command === 'COMMIT') await state.assertCommittable();\n } catch (error) {\n failure = { error };\n }\n settle(command === 'COMMIT' && !failure ? 'commit' : 'rollback');\n try {\n await finished;\n } catch (error) {\n failure ??= { error };\n } finally {\n release();\n }\n if (failure) throw failure.error;\n };\n\n return scoped as TransactionHandle;\n } catch (error) {\n release();\n throw error;\n }\n };\n\n const initializeSchemas = async (\n schemaOptions: SchemaInitializationOptions,\n ): Promise<void> => {\n const schemaManager = new DatabaseSchemaManager();\n const currentDb: DatabaseInterface = {\n url: dataDir,\n client: pg,\n insert,\n update,\n upsert,\n get,\n getOrInsert,\n delete: deleteRecords,\n count,\n list,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n transaction,\n };\n\n await schemaManager.initializeSchemas(currentDb, schemaOptions);\n };\n\n db = {\n url: dataDir,\n client: pg,\n insert,\n update,\n upsert,\n get,\n getOrInsert,\n delete: deleteRecords,\n count,\n list,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n initializeSchemas,\n transaction,\n beginTransaction,\n getTableSchema,\n alterTable,\n vector: createVectorCapabilities(client),\n close: async () => {\n closePromise ??= (async () => {\n connectionCache.forget(db);\n if (ownsClient) await pg.close();\n })();\n return closePromise;\n },\n };\n\n return db;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,iBAAiB;;;;AAKvB,IAAa,yBAAb,cAA4C,cAAc;CACxD,YAAY,OAAiB;EAC3B,MACE,+DAA+D,eAAe,+BAA+B,eAAe,mCAC5H;GAAE,SAAS;GAAU,MAAM;EAAe,GAC1C,KACF;EACA,KAAK,OAAO;CACd;AACF;AAkHA,IAAM,kBAAkB,IAAI,gBAAmC;;;;;AAM/D,eAAsB,6BAA4C;CAChE,MAAM,gBAAgB,MAAM,OAAO,OAAO;EACxC,MAAM,GAAG,QAAQ;CACnB,CAAC;AACH;;;;;AAkBA,eAAsB,eAAsC;CAC1D,IAAI;EACF,MAAM,SAAU,MAAM,OAAO;EAG7B,MAAM,SACJ,OAAO,QAAQ,QAAQ,WAAW,aAAa,SAAS,QAAQ;EAClE,IAAI,OAAO,QAAQ,QAAQ,WAAW,YACpC,MAAM,IAAI,MAAM,GAAG,eAAe,uBAAuB;EAE3D,OAAO;CACT,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF;AAEA,IAAM,YAAY;AAElB,IAAM,cAAc,YAClB,YAAY,MAAM,QAAQ,WAAW,SAAS;;;;;;;;AAShD,eAAsB,YACpB,UAAyB,CAAC,GACE;CAC5B,6BAA6B,OAAO;CACpC,MAAM,UAAU,QAAQ,WAAW,QAAQ,OAAO;CAClD,MAAM,MAAM,QAAQ,SAChB,KAAA,IACA,QAAQ,OACN,aAAa,QAAQ,SACrB,WAAW,OAAO,IAChB,KAAA,IACA,cAAc;CACtB,OAAO,gBAAgB,YACrB,KACA,eACM,eAAe,SAAS,OAAO,GACrC,OAAO,OAAO;EACZ,MAAM,GAAG,QAAQ;CACnB,CACF;AACF;;;;;AAMA,eAAe,eACb,SACA,SAC4B;CAC5B,MAAM,SAAS,MAAM,aAAa;CAClC,MAAM,oBAAoB,UACxB,iBAAiB,OAAO,SAAS;CAEnC,MAAM,aAAa,CAAC,QAAQ;CAC5B,MAAM,KACJ,QAAQ,UACP,MAAM,OAAO,OAAO,OAAO,SAAS;EACnC,GAAG,QAAQ;EACX,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;CACjE,CAAC;CAOH,MAAM,OAAO,sBAAsB,UAAU,QAAQ,uBAAuB;CAE5E,IAAI;CACJ,IAAI;;;;;CAQJ,MAAM,MAAM,OACV,QACA,KACA,WACsD;EACtD,MAAM,SACJ,UAAU,OAAO,SAAS,IACtB,MAAM,OAAO,MAAM,KAAK,MAAM,KAC7B,MAAM,OAAO,KAAK,GAAG,EAAA,CAAG,GAAG,EAAE;EACpC,IAAI,CAAC,QAAQ,OAAO;GAAE,MAAM,CAAC;GAAG,UAAU;EAAE;EAC5C,OAAO;GACL,MAAM,OAAO;GACb,UAAU,OAAO,YAAY,OAAO,gBAAgB,OAAO,KAAK;EAClE;CACF;CAGA,MAAM,SAAgC,EACpC,QAAQ,KAAK,WAAW,KAAK,UAAU,IAAI,IAAI,KAAK,MAAM,CAAC,EAC7D;;;;;;CAOA,MAAM,0BAA0B,SAAgC;EAC9D,MAAM,UAAU,8BAA8B,gBAAgB;EAC9D,MAAM,WAAkC,EACtC,QAAQ,KAAK,WAAW,QAAQ,MAAM,IAAI,MAAM,KAAK,MAAM,GAAG,GAAG,EACnE;EAGA,MAAM,YAAY;GAChB,QAAQ,KAAa,WACnB,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,GAAG,GAAG;GAC5C,OAAO,QAAgB,QAAQ,MAAM,KAAK,KAAK,GAAG,GAAG,GAAG;EAC1D;;;;;;;EAQA,MAAM,oBAAoB,YAA2B;GACnD,MAAM,QAAQ,MAAM;GACpB,MAAM,iBAAiB,QAAQ,SAAS;GACxC,IAAI,mBAAmB,KAAA,GAAW,MAAM;GACxC,IAAI;IACF,MAAM,KAAK,MAAM,UAAU;GAC7B,SAAS,OAAO;IACd,MAAM,QAAQ,SAAS,KAAK,QAAQ,oBAAoB,KAAK;GAC/D;GACA,MAAM,cAAc,QAAQ,oBAAoB;GAChD,IAAI,gBAAgB,KAAA,GAAW,MAAM;EACvC;EAEA,OAAO;GAAE;GAAS;GAAU;GAAW;EAAkB;CAC3D;CAgBA,MAAM,EAAE,iBAAiB,qBAAqB,4BAd9B,sBAAsB;EACpC;EAEA,gBAAgB,aAAa;EAC7B,mBAAmB,SACjB,KAAK,UACH,GAAG,YAAY,OAAO,SAAS;GAC7B,MAAM,QAAQ,uBAAuB,IAAI;GACzC,MAAM,SAAS,MAAM,KAAK,MAAM,QAAQ;GACxC,MAAM,MAAM,kBAAkB;GAC9B,OAAO;EACT,CAAC,CACH;CACJ,CAEE;CAEF,MAAM,EACJ,QACA,KACA,MACA,QACA,QACA,aACA,QAAQ,eACR,OACA,OACA,MACA,QACA,OACA,SACA,OACA,IACA,IACA,IACA,IACA,aACA,eACE,oBAAoB,QAAQ,OAAO,eAAe;CACtD,MAAM,iBAAiB,qBAAqB;EAAE;EAAM;CAAY,CAAC;CACjE,MAAM,aAAa,iBAAiB,MAAM;;;;;;;;CAS1C,MAAM,wBACJ,MACA,eAAuE,CAAC,OACrE;EACH,MAAM,QAAQ,uBAAuB,IAAI;EACzC,IAAI;EACJ,MAAM,SAAS,wBACb,MAAM,WACL,aAAc,WAAW;GAAE,GAAG;GAAQ,aAAa;EAAS,IAAI,QACjE,MAAM,QAAQ,UAChB;EACA,SAAS;GACP,KAAK;GACL,QAAQ,MAAM;GACd,GAAG,oBACD,MAAM,UACN,MACA,iBACA,MAAM,QAAQ,UAChB;GACA,aAAa;EACf;EACA,OAAO,OAAO,QAAQ,MAAM,MAAM,CAAC;EACnC,OAAO;GAAE;GAAO;GAAQ;EAAO;CACjC;;;;;CAMA,MAAM,cAAc,OAClB,aACe;EACf,MAAM,UAAU,MAAM,KAAK,QAAQ;EACnC,IAAI;GACF,OAAO,MAAM,GAAG,YAAY,OAAO,SAAS;IAC1C,MAAM,EAAE,OAAO,QAAQ,WAAW,qBAAqB,IAAI;IAC3D,IAAI;KACF,MAAM,SAAS,MAAM,SAAS,MAAM;KACpC,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,kBAAkB;KAC9B,OAAO;IACT,SAAS,OAAO;KAGd,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,QAAQ,MAAM;KAC1B,MAAM;IACR;GACF,CAAC;EACH,UAAU;GACR,QAAQ;EACV;CACF;;;;;;;CAQA,MAAM,mBAAmB,YAAwC;EAC/D,MAAM,UAAU,MAAM,KAAK,QAAQ;EACnC,IAAI;GACF,IAAI;GACJ,MAAM,UAAU,IAAI,SAAgC,YAAY;IAC9D,SAAS;GACX,CAAC;GACD,IAAI;GACJ,IAAI;GACJ,MAAM,UAAU,IAAI,SAAgC,SAAS,WAAW;IACtE,QAAQ;IACR,gBAAgB;GAClB,CAAC;GACD,MAAM,WAAW,GAAG,YAAY,OAAO,SAAS;IAC9C,MAAM,IAAI;IACV,IAAK,MAAM,YAAa,YAAY,MAAM,KAAK,SAAS;GAC1D,CAAC;GAED,SAAS,MAAM,aAAa;GAC5B,MAAM,OAAO,MAAM;GAEnB,IAAI,SAAS;GACb,MAAM,EAAE,OAAO,QAAQ,WAAW,qBAAqB,aAAa;IAClE,cAAc,IAAI,QAAQ;IAC1B,gBAAgB,IAAI,UAAU;IAC9B,gBAAgB;GAClB,EAAE;GAIF,MAAM,MAAM,OAAO,YAAkD;IACnE,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,6BAA6B,CAAC,CAAC;IAEzD,SAAS;IACT,IAAI;IACJ,IAAI;KACF,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,QAAQ,MAAM;KAC1B,IAAI,YAAY,UAAU,MAAM,MAAM,kBAAkB;IAC1D,SAAS,OAAO;KACd,UAAU,EAAE,MAAM;IACpB;IACA,OAAO,YAAY,YAAY,CAAC,UAAU,WAAW,UAAU;IAC/D,IAAI;KACF,MAAM;IACR,SAAS,OAAO;KACd,YAAY,EAAE,MAAM;IACtB,UAAU;KACR,QAAQ;IACV;IACA,IAAI,SAAS,MAAM,QAAQ;GAC7B;GAEA,OAAO;EACT,SAAS,OAAO;GACd,QAAQ;GACR,MAAM;EACR;CACF;CAEA,MAAM,oBAAoB,OACxB,kBACkB;EAClB,MAAM,gBAAgB,IAAI,sBAAsB;EAChD,MAAM,YAA+B;GACnC,KAAK;GACL,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAEA,MAAM,cAAc,kBAAkB,WAAW,aAAa;CAChE;CAEA,KAAK;EACH,KAAK;EACL,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ,yBAAyB,MAAM;EACvC,OAAO,YAAY;GACjB,kBAAkB,YAAY;IAC5B,gBAAgB,OAAO,EAAE;IACzB,IAAI,YAAY,MAAM,GAAG,MAAM;GACjC,EAAA,CAAG;GACH,OAAO;EACT;CACF;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"pglite.js","names":[],"sources":["../src/pglite.ts"],"sourcesContent":["/**\n * PGlite adapter: real PostgreSQL compiled to WebAssembly, in the browser and\n * in Node.\n *\n * `@electric-sql/pglite` is an optional peer dependency, imported only when\n * this adapter creates a database. The module's import graph is browser-safe —\n * no `node:` built-ins and no `pg` — so it can be bundled for a page. Import it\n * from `@happyvertical/sql/pglite` there; the package root also reaches\n * Node-only adapters.\n *\n * It speaks the same Postgres dialect as the `pg` adapter (native UUID ids and\n * foreign keys, BIGINT, migrations): both run the statement logic in\n * `./shared/postgres-dialect`.\n *\n * @packageDocumentation\n */\n\nimport { DatabaseError } from '@happyvertical/utils';\nimport { DatabaseSchemaManager } from './schema-manager';\nimport {\n ConnectionCache,\n validateDatabaseCacheOptions,\n} from './shared/connection-cache';\nimport {\n createAlterTable,\n createGetTableSchema,\n createPostgresDialect,\n createTransactionErrorTracker,\n createVectorCapabilities,\n type PostgresQueryExecutor,\n} from './shared/postgres-dialect';\nimport { createTransactionLock } from './shared/transaction-lock';\nimport type {\n DatabaseCacheOptions,\n DatabaseInterface,\n SchemaInitializationOptions,\n SchemasOption,\n TransactionHandle,\n} from './shared/types';\n\n/** Package an install hint should name. */\nconst PGLITE_PACKAGE = '@electric-sql/pglite';\n\n/**\n * Thrown when the optional `@electric-sql/pglite` peer cannot be imported.\n */\nexport class PGlitePeerMissingError extends DatabaseError {\n constructor(cause?: unknown) {\n super(\n `The pglite database type needs the optional peer dependency ${PGLITE_PACKAGE}. Install it with \\`pnpm add ${PGLITE_PACKAGE}\\` (or the npm/yarn equivalent).`,\n { adapter: 'pglite', peer: PGLITE_PACKAGE },\n cause,\n );\n this.name = 'PGlitePeerMissingError';\n }\n}\n\n/**\n * The part of a PGlite query result this adapter reads.\n */\ninterface PGliteResultLike {\n rows: any[];\n affectedRows?: number;\n rowCount?: number;\n command?: string;\n}\n\n/**\n * The part of a PGlite transaction object this adapter drives.\n */\ninterface PGliteTransactionLike {\n query(sql: string, params?: any[]): Promise<PGliteResultLike>;\n exec(sql: string): Promise<PGliteResultLike[]>;\n rollback(): Promise<void>;\n readonly closed: boolean;\n}\n\n/**\n * The part of a PGlite instance this adapter drives. A `PGlite` and a\n * `PGliteWorker` both satisfy it.\n */\nexport interface PGliteLike {\n query(sql: string, params?: any[]): Promise<PGliteResultLike>;\n exec(sql: string): Promise<PGliteResultLike[]>;\n transaction<T>(\n callback: (tx: PGliteTransactionLike) => Promise<T>,\n ): Promise<T>;\n close(): Promise<void>;\n}\n\n/**\n * Configuration options for PGlite databases\n */\nexport interface PGliteOptions extends DatabaseCacheOptions {\n /**\n * Database type identifier\n */\n type?: 'pglite';\n\n /**\n * Where the database lives.\n *\n * - `memory://` or omitted: in memory, gone when the instance closes\n * - `idb://<name>`: IndexedDB (browser)\n * - `opfs-ahp://<name>`: Origin Private File System (browser, in a worker)\n * - any other value: a filesystem directory (Node, Bun, Deno)\n *\n * `url` is accepted as an alias so `HAVE_SQL_URL` selects a location.\n */\n dataDir?: string;\n\n /**\n * Alias for {@link dataDir}.\n */\n url?: string;\n\n /**\n * PGlite extensions to load, keyed by the name `CREATE EXTENSION` uses.\n * Forwarded as-is to PGlite, so pass the extension objects the PGlite\n * packages export, for example:\n *\n * ```typescript\n * import { vector } from '@electric-sql/pglite-pgvector';\n * await getDatabase({ type: 'pglite', extensions: { vector } });\n * ```\n */\n extensions?: Record<string, unknown>;\n\n /**\n * Further options forwarded to PGlite's constructor (`relaxedDurability`,\n * `initialMemory`, `loadDataDir`, `fs`, `parsers`, ...). `dataDir` and\n * `extensions` are taken from the options above.\n */\n pglite?: Record<string, unknown>;\n\n /**\n * Use a PGlite instance you created yourself, such as a `PGliteWorker` that\n * keeps the database off the main thread. The adapter never closes it and\n * never caches the database it builds around it.\n */\n client?: PGliteLike;\n\n /**\n * Opaque caller-owned cache identity. Calls with the same `dbid` share one\n * database. Without one, a persistent `dataDir` is its own identity — two\n * PGlite instances on the same storage would corrupt it — while an in-memory\n * database is private to the call that made it.\n */\n dbid?: string;\n\n /**\n * How long a queued transaction — or a statement issued while a transaction\n * is open — waits for the connection, in milliseconds. PGlite runs one\n * connection, so these run one at a time. Must be positive and finite.\n *\n * @default 30000\n */\n transactionQueueTimeout?: number;\n\n /**\n * Schema definitions for tables.\n *\n * **PGlite ignores this option** — like PostgreSQL, tables are managed by\n * migrations. It exists so callers can pass schemas uniformly to all\n * adapters.\n */\n schemas?: SchemasOption;\n}\n\nconst connectionCache = new ConnectionCache<DatabaseInterface>();\n\n/**\n * Clears the PGlite connection cache, closing every cached database.\n * Useful for test isolation.\n */\nexport async function clearPGliteConnectionCache(): Promise<void> {\n await connectionCache.clear(async (db) => {\n await db.close?.();\n });\n}\n\ninterface PGliteModule {\n // biome-ignore lint/style/useNamingConvention: mirrors the package's export\n PGlite: {\n create(\n dataDir?: string,\n options?: Record<string, unknown>,\n ): Promise<PGliteLike>;\n };\n // biome-ignore lint/style/useNamingConvention: mirrors the package's export\n messages: { DatabaseError: new (...args: any[]) => Error };\n}\n\n/**\n * Imports the optional peer. Isolated so a missing install becomes a typed\n * {@link PGlitePeerMissingError} instead of a bare module-resolution failure.\n */\nexport async function importPGlite(): Promise<PGliteModule> {\n try {\n const loaded = (await import('@electric-sql/pglite')) as unknown as\n | (PGliteModule & { default?: PGliteModule })\n | undefined;\n const module =\n typeof loaded?.PGlite?.create === 'function' ? loaded : loaded?.default;\n if (typeof module?.PGlite?.create !== 'function') {\n throw new Error(`${PGLITE_PACKAGE} did not export PGlite`);\n }\n return module;\n } catch (error) {\n throw new PGlitePeerMissingError(error);\n }\n}\n\nconst IN_MEMORY = 'memory://';\n\nconst isInMemory = (dataDir: string): boolean =>\n dataDir === '' || dataDir.startsWith(IN_MEMORY);\n\n/**\n * Creates a PGlite database adapter.\n *\n * @param options - PGlite options\n * @returns Database interface for PGlite\n * @throws {PGlitePeerMissingError} When `@electric-sql/pglite` is not installed\n */\nexport async function getDatabase(\n options: PGliteOptions = {},\n): Promise<DatabaseInterface> {\n validateDatabaseCacheOptions(options);\n const dataDir = options.dataDir ?? options.url ?? IN_MEMORY;\n const key = options.client\n ? undefined\n : options.dbid\n ? `pglite:id:${options.dbid}`\n : isInMemory(dataDir)\n ? undefined\n : `pglite:dir:${dataDir}`;\n return connectionCache.getOrCreate(\n key,\n options,\n () => createDatabase(options, dataDir),\n async (db) => {\n await db.close?.();\n },\n );\n}\n\n/**\n * Internal: creates the database. Loads the peer, so it never runs for a\n * cached hit.\n */\nasync function createDatabase(\n options: PGliteOptions,\n dataDir: string,\n): Promise<DatabaseInterface> {\n const pglite = await importPGlite();\n const isStatementError = (error: unknown): boolean =>\n error instanceof pglite.messages.DatabaseError;\n\n const ownsClient = !options.client;\n const pg: PGliteLike =\n options.client ??\n (await pglite.PGlite.create(dataDir, {\n ...options.pglite,\n ...(options.extensions ? { extensions: options.extensions } : {}),\n }));\n\n // One connection: transactions take turns, and so do top-level statements\n // while a transaction is open. PGlite queues them itself, but without a\n // deadline — a transaction handle that is never ended, or a top-level call\n // made from inside a transaction callback, would hang forever rather than\n // fail with an explanation.\n const lock = createTransactionLock('pglite', options.transactionQueueTimeout);\n\n let db: DatabaseInterface;\n let closePromise: Promise<void> | undefined;\n\n type Target = Pick<PGliteTransactionLike, 'query' | 'exec'>;\n\n /**\n * Runs one statement. Without parameters PostgreSQL's simple protocol is\n * used, like `pg`, so a multi-statement script works through `query()`.\n */\n const run = async (\n target: Target,\n sql: string,\n values?: any[],\n ): Promise<{ rows: any[]; rowCount: number | null }> => {\n const result =\n values && values.length > 0\n ? await target.query(sql, values)\n : (await target.exec(sql)).at(-1);\n if (!result) return { rows: [], rowCount: 0 };\n return {\n rows: result.rows,\n rowCount: result.rowCount ?? result.affectedRows ?? result.rows.length,\n };\n };\n\n // Statements outside a transaction.\n const client: PostgresQueryExecutor = {\n query: (sql, values) => lock.run(() => run(pg, sql, values)),\n };\n\n /**\n * Binds the statement-error tracking to one PGlite transaction object.\n * See `createTransactionErrorTracker` for why a statement failure has to\n * outlive the `catch` that swallowed it.\n */\n const createTransactionState = (pgTx: PGliteTransactionLike) => {\n const tracker = createTransactionErrorTracker(isStatementError);\n const executor: PostgresQueryExecutor = {\n query: (sql, values) => tracker.track(run(pgTx, sql, values), sql),\n };\n // `tx.client` is PGlite's own surface; route it through the tracker too so\n // raw use cannot hide a failure from the commit check.\n const rawClient = {\n query: (sql: string, params?: any[]) =>\n tracker.track(pgTx.query(sql, params), sql),\n exec: (sql: string) => tracker.track(pgTx.exec(sql), sql),\n };\n\n /**\n * Fails if the transaction can no longer commit. PostgreSQL answers COMMIT\n * on an aborted transaction with a ROLLBACK tag and no error, and PGlite\n * issues that COMMIT itself, so the aborted state is probed for here while\n * there is still an error to throw.\n */\n const assertCommittable = async (): Promise<void> => {\n await tracker.drain();\n const statementError = tracker.getError();\n if (statementError !== undefined) throw statementError;\n try {\n await pgTx.query('SELECT 1');\n } catch (error) {\n throw tracker.getError() ?? tracker.getUnconfirmedError() ?? error;\n }\n const unconfirmed = tracker.getUnconfirmedError();\n if (unconfirmed !== undefined) throw unconfirmed;\n };\n\n return { tracker, executor, rawClient, assertCommittable };\n };\n\n const dialect = createPostgresDialect({\n client,\n // One connection, so a probe cannot be answered from a second one.\n probeExecutor: (executor) => executor,\n runInTransaction: (work) =>\n lock.run(() =>\n pg.transaction(async (pgTx) => {\n const state = createTransactionState(pgTx);\n const result = await work(state.executor);\n await state.assertCommittable();\n return result;\n }),\n ),\n });\n const { serializeRecord, createClientMethods, createNestedTransaction } =\n dialect;\n\n const {\n insert,\n get,\n list,\n update,\n upsert,\n getOrInsert,\n delete: deleteRecords,\n count,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n } = createClientMethods(client, false, serializeRecord);\n const getTableSchema = createGetTableSchema({ many, tableExists });\n const alterTable = createAlterTable(client);\n\n /**\n * Builds the interface handed to a transaction callback.\n *\n * There is no `AsyncLocalStorage` in a browser, so nested scopes are not\n * tracked ambiently: each nested callback receives a handle bound to its own\n * savepoint. Nested work must call the handle it was given.\n */\n const createScopedDatabase = (\n pgTx: PGliteTransactionLike,\n extra: (scoped: DatabaseInterface) => Record<string, unknown> = () => ({}),\n ) => {\n const state = createTransactionState(pgTx);\n let scoped!: DatabaseInterface;\n const nested = createNestedTransaction(\n state.executor,\n (override) => (override ? { ...scoped, transaction: override } : scoped),\n state.tracker.clearError,\n );\n scoped = {\n url: dataDir,\n client: state.rawClient,\n ...createClientMethods(\n state.executor,\n true,\n serializeRecord,\n state.tracker.clearError,\n ),\n transaction: nested,\n } as DatabaseInterface;\n Object.assign(scoped, extra(scoped));\n return { state, nested, scoped };\n };\n\n /**\n * Executes a callback within a database transaction. Commits on success and\n * rolls back when the callback throws.\n */\n const transaction = async <T>(\n callback: (tx: DatabaseInterface) => Promise<T>,\n ): Promise<T> => {\n const release = await lock.acquire();\n try {\n return await pg.transaction(async (pgTx) => {\n const { state, nested, scoped } = createScopedDatabase(pgTx);\n try {\n const result = await callback(scoped);\n await nested.drain();\n await state.assertCommittable();\n return result;\n } catch (error) {\n // A rejected Promise.all can leave queued savepoint work running;\n // PGlite rolls back as soon as this callback settles.\n await nested.drain();\n await state.tracker.drain();\n throw error;\n }\n });\n } finally {\n release();\n }\n };\n\n /**\n * Begins a transaction and returns a handle for manual control.\n *\n * PGlite only offers a callback form, so the callback is held open until the\n * handle is committed or rolled back.\n */\n const beginTransaction = async (): Promise<TransactionHandle> => {\n const release = await lock.acquire();\n try {\n let settle!: (outcome: 'commit' | 'rollback') => void;\n const outcome = new Promise<'commit' | 'rollback'>((resolve) => {\n settle = resolve;\n });\n let begun!: (tx: PGliteTransactionLike) => void;\n let failedToBegin!: (error: unknown) => void;\n const started = new Promise<PGliteTransactionLike>((resolve, reject) => {\n begun = resolve;\n failedToBegin = reject;\n });\n const finished = pg.transaction(async (pgTx) => {\n begun(pgTx);\n if ((await outcome) === 'rollback') await pgTx.rollback();\n });\n // BEGIN failing rejects `finished` before the callback ever runs.\n finished.catch(failedToBegin);\n const pgTx = await started;\n\n let active = true;\n const { state, nested, scoped } = createScopedDatabase(pgTx, () => ({\n commit: () => end('COMMIT'),\n rollback: () => end('ROLLBACK'),\n isActive: () => active,\n }));\n\n // The transaction is over however this ends, so the connection is\n // returned whether COMMIT succeeds, throws, or was never reachable.\n const end = async (command: 'COMMIT' | 'ROLLBACK'): Promise<void> => {\n if (!active) {\n throw new DatabaseError('Transaction already ended', {});\n }\n active = false;\n let failure: { error: unknown } | undefined;\n try {\n await nested.drain();\n await state.tracker.drain();\n if (command === 'COMMIT') await state.assertCommittable();\n } catch (error) {\n failure = { error };\n }\n settle(command === 'COMMIT' && !failure ? 'commit' : 'rollback');\n try {\n await finished;\n } catch (error) {\n failure ??= { error };\n } finally {\n release();\n }\n if (failure) throw failure.error;\n };\n\n return scoped as TransactionHandle;\n } catch (error) {\n release();\n throw error;\n }\n };\n\n const initializeSchemas = async (\n schemaOptions: SchemaInitializationOptions,\n ): Promise<void> => {\n const schemaManager = new DatabaseSchemaManager();\n const currentDb: DatabaseInterface = {\n url: dataDir,\n client: pg,\n insert,\n update,\n upsert,\n get,\n getOrInsert,\n delete: deleteRecords,\n count,\n list,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n transaction,\n };\n\n await schemaManager.initializeSchemas(currentDb, schemaOptions);\n };\n\n db = {\n url: dataDir,\n client: pg,\n insert,\n update,\n upsert,\n get,\n getOrInsert,\n delete: deleteRecords,\n count,\n list,\n table,\n many,\n single,\n pluck,\n execute,\n query,\n oo,\n oO,\n ox,\n xx,\n tableExists,\n syncSchema,\n initializeSchemas,\n transaction,\n beginTransaction,\n getTableSchema,\n alterTable,\n vector: createVectorCapabilities(client),\n close: async () => {\n closePromise ??= (async () => {\n connectionCache.forget(db);\n if (ownsClient) await pg.close();\n })();\n return closePromise;\n },\n };\n\n return db;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,iBAAiB;;;;AAKvB,IAAa,yBAAb,cAA4C,cAAc;CACxD,YAAY,OAAiB;EAC3B,MACE,+DAA+D,eAAe,+BAA+B,eAAe,mCAC5H;GAAE,SAAS;GAAU,MAAM;EAAe,GAC1C,KACF;EACA,KAAK,OAAO;CACd;AACF;AAkHA,IAAM,kBAAkB,IAAI,gBAAmC;;;;;AAM/D,eAAsB,6BAA4C;CAChE,MAAM,gBAAgB,MAAM,OAAO,OAAO;EACxC,MAAM,GAAG,QAAQ;CACnB,CAAC;AACH;;;;;AAkBA,eAAsB,eAAsC;CAC1D,IAAI;EACF,MAAM,SAAU,MAAM,OAAO;EAG7B,MAAM,SACJ,OAAO,QAAQ,QAAQ,WAAW,aAAa,SAAS,QAAQ;EAClE,IAAI,OAAO,QAAQ,QAAQ,WAAW,YACpC,MAAM,IAAI,MAAM,GAAG,eAAe,uBAAuB;EAE3D,OAAO;CACT,SAAS,OAAO;EACd,MAAM,IAAI,uBAAuB,KAAK;CACxC;AACF;AAEA,IAAM,YAAY;AAElB,IAAM,cAAc,YAClB,YAAY,MAAM,QAAQ,WAAW,SAAS;;;;;;;;AAShD,eAAsB,YACpB,UAAyB,CAAC,GACE;CAC5B,6BAA6B,OAAO;CACpC,MAAM,UAAU,QAAQ,WAAW,QAAQ,OAAO;CAClD,MAAM,MAAM,QAAQ,SAChB,KAAA,IACA,QAAQ,OACN,aAAa,QAAQ,SACrB,WAAW,OAAO,IAChB,KAAA,IACA,cAAc;CACtB,OAAO,gBAAgB,YACrB,KACA,eACM,eAAe,SAAS,OAAO,GACrC,OAAO,OAAO;EACZ,MAAM,GAAG,QAAQ;CACnB,CACF;AACF;;;;;AAMA,eAAe,eACb,SACA,SAC4B;CAC5B,MAAM,SAAS,MAAM,aAAa;CAClC,MAAM,oBAAoB,UACxB,iBAAiB,OAAO,SAAS;CAEnC,MAAM,aAAa,CAAC,QAAQ;CAC5B,MAAM,KACJ,QAAQ,UACP,MAAM,OAAO,OAAO,OAAO,SAAS;EACnC,GAAG,QAAQ;EACX,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;CACjE,CAAC;CAOH,MAAM,OAAO,sBAAsB,UAAU,QAAQ,uBAAuB;CAE5E,IAAI;CACJ,IAAI;;;;;CAQJ,MAAM,MAAM,OACV,QACA,KACA,WACsD;EACtD,MAAM,SACJ,UAAU,OAAO,SAAS,IACtB,MAAM,OAAO,MAAM,KAAK,MAAM,KAC7B,MAAM,OAAO,KAAK,GAAG,EAAA,CAAG,GAAG,EAAE;EACpC,IAAI,CAAC,QAAQ,OAAO;GAAE,MAAM,CAAC;GAAG,UAAU;EAAE;EAC5C,OAAO;GACL,MAAM,OAAO;GACb,UAAU,OAAO,YAAY,OAAO,gBAAgB,OAAO,KAAK;EAClE;CACF;CAGA,MAAM,SAAgC,EACpC,QAAQ,KAAK,WAAW,KAAK,UAAU,IAAI,IAAI,KAAK,MAAM,CAAC,EAC7D;;;;;;CAOA,MAAM,0BAA0B,SAAgC;EAC9D,MAAM,UAAU,8BAA8B,gBAAgB;EAC9D,MAAM,WAAkC,EACtC,QAAQ,KAAK,WAAW,QAAQ,MAAM,IAAI,MAAM,KAAK,MAAM,GAAG,GAAG,EACnE;EAGA,MAAM,YAAY;GAChB,QAAQ,KAAa,WACnB,QAAQ,MAAM,KAAK,MAAM,KAAK,MAAM,GAAG,GAAG;GAC5C,OAAO,QAAgB,QAAQ,MAAM,KAAK,KAAK,GAAG,GAAG,GAAG;EAC1D;;;;;;;EAQA,MAAM,oBAAoB,YAA2B;GACnD,MAAM,QAAQ,MAAM;GACpB,MAAM,iBAAiB,QAAQ,SAAS;GACxC,IAAI,mBAAmB,KAAA,GAAW,MAAM;GACxC,IAAI;IACF,MAAM,KAAK,MAAM,UAAU;GAC7B,SAAS,OAAO;IACd,MAAM,QAAQ,SAAS,KAAK,QAAQ,oBAAoB,KAAK;GAC/D;GACA,MAAM,cAAc,QAAQ,oBAAoB;GAChD,IAAI,gBAAgB,KAAA,GAAW,MAAM;EACvC;EAEA,OAAO;GAAE;GAAS;GAAU;GAAW;EAAkB;CAC3D;CAgBA,MAAM,EAAE,iBAAiB,qBAAqB,4BAd9B,sBAAsB;EACpC;EAEA,gBAAgB,aAAa;EAC7B,mBAAmB,SACjB,KAAK,UACH,GAAG,YAAY,OAAO,SAAS;GAC7B,MAAM,QAAQ,uBAAuB,IAAI;GACzC,MAAM,SAAS,MAAM,KAAK,MAAM,QAAQ;GACxC,MAAM,MAAM,kBAAkB;GAC9B,OAAO;EACT,CAAC,CACH;CACJ,CAEE;CAEF,MAAM,EACJ,QACA,KACA,MACA,QACA,QACA,aACA,QAAQ,eACR,OACA,OACA,MACA,QACA,OACA,SACA,OACA,IACA,IACA,IACA,IACA,aACA,eACE,oBAAoB,QAAQ,OAAO,eAAe;CACtD,MAAM,iBAAiB,qBAAqB;EAAE;EAAM;CAAY,CAAC;CACjE,MAAM,aAAa,iBAAiB,MAAM;;;;;;;;CAS1C,MAAM,wBACJ,MACA,eAAuE,CAAC,OACrE;EACH,MAAM,QAAQ,uBAAuB,IAAI;EACzC,IAAI;EACJ,MAAM,SAAS,wBACb,MAAM,WACL,aAAc,WAAW;GAAE,GAAG;GAAQ,aAAa;EAAS,IAAI,QACjE,MAAM,QAAQ,UAChB;EACA,SAAS;GACP,KAAK;GACL,QAAQ,MAAM;GACd,GAAG,oBACD,MAAM,UACN,MACA,iBACA,MAAM,QAAQ,UAChB;GACA,aAAa;EACf;EACA,OAAO,OAAO,QAAQ,MAAM,MAAM,CAAC;EACnC,OAAO;GAAE;GAAO;GAAQ;EAAO;CACjC;;;;;CAMA,MAAM,cAAc,OAClB,aACe;EACf,MAAM,UAAU,MAAM,KAAK,QAAQ;EACnC,IAAI;GACF,OAAO,MAAM,GAAG,YAAY,OAAO,SAAS;IAC1C,MAAM,EAAE,OAAO,QAAQ,WAAW,qBAAqB,IAAI;IAC3D,IAAI;KACF,MAAM,SAAS,MAAM,SAAS,MAAM;KACpC,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,kBAAkB;KAC9B,OAAO;IACT,SAAS,OAAO;KAGd,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,QAAQ,MAAM;KAC1B,MAAM;IACR;GACF,CAAC;EACH,UAAU;GACR,QAAQ;EACV;CACF;;;;;;;CAQA,MAAM,mBAAmB,YAAwC;EAC/D,MAAM,UAAU,MAAM,KAAK,QAAQ;EACnC,IAAI;GACF,IAAI;GACJ,MAAM,UAAU,IAAI,SAAgC,YAAY;IAC9D,SAAS;GACX,CAAC;GACD,IAAI;GACJ,IAAI;GACJ,MAAM,UAAU,IAAI,SAAgC,SAAS,WAAW;IACtE,QAAQ;IACR,gBAAgB;GAClB,CAAC;GACD,MAAM,WAAW,GAAG,YAAY,OAAO,SAAS;IAC9C,MAAM,IAAI;IACV,IAAK,MAAM,YAAa,YAAY,MAAM,KAAK,SAAS;GAC1D,CAAC;GAED,SAAS,MAAM,aAAa;GAC5B,MAAM,OAAO,MAAM;GAEnB,IAAI,SAAS;GACb,MAAM,EAAE,OAAO,QAAQ,WAAW,qBAAqB,aAAa;IAClE,cAAc,IAAI,QAAQ;IAC1B,gBAAgB,IAAI,UAAU;IAC9B,gBAAgB;GAClB,EAAE;GAIF,MAAM,MAAM,OAAO,YAAkD;IACnE,IAAI,CAAC,QACH,MAAM,IAAI,cAAc,6BAA6B,CAAC,CAAC;IAEzD,SAAS;IACT,IAAI;IACJ,IAAI;KACF,MAAM,OAAO,MAAM;KACnB,MAAM,MAAM,QAAQ,MAAM;KAC1B,IAAI,YAAY,UAAU,MAAM,MAAM,kBAAkB;IAC1D,SAAS,OAAO;KACd,UAAU,EAAE,MAAM;IACpB;IACA,OAAO,YAAY,YAAY,CAAC,UAAU,WAAW,UAAU;IAC/D,IAAI;KACF,MAAM;IACR,SAAS,OAAO;KACd,YAAY,EAAE,MAAM;IACtB,UAAU;KACR,QAAQ;IACV;IACA,IAAI,SAAS,MAAM,QAAQ;GAC7B;GAEA,OAAO;EACT,SAAS,OAAO;GACd,QAAQ;GACR,MAAM;EACR;CACF;CAEA,MAAM,oBAAoB,OACxB,kBACkB;EAClB,MAAM,gBAAgB,IAAI,sBAAsB;EAChD,MAAM,YAA+B;GACnC,KAAK;GACL,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA,QAAQ;GACR;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF;EAEA,MAAM,cAAc,kBAAkB,WAAW,aAAa;CAChE;CAEA,KAAK;EACH,KAAK;EACL,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ,yBAAyB,MAAM;EACvC,OAAO,YAAY;GACjB,kBAAkB,YAAY;IAC5B,gBAAgB,OAAO,EAAE;IACzB,IAAI,YAAY,MAAM,GAAG,MAAM;GACjC,EAAA,CAAG;GACH,OAAO;EACT;CACF;CAEA,OAAO;AACT"}
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe entry for the driver-independent query helpers.
|
|
3
|
+
*
|
|
4
|
+
* Everything here is pure SQL-fragment construction or operates on an
|
|
5
|
+
* already-open `DatabaseInterface`. Nothing on this import graph reaches `pg`,
|
|
6
|
+
* the libsql or DuckDB adapters, or any `node:` built-in, so browser-bundled
|
|
7
|
+
* packages can import it instead of the package root. The root entry
|
|
8
|
+
* re-exports all of it.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { buildWhere, validateColumnName } from '@happyvertical/sql/query';
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
import type { DatabaseInterface } from './shared/types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a table exists in the database
|
|
20
|
+
*
|
|
21
|
+
* @param db - Database interface to use
|
|
22
|
+
* @param tableName - Name of the table to check
|
|
23
|
+
* @returns Promise resolving to boolean indicating if the table exists
|
|
24
|
+
*/
|
|
25
|
+
export declare function tableExists(db: DatabaseInterface, tableName: string): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Escapes and formats a value for use in SQL queries
|
|
28
|
+
*
|
|
29
|
+
* @param value - Value to escape
|
|
30
|
+
* @returns String representation of the value safe for SQL use
|
|
31
|
+
*/
|
|
32
|
+
export declare function escapeSqlValue(value: any): string;
|
|
33
|
+
/**
|
|
34
|
+
* Validates a column name for use in SQL queries
|
|
35
|
+
*
|
|
36
|
+
* @param column - Column name to validate
|
|
37
|
+
* @returns The validated column name
|
|
38
|
+
* @throws Error if the column name contains invalid characters
|
|
39
|
+
*/
|
|
40
|
+
export declare function validateColumnName(column: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Synchronizes a SQL schema definition with a database
|
|
43
|
+
* Creates tables if they don't exist and adds missing columns to existing tables
|
|
44
|
+
*
|
|
45
|
+
* @param options - Object containing database and schema
|
|
46
|
+
* @param options.db - Database interface to use
|
|
47
|
+
* @param options.schema - SQL schema definition
|
|
48
|
+
* @throws Error if db or schema are missing or if the database doesn't support syncSchema
|
|
49
|
+
*/
|
|
50
|
+
export declare function syncSchema(options: {
|
|
51
|
+
db: DatabaseInterface;
|
|
52
|
+
schema: string;
|
|
53
|
+
}): Promise<void>;
|
|
54
|
+
export { type AggregateBuildResult, type AggregateFunction, type AggregateSelectExpr, type AggregateSpec, type AggregateTimeBucketUnit, bucketExpr, buildAggregate, } from './aggregate.js';
|
|
55
|
+
export type { SchemaInitializationResult } from './schema-manager.js';
|
|
56
|
+
export { DatabaseSchemaManager } from './schema-manager.js';
|
|
57
|
+
export { convertUniqueIndexesToInlineConstraints } from './shared/duckdb-schema-utils.js';
|
|
58
|
+
export * from './shared/types.js';
|
|
59
|
+
export type { RawSqlKey, SqlAdapterType } from './shared/utils.js';
|
|
60
|
+
export { buildWhere, formatDbError, raw } from './shared/utils.js';
|
|
61
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,EAAE,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,oBAEzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,CAejD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CASzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE;IACxC,EAAE,EAAE,iBAAiB,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB,iBAYA;AAED,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,uBAAuB,EAC5B,UAAU,EACV,cAAc,GACf,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,uCAAuC,EAAE,MAAM,iCAAiC,CAAC;AAC1F,cAAc,mBAAmB,CAAC;AAClC,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as raw, n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./chunks/schema-manager-cxsySSDA.js";
|
|
2
|
+
import { a as NestedTransactionError, c as bucketExpr, i as validateColumnName, l as buildAggregate, n as syncSchema, o as resolveSchemas, r as tableExists, s as convertUniqueIndexesToInlineConstraints, t as escapeSqlValue } from "./chunks/query-RzXJ4B9s.js";
|
|
3
|
+
export { DatabaseSchemaManager, NestedTransactionError, bucketExpr, buildAggregate, buildWhere, convertUniqueIndexesToInlineConstraints, escapeSqlValue, formatDbError, raw, resolveSchemas, syncSchema, tableExists, validateColumnName };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/sql",
|
|
3
|
-
"version": "0.102.
|
|
3
|
+
"version": "0.102.6",
|
|
4
4
|
"description": "Database interface with support for SQLite, PostgreSQL, and DuckDB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"import": "./dist/backup/index.js",
|
|
38
38
|
"types": "./dist/backup/index.d.ts"
|
|
39
39
|
},
|
|
40
|
+
"./query": {
|
|
41
|
+
"import": "./dist/query.js",
|
|
42
|
+
"types": "./dist/query.d.ts"
|
|
43
|
+
},
|
|
40
44
|
"./pglite": {
|
|
41
45
|
"import": "./dist/pglite.js",
|
|
42
46
|
"types": "./dist/pglite.d.ts"
|
|
@@ -47,7 +51,7 @@
|
|
|
47
51
|
"license": "ISC",
|
|
48
52
|
"dependencies": {
|
|
49
53
|
"@duckdb/node-api": "1.4.3-r.3",
|
|
50
|
-
"@happyvertical/utils": "0.102.
|
|
54
|
+
"@happyvertical/utils": "0.102.6",
|
|
51
55
|
"@libsql/client": "0.17.2",
|
|
52
56
|
"pg": "^8.20.0",
|
|
53
57
|
"sqlite-vss": "^0.1.2"
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
//#region src/shared/duckdb-schema-utils.ts
|
|
2
|
-
/**
|
|
3
|
-
* Parses a CREATE INDEX statement to extract index details
|
|
4
|
-
*
|
|
5
|
-
* @param indexSQL - CREATE INDEX statement
|
|
6
|
-
* @returns Parsed index definition or null if parsing fails
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```typescript
|
|
10
|
-
* const index = parseIndexStatement(
|
|
11
|
-
* 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'
|
|
12
|
-
* );
|
|
13
|
-
* // Returns: {
|
|
14
|
-
* // name: 'ds_slug_context_idx',
|
|
15
|
-
* // columns: ['slug', 'context'],
|
|
16
|
-
* // unique: true,
|
|
17
|
-
* // sql: '...'
|
|
18
|
-
* // }
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
function parseIndexStatement(indexSQL) {
|
|
22
|
-
const trimmed = indexSQL.trim();
|
|
23
|
-
const match = trimmed.match(/CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+)\s+ON\s+(?:"?\w+"?)\s*\(([^)]+)\)/i);
|
|
24
|
-
if (!match) return null;
|
|
25
|
-
const [, uniqueKeyword, name, columnsStr] = match;
|
|
26
|
-
return {
|
|
27
|
-
name,
|
|
28
|
-
columns: columnsStr.split(",").map((col) => col.trim().replace(/^"|"$/g, "")),
|
|
29
|
-
unique: Boolean(uniqueKeyword),
|
|
30
|
-
sql: trimmed
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility
|
|
35
|
-
*
|
|
36
|
-
* DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.
|
|
37
|
-
* This function transforms SMRT-generated schemas to work with DuckDB's limitations.
|
|
38
|
-
*
|
|
39
|
-
* @param ddl - CREATE TABLE DDL statement
|
|
40
|
-
* @param indexes - Array of CREATE INDEX statements (optional)
|
|
41
|
-
* @returns Transformed DDL with inline UNIQUE constraints and filtered index list
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```typescript
|
|
45
|
-
* const ddl = `
|
|
46
|
-
* CREATE TABLE ds (
|
|
47
|
-
* id TEXT PRIMARY KEY,
|
|
48
|
-
* slug TEXT NOT NULL,
|
|
49
|
-
* context TEXT NOT NULL
|
|
50
|
-
* )
|
|
51
|
-
* `;
|
|
52
|
-
* const indexes = [
|
|
53
|
-
* 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'
|
|
54
|
-
* ];
|
|
55
|
-
*
|
|
56
|
-
* const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);
|
|
57
|
-
* // result.ddl:
|
|
58
|
-
* // CREATE TABLE ds (
|
|
59
|
-
* // id TEXT PRIMARY KEY,
|
|
60
|
-
* // slug TEXT NOT NULL,
|
|
61
|
-
* // context TEXT NOT NULL,
|
|
62
|
-
* // UNIQUE(slug, context)
|
|
63
|
-
* // )
|
|
64
|
-
* // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
|
-
function convertUniqueIndexesToInlineConstraints(ddl, indexes) {
|
|
68
|
-
if (!indexes || indexes.length === 0) return {
|
|
69
|
-
ddl,
|
|
70
|
-
indexes: []
|
|
71
|
-
};
|
|
72
|
-
const parsedIndexes = indexes.map(parseIndexStatement).filter((idx) => idx !== null);
|
|
73
|
-
const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);
|
|
74
|
-
const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);
|
|
75
|
-
if (uniqueIndexes.length === 0) return {
|
|
76
|
-
ddl,
|
|
77
|
-
indexes
|
|
78
|
-
};
|
|
79
|
-
const lines = ddl.split("\n");
|
|
80
|
-
let closingParenIndex = -1;
|
|
81
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
82
|
-
const trimmed = lines[i].trim();
|
|
83
|
-
if (trimmed === ")" || trimmed.startsWith(");")) {
|
|
84
|
-
closingParenIndex = i;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (closingParenIndex === -1) {
|
|
89
|
-
console.warn("[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement");
|
|
90
|
-
return {
|
|
91
|
-
ddl,
|
|
92
|
-
indexes
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
const uniqueConstraints = uniqueIndexes.map((idx) => {
|
|
96
|
-
return ` UNIQUE(${idx.columns.map((col) => `"${col}"`).join(", ")})`;
|
|
97
|
-
});
|
|
98
|
-
const modifiedLines = [...lines];
|
|
99
|
-
modifiedLines[closingParenIndex].trim();
|
|
100
|
-
if (closingParenIndex > 0) {
|
|
101
|
-
if (!modifiedLines[closingParenIndex - 1].trim().endsWith(",")) modifiedLines[closingParenIndex - 1] = modifiedLines[closingParenIndex - 1].replace(/\s*$/, ",");
|
|
102
|
-
}
|
|
103
|
-
const constraintLines = uniqueConstraints.map((constraint, idx) => {
|
|
104
|
-
return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;
|
|
105
|
-
});
|
|
106
|
-
modifiedLines.splice(closingParenIndex, 0, ...constraintLines);
|
|
107
|
-
return {
|
|
108
|
-
ddl: modifiedLines.join("\n"),
|
|
109
|
-
indexes: regularIndexes.map((idx) => idx.sql)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region src/shared/duckdb-resources.ts
|
|
114
|
-
/** Best-effort DuckDB resource closer. Successful resources are never closed twice. */
|
|
115
|
-
function createDuckDBResourceCloser(connection, instance) {
|
|
116
|
-
let connectionClosed = !connection;
|
|
117
|
-
let instanceClosed = !instance;
|
|
118
|
-
let active;
|
|
119
|
-
return async () => {
|
|
120
|
-
if (connectionClosed && instanceClosed) return;
|
|
121
|
-
active ??= (async () => {
|
|
122
|
-
const errors = [];
|
|
123
|
-
if (!connectionClosed) try {
|
|
124
|
-
connection?.closeSync();
|
|
125
|
-
connectionClosed = true;
|
|
126
|
-
} catch (error) {
|
|
127
|
-
errors.push(error);
|
|
128
|
-
}
|
|
129
|
-
if (!instanceClosed) try {
|
|
130
|
-
instance?.closeSync();
|
|
131
|
-
instanceClosed = true;
|
|
132
|
-
} catch (error) {
|
|
133
|
-
errors.push(error);
|
|
134
|
-
}
|
|
135
|
-
if (errors.length === 1) throw errors[0];
|
|
136
|
-
if (errors.length > 1) throw new AggregateError(errors, "Failed to close DuckDB resources");
|
|
137
|
-
})();
|
|
138
|
-
try {
|
|
139
|
-
await active;
|
|
140
|
-
} finally {
|
|
141
|
-
active = void 0;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
async function throwWithDuckDBCleanup(error, close) {
|
|
146
|
-
try {
|
|
147
|
-
await close();
|
|
148
|
-
} catch (cleanupError) {
|
|
149
|
-
throw new AggregateError([error, cleanupError], "DuckDB initialization and cleanup failed");
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
//#endregion
|
|
154
|
-
export { throwWithDuckDBCleanup as n, convertUniqueIndexesToInlineConstraints as r, createDuckDBResourceCloser as t };
|
|
155
|
-
|
|
156
|
-
//# sourceMappingURL=duckdb-resources-Cci6Lo6H.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"duckdb-resources-Cci6Lo6H.js","names":[],"sources":["../../src/shared/duckdb-schema-utils.ts","../../src/shared/duckdb-resources.ts"],"sourcesContent":["/**\n * DuckDB-specific schema transformation utilities\n *\n * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses\n * fail with UNIQUE INDEX but work with inline UNIQUE constraints.\n *\n * This module provides utilities to transform SMRT-generated schemas\n * (which use separate CREATE UNIQUE INDEX statements) into DuckDB-compatible\n * schemas (which use inline UNIQUE constraints).\n */\n\n/**\n * Index definition extracted from CREATE INDEX statement\n */\ninterface ParsedIndex {\n name: string;\n columns: string[];\n unique: boolean;\n sql: string;\n}\n\n/**\n * Parses a CREATE INDEX statement to extract index details\n *\n * @param indexSQL - CREATE INDEX statement\n * @returns Parsed index definition or null if parsing fails\n *\n * @example\n * ```typescript\n * const index = parseIndexStatement(\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * );\n * // Returns: {\n * // name: 'ds_slug_context_idx',\n * // columns: ['slug', 'context'],\n * // unique: true,\n * // sql: '...'\n * // }\n * ```\n */\nexport function parseIndexStatement(indexSQL: string): ParsedIndex | null {\n const trimmed = indexSQL.trim();\n\n // Match: CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (columns)\n // Support both quoted and unquoted identifiers: table, \"table\", columns, \"columns\"\n const match = trimmed.match(\n /CREATE\\s+(UNIQUE\\s+)?INDEX\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?(\\w+)\\s+ON\\s+(?:\"?\\w+\"?)\\s*\\(([^)]+)\\)/i,\n );\n\n if (!match) {\n return null;\n }\n\n const [, uniqueKeyword, name, columnsStr] = match;\n\n // Remove quotes from column names\n const columns = columnsStr\n .split(',')\n .map((col) => col.trim().replace(/^\"|\"$/g, ''));\n\n return {\n name,\n columns,\n unique: Boolean(uniqueKeyword),\n sql: trimmed,\n };\n}\n\n/**\n * Converts UNIQUE INDEX statements to inline UNIQUE constraints for DuckDB compatibility\n *\n * DuckDB's ON CONFLICT clause requires inline UNIQUE constraints, not separate indexes.\n * This function transforms SMRT-generated schemas to work with DuckDB's limitations.\n *\n * @param ddl - CREATE TABLE DDL statement\n * @param indexes - Array of CREATE INDEX statements (optional)\n * @returns Transformed DDL with inline UNIQUE constraints and filtered index list\n *\n * @example\n * ```typescript\n * const ddl = `\n * CREATE TABLE ds (\n * id TEXT PRIMARY KEY,\n * slug TEXT NOT NULL,\n * context TEXT NOT NULL\n * )\n * `;\n * const indexes = [\n * 'CREATE UNIQUE INDEX ds_slug_context_idx ON ds (slug, context)'\n * ];\n *\n * const result = convertUniqueIndexesToInlineConstraints(ddl, indexes);\n * // result.ddl:\n * // CREATE TABLE ds (\n * // id TEXT PRIMARY KEY,\n * // slug TEXT NOT NULL,\n * // context TEXT NOT NULL,\n * // UNIQUE(slug, context)\n * // )\n * // result.indexes: [] (UNIQUE index removed, non-UNIQUE indexes preserved)\n * ```\n */\nexport function convertUniqueIndexesToInlineConstraints(\n ddl: string,\n indexes?: string[],\n): {\n ddl: string;\n indexes: string[];\n} {\n if (!indexes || indexes.length === 0) {\n return { ddl, indexes: [] };\n }\n\n // Parse all indexes\n const parsedIndexes = indexes\n .map(parseIndexStatement)\n .filter((idx): idx is ParsedIndex => idx !== null);\n\n // Separate UNIQUE indexes from regular indexes\n const uniqueIndexes = parsedIndexes.filter((idx) => idx.unique);\n const regularIndexes = parsedIndexes.filter((idx) => !idx.unique);\n\n // If no UNIQUE indexes, return unchanged\n if (uniqueIndexes.length === 0) {\n return { ddl, indexes };\n }\n\n // Find the closing parenthesis of the CREATE TABLE statement\n const lines = ddl.split('\\n');\n let closingParenIndex = -1;\n\n for (let i = lines.length - 1; i >= 0; i--) {\n const trimmed = lines[i].trim();\n if (trimmed === ')' || trimmed.startsWith(');')) {\n closingParenIndex = i;\n break;\n }\n }\n\n if (closingParenIndex === -1) {\n // Can't find closing paren - return unchanged\n console.warn(\n '[duckdb-schema-utils] Could not find closing parenthesis in CREATE TABLE statement',\n );\n return { ddl, indexes };\n }\n\n // Build inline UNIQUE constraints with quoted column names\n const uniqueConstraints = uniqueIndexes.map((idx) => {\n const columns = idx.columns.map((col) => `\"${col}\"`).join(', ');\n return ` UNIQUE(${columns})`;\n });\n\n // Insert UNIQUE constraints before closing parenthesis\n const modifiedLines = [...lines];\n const closingLine = modifiedLines[closingParenIndex].trim();\n\n // Add comma to previous line if it doesn't have one\n if (closingParenIndex > 0) {\n const prevLine = modifiedLines[closingParenIndex - 1].trim();\n if (!prevLine.endsWith(',')) {\n modifiedLines[closingParenIndex - 1] = modifiedLines[\n closingParenIndex - 1\n ].replace(/\\s*$/, ',');\n }\n }\n\n // Insert UNIQUE constraints\n const constraintLines = uniqueConstraints.map((constraint, idx) => {\n // Add comma after all but the last constraint\n return idx < uniqueConstraints.length - 1 ? `${constraint},` : constraint;\n });\n\n modifiedLines.splice(closingParenIndex, 0, ...constraintLines);\n\n const transformedDDL = modifiedLines.join('\\n');\n\n // Return regular (non-UNIQUE) indexes only\n const remainingIndexes = regularIndexes.map((idx) => idx.sql);\n\n return {\n ddl: transformedDDL,\n indexes: remainingIndexes,\n };\n}\n\n/**\n * Checks if a schema definition contains UNIQUE indexes that need conversion\n *\n * @param indexes - Array of CREATE INDEX statements\n * @returns True if any UNIQUE indexes are present\n */\nexport function hasUniqueIndexes(indexes?: string[]): boolean {\n if (!indexes || indexes.length === 0) {\n return false;\n }\n\n return indexes.some((indexSQL) => {\n const parsed = parseIndexStatement(indexSQL);\n return parsed?.unique === true;\n });\n}\n","type SyncClosable = { closeSync(): void };\n\n/** Best-effort DuckDB resource closer. Successful resources are never closed twice. */\nexport function createDuckDBResourceCloser(\n connection: SyncClosable | undefined,\n instance: SyncClosable | undefined,\n): () => Promise<void> {\n let connectionClosed = !connection;\n let instanceClosed = !instance;\n let active: Promise<void> | undefined;\n\n return async () => {\n if (connectionClosed && instanceClosed) return;\n active ??= (async () => {\n const errors: unknown[] = [];\n if (!connectionClosed) {\n try {\n connection?.closeSync();\n connectionClosed = true;\n } catch (error) {\n errors.push(error);\n }\n }\n if (!instanceClosed) {\n try {\n instance?.closeSync();\n instanceClosed = true;\n } catch (error) {\n errors.push(error);\n }\n }\n if (errors.length === 1) throw errors[0];\n if (errors.length > 1)\n throw new AggregateError(errors, 'Failed to close DuckDB resources');\n })();\n try {\n await active;\n } finally {\n active = undefined;\n }\n };\n}\n\nexport async function throwWithDuckDBCleanup(\n error: unknown,\n close: () => Promise<void>,\n): Promise<never> {\n try {\n await close();\n } catch (cleanupError) {\n throw new AggregateError(\n [error, cleanupError],\n 'DuckDB initialization and cleanup failed',\n );\n }\n throw error;\n}\n"],"mappings":"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"}
|