measurr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,303 @@
1
+ import { type Expr, type ExprNode, type SqlType, type Table, type TableRef, type TimeBucketUnit, type TimePartName } from './expr.js';
2
+ declare const ratioBrand: unique symbol;
3
+ /** A share or ratio between 0 and 1. Branded so nobody renders 0.42 as "0.42%". */
4
+ export type Ratio = number & {
5
+ readonly [ratioBrand]: 'zeroToOne';
6
+ };
7
+ type Described = {
8
+ label: string;
9
+ description?: string | undefined;
10
+ };
11
+ /**
12
+ * `count` results are never null. `number` results (sum, avg, median) are null for an empty
13
+ * group. `ratio` results (share, ratio) are a 0-1 `Ratio`, null when the base is zero.
14
+ */
15
+ export type MeasureResult = 'count' | 'number' | 'ratio';
16
+ export type AggregateMeasure<R extends 'count' | 'number' = 'count' | 'number'> = {
17
+ readonly kind: 'aggregate';
18
+ readonly result: R;
19
+ readonly label: string;
20
+ readonly description: string | undefined;
21
+ readonly node: Extract<ExprNode, {
22
+ kind: 'aggregate';
23
+ }>;
24
+ };
25
+ export type ShareMeasure = {
26
+ readonly kind: 'share';
27
+ readonly result: 'ratio';
28
+ readonly label: string;
29
+ readonly description: string | undefined;
30
+ readonly numerator: ExprNode;
31
+ readonly denominator: ExprNode | null;
32
+ };
33
+ export type RatioMeasure<A extends string = string, B extends string = string> = {
34
+ readonly kind: 'ratio';
35
+ readonly result: 'ratio';
36
+ readonly label: string;
37
+ readonly description: string | undefined;
38
+ readonly numerator: A;
39
+ readonly denominator: B;
40
+ };
41
+ export type AnyMeasure = AggregateMeasure | ShareMeasure | RatioMeasure;
42
+ /** The TypeScript type of a measure's value in a result row. */
43
+ export type MeasureValue<M> = M extends {
44
+ result: 'count';
45
+ } ? number : M extends {
46
+ result: 'number';
47
+ } ? number | null : Ratio | null;
48
+ type Filtered = Described & {
49
+ where?: Expr<'boolean'> | undefined;
50
+ };
51
+ export declare const measure: {
52
+ /** Number of rows, optionally only those matching `where`. */
53
+ count(options: Filtered): AggregateMeasure<'count'>;
54
+ /** Number of rows matching `condition`. */
55
+ countWhere(condition: Expr<'boolean'>, options: Described): AggregateMeasure<'count'>;
56
+ /** Number of distinct non-null values of `expr`. */
57
+ countDistinct(expr: Expr<SqlType>, options: Filtered): AggregateMeasure<'count'>;
58
+ sum(expr: Expr<'number'>, options: Filtered): AggregateMeasure<'number'>;
59
+ avg(expr: Expr<'number'>, options: Filtered): AggregateMeasure<'number'>;
60
+ median(expr: Expr<'number'>, options: Filtered): AggregateMeasure<'number'>;
61
+ /**
62
+ * The share of rows matching `numerator` among rows matching `denominator` (all rows when
63
+ * omitted). A `Ratio` from 0 to 1.
64
+ */
65
+ share(options: Described & {
66
+ numerator: Expr<'boolean'>;
67
+ denominator?: Expr<'boolean'> | undefined;
68
+ }): ShareMeasure;
69
+ /**
70
+ * One measure of this dataset over another, computed after aggregation. Both keys must be
71
+ * measures of the same dataset; `defineDataset` checks them.
72
+ */
73
+ ratio<const A extends string, const B extends string>(numerator: A, denominator: B, options: Described): RatioMeasure<A, B>;
74
+ };
75
+ /**
76
+ * How a dimension filters:
77
+ * - `closed`: a fixed set of keys (enum, computed) - `in`, `notIn`, `isEmpty`, `isNotEmpty`;
78
+ * - `open`: keys from the data (relation, many-to-many) - the same operators, with values given
79
+ * as keys or labels and resolved at query time;
80
+ * - `numeric`: numbers (numeric columns, weekday, hour) - `between`, `isEmpty`, `isNotEmpty`;
81
+ * - `time`: time buckets keyed by ISO date - `between`, `isEmpty`, `isNotEmpty`.
82
+ */
83
+ export type DimensionCategory = 'closed' | 'open' | 'numeric' | 'time';
84
+ type DimensionOptions<G extends boolean = boolean, F extends boolean = boolean> = Described & {
85
+ /** Only offered for grouping. */
86
+ groupOnly?: G | undefined;
87
+ /** Only offered for filtering. */
88
+ filterOnly?: F | undefined;
89
+ };
90
+ /** Time dimensions name themselves after their unit unless given a label. */
91
+ type TimeDimensionOptions<G extends boolean, F extends boolean, Key> = Omit<DimensionOptions<G, F>, 'label'> & {
92
+ label?: string | undefined;
93
+ labelFor?: ((key: Key) => string) | undefined;
94
+ };
95
+ /**
96
+ * `groupOnly: true` makes `filterable` the literal `false` (and `filterOnly`, `groupable`), so
97
+ * queries cannot filter or group by what the dimension does not offer.
98
+ */
99
+ export type DimensionAccess<G extends boolean, F extends boolean> = {
100
+ readonly groupable: [F] extends [true] ? false : boolean;
101
+ readonly filterable: [G] extends [true] ? false : boolean;
102
+ };
103
+ type DimensionBase<C extends DimensionCategory, Key> = {
104
+ readonly category: C;
105
+ readonly label: string;
106
+ readonly description: string | undefined;
107
+ readonly groupable: boolean;
108
+ readonly filterable: boolean;
109
+ /** Phantom: the key type in result rows. */
110
+ readonly keyType?: Key;
111
+ };
112
+ /** `Empty` is `null` when the expression can be null, `never` when it cannot. */
113
+ export type EnumDimension<K extends string = string, Empty extends null = null> = DimensionBase<'closed', K | Empty> & {
114
+ readonly kind: 'enum' | 'computed';
115
+ readonly expr: ExprNode;
116
+ readonly values: Readonly<Record<K, string>>;
117
+ readonly empty: string | undefined;
118
+ };
119
+ export type RelationDimension<K = unknown> = DimensionBase<'open', K> & {
120
+ readonly kind: 'relation';
121
+ readonly key: ExprNode;
122
+ readonly name: ExprNode;
123
+ readonly empty: string | undefined;
124
+ };
125
+ export type ManyToManyDimension = DimensionBase<'open', string> & {
126
+ readonly kind: 'manyToMany';
127
+ readonly table: TableRef;
128
+ readonly on: ExprNode;
129
+ readonly key: ExprNode;
130
+ readonly name: ExprNode;
131
+ readonly empty: string | undefined;
132
+ readonly note: string | undefined;
133
+ };
134
+ export type NumberDimension = DimensionBase<'numeric', number> & {
135
+ readonly kind: 'number';
136
+ readonly expr: ExprNode;
137
+ readonly labelFor: ((key: number) => string) | undefined;
138
+ };
139
+ export type TimePartDimension<T extends string = string> = DimensionBase<'numeric', number> & {
140
+ readonly kind: 'timePart';
141
+ readonly time: T;
142
+ readonly part: TimePartName;
143
+ readonly labelFor: ((key: number) => string) | undefined;
144
+ };
145
+ export type TimeBucketDimension<T extends string = string> = DimensionBase<'time', string> & {
146
+ readonly kind: 'timeBucket';
147
+ readonly time: T;
148
+ readonly unit: TimeBucketUnit;
149
+ readonly labelFor: ((key: string) => string) | undefined;
150
+ };
151
+ export type AnyDimension = EnumDimension | RelationDimension | ManyToManyDimension | NumberDimension | TimePartDimension | TimeBucketDimension;
152
+ /**
153
+ * `{ key, label }` as it appears in a result row for this dimension. Enum and computed keys
154
+ * are null only when their expression can be null; any other key is null when the row has no
155
+ * value (no related row, a null column, a time field the period does not apply to).
156
+ */
157
+ export type DimensionValue<Dim> = Dim extends {
158
+ kind: 'enum' | 'computed';
159
+ keyType?: infer K;
160
+ } ? {
161
+ key: K;
162
+ label: string;
163
+ } : Dim extends {
164
+ keyType?: infer K;
165
+ } ? {
166
+ key: K | null;
167
+ label: string;
168
+ } : never;
169
+ /** `null` when an expression's values `V` include null, otherwise `never`. */
170
+ type EmptyOf<V> = null extends V ? null : never;
171
+ /**
172
+ * Makes `expr` fail typecheck when it can produce a value `Allowed` does not list, naming the
173
+ * missing values. Expressions typed only as `string` pass; `defineDataset` checks those.
174
+ */
175
+ type CoversValues<V, Allowed> = string extends NonNullable<V> ? unknown : [Exclude<NonNullable<V>, Allowed>] extends [never] ? unknown : {
176
+ readonly missingFromValues: Exclude<NonNullable<V>, Allowed>;
177
+ };
178
+ /** What a closed dimension's null key is called. */
179
+ type EmptyOption = {
180
+ /** The label of rows whose value is null. "None" when omitted. */
181
+ empty?: string | undefined;
182
+ };
183
+ export declare const dimension: {
184
+ /**
185
+ * A column holding one value of a closed set, such as a Prisma enum. `labels` must name
186
+ * every value, so adding an enum value fails typecheck until it has a label, and a column
187
+ * typed with values outside `values` fails too. A nullable column gives null keys.
188
+ */
189
+ enum<const E extends Record<string, string>, V extends string | null, G extends boolean = boolean, F extends boolean = boolean>(expr: Expr<'string', V> & CoversValues<V, E[keyof E]>, values: E, options: DimensionOptions<G, F> & EmptyOption & {
190
+ labels: Record<E[keyof E], string>;
191
+ }): EnumDimension<E[keyof E], EmptyOf<V>> & DimensionAccess<G, F>;
192
+ /**
193
+ * A value computed from other columns with the expression builders, usually `caseWhen`.
194
+ * `values` lists every value the expression can produce, with its label; a plain value of
195
+ * the expression missing from `values` fails typecheck. A `caseWhen` without `otherwise`
196
+ * gives null keys.
197
+ */
198
+ computed<V extends string | null, const K extends string, G extends boolean = boolean, F extends boolean = boolean>(options: DimensionOptions<G, F> & EmptyOption & {
199
+ expr: Expr<'string', V> & CoversValues<V, NoInfer<K>>;
200
+ values: Record<K, string>;
201
+ }): EnumDimension<K, EmptyOf<V>> & DimensionAccess<G, F>;
202
+ /**
203
+ * A to-one relation such as a team. The key type comes from the `key` column, so a branded
204
+ * id column types the key in filters and rows. Rows without a related record get the
205
+ * `empty` label and a null key.
206
+ */
207
+ relation<Key extends string | number, G extends boolean = boolean, F extends boolean = boolean>(options: DimensionOptions<G, F> & {
208
+ key: Expr<Key extends number ? 'number' : 'string', Key | null>;
209
+ name: Expr<'string'>;
210
+ empty?: string | undefined;
211
+ }): RelationDimension<Key> & DimensionAccess<G, F>;
212
+ /**
213
+ * A to-many relation through a link table, such as tags. Grouping counts a row once per
214
+ * related value, so totals for such a grouping come from a separate query. Filtering
215
+ * compiles to `EXISTS`, so it never multiplies rows.
216
+ *
217
+ * `through.table` is the link table as a standalone `table()` (further joins hang off it),
218
+ * and `through.on` links it to the dataset's table.
219
+ */
220
+ manyToMany<Row = unknown, G extends boolean = boolean, F extends boolean = boolean>(options: DimensionOptions<G, F> & {
221
+ through: {
222
+ table: Table<Row>;
223
+ on: Expr<'boolean'>;
224
+ key: Expr<'string' | 'number'>;
225
+ name: Expr<'string'>;
226
+ };
227
+ /** Explains the overlap to the reader of a grouped result. */
228
+ note?: string | undefined;
229
+ empty?: string | undefined;
230
+ }): ManyToManyDimension & DimensionAccess<G, F>;
231
+ /** A numeric column, grouped by its exact values and filtered with `between`. */
232
+ number<G extends boolean = boolean, F extends boolean = boolean>(expr: Expr<'number'>, options: DimensionOptions<G, F> & {
233
+ labelFor?: ((key: number) => string) | undefined;
234
+ }): NumberDimension & DimensionAccess<G, F>;
235
+ /** Day, ISO week (starting Monday) or month of a time field, in the tenant's timezone. */
236
+ timeBucket<const T extends string, G extends boolean = boolean, F extends boolean = boolean>(time: T, unit: TimeBucketUnit, options?: TimeDimensionOptions<G, F, string>): TimeBucketDimension<T> & DimensionAccess<G, F>;
237
+ /** ISO weekday (1 = Monday) or hour (0-23) of a time field, in the tenant's timezone. */
238
+ timePart<const T extends string, G extends boolean = boolean, F extends boolean = boolean>(time: T, part: TimePartName, options?: TimeDimensionOptions<G, F, number>): TimePartDimension<T> & DimensionAccess<G, F>;
239
+ };
240
+ export type TimeField = {
241
+ readonly column: ExprNode;
242
+ readonly label: string;
243
+ };
244
+ export type Dataset<K extends string = string, M extends Record<string, AnyMeasure> = Record<string, AnyMeasure>, Dm extends Record<string, AnyDimension> = Record<string, AnyDimension>, T extends string = string> = {
245
+ readonly key: K;
246
+ readonly label: string;
247
+ readonly description: string;
248
+ readonly from: TableRef;
249
+ readonly source: string | undefined;
250
+ readonly tenantColumn: ExprNode;
251
+ readonly scope: ExprNode | null;
252
+ readonly time: Readonly<Record<T, TimeField>>;
253
+ readonly defaultTime: T;
254
+ readonly measures: M;
255
+ readonly dimensions: Dm;
256
+ };
257
+ export type AnyDataset = Dataset<string, any, any, string>;
258
+ /** Ratio measures may only name other measures of the same dataset. */
259
+ type CheckMeasures<M> = {
260
+ [Key in keyof M]: M[Key] extends {
261
+ kind: 'ratio';
262
+ } ? {
263
+ numerator: Exclude<keyof M & string, Key>;
264
+ denominator: Exclude<keyof M & string, Key>;
265
+ } : unknown;
266
+ };
267
+ /** Time dimensions may only name declared time fields. */
268
+ type CheckDimensions<Dm, T extends string> = {
269
+ [Key in keyof Dm]: Dm[Key] extends {
270
+ kind: 'timeBucket' | 'timePart';
271
+ } ? {
272
+ time: T;
273
+ } : unknown;
274
+ };
275
+ export type DatasetDefinition<K extends string, M extends Record<string, AnyMeasure>, Dm extends Record<string, AnyDimension>, T extends string> = {
276
+ key: K;
277
+ label: string;
278
+ /** Tells a reader (and a model) what one row of the dataset is. */
279
+ description: string;
280
+ from: {
281
+ readonly ref: TableRef;
282
+ };
283
+ /** Which source (database) the dataset runs on; the default when there is one source. */
284
+ source?: string | undefined;
285
+ /** The column every query is filtered by; the value comes from `tenant(ctx)`. */
286
+ tenantColumn: Expr<SqlType>;
287
+ /** Always applied, for example excluding test rows. */
288
+ scope?: Expr<'boolean'> | undefined;
289
+ time: Record<T, {
290
+ column: Expr<'timestamp'>;
291
+ label: string;
292
+ }>;
293
+ /** The time field periods apply to when a query names none; the first one by default. */
294
+ defaultTime?: NoInfer<T> | undefined;
295
+ measures: M & CheckMeasures<M>;
296
+ dimensions: Dm & CheckDimensions<Dm, NoInfer<T>>;
297
+ };
298
+ /**
299
+ * Declares a dataset: one fact table with its tenant column, time fields, measures and
300
+ * dimensions. Keys stay literal types, so queries and result rows are typed from it.
301
+ */
302
+ export declare function defineDataset<const K extends string, const M extends Record<string, AnyMeasure>, const Dm extends Record<string, AnyDimension>, const T extends string>(definition: DatasetDefinition<K, M, Dm, T>): Dataset<K, M, Dm, T>;
303
+ export {};
@@ -0,0 +1,16 @@
1
+ import { type Dialect } from '../compile.js';
2
+ export type PostgresDialectOptions = {
3
+ /**
4
+ * How time columns are stored:
5
+ * - `timestamptz` (default): `timestamp with time zone`;
6
+ * - `utc`: `timestamp without time zone` holding UTC wall-clock time, which is what Prisma's
7
+ * `DateTime` creates.
8
+ */
9
+ timestamps?: 'timestamptz' | 'utc';
10
+ };
11
+ /**
12
+ * Postgres: `$n` parameters, double-quoted identifiers, `FILTER (WHERE ...)` aggregates and
13
+ * buckets with `AT TIME ZONE`. Values are passed untyped so Postgres infers them from the
14
+ * column they meet, which keeps enum columns working.
15
+ */
16
+ export declare function postgresDialect(options?: PostgresDialectOptions): Dialect;
@@ -0,0 +1,51 @@
1
+ import {
2
+ decodeNumber2,
3
+ renderStatement2
4
+ } from "../index-nrjdssft.js";
5
+
6
+ // src/dialects/postgres.ts
7
+ function postgresDialect(options = {}) {
8
+ const timestamps = options.timestamps ?? "timestamptz";
9
+ const local = (expr, timezone) => timestamps === "utc" ? `((${expr}) AT TIME ZONE 'UTC' AT TIME ZONE '${timezone}')` : `((${expr}) AT TIME ZONE '${timezone}')`;
10
+ const renderer = {
11
+ name: "postgres",
12
+ quote: (identifier) => `"${identifier.replaceAll('"', '""')}"`,
13
+ param(value, index) {
14
+ const placeholder = `$${index}`;
15
+ if (!(value instanceof Date))
16
+ return { placeholder, value };
17
+ const iso = value.toISOString();
18
+ return {
19
+ placeholder,
20
+ value: timestamps === "utc" ? iso.replace("T", " ").replace("Z", "") : iso
21
+ };
22
+ },
23
+ aggregate(fn, arg, where) {
24
+ const filter = where ? ` FILTER (WHERE ${where})` : "";
25
+ switch (fn) {
26
+ case "count":
27
+ return `count(*)${filter}`;
28
+ case "countDistinct":
29
+ return `count(DISTINCT ${arg})${filter}`;
30
+ case "sum":
31
+ return `sum(${arg})${filter}`;
32
+ case "avg":
33
+ return `avg(${arg})${filter}`;
34
+ case "median":
35
+ return `percentile_cont(0.5) WITHIN GROUP (ORDER BY (${arg})::double precision)${filter}`;
36
+ }
37
+ },
38
+ divide: (left, right) => `((${left})::double precision / NULLIF(${right}, 0))`,
39
+ toText: (expr) => `CAST(${expr} AS text)`,
40
+ timeBucket: (unit, expr, timezone) => `to_char(date_trunc('${unit}', ${local(expr, timezone)}), 'YYYY-MM-DD')`,
41
+ timePart: (part, expr, timezone) => `CAST(extract(${part} FROM ${local(expr, timezone)}) AS integer)`
42
+ };
43
+ return {
44
+ name: "postgres",
45
+ compile: (statement) => renderStatement2(statement, renderer),
46
+ decodeNumber: decodeNumber2
47
+ };
48
+ }
49
+ export {
50
+ postgresDialect
51
+ };
package/dist/expr.d.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * The expression tree datasets are written with. Builders return `Expr` values that carry a
3
+ * small, dialect-free node tree; no SQL text exists until a dialect renders a planned
4
+ * statement. Every expression knows its SQL type, so `avg` over a string or `timeBucket` over a
5
+ * number fails typecheck.
6
+ */
7
+ export type SqlType = 'number' | 'string' | 'boolean' | 'timestamp';
8
+ /** A value a query parameter can hold. Timestamps are passed as `Date`. */
9
+ export type ParamValue = string | number | boolean | Date | null;
10
+ export type CompareOp = '=' | '<>' | '>' | '>=' | '<' | '<=';
11
+ export type TimeBucketUnit = 'day' | 'week' | 'month';
12
+ export type TimePartName = 'isodow' | 'hour';
13
+ export type AggregateFn = 'count' | 'countDistinct' | 'sum' | 'avg' | 'median';
14
+ /**
15
+ * A table occurrence. A joined table points at the table it hangs off and carries its own
16
+ * `ON` clause, so any column read from it brings the join along.
17
+ */
18
+ export type TableRef = {
19
+ readonly name: string;
20
+ readonly alias: string | undefined;
21
+ readonly join: TableJoin | undefined;
22
+ };
23
+ export type TableJoin = {
24
+ readonly type: 'left' | 'inner';
25
+ readonly parent: TableRef;
26
+ readonly on: ExprNode;
27
+ };
28
+ export type ExprNode = {
29
+ readonly kind: 'column';
30
+ readonly table: TableRef;
31
+ readonly column: string;
32
+ } | {
33
+ readonly kind: 'param';
34
+ readonly value: ParamValue;
35
+ } | {
36
+ readonly kind: 'compare';
37
+ readonly op: CompareOp;
38
+ readonly left: ExprNode;
39
+ readonly right: ExprNode;
40
+ } | {
41
+ readonly kind: 'logical';
42
+ readonly op: 'and' | 'or';
43
+ readonly items: readonly ExprNode[];
44
+ } | {
45
+ readonly kind: 'not';
46
+ readonly item: ExprNode;
47
+ } | {
48
+ readonly kind: 'isNull';
49
+ readonly item: ExprNode;
50
+ readonly negated: boolean;
51
+ } | {
52
+ readonly kind: 'in';
53
+ readonly item: ExprNode;
54
+ readonly values: readonly ExprNode[];
55
+ readonly negated: boolean;
56
+ } | {
57
+ readonly kind: 'case';
58
+ readonly branches: readonly {
59
+ readonly when: ExprNode;
60
+ readonly result: ExprNode;
61
+ }[];
62
+ readonly otherwise: ExprNode | null;
63
+ } | {
64
+ readonly kind: 'exists';
65
+ readonly table: TableRef;
66
+ readonly where: ExprNode;
67
+ readonly negated: boolean;
68
+ } | {
69
+ readonly kind: 'coalesce';
70
+ readonly items: readonly ExprNode[];
71
+ } | {
72
+ readonly kind: 'arithmetic';
73
+ readonly op: '+' | '-' | '*' | '/';
74
+ readonly left: ExprNode;
75
+ readonly right: ExprNode;
76
+ } | {
77
+ readonly kind: 'lower';
78
+ readonly item: ExprNode;
79
+ } | {
80
+ readonly kind: 'toText';
81
+ readonly item: ExprNode;
82
+ } | {
83
+ readonly kind: 'aggregate';
84
+ readonly fn: AggregateFn;
85
+ readonly arg: ExprNode | null;
86
+ readonly where: ExprNode | null;
87
+ } | {
88
+ readonly kind: 'timeBucket';
89
+ readonly unit: TimeBucketUnit;
90
+ readonly item: ExprNode;
91
+ readonly timezone: string;
92
+ } | {
93
+ readonly kind: 'timePart';
94
+ readonly part: TimePartName;
95
+ readonly item: ExprNode;
96
+ readonly timezone: string;
97
+ }
98
+ /** The moment the query runs, minus `minutes`; rendered as a timestamp parameter. */
99
+ | {
100
+ readonly kind: 'minutesAgo';
101
+ readonly minutes: number;
102
+ };
103
+ /** What a raw value of a SQL type looks like in TypeScript. */
104
+ export type ValueOf<T extends SqlType> = T extends 'number' ? number : T extends 'string' ? string : T extends 'boolean' ? boolean : Date;
105
+ /**
106
+ * An expression of SQL type `T`, or a plain value of `V` that becomes a parameter. `V` narrows
107
+ * plain values, so `eq(order.col('status'), 'PAD')` fails when `status` is an enum. Plain
108
+ * values are never null: compare with `isNull` instead.
109
+ */
110
+ export type Operand<T extends SqlType, V = ValueOf<T>> = Expr<T> | V;
111
+ /** The values an operand or a `caseWhen` branch can produce. */
112
+ type ValuesOf<O> = O extends Expr<SqlType, infer V> ? V : O;
113
+ /** `null` when `V` can be null or undefined (an optional field), otherwise `never`. */
114
+ type NullOf<V> = null extends V ? null : undefined extends V ? null : never;
115
+ /**
116
+ * `T` is the SQL type; `V` is the TypeScript type of its values, including `null` when the
117
+ * expression can be null. `V` defaults to "any value of `T`, or null", the SQL default; columns,
118
+ * literals and `caseWhen` narrow it (an enum's members, a branded id, a non-null column).
119
+ */
120
+ export declare class Expr<T extends SqlType, V = ValueOf<T> | null> {
121
+ readonly node: ExprNode;
122
+ /** Phantom: the SQL type this expression evaluates to. Not `declare`: Playwright's Babel transform rejects it. */
123
+ readonly sqlType: T;
124
+ /** Phantom: the values this expression can produce. */
125
+ readonly valueType: V;
126
+ constructor(node: ExprNode);
127
+ isNull(): Expr<'boolean', boolean>;
128
+ isNotNull(): Expr<'boolean', boolean>;
129
+ add(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'>;
130
+ sub(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'>;
131
+ mul(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'>;
132
+ /** Always a floating-point division, and `null` when dividing by zero. */
133
+ div(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'>;
134
+ }
135
+ /** Maps a column's TypeScript type to its SQL type; `never` for columns we cannot use. */
136
+ export type SqlTypeOf<V> = [NonNullable<V>] extends [never] ? never : [NonNullable<V>] extends [Date] ? 'timestamp' : [NonNullable<V>] extends [number | bigint] ? 'number' : [NonNullable<V>] extends [boolean] ? 'boolean' : [NonNullable<V>] extends [string] ? 'string' : [NonNullable<V>] extends [{
137
+ toNumber(): number;
138
+ }] ? 'number' : never;
139
+ /**
140
+ * The value type a column keeps: its own type when it fits its SQL type (an enum, a branded
141
+ * id), plus `null` when the field is nullable or optional.
142
+ */
143
+ export type ColumnValueOf<V> = ([NonNullable<V>] extends [ValueOf<SqlTypeOf<V>>] ? NonNullable<V> : ValueOf<SqlTypeOf<V>>) | NullOf<V>;
144
+ /** Keys of `Row` whose values map to a SQL type (JSON and relation fields are excluded). */
145
+ export type ColumnKey<Row> = {
146
+ [K in keyof Row]-?: [SqlTypeOf<Row[K]>] extends [never] ? never : K;
147
+ }[keyof Row] & string;
148
+ /**
149
+ * A typed handle on a table. `Row` is any object type (a Prisma model, a hand-written type);
150
+ * `col` only accepts its keys and returns an expression of the matching SQL type.
151
+ */
152
+ export declare class Table<Row> {
153
+ readonly ref: TableRef;
154
+ readonly row: Row;
155
+ constructor(ref: TableRef);
156
+ col<K extends ColumnKey<Row>>(key: K): Expr<SqlTypeOf<Row[K]>, ColumnValueOf<Row[K]>>;
157
+ /** Joins `other` to this table. Columns of the returned handle bring the join with them. */
158
+ leftJoin<Other>(other: Table<Other>, on: (joined: Table<Other>) => Expr<'boolean'>): Table<Other>;
159
+ innerJoin<Other>(other: Table<Other>, on: (joined: Table<Other>) => Expr<'boolean'>): Table<Other>;
160
+ private join;
161
+ }
162
+ /** A table handle. The alias is optional; the compiler makes aliases unique per statement. */
163
+ export declare function table<Row>(name: string, alias?: string): Table<Row>;
164
+ /** A literal value, always sent as a query parameter. */
165
+ export declare function literal<V extends string | number | boolean | Date>(value: V): Expr<V extends number ? 'number' : V extends boolean ? 'boolean' : V extends Date ? 'timestamp' : 'string', V>;
166
+ export declare function eq<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
167
+ export declare function ne<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
168
+ export declare function gt<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
169
+ export declare function gte<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
170
+ export declare function lt<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
171
+ export declare function lte<T extends SqlType, V extends ParamValue>(left: Expr<T, V>, right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>): Expr<'boolean'>;
172
+ export declare function and(...items: Expr<'boolean'>[]): Expr<'boolean'>;
173
+ export declare function or(...items: Expr<'boolean'>[]): Expr<'boolean'>;
174
+ export declare function not(item: Expr<'boolean'>): Expr<'boolean'>;
175
+ export declare function isNull(item: Expr<SqlType, unknown>): Expr<'boolean', boolean>;
176
+ export declare function isNotNull(item: Expr<SqlType, unknown>): Expr<'boolean', boolean>;
177
+ type CaseValue = string | number | boolean | Expr<SqlType, unknown>;
178
+ type CaseType<V> = V extends Expr<infer T, unknown> ? T : V extends string ? 'string' : V extends number ? 'number' : V extends boolean ? 'boolean' : never;
179
+ /**
180
+ * `CASE WHEN ... THEN ... ELSE ... END`. Branches are checked in order. Plain values become
181
+ * parameters; a computed dimension's `values` must list every plain value used here. Without
182
+ * `otherwise`, rows matching no branch are null, and the type says so.
183
+ */
184
+ export declare function caseWhen<const V extends CaseValue>(branches: readonly (readonly [Expr<'boolean', unknown>, V])[]): Expr<CaseType<V>, ValuesOf<V> | null>;
185
+ export declare function caseWhen<const V extends CaseValue, const O extends CaseValue>(branches: readonly (readonly [Expr<'boolean', unknown>, V])[], otherwise: O): Expr<CaseType<V | O>, ValuesOf<V | O>>;
186
+ /**
187
+ * A correlated `EXISTS`: true when `from` has a row matching `where`. `where` usually links
188
+ * the subquery table to the outer table, for example
189
+ * `exists(message, eq(message.col('sessionId'), session.col('id')))`.
190
+ */
191
+ export declare function exists<Row>(from: Table<Row>, where: Expr<'boolean'>): Expr<'boolean', boolean>;
192
+ /** Null only when every item can be null; a plain fallback value is never null. */
193
+ type Coalesced<V, Rest> = [Rest] extends [never] ? V : null extends Rest ? V | Rest : NonNullable<V> | Rest;
194
+ export declare function coalesce<T extends SqlType, V extends ParamValue, const R extends readonly Operand<NoInfer<T>>[]>(first: Expr<T, V>, ...rest: R): Expr<T, Coalesced<V, ValuesOf<R[number]>>>;
195
+ /** Matches `item` against a fixed list of values. */
196
+ export declare function inList<T extends SqlType, V extends ParamValue>(item: Expr<T, V>, values: readonly NonNullable<NoInfer<V>>[]): Expr<'boolean'>;
197
+ /** `item` as text, for example to compare an enum column with a value outside the enum. */
198
+ export declare function toText<V>(item: Expr<SqlType, V>): Expr<'string', string | NullOf<V>>;
199
+ /**
200
+ * The moment the query runs, minus `minutes`, such as "created in the last 15 minutes". The
201
+ * query's clock supplies the moment, so it follows the `now` option of `createAnalytics`.
202
+ */
203
+ export declare function minutesAgo(minutes: number): Expr<'timestamp', Date>;
204
+ /** Follows the join chain up to the table that is not joined to anything. */
205
+ export declare function rootOf(ref: TableRef): TableRef;
206
+ /** The nodes directly below `node`, including an `exists` subquery's condition. */
207
+ export declare function directChildren(node: ExprNode): readonly ExprNode[];
208
+ /** Visits every node in a tree, depth first. */
209
+ export declare function walk(node: ExprNode, visit: (node: ExprNode) => void): void;
210
+ export {};
@@ -0,0 +1,5 @@
1
+ /**
2
+ * How a model should use an analytics query tool, as markdown bullets for its instructions.
3
+ * It does not name the tool, so add a line that does.
4
+ */
5
+ export declare const analyticsToolGuidance = "- Answer every \"how many\", \"how much\", \"per week\" or \"split by\" question with the analytics tool. Never count by hand from lists or pages.\n- Pick the dataset whose description matches the question, the fewest measures that answer it and at most two groupings.\n- Say which period and filters the numbers cover, using the dates in the result (\"1-30 September\").\n- Compare with the previous period (compareToPrevious) when the change is the point, such as \"is it going up\".\n- When the result has notes (overlapping rows, such as a record counted once per tag), say so in a few words.\n- Ask one short question only when the period is genuinely ambiguous (\"this season\") and someone can answer; otherwise take the natural reading and state it.";