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.
- package/LICENSE +21 -0
- package/README.md +340 -0
- package/dist/analytics.d.ts +133 -0
- package/dist/compile.d.ts +84 -0
- package/dist/dataset.d.ts +303 -0
- package/dist/dialects/postgres.d.ts +16 -0
- package/dist/dialects/postgres.js +51 -0
- package/dist/expr.d.ts +210 -0
- package/dist/guidance.d.ts +5 -0
- package/dist/index-e6k30sfp.js +218 -0
- package/dist/index-m0czpsyv.js +1292 -0
- package/dist/index-nrjdssft.js +200 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +78 -0
- package/dist/period.d.ts +44 -0
- package/dist/plan.d.ts +86 -0
- package/dist/query.d.ts +619 -0
- package/dist/testing.d.ts +38 -0
- package/dist/testing.js +191 -0
- package/package.json +84 -0
- package/src/analytics.ts +663 -0
- package/src/compile.ts +332 -0
- package/src/dataset.ts +783 -0
- package/src/dialects/postgres.ts +74 -0
- package/src/expr.ts +539 -0
- package/src/guidance.ts +10 -0
- package/src/index.ts +74 -0
- package/src/period.ts +212 -0
- package/src/plan.ts +588 -0
- package/src/query.ts +399 -0
- package/src/testing.ts +283 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Dialect,
|
|
3
|
+
type SqlRenderer,
|
|
4
|
+
decodeNumber,
|
|
5
|
+
renderStatement
|
|
6
|
+
} from '../compile.js'
|
|
7
|
+
|
|
8
|
+
export type PostgresDialectOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* How time columns are stored:
|
|
11
|
+
* - `timestamptz` (default): `timestamp with time zone`;
|
|
12
|
+
* - `utc`: `timestamp without time zone` holding UTC wall-clock time, which is what Prisma's
|
|
13
|
+
* `DateTime` creates.
|
|
14
|
+
*/
|
|
15
|
+
timestamps?: 'timestamptz' | 'utc'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Postgres: `$n` parameters, double-quoted identifiers, `FILTER (WHERE ...)` aggregates and
|
|
20
|
+
* buckets with `AT TIME ZONE`. Values are passed untyped so Postgres infers them from the
|
|
21
|
+
* column they meet, which keeps enum columns working.
|
|
22
|
+
*/
|
|
23
|
+
export function postgresDialect(options: PostgresDialectOptions = {}): Dialect {
|
|
24
|
+
const timestamps = options.timestamps ?? 'timestamptz'
|
|
25
|
+
|
|
26
|
+
const local = (expr: string, timezone: string) =>
|
|
27
|
+
timestamps === 'utc'
|
|
28
|
+
? `((${expr}) AT TIME ZONE 'UTC' AT TIME ZONE '${timezone}')`
|
|
29
|
+
: `((${expr}) AT TIME ZONE '${timezone}')`
|
|
30
|
+
|
|
31
|
+
const renderer: SqlRenderer = {
|
|
32
|
+
name: 'postgres',
|
|
33
|
+
quote: (identifier) => `"${identifier.replaceAll('"', '""')}"`,
|
|
34
|
+
param(value, index) {
|
|
35
|
+
const placeholder = `$${index}`
|
|
36
|
+
if (!(value instanceof Date)) return { placeholder, value }
|
|
37
|
+
|
|
38
|
+
const iso = value.toISOString()
|
|
39
|
+
return {
|
|
40
|
+
placeholder,
|
|
41
|
+
value:
|
|
42
|
+
timestamps === 'utc' ? iso.replace('T', ' ').replace('Z', '') : iso
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
aggregate(fn, arg, where) {
|
|
46
|
+
const filter = where ? ` FILTER (WHERE ${where})` : ''
|
|
47
|
+
switch (fn) {
|
|
48
|
+
case 'count':
|
|
49
|
+
return `count(*)${filter}`
|
|
50
|
+
case 'countDistinct':
|
|
51
|
+
return `count(DISTINCT ${arg})${filter}`
|
|
52
|
+
case 'sum':
|
|
53
|
+
return `sum(${arg})${filter}`
|
|
54
|
+
case 'avg':
|
|
55
|
+
return `avg(${arg})${filter}`
|
|
56
|
+
case 'median':
|
|
57
|
+
return `percentile_cont(0.5) WITHIN GROUP (ORDER BY (${arg})::double precision)${filter}`
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
divide: (left, right) =>
|
|
61
|
+
`((${left})::double precision / NULLIF(${right}, 0))`,
|
|
62
|
+
toText: (expr) => `CAST(${expr} AS text)`,
|
|
63
|
+
timeBucket: (unit, expr, timezone) =>
|
|
64
|
+
`to_char(date_trunc('${unit}', ${local(expr, timezone)}), 'YYYY-MM-DD')`,
|
|
65
|
+
timePart: (part, expr, timezone) =>
|
|
66
|
+
`CAST(extract(${part} FROM ${local(expr, timezone)}) AS integer)`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
name: 'postgres',
|
|
71
|
+
compile: (statement) => renderStatement(statement, renderer),
|
|
72
|
+
decodeNumber
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/expr.ts
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
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
|
+
|
|
8
|
+
export type SqlType = 'number' | 'string' | 'boolean' | 'timestamp'
|
|
9
|
+
|
|
10
|
+
/** A value a query parameter can hold. Timestamps are passed as `Date`. */
|
|
11
|
+
export type ParamValue = string | number | boolean | Date | null
|
|
12
|
+
|
|
13
|
+
export type CompareOp = '=' | '<>' | '>' | '>=' | '<' | '<='
|
|
14
|
+
|
|
15
|
+
export type TimeBucketUnit = 'day' | 'week' | 'month'
|
|
16
|
+
export type TimePartName = 'isodow' | 'hour'
|
|
17
|
+
export type AggregateFn = 'count' | 'countDistinct' | 'sum' | 'avg' | 'median'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A table occurrence. A joined table points at the table it hangs off and carries its own
|
|
21
|
+
* `ON` clause, so any column read from it brings the join along.
|
|
22
|
+
*/
|
|
23
|
+
export type TableRef = {
|
|
24
|
+
readonly name: string
|
|
25
|
+
readonly alias: string | undefined
|
|
26
|
+
readonly join: TableJoin | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type TableJoin = {
|
|
30
|
+
readonly type: 'left' | 'inner'
|
|
31
|
+
readonly parent: TableRef
|
|
32
|
+
readonly on: ExprNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ExprNode =
|
|
36
|
+
| {
|
|
37
|
+
readonly kind: 'column'
|
|
38
|
+
readonly table: TableRef
|
|
39
|
+
readonly column: string
|
|
40
|
+
}
|
|
41
|
+
| { readonly kind: 'param'; readonly value: ParamValue }
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: 'compare'
|
|
44
|
+
readonly op: CompareOp
|
|
45
|
+
readonly left: ExprNode
|
|
46
|
+
readonly right: ExprNode
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: 'logical'
|
|
50
|
+
readonly op: 'and' | 'or'
|
|
51
|
+
readonly items: readonly ExprNode[]
|
|
52
|
+
}
|
|
53
|
+
| { readonly kind: 'not'; readonly item: ExprNode }
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'isNull'
|
|
56
|
+
readonly item: ExprNode
|
|
57
|
+
readonly negated: boolean
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
readonly kind: 'in'
|
|
61
|
+
readonly item: ExprNode
|
|
62
|
+
readonly values: readonly ExprNode[]
|
|
63
|
+
readonly negated: boolean
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
readonly kind: 'case'
|
|
67
|
+
readonly branches: readonly {
|
|
68
|
+
readonly when: ExprNode
|
|
69
|
+
readonly result: ExprNode
|
|
70
|
+
}[]
|
|
71
|
+
readonly otherwise: ExprNode | null
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
readonly kind: 'exists'
|
|
75
|
+
readonly table: TableRef
|
|
76
|
+
readonly where: ExprNode
|
|
77
|
+
readonly negated: boolean
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
readonly kind: 'coalesce'
|
|
81
|
+
readonly items: readonly ExprNode[]
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
readonly kind: 'arithmetic'
|
|
85
|
+
readonly op: '+' | '-' | '*' | '/'
|
|
86
|
+
readonly left: ExprNode
|
|
87
|
+
readonly right: ExprNode
|
|
88
|
+
}
|
|
89
|
+
| { readonly kind: 'lower'; readonly item: ExprNode }
|
|
90
|
+
| { readonly kind: 'toText'; readonly item: ExprNode }
|
|
91
|
+
| {
|
|
92
|
+
readonly kind: 'aggregate'
|
|
93
|
+
readonly fn: AggregateFn
|
|
94
|
+
readonly arg: ExprNode | null
|
|
95
|
+
readonly where: ExprNode | null
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
readonly kind: 'timeBucket'
|
|
99
|
+
readonly unit: TimeBucketUnit
|
|
100
|
+
readonly item: ExprNode
|
|
101
|
+
readonly timezone: string
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
readonly kind: 'timePart'
|
|
105
|
+
readonly part: TimePartName
|
|
106
|
+
readonly item: ExprNode
|
|
107
|
+
readonly timezone: string
|
|
108
|
+
}
|
|
109
|
+
/** The moment the query runs, minus `minutes`; rendered as a timestamp parameter. */
|
|
110
|
+
| { readonly kind: 'minutesAgo'; readonly minutes: number }
|
|
111
|
+
|
|
112
|
+
/** What a raw value of a SQL type looks like in TypeScript. */
|
|
113
|
+
export type ValueOf<T extends SqlType> = T extends 'number'
|
|
114
|
+
? number
|
|
115
|
+
: T extends 'string'
|
|
116
|
+
? string
|
|
117
|
+
: T extends 'boolean'
|
|
118
|
+
? boolean
|
|
119
|
+
: Date
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* An expression of SQL type `T`, or a plain value of `V` that becomes a parameter. `V` narrows
|
|
123
|
+
* plain values, so `eq(order.col('status'), 'PAD')` fails when `status` is an enum. Plain
|
|
124
|
+
* values are never null: compare with `isNull` instead.
|
|
125
|
+
*/
|
|
126
|
+
export type Operand<T extends SqlType, V = ValueOf<T>> = Expr<T> | V
|
|
127
|
+
|
|
128
|
+
/** The values an operand or a `caseWhen` branch can produce. */
|
|
129
|
+
type ValuesOf<O> = O extends Expr<SqlType, infer V> ? V : O
|
|
130
|
+
|
|
131
|
+
/** `null` when `V` can be null or undefined (an optional field), otherwise `never`. */
|
|
132
|
+
type NullOf<V> = null extends V ? null : undefined extends V ? null : never
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `T` is the SQL type; `V` is the TypeScript type of its values, including `null` when the
|
|
136
|
+
* expression can be null. `V` defaults to "any value of `T`, or null", the SQL default; columns,
|
|
137
|
+
* literals and `caseWhen` narrow it (an enum's members, a branded id, a non-null column).
|
|
138
|
+
*/
|
|
139
|
+
export class Expr<T extends SqlType, V = ValueOf<T> | null> {
|
|
140
|
+
/** Phantom: the SQL type this expression evaluates to. Not `declare`: Playwright's Babel transform rejects it. */
|
|
141
|
+
readonly sqlType!: T
|
|
142
|
+
/** Phantom: the values this expression can produce. */
|
|
143
|
+
readonly valueType!: V
|
|
144
|
+
|
|
145
|
+
constructor(readonly node: ExprNode) {}
|
|
146
|
+
|
|
147
|
+
isNull(): Expr<'boolean', boolean> {
|
|
148
|
+
return new Expr({ kind: 'isNull', item: this.node, negated: false })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
isNotNull(): Expr<'boolean', boolean> {
|
|
152
|
+
return new Expr({ kind: 'isNull', item: this.node, negated: true })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
add(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'> {
|
|
156
|
+
return arithmetic('+', this, other)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
sub(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'> {
|
|
160
|
+
return arithmetic('-', this, other)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
mul(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'> {
|
|
164
|
+
return arithmetic('*', this, other)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Always a floating-point division, and `null` when dividing by zero. */
|
|
168
|
+
div(this: Expr<'number'>, other: Operand<'number'>): Expr<'number'> {
|
|
169
|
+
return arithmetic('/', this, other)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function arithmetic(
|
|
174
|
+
op: '+' | '-' | '*' | '/',
|
|
175
|
+
left: Expr<'number'>,
|
|
176
|
+
right: Operand<'number'>
|
|
177
|
+
): Expr<'number'> {
|
|
178
|
+
return new Expr({
|
|
179
|
+
kind: 'arithmetic',
|
|
180
|
+
op,
|
|
181
|
+
left: left.node,
|
|
182
|
+
right: toNode(right)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Maps a column's TypeScript type to its SQL type; `never` for columns we cannot use. */
|
|
187
|
+
export type SqlTypeOf<V> = [NonNullable<V>] extends [never]
|
|
188
|
+
? never
|
|
189
|
+
: [NonNullable<V>] extends [Date]
|
|
190
|
+
? 'timestamp'
|
|
191
|
+
: [NonNullable<V>] extends [number | bigint]
|
|
192
|
+
? 'number'
|
|
193
|
+
: [NonNullable<V>] extends [boolean]
|
|
194
|
+
? 'boolean'
|
|
195
|
+
: [NonNullable<V>] extends [string]
|
|
196
|
+
? 'string'
|
|
197
|
+
: [NonNullable<V>] extends [{ toNumber(): number }]
|
|
198
|
+
? 'number'
|
|
199
|
+
: never
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The value type a column keeps: its own type when it fits its SQL type (an enum, a branded
|
|
203
|
+
* id), plus `null` when the field is nullable or optional.
|
|
204
|
+
*/
|
|
205
|
+
export type ColumnValueOf<V> =
|
|
206
|
+
| ([NonNullable<V>] extends [ValueOf<SqlTypeOf<V>>]
|
|
207
|
+
? NonNullable<V>
|
|
208
|
+
: ValueOf<SqlTypeOf<V>>)
|
|
209
|
+
| NullOf<V>
|
|
210
|
+
|
|
211
|
+
/** Keys of `Row` whose values map to a SQL type (JSON and relation fields are excluded). */
|
|
212
|
+
export type ColumnKey<Row> = {
|
|
213
|
+
[K in keyof Row]-?: [SqlTypeOf<Row[K]>] extends [never] ? never : K
|
|
214
|
+
}[keyof Row] &
|
|
215
|
+
string
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A typed handle on a table. `Row` is any object type (a Prisma model, a hand-written type);
|
|
219
|
+
* `col` only accepts its keys and returns an expression of the matching SQL type.
|
|
220
|
+
*/
|
|
221
|
+
export class Table<Row> {
|
|
222
|
+
readonly row!: Row
|
|
223
|
+
|
|
224
|
+
constructor(readonly ref: TableRef) {}
|
|
225
|
+
|
|
226
|
+
col<K extends ColumnKey<Row>>(
|
|
227
|
+
key: K
|
|
228
|
+
): Expr<SqlTypeOf<Row[K]>, ColumnValueOf<Row[K]>> {
|
|
229
|
+
return new Expr({
|
|
230
|
+
kind: 'column',
|
|
231
|
+
table: this.ref,
|
|
232
|
+
column: key
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Joins `other` to this table. Columns of the returned handle bring the join with them. */
|
|
237
|
+
leftJoin<Other>(
|
|
238
|
+
other: Table<Other>,
|
|
239
|
+
on: (joined: Table<Other>) => Expr<'boolean'>
|
|
240
|
+
): Table<Other> {
|
|
241
|
+
return this.join('left', other, on)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
innerJoin<Other>(
|
|
245
|
+
other: Table<Other>,
|
|
246
|
+
on: (joined: Table<Other>) => Expr<'boolean'>
|
|
247
|
+
): Table<Other> {
|
|
248
|
+
return this.join('inner', other, on)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private join<Other>(
|
|
252
|
+
type: 'left' | 'inner',
|
|
253
|
+
other: Table<Other>,
|
|
254
|
+
on: (joined: Table<Other>) => Expr<'boolean'>
|
|
255
|
+
): Table<Other> {
|
|
256
|
+
const ref: { -readonly [K in keyof TableRef]: TableRef[K] } = {
|
|
257
|
+
name: other.ref.name,
|
|
258
|
+
alias: other.ref.alias,
|
|
259
|
+
join: undefined
|
|
260
|
+
}
|
|
261
|
+
const joined = new Table<Other>(ref)
|
|
262
|
+
ref.join = { type, parent: this.ref, on: on(joined).node }
|
|
263
|
+
|
|
264
|
+
return joined
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A table handle. The alias is optional; the compiler makes aliases unique per statement. */
|
|
269
|
+
export function table<Row>(name: string, alias?: string): Table<Row> {
|
|
270
|
+
return new Table<Row>({ name, alias, join: undefined })
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function toNode(value: Expr<SqlType, unknown> | ParamValue): ExprNode {
|
|
274
|
+
if (value instanceof Expr) return value.node
|
|
275
|
+
|
|
276
|
+
return { kind: 'param', value }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** A literal value, always sent as a query parameter. */
|
|
280
|
+
export function literal<V extends string | number | boolean | Date>(
|
|
281
|
+
value: V
|
|
282
|
+
): Expr<
|
|
283
|
+
V extends number
|
|
284
|
+
? 'number'
|
|
285
|
+
: V extends boolean
|
|
286
|
+
? 'boolean'
|
|
287
|
+
: V extends Date
|
|
288
|
+
? 'timestamp'
|
|
289
|
+
: 'string',
|
|
290
|
+
V
|
|
291
|
+
> {
|
|
292
|
+
return new Expr({ kind: 'param', value })
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function compare<T extends SqlType, V extends ParamValue>(
|
|
296
|
+
op: CompareOp,
|
|
297
|
+
left: Expr<T, V>,
|
|
298
|
+
right: Operand<T, NonNullable<V>>
|
|
299
|
+
): Expr<'boolean'> {
|
|
300
|
+
return new Expr({
|
|
301
|
+
kind: 'compare',
|
|
302
|
+
op,
|
|
303
|
+
left: left.node,
|
|
304
|
+
right: toNode(right)
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function eq<T extends SqlType, V extends ParamValue>(
|
|
309
|
+
left: Expr<T, V>,
|
|
310
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
311
|
+
): Expr<'boolean'> {
|
|
312
|
+
return compare('=', left, right)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function ne<T extends SqlType, V extends ParamValue>(
|
|
316
|
+
left: Expr<T, V>,
|
|
317
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
318
|
+
): Expr<'boolean'> {
|
|
319
|
+
return compare('<>', left, right)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function gt<T extends SqlType, V extends ParamValue>(
|
|
323
|
+
left: Expr<T, V>,
|
|
324
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
325
|
+
): Expr<'boolean'> {
|
|
326
|
+
return compare('>', left, right)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function gte<T extends SqlType, V extends ParamValue>(
|
|
330
|
+
left: Expr<T, V>,
|
|
331
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
332
|
+
): Expr<'boolean'> {
|
|
333
|
+
return compare('>=', left, right)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function lt<T extends SqlType, V extends ParamValue>(
|
|
337
|
+
left: Expr<T, V>,
|
|
338
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
339
|
+
): Expr<'boolean'> {
|
|
340
|
+
return compare('<', left, right)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function lte<T extends SqlType, V extends ParamValue>(
|
|
344
|
+
left: Expr<T, V>,
|
|
345
|
+
right: Operand<NoInfer<T>, NonNullable<NoInfer<V>>>
|
|
346
|
+
): Expr<'boolean'> {
|
|
347
|
+
return compare('<=', left, right)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function and(...items: Expr<'boolean'>[]): Expr<'boolean'> {
|
|
351
|
+
return new Expr({
|
|
352
|
+
kind: 'logical',
|
|
353
|
+
op: 'and',
|
|
354
|
+
items: items.map((i) => i.node)
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function or(...items: Expr<'boolean'>[]): Expr<'boolean'> {
|
|
359
|
+
return new Expr({
|
|
360
|
+
kind: 'logical',
|
|
361
|
+
op: 'or',
|
|
362
|
+
items: items.map((i) => i.node)
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function not(item: Expr<'boolean'>): Expr<'boolean'> {
|
|
367
|
+
if (item.node.kind === 'exists') {
|
|
368
|
+
return new Expr({ ...item.node, negated: !item.node.negated })
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return new Expr({ kind: 'not', item: item.node })
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function isNull(item: Expr<SqlType, unknown>): Expr<'boolean', boolean> {
|
|
375
|
+
return item.isNull()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function isNotNull(
|
|
379
|
+
item: Expr<SqlType, unknown>
|
|
380
|
+
): Expr<'boolean', boolean> {
|
|
381
|
+
return item.isNotNull()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
type CaseValue = string | number | boolean | Expr<SqlType, unknown>
|
|
385
|
+
|
|
386
|
+
type CaseType<V> =
|
|
387
|
+
V extends Expr<infer T, unknown>
|
|
388
|
+
? T
|
|
389
|
+
: V extends string
|
|
390
|
+
? 'string'
|
|
391
|
+
: V extends number
|
|
392
|
+
? 'number'
|
|
393
|
+
: V extends boolean
|
|
394
|
+
? 'boolean'
|
|
395
|
+
: never
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* `CASE WHEN ... THEN ... ELSE ... END`. Branches are checked in order. Plain values become
|
|
399
|
+
* parameters; a computed dimension's `values` must list every plain value used here. Without
|
|
400
|
+
* `otherwise`, rows matching no branch are null, and the type says so.
|
|
401
|
+
*/
|
|
402
|
+
export function caseWhen<const V extends CaseValue>(
|
|
403
|
+
branches: readonly (readonly [Expr<'boolean', unknown>, V])[]
|
|
404
|
+
): Expr<CaseType<V>, ValuesOf<V> | null>
|
|
405
|
+
export function caseWhen<const V extends CaseValue, const O extends CaseValue>(
|
|
406
|
+
branches: readonly (readonly [Expr<'boolean', unknown>, V])[],
|
|
407
|
+
otherwise: O
|
|
408
|
+
): Expr<CaseType<V | O>, ValuesOf<V | O>>
|
|
409
|
+
export function caseWhen(
|
|
410
|
+
branches: readonly (readonly [Expr<'boolean', unknown>, CaseValue])[],
|
|
411
|
+
otherwise?: CaseValue
|
|
412
|
+
): Expr<SqlType, unknown> {
|
|
413
|
+
if (branches.length === 0) {
|
|
414
|
+
throw new Error('caseWhen needs at least one branch')
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return new Expr({
|
|
418
|
+
kind: 'case',
|
|
419
|
+
branches: branches.map(([when, then]) => ({
|
|
420
|
+
when: when.node,
|
|
421
|
+
result: toNode(then)
|
|
422
|
+
})),
|
|
423
|
+
otherwise: otherwise === undefined ? null : toNode(otherwise)
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A correlated `EXISTS`: true when `from` has a row matching `where`. `where` usually links
|
|
429
|
+
* the subquery table to the outer table, for example
|
|
430
|
+
* `exists(message, eq(message.col('sessionId'), session.col('id')))`.
|
|
431
|
+
*/
|
|
432
|
+
export function exists<Row>(
|
|
433
|
+
from: Table<Row>,
|
|
434
|
+
where: Expr<'boolean'>
|
|
435
|
+
): Expr<'boolean', boolean> {
|
|
436
|
+
return new Expr({
|
|
437
|
+
kind: 'exists',
|
|
438
|
+
table: from.ref,
|
|
439
|
+
where: where.node,
|
|
440
|
+
negated: false
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Null only when every item can be null; a plain fallback value is never null. */
|
|
445
|
+
type Coalesced<V, Rest> = [Rest] extends [never]
|
|
446
|
+
? V
|
|
447
|
+
: null extends Rest
|
|
448
|
+
? V | Rest
|
|
449
|
+
: NonNullable<V> | Rest
|
|
450
|
+
|
|
451
|
+
export function coalesce<
|
|
452
|
+
T extends SqlType,
|
|
453
|
+
V extends ParamValue,
|
|
454
|
+
const R extends readonly Operand<NoInfer<T>>[]
|
|
455
|
+
>(first: Expr<T, V>, ...rest: R): Expr<T, Coalesced<V, ValuesOf<R[number]>>> {
|
|
456
|
+
return new Expr({
|
|
457
|
+
kind: 'coalesce',
|
|
458
|
+
items: [first.node, ...rest.map((item) => toNode(item))]
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Matches `item` against a fixed list of values. */
|
|
463
|
+
export function inList<T extends SqlType, V extends ParamValue>(
|
|
464
|
+
item: Expr<T, V>,
|
|
465
|
+
values: readonly NonNullable<NoInfer<V>>[]
|
|
466
|
+
): Expr<'boolean'> {
|
|
467
|
+
return new Expr({
|
|
468
|
+
kind: 'in',
|
|
469
|
+
item: item.node,
|
|
470
|
+
values: values.map((value) => toNode(value)),
|
|
471
|
+
negated: false
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** `item` as text, for example to compare an enum column with a value outside the enum. */
|
|
476
|
+
export function toText<V>(
|
|
477
|
+
item: Expr<SqlType, V>
|
|
478
|
+
): Expr<'string', string | NullOf<V>> {
|
|
479
|
+
return new Expr({ kind: 'toText', item: item.node })
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The moment the query runs, minus `minutes`, such as "created in the last 15 minutes". The
|
|
484
|
+
* query's clock supplies the moment, so it follows the `now` option of `createAnalytics`.
|
|
485
|
+
*/
|
|
486
|
+
export function minutesAgo(minutes: number): Expr<'timestamp', Date> {
|
|
487
|
+
return new Expr({ kind: 'minutesAgo', minutes })
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Follows the join chain up to the table that is not joined to anything. */
|
|
491
|
+
export function rootOf(ref: TableRef): TableRef {
|
|
492
|
+
let current = ref
|
|
493
|
+
while (current.join) current = current.join.parent
|
|
494
|
+
|
|
495
|
+
return current
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** The nodes directly below `node`, including an `exists` subquery's condition. */
|
|
499
|
+
export function directChildren(node: ExprNode): readonly ExprNode[] {
|
|
500
|
+
switch (node.kind) {
|
|
501
|
+
case 'compare':
|
|
502
|
+
case 'arithmetic':
|
|
503
|
+
return [node.left, node.right]
|
|
504
|
+
case 'logical':
|
|
505
|
+
case 'coalesce':
|
|
506
|
+
return node.items
|
|
507
|
+
case 'not':
|
|
508
|
+
case 'isNull':
|
|
509
|
+
case 'lower':
|
|
510
|
+
case 'toText':
|
|
511
|
+
case 'timeBucket':
|
|
512
|
+
case 'timePart':
|
|
513
|
+
return [node.item]
|
|
514
|
+
case 'in':
|
|
515
|
+
return [node.item, ...node.values]
|
|
516
|
+
case 'case':
|
|
517
|
+
return [
|
|
518
|
+
...node.branches.flatMap((branch) => [branch.when, branch.result]),
|
|
519
|
+
...(node.otherwise ? [node.otherwise] : [])
|
|
520
|
+
]
|
|
521
|
+
case 'aggregate':
|
|
522
|
+
return [
|
|
523
|
+
...(node.arg ? [node.arg] : []),
|
|
524
|
+
...(node.where ? [node.where] : [])
|
|
525
|
+
]
|
|
526
|
+
case 'exists':
|
|
527
|
+
return [node.where]
|
|
528
|
+
case 'column':
|
|
529
|
+
case 'param':
|
|
530
|
+
case 'minutesAgo':
|
|
531
|
+
return []
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** Visits every node in a tree, depth first. */
|
|
536
|
+
export function walk(node: ExprNode, visit: (node: ExprNode) => void): void {
|
|
537
|
+
visit(node)
|
|
538
|
+
for (const child of directChildren(node)) walk(child, visit)
|
|
539
|
+
}
|
package/src/guidance.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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 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.
|
|
6
|
+
- Pick the dataset whose description matches the question, the fewest measures that answer it and at most two groupings.
|
|
7
|
+
- Say which period and filters the numbers cover, using the dates in the result ("1-30 September").
|
|
8
|
+
- Compare with the previous period (compareToPrevious) when the change is the point, such as "is it going up".
|
|
9
|
+
- When the result has notes (overlapping rows, such as a record counted once per tag), say so in a few words.
|
|
10
|
+
- 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.`
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type Analytics,
|
|
3
|
+
AnalyticsError,
|
|
4
|
+
type AnalyticsErrorCode,
|
|
5
|
+
type AnalyticsErrorDetails,
|
|
6
|
+
type AnalyticsErrorOf,
|
|
7
|
+
type AnalyticsOptions,
|
|
8
|
+
type AnalyticsQuery,
|
|
9
|
+
type AnalyticsQueryEvent,
|
|
10
|
+
type AnalyticsResult,
|
|
11
|
+
type AnalyticsSource,
|
|
12
|
+
createAnalytics,
|
|
13
|
+
type DatasetCatalogEntry,
|
|
14
|
+
isAnalyticsError
|
|
15
|
+
} from './analytics.js'
|
|
16
|
+
export {
|
|
17
|
+
CompileError,
|
|
18
|
+
type CompiledStatement,
|
|
19
|
+
type Dialect,
|
|
20
|
+
type JoinClause,
|
|
21
|
+
type SelectItem,
|
|
22
|
+
type SelectStatement,
|
|
23
|
+
type SqlRenderer,
|
|
24
|
+
decodeNumber,
|
|
25
|
+
renderStatement
|
|
26
|
+
} from './compile.js'
|
|
27
|
+
export {
|
|
28
|
+
type AnyDataset,
|
|
29
|
+
type AnyDimension,
|
|
30
|
+
type AnyMeasure,
|
|
31
|
+
type Dataset,
|
|
32
|
+
type DimensionValue,
|
|
33
|
+
defineDataset,
|
|
34
|
+
dimension,
|
|
35
|
+
type MeasureValue,
|
|
36
|
+
measure,
|
|
37
|
+
type Ratio
|
|
38
|
+
} from './dataset.js'
|
|
39
|
+
export {
|
|
40
|
+
and,
|
|
41
|
+
caseWhen,
|
|
42
|
+
coalesce,
|
|
43
|
+
type ColumnKey,
|
|
44
|
+
eq,
|
|
45
|
+
Expr,
|
|
46
|
+
type ExprNode,
|
|
47
|
+
exists,
|
|
48
|
+
gt,
|
|
49
|
+
gte,
|
|
50
|
+
inList,
|
|
51
|
+
isNotNull,
|
|
52
|
+
isNull,
|
|
53
|
+
literal,
|
|
54
|
+
lt,
|
|
55
|
+
lte,
|
|
56
|
+
minutesAgo,
|
|
57
|
+
ne,
|
|
58
|
+
not,
|
|
59
|
+
or,
|
|
60
|
+
type SqlType,
|
|
61
|
+
Table,
|
|
62
|
+
type TableRef,
|
|
63
|
+
table,
|
|
64
|
+
toText
|
|
65
|
+
} from './expr.js'
|
|
66
|
+
export { analyticsToolGuidance } from './guidance.js'
|
|
67
|
+
export type { PeriodInput, PeriodPreset } from './period.js'
|
|
68
|
+
export {
|
|
69
|
+
AnalyticsResultSchema,
|
|
70
|
+
type DatasetFilter,
|
|
71
|
+
type DatasetQuery,
|
|
72
|
+
type QueryResult,
|
|
73
|
+
type ResultOf
|
|
74
|
+
} from './query.js'
|