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,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
+ }
@@ -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'