diff --git a/src/builder/insert.test.ts b/src/builder/insert.test.ts index b677dd23..43d4658c 100644 --- a/src/builder/insert.test.ts +++ b/src/builder/insert.test.ts @@ -35,6 +35,48 @@ test("insert", async () => { }); }); +test("VALUES accept typegres expressions, not just primitives", async () => { + await withinTransaction(async (tx) => { + await tx.execute(sql`CREATE TABLE users ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + name text NOT NULL + )`); + await tx.execute(sql`CREATE TABLE posts ( + id int8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + author_id int8 NOT NULL, + body text NOT NULL + )`); + class Users extends db.Table("users") { + id = Int8.column({ nonNull: true, generated: true }); + name = Text.column({ nonNull: true }); + } + class Posts extends db.Table("posts") { + id = Int8.column({ nonNull: true, generated: true }); + author_id = Int8.column({ nonNull: true }); + body = Text.column({ nonNull: true }); + } + + // A hydrated row's columns are typegres expressions, not primitives — + // and they flow straight into another table's VALUES (parity with SET). + await tx.execute(Users.insert({ name: "alice" })); + const [alice] = await tx.hydrate(Users.from().where(({ users }) => users.name.eq("alice"))); + + const [post] = await tx.execute( + Posts.insert({ author_id: alice!.id, body: "hi" }).returning(({ posts }) => ({ + author_id: posts.author_id, + })), + ); + + // The FK landed alice's id: joining back recovers her name. + const [row] = await tx.execute( + Users.from() + .where(({ users }) => users.id.eq(post!.author_id)) + .select(({ users }) => ({ name: users.name })), + ); + expect(row).toEqual({ name: "alice" }); + }); +}); + test("insert returning", async () => { await withinTransaction(async (tx) => { await tx.execute(sql`CREATE TABLE items ( diff --git a/src/builder/insert.ts b/src/builder/insert.ts index 35ef6765..295fe4b0 100644 --- a/src/builder/insert.ts +++ b/src/builder/insert.ts @@ -4,7 +4,7 @@ import type { RowType, RowTypeToTsType } from "./query"; import { compileSelectList, isRowType, mergeReturning, reAlias } from "./query"; import type { TableBase } from "../table"; import { Connection } from "../database"; -import { getColumn } from "../types/sql-value"; +import { getColumn, SqlValue } from "../types/sql-value"; import { meta } from "../types/sql-value"; import { fn, expose } from "../exoeval/tool"; import z from "zod"; @@ -63,6 +63,11 @@ export class FinalizedInsert = { [K in ColumnKeys]: IsRequired extends true ? never : K; }[ColumnKeys]; -// Insert row: required columns + optional columns (as TsTypeOf) -export type InsertRow = { [K in RequiredKeys]: TsTypeOf } & { - [K in OptionalKeys]?: TsTypeOf; +// Insert row: required columns + optional columns. Like SET (SetRow), each +// value is either the JS-side primitive or a typegres expression of the +// same class & nullability — a hydrated column from another row composes +// straight into VALUES. +export type InsertRow = { [K in RequiredKeys]: TsTypeOf | StripRequired } & { + [K in OptionalKeys]?: TsTypeOf | StripRequired; }; // Drop the column-only `__required` marker from a column type's meta.