import { getPrimaryKeyColumns } from "@/drizzle/index.js"; import { BigIntSerializationError, NotNullConstraintError, } from "@/internal/errors.js"; import { prettyPrint } from "@/utils/print.js"; import { type Column, type SQL, type SQLWrapper, type Table, and, eq, getTableColumns, getTableName, } from "drizzle-orm"; import { PgArray } from "drizzle-orm/pg-core"; /** * Returns true if the column has a "default" value that is used when no value is passed. * Handles `.default`, `.$defaultFn()`, `.$onUpdateFn()`. */ export const hasEmptyValue = (column: Column) => { return column.hasDefault; }; /** Returns the "default" value for `column`. */ export const getEmptyValue = (column: Column, isUpdate: boolean) => { if (isUpdate && column.onUpdateFn) { return column.onUpdateFn(); } if (column.default !== undefined) return column.default; if (column.defaultFn !== undefined) return column.defaultFn(); if (column.onUpdateFn !== undefined) return column.onUpdateFn(); // TODO(kyle) is it an invariant that it doesn't get here return undefined; }; export const getPrimaryKeyCache = (tables: Table[]) => { const cache = new Map(); for (const table of tables) { cache.set(table, []); for (const { js } of getPrimaryKeyColumns(table)) { // @ts-expect-error cache.get(table)!.push([js, table[js]!]); } } return cache; }; export const normalizeColumn = ( column: Column, value: unknown, isUpdate: boolean, ): unknown => { if (value === undefined) { if (hasEmptyValue(column)) return getEmptyValue(column, isUpdate); return null; } if (value === null) return null; if (column.mapToDriverValue === undefined) return value; try { if (Array.isArray(value) && column instanceof PgArray) { return value.map((v) => { if (column.baseColumn.columnType === "PgTimestamp") { return v; } return column.baseColumn.mapFromDriverValue( column.baseColumn.mapToDriverValue(v), ); }); } if (column.columnType === "PgTimestamp") { return value; } return column.mapFromDriverValue(column.mapToDriverValue(value)); } catch (e) { if ( (e as Error)?.message?.includes("Do not know how to serialize a BigInt") ) { const error = new BigIntSerializationError((e as Error).message); error.meta.push( "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); throw error; } throw e; } }; export const normalizeRow = ( table: Table, row: { [key: string]: unknown }, isUpdate: boolean, ) => { for (const [columnName, column] of Object.entries(getTableColumns(table))) { // not-null constraint if ( isUpdate === false && (row[columnName] === undefined || row[columnName] === null) && column.notNull && hasEmptyValue(column) === false ) { const error = new NotNullConstraintError( `Column '${getTableName( table, )}.${columnName}' violates not-null constraint.`, ); error.meta.push(`db.insert arguments:\n${prettyPrint(row)}`); throw error; } row[columnName] = normalizeColumn(column, row[columnName], isUpdate); } return row; }; export const getCacheKey = ( table: Table, key: object, cache: Map, ): string => { const primaryKeys = cache.get(table)!; if (primaryKeys.length === 1) { const [pk, col] = primaryKeys[0]!; // @ts-expect-error return String(normalizeColumn(col, key[pk]!, false)); } let result = ""; for (let i = 0; i < primaryKeys.length; i++) { const [pk, col] = primaryKeys[i]!; // @ts-expect-error const value = normalizeColumn(col, key[pk]!, false); result += `${String(value)}_`; } return result; }; /** Returns an sql where condition for `table` with `key`. */ export const getWhereCondition = (table: Table, key: Object): SQL => { const conditions: SQLWrapper[] = []; for (const { js } of getPrimaryKeyColumns(table)) { // @ts-ignore conditions.push(eq(table[js]!, key[js])); } return and(...conditions)!; };