Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/export-param-types.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,13 @@ decoded type of a column), `InferEncoded` (its wire type), `ColumnDefs`, `Nullab
over a query's output takes `CHQuery<…, Output> & NeedsSelect<Output>` so it can pass the query on
to `compile`.

**Params a query carries** — `ParamEntry<Name, Value>` (one `param.*` a query needs, in its type),
`OptionalParamEntry<Name, Value>` and `OptionalParams<P>` (the same for a param under `when` /
`whenTrue`, or one whose name is a union, which the params object may leave out) and
`ParamsIn<X>` (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`,
Expand Down
17 changes: 17 additions & 0 deletions scripts/check-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 10 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
49 changes: 49 additions & 0 deletions tests/package-consumer-declarations.mts
Original file line number Diff line number Diff line change
@@ -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>], 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 })))
Loading