diff --git a/.changeset/export-param-types.md b/.changeset/export-param-types.md new file mode 100644 index 0000000..5d0e9bc --- /dev/null +++ b/.changeset/export-param-types.md @@ -0,0 +1,5 @@ +--- +"@maple-dev/effect-orm": patch +--- + +`ParamEntry`, `OptionalParamEntry`, `OptionalParams` and `ParamsIn` are exported from every dialect entry point. A package that exports a query with a `param.*`, or a `defineFn` helper, failed `tsc --declaration` with TS4023 / TS2742: their types name these, and tsc could only reach them through a hashed internal chunk. The package-consumer check now emits declarations for exported queries with plain, optional (`when` / `whenTrue`) and union-named params, joins, CTEs, subqueries, unions and a `defineFn` helper, on TypeScript 7 and 5. diff --git a/docs/reference.md b/docs/reference.md index 66e108a..ae09e57 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -186,6 +186,13 @@ decoded type of a column), `InferEncoded` (its wire type), `ColumnDefs`, `Nullab over a query's output takes `CHQuery<…, Output> & NeedsSelect` so it can pass the query on to `compile`. +**Params a query carries** — `ParamEntry` (one `param.*` a query needs, in its type), +`OptionalParamEntry` and `OptionalParams

` (the same for a param under `when` / +`whenTrue`, or one whose name is a union, which the params object may leave out) and +`ParamsIn` (the params an expression or argument list carries). You rarely write them, but +`tsc --declaration` names them when a package exports a query with a param or a `defineFn` helper, +so they are public. + **Everything else** — `Table` (what `from` and the write builders accept; every `table` value is one), `Expr`, `ColumnRef`, `Condition`, `Comparable` (what a value of a type may be compared against), `MapValueOf`, `Subquery`, `ParamMarker`, `ParamKind`, `CHQuery`, `CHUnionQuery`, diff --git a/scripts/check-package.ts b/scripts/check-package.ts index f9bb490..08ecf8d 100644 --- a/scripts/check-package.ts +++ b/scripts/check-package.ts @@ -90,6 +90,23 @@ const program = Effect.gen(function* () { yield* runCommand(path.join(temporary, "node_modules/.bin/tsc"), ["-p", ts5Config], temporary) yield* runCommand("node", [path.join(temporary, "node_modules/typescript-5/bin/tsc"), "-p", ts5Config], temporary) yield* runCommand("node", ["consumer-ts5.mts"], temporary) + // Exported queries and helpers must survive declaration emit, which is what a + // package that shares its query definitions runs. + yield* fs.copyFile( + path.join(root, "tests/package-consumer-declarations.mts"), + path.join(temporary, "consumer-declarations.mts"), + ) + const declarationConfig = path.join(temporary, "tsconfig.declarations.json") + yield* fs.writeFileString( + declarationConfig, + yield* Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown))({ + extends: "./tsconfig.json", + compilerOptions: { noEmit: false, declaration: true, emitDeclarationOnly: true, outDir: "declarations" }, + include: ["consumer-declarations.mts"], + }), + ) + yield* runCommand(path.join(temporary, "node_modules/.bin/tsc"), ["-p", declarationConfig], temporary) + yield* runCommand("node", [path.join(temporary, "node_modules/typescript-5/bin/tsc"), "-p", declarationConfig], temporary) // The script runs on Bun; the published library must also work in Node. yield* runCommand("node", ["consumer.mts"], temporary) // And from CommonJS, through `require(esm)`: every entry, from the real tarball. diff --git a/src/core.ts b/src/core.ts index 1570b94..b32adcf 100644 --- a/src/core.ts +++ b/src/core.ts @@ -95,7 +95,16 @@ export { fromQuery, fromUnion, } from "./ch/query" -export { type ParamsSatisfied } from "./ch/expr" +// The param types a consumer's exported query or `defineFn` helper names in its +// declarations. Without them public, `tsc --declaration` fails with TS4023 / +// TS2742 pointing into a hashed internal chunk. +export { + type OptionalParamEntry, + type OptionalParams, + type ParamEntry, + type ParamsIn, + type ParamsSatisfied, +} from "./ch/expr" export { type CHInsert, diff --git a/tests/package-consumer-declarations.mts b/tests/package-consumer-declarations.mts new file mode 100644 index 0000000..616c47b --- /dev/null +++ b/tests/package-consumer-declarations.mts @@ -0,0 +1,49 @@ +// Copied into an isolated directory by check-package.ts and compiled with +// `declaration: true`. A library that defines its tables and queries in one +// package and runs them from another exports them, and tsc must then be able to +// name every type they carry through the public entry points. Each export below +// once failed with TS4023 or TS2742, pointing into a hashed internal chunk. +import type { DateTime } from "effect" +import * as CH from "@maple-dev/effect-orm/clickhouse" +import * as PG from "@maple-dev/effect-orm/postgres" + +export const Events = CH.table("events", { external: true, columns: { appId: CH.uint32, name: CH.string } }) + +// A query with a param: its type carries `ParamEntry`. +export const eventsByApp = CH.from(Events) + .select(($) => ({ name: $.name })) + .where(($) => [$.appId.eq(CH.param.int("appId"))]) + +// A custom function: its type carries `ParamsIn`. +export const toStartOfDay = CH.defineFn<[CH.Expr], DateTime.Utc>("toStartOfDay", CH.dateTime) + +export const Accounts = PG.table("accounts", { columns: { id: PG.int4, org: PG.text }, primaryKey: ["id"] }) +export const accountsByOrg = PG.from(Accounts) + .select(($) => ({ id: $.id })) + .where(($) => [$.org.eq(PG.param.string("org"))]) + +// An optional filter (`when`, `whenTrue`) and a param named by a union carry +// `OptionalParamEntry` / `OptionalParams`. +export const eventsMaybeByName = (name: string | undefined) => + CH.from(Events) + .select(($) => ({ appId: $.appId })) + .where(($) => [CH.when(name, () => $.name.eq(CH.param.string("name")))]) +export const eventsByAppOptional = CH.from(Events) + .select(($) => ({ name: $.name })) + .where(($) => [CH.whenTrue(true, () => $.appId.eq(CH.param.int("appId")))]) + +export const byUnionNamedParam = (byApp: boolean) => + CH.from(Events) + .select(($) => ({ name: $.name })) + .where(($) => [$.appId.eq(CH.param.int(byApp ? "appId" : "fallbackAppId"))]) + +// Joins, CTEs, subqueries and unions over parameterized queries. +export const Sessions = CH.table("sessions", { external: true, columns: { appId: CH.uint32, id: CH.string } }) +export const joined = CH.from(Events) + .innerJoin(Sessions, "s", (e, s) => e.appId.eq(s.appId)) + .select(($) => ({ name: $.name })) + .where(($) => [$.appId.eq(CH.param.int("appId"))]) +export const Recent = CH.table("recent", { external: true, columns: { name: CH.string } }) +export const withRecent = CH.from(Recent).withCTE("recent", eventsByApp).select(($) => ({ name: $.name })) +export const fromSub = CH.fromQuery(eventsByApp, "sub").select(($) => ({ name: $.name })) +export const both = CH.unionAll(eventsByApp, CH.from(Events).select(($) => ({ name: $.name })))