diff --git a/packages/core/src/database/index.test.ts b/packages/core/src/database/index.test.ts index 8d5e3576b..1ffb2dfa0 100644 --- a/packages/core/src/database/index.test.ts +++ b/packages/core/src/database/index.test.ts @@ -14,6 +14,7 @@ import { } from "@/utils/checkpoint.js"; import { wait } from "@/utils/wait.js"; import { sql } from "kysely"; +import { hexToBytes, zeroAddress } from "viem"; import { beforeEach, expect, test } from "vitest"; import { type Database, createDatabase } from "./index.js"; @@ -58,6 +59,40 @@ const schemaTwo = createSchema((p) => ({ }), })); +const dogWithDefaults = createSchema((p) => ({ + Dog: p.createTable({ + id: p.string().default("0"), + name: p.string().default("firstname"), + age: p.int().default(5).optional(), + bigAge: p.bigint().default(5n).optional(), + bowl: p.hex().default(zeroAddress), + toys: p.json().default({ + bone: "sofa", + ball: "bed", + }), + commands: p.json().default([ + "sit", + "stay", + { + paw: { + right: true, + left: false, + }, + }, + ]), + }), +})); + +type Dog = { + id: string; + name: string; + age: string; + bigAge: bigint; + bowl: string; + toys: {}; + commands: (string | {})[]; +}; + function createCheckpoint(index: number): Checkpoint { return { ...zeroCheckpoint, blockTimestamp: index }; } @@ -759,6 +794,97 @@ test("revert() updates versions with intermediate logs", async (context) => { await cleanup(); }); +test("information about columns can be queried", async (context) => { + const database = createDatabase({ + common: context.common, + schema: dogWithDefaults, + databaseConfig: context.databaseConfig, + }); + await database.setup({ buildId: "abc" }); + + const defaults = await getTableDefaults(database, "Dog"); + const dogTable = dogWithDefaults.Dog.table; + for (const [column, metadata] of Object.entries(defaults)) { + expect(dogTable[column as keyof typeof dogTable]).not.to.be.empty; + expect(metadata.default).to.be.toBeTypeOf("string"); + expect(metadata.type).to.be.toBeTypeOf("string"); + } + await database.kill(); +}); + +test("default values are populated during insertion", async (context) => { + const database = createDatabase({ + common: context.common, + schema: dogWithDefaults, + databaseConfig: context.databaseConfig, + }); + await database.setup({ buildId: "abc" }); + + const { rows } = await database.qb.internal.executeQuery( + (database.dialect === "sqlite" + ? sql` + INSERT INTO Dog + DEFAULT VALUES + RETURNING * + ` + : sql`INSERT INTO "Dog"(id, name, age, "bigAge", bowl, toys) + VALUES(DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) + RETURNING *` + ).compile(database.qb.internal), + ); + const sqliteRows = [ + { + id: "0", + name: "firstname", + age: 5, + bigAge: "5", + bowl: `\\x${zeroAddress.slice(2)}`, + commands: JSON.stringify([ + "sit", + "stay", + { + paw: { + right: true, + left: false, + }, + }, + ]), + toys: JSON.stringify({ + bone: "sofa", + ball: "bed", + }), + }, + ]; + const pgRows = [ + { + id: "0", + name: "firstname", + age: 5, + bigAge: 5n, + bowl: Buffer.from(hexToBytes(zeroAddress)), + commands: [ + "sit", + "stay", + { + paw: { + right: true, + left: false, + }, + }, + ], + toys: { + bone: "sofa", + ball: "bed", + }, + }, + ]; + expect(rows).to.deep.equal( + database.dialect === "sqlite" ? sqliteRows : pgRows, + ); + + await database.kill(); +}); + async function getUserTableNames(database: Database) { const { rows } = await database.qb.internal.executeQuery<{ name: string }>( database.dialect === "sqlite" @@ -793,3 +919,48 @@ async function getUserIndexNames(database: Database, tableName: string) { ); return rows.map((r) => r.name); } + +async function getTableDefaults(db: Database, tableName: string) { + if (db.dialect === "sqlite") { + const { rows } = await db.qb.internal.executeQuery<{ + name: string; + type: string; + dflt_value: string | null; + }>( + sql`SELECT * from ${sql.raw(db.namespace)}.pragma_table_info('${sql.raw(tableName)}')`.compile( + db.qb.internal, + ), + ); + return rows.reduce( + (obj, column) => { + obj[column.name] = { + type: column.type, + default: column.dflt_value, + }; + return obj; + }, + {} as Record, + ); + } else { + const { rows } = await db.qb.internal.executeQuery<{ + column_name: string; + data_type: string; + column_default: string; + }>( + sql`SELECT * +FROM information_schema.columns +WHERE table_schema = '${sql.raw(db.namespace)}' + AND table_name = '${sql.raw(tableName)}'`.compile(db.qb.internal), + ); + return rows.reduce( + (obj, column) => { + obj[column.column_name] = { + type: column.data_type, + default: column.column_default, + }; + return obj; + }, + {} as Record, + ); + } +} diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 2dec76b88..97db4f0ba 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -5,6 +5,7 @@ import { NonRetryableError } from "@/common/errors.js"; import type { DatabaseConfig } from "@/config/database.js"; import type { Schema } from "@/schema/common.js"; import { + applyDefault, getEnums, getTables, isEnumColumn, @@ -698,6 +699,7 @@ export const createDatabase = (args: { columnName, "text", (col) => { + col = applyDefault(col, column); if (isOptionalColumn(column) === false) col = col.notNull(); if (isListColumn(column) === false) { @@ -729,6 +731,7 @@ export const createDatabase = (args: { columnName, "jsonb", (col) => { + col = applyDefault(col, column); if (isOptionalColumn(column) === false) col = col.notNull(); return col; @@ -742,6 +745,7 @@ export const createDatabase = (args: { ? scalarToSqliteType : scalarToPostgresType)[column[" scalar"]], (col) => { + col = applyDefault(col, column); if (isOptionalColumn(column) === false) col = col.notNull(); if (columnName === "id") col = col.primaryKey(); diff --git a/packages/core/src/database/schemas-test.ts b/packages/core/src/database/schemas-test.ts deleted file mode 100644 index 9041603d5..000000000 --- a/packages/core/src/database/schemas-test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createSchema } from "@/schema/schema.js"; -import { zeroAddress } from "viem"; - -export const petPerson = createSchema((p) => ({ - PetKind: p.createEnum(["CAT", "DOG"]), - Pet: p.createTable( - { - id: p.string(), - name: p.string(), - age: p.int().optional(), - bigAge: p.bigint().optional(), - kind: p.enum("PetKind").optional(), - }, - { - multiIndex: p.index(["name", "age"]), - }, - ), - Person: p.createTable( - { - id: p.string(), - name: p.string(), - }, - { - nameIndex: p.index("name"), - }, - ), -})); - -export const dogApple = createSchema((p) => ({ - Dog: p.createTable({ - id: p.string(), - name: p.string(), - age: p.int().optional(), - bigAge: p.bigint().optional(), - }), - Apple: p.createTable({ - id: p.string(), - name: p.string(), - }), -})); - -export const dogWithDefaults = createSchema((p) => ({ - Dog: p.createTable({ - id: p.string().default("0"), - name: p.string().default("firstname"), - age: p.int().default(5).optional(), - bigAge: p.bigint().default(5n).optional(), - bowl: p.hex().default(zeroAddress), - toys: p.json().default({ - bone: "sofa", - ball: "bed", - }), - commands: p.json().default([ - "sit", - "stay", - { - paw: { - right: true, - left: false, - }, - }, - ]), - }), -})); diff --git a/packages/core/src/indexing-store/utils/encoding.ts b/packages/core/src/indexing-store/utils/encoding.ts index 75371700b..4fac6d299 100644 --- a/packages/core/src/indexing-store/utils/encoding.ts +++ b/packages/core/src/indexing-store/utils/encoding.ts @@ -389,7 +389,7 @@ function decodeValue({ return value === 1; } else if (column[" scalar"] === "hex") { return bytesToHex(value as Uint8Array); - } else if (column[" scalar"] === "bigint" && encoding === "sqlite") { + } else if (column[" scalar"] === "bigint" && dialect === "sqlite") { return decodeToBigInt(value as string); } else { return value as UserValue;