@pythia-software/query-table-core 0.2.0 → 0.3.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/README.md CHANGED
@@ -40,5 +40,13 @@ Query state is bounded whenever it crosses the library's URL, storage, or
40
40
  server-projection boundaries. SQL expressions remain trusted server-side schema
41
41
  configuration; request values are never SQL fragments.
42
42
 
43
+ Text filters include `matches_regex` / `not_matches_regex`. An order term may
44
+ set `extract: { regex: "..." }` to compare the first capture group (or the whole
45
+ match when there is no capture group); non-matches sort as nulls.
46
+
43
47
  See the [repository README](https://github.com/Pythia-Software/query-table#readme)
44
48
  for the complete schema and backend documentation.
49
+
50
+ ## Computed SELECT definitions
51
+
52
+ Exports include `ComputedColumnStore`, `httpComputedColumnStore`, `memoryComputedColumnStore`, `compileFormula`, `FORMULA_FUNCTIONS`, and `formulaRuntime`. Shared source definitions are versioned separately from queries; query SELECT entries use `{ field: "@computed/<id>" }`. The React package executes the interpreter in a bounded worker. Do not run user regex on the browser main thread. See the repository’s `docs/computed-columns.md` for semantics and persistence details.
package/dist/index.d.ts CHANGED
@@ -1,15 +1,42 @@
1
1
  /** All filter operators across both source projects, unioned. Every value can
2
2
  * be NULL, so `is_null`/`is_not_null` are valid for every field type. */
3
- type FilterOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "contains" | "starts_with" | "ends_with" | "includes" | "is_null" | "is_not_null";
4
- /** A single AND-combined filter. `value` is always the raw string the UI
5
- * captured; coercion to number/bool/date happens at apply/compile time based on
6
- * the field's declared type (never on the value's runtime shape). Unused for
7
- * the nullary ops (`is_null`/`is_not_null`). */
3
+ type FilterOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "contains" | "starts_with" | "ends_with" | "matches_regex" | "not_matches_regex" | "includes" | "is_null" | "is_not_null";
4
+ /** A single filter predicate (a "literal" in CNF terms). `value` is always the
5
+ * raw string the UI captured; coercion to number/bool/date happens at
6
+ * apply/compile time based on the field's declared type (never on the value's
7
+ * runtime shape). Unused for the nullary ops (`is_null`/`is_not_null`).
8
+ *
9
+ * `negated` wraps the predicate in a logical NOT. It is only ever set for ops
10
+ * that have no complementary operator (`contains`/`starts_with`/`ends_with`/
11
+ * `includes`); ops with a complement negate by flipping the op itself (`>=`→`<`,
12
+ * `=`→`!=`, `is_null`→`is_not_null`, `matches_regex`→`not_matches_regex`) — see
13
+ * `negateClause` in ops.ts. NOT is null-exclusive: a NULL/empty value satisfies
14
+ * neither the predicate nor its negation, matching `not_matches_regex`. */
8
15
  interface WhereClause {
9
16
  field: string;
10
17
  op: FilterOp;
11
18
  value: string;
19
+ negated?: boolean;
20
+ }
21
+ /** A disjunction of predicates — the inner OR of conjunctive normal form. Every
22
+ * member is a plain literal; nesting is intentionally not allowed yet (CNF
23
+ * only), but the object shape leaves room to grow. */
24
+ interface OrGroup {
25
+ any: WhereClause[];
12
26
  }
27
+ /** One conjunct of the WHERE clause: either a single predicate or an OR group.
28
+ * `QueryState.where` is the AND of these terms, so the whole WHERE is
29
+ * conjunctive normal form — `(a) AND (b OR c) AND (d)`. A bare literal is
30
+ * equivalent to a one-element OR group; normalization flattens singleton groups
31
+ * back to a literal so equality/encoding stay canonical. */
32
+ type WhereTerm = WhereClause | OrGroup;
33
+ /** Narrow a term to an OR group. A literal always carries `field`; a group never
34
+ * does and always carries an `any` array. */
35
+ declare function isOrGroup(term: WhereTerm): term is OrGroup;
36
+ /** The predicate literals a term contributes, in order: the term itself for a
37
+ * literal, or its members for a group. Handy for anything that must visit every
38
+ * predicate regardless of grouping (field allowlisting, SELECT projection). */
39
+ declare function predicatesOf(term: WhereTerm): WhereClause[];
13
40
  /** One ORDER BY term. Array order in `QueryState.orderBy` is the sort priority. */
14
41
  interface OrderByClause {
15
42
  field: string;
@@ -17,6 +44,14 @@ interface OrderByClause {
17
44
  /** NULL placement. Omitted = "last" (the historical default both projects used,
18
45
  * preserved so old `?q=` links round-trip identically). */
19
46
  nulls?: "first" | "last";
47
+ /** Sort by the first capture group of a regex match (or by the whole match
48
+ * when the pattern has no capture group). A non-match produces NULL. Regex
49
+ * syntax is interpreted by the executor (JavaScript locally, PostgreSQL on
50
+ * the bundled Go backend), so portable patterns should use their common
51
+ * syntax subset. */
52
+ extract?: {
53
+ regex: string;
54
+ };
20
55
  }
21
56
  /** One column in the SELECT list, plus the view state that travels with it.
22
57
  * Order in `QueryState.select` is the left-to-right display order. */
@@ -53,8 +88,10 @@ interface AggregationClause {
53
88
  interface QueryState {
54
89
  /** Ordered SELECT list + per-column widths. Empty = the schema's default columns. */
55
90
  select: SelectColumn[];
56
- /** AND-combined filters. */
57
- where: WhereClause[];
91
+ /** WHERE in conjunctive normal form: the AND of these terms, each a single
92
+ * predicate or an OR group. A flat list of literals (no groups) is the common
93
+ * case and the legacy shape, decoded unchanged. */
94
+ where: WhereTerm[];
58
95
  /** Multi-sort terms in priority order. Empty = the schema's default sort. */
59
96
  orderBy: OrderByClause[];
60
97
  /** Page size. */
@@ -117,6 +154,10 @@ interface BackendField {
117
154
  * pushed to the server; client-side filtering/sorting (if any) reads `accessor`. */
118
155
  interface DerivedField<Row = any, V = unknown> {
119
156
  kind: "derived";
157
+ /** Backend inputs required for accessors and computed formulas. */
158
+ dependencies?: string[];
159
+ /** Reusable computed column ID; always select-only. */
160
+ computedId?: string;
120
161
  /** Compute the value from the row for client filter/sort. Omit for render-only
121
162
  * columns (the renderer reads the whole row from CellContext instead). */
122
163
  accessor?: (row: Row) => V;
@@ -222,6 +263,35 @@ declare function readFieldValue<Row, V = unknown>(field: FieldDef<Row, V>, row:
222
263
  * renderer at runtime. Throws with a readable message on a malformed document. */
223
264
  declare function loadSchema<Row = any>(doc: unknown): FieldSchema<Row>;
224
265
 
266
+ /** The logical negation of a predicate. Prefers flipping to the complementary
267
+ * operator (nicer to read, and the server's op allowlist already covers it);
268
+ * falls back to toggling the `negated` flag when the op has no complement, or
269
+ * when the complement is not in `allowedOps` (a field's `filter.ops` override
270
+ * could disable it, and the server would reject a disabled op). `allowedOps`,
271
+ * when given, is the field's effective operator set from `opsForField`. */
272
+ declare function negateClause(clause: WhereClause, allowedOps?: readonly FilterOp[]): WhereClause;
273
+ /** Whether a predicate is negative — either an op that reads as a negation
274
+ * (`!=`, `not_matches_regex`, `is_not_null`) or one carrying the `negated`
275
+ * flag. Used to sort candidate filters into the CellMenu's two columns. */
276
+ declare function isNegativePredicate(clause: Pick<WhereClause, "op" | "negated">): boolean;
277
+ /** A specific operator + negation state, e.g. `{ op: "<", negated: false }` or
278
+ * `{ op: "contains", negated: true }`. */
279
+ interface OpChoice {
280
+ op: FilterOp;
281
+ negated: boolean;
282
+ }
283
+ /** A positive operator ("keep") and its logical negation ("exclude") — the row
284
+ * shape rendered by both the CellMenu quick-filters and the WHERE operator
285
+ * picker, so the two read identically. */
286
+ interface OpPair {
287
+ keep: OpChoice;
288
+ exclude: OpChoice;
289
+ }
290
+ /** The keep/exclude operator pairs offered for a field: each positive op with
291
+ * its negation opposite, then a nullity row (`is null`/`is not null`) when the
292
+ * field allows it. Derived from the field's effective op set and `negateClause`,
293
+ * so a `filter.ops` override narrows the offered pairs too. */
294
+ declare function opPairsForField(field: Pick<FieldDef, "type" | "filter">): OpPair[];
225
295
  /** Default operator set per type. A FieldDef.filter.ops overrides this. */
226
296
  declare const OPS_BY_TYPE: Record<FieldType, FilterOp[]>;
227
297
  /** Operators that take no value (the value input is hidden). */
@@ -269,8 +339,8 @@ declare function decodeQuery(token: string): QueryState;
269
339
  interface ServerQuery {
270
340
  /** field names to return — visible columns ∪ fields referenced by where/orderBy. */
271
341
  select: string[];
272
- /** only clauses on pushdown-filterable fields. */
273
- where: WhereClause[];
342
+ /** only terms whose every predicate is on a pushdown-filterable field. */
343
+ where: WhereTerm[];
274
344
  /** only terms on server-sortable fields, with `field` already remapped to the
275
345
  * field's server sort key (FieldDef.sort.field) when set. */
276
346
  orderBy: OrderByClause[];
@@ -285,7 +355,7 @@ declare function toServerQuery<Row>(q: QueryState, schema: FieldSchema<Row>): Se
285
355
  * metrics describe every matching row, not the visible page. Mirrors the Go
286
356
  * AggSpec list. */
287
357
  interface AggregationRequest {
288
- where: WhereClause[];
358
+ where: WhereTerm[];
289
359
  aggregations: AggregationClause[];
290
360
  }
291
361
  /** One group's result. `keys` has one entry per AggregationClause.groupBy field,
@@ -420,4 +490,117 @@ declare function memoryStorageAdapter(): StorageAdapter;
420
490
  * datasets. Falls back to no persistence when storage is unavailable. */
421
491
  declare function localStorageAdapter(): StorageAdapter;
422
492
 
423
- export { AGG_OPS_BY_TYPE, AGG_OPS_NEEDING_FIELD, type AggOp, type AggregateConfig, type AggregationBucket, type AggregationClause, type AggregationRequest, type AggregationResult, type AggregationResultEntry, type Align, type ApplyResult, type BackendField, type CellRenderer, type DerivedField, type DistinctValuesQuery, type DistinctValuesResult, EMPTY_QUERY, type FetchRowsResult, type FieldDef, type FieldSchema, type FieldSource, type FieldStats, type FieldType, type FilterConfig, type FilterOp, type FilterValues, MAX_AGGREGATIONS, MAX_GROUP_BY_FIELDS, MAX_ORDER_BY_TERMS, MAX_QUERY_LIMIT, MAX_QUERY_OFFSET, MAX_QUERY_TOKEN_LENGTH, MAX_SELECT_COLUMNS, MAX_WHERE_CLAUSES, NULLARY_OPS, OPS_BY_TYPE, type OrderByClause, type QueryState, type RowId, type SavedQuery, type SelectColumn, type SelectConfig, type ServerQuery, type SortConfig, type StorageAdapter, type Transport, type WhereClause, aggOpAllowedForType, aggOpNeedsField, aggOpsForField, applyAggregations, applyQuery, coerceValue, decodeQuery, encodeQuery, filterValues, indexFields, isFilterable, isGroupable, isMeasurable, isPushdownFilter, isSelectable, isSortable, loadSchema, localStorageAdapter, matchesClause, memoryStorageAdapter, normalizeQueryState, opAllowedForType, opsForField, queriesEqual, readFieldValue, selectedFields, toAggregationQuery, toServerQuery };
493
+ type FormulaType = Exclude<FieldType, "enum"> | "null";
494
+ type FormulaValue = string | number | boolean | string[] | null;
495
+ interface FormulaDiagnostic {
496
+ message: string;
497
+ from: number;
498
+ to: number;
499
+ }
500
+ type FormulaNode = {
501
+ kind: "literal";
502
+ value: FormulaValue;
503
+ from: number;
504
+ to: number;
505
+ } | {
506
+ kind: "field";
507
+ name: string;
508
+ valueType?: FormulaType;
509
+ from: number;
510
+ to: number;
511
+ } | {
512
+ kind: "call";
513
+ name: string;
514
+ args: FormulaNode[];
515
+ from: number;
516
+ to: number;
517
+ };
518
+ interface FormulaPlan {
519
+ ast: FormulaNode;
520
+ dependencies: string[];
521
+ type: FormulaType;
522
+ }
523
+ interface RegexInspection {
524
+ input: string;
525
+ start: number;
526
+ end: number;
527
+ groups: (string | null)[];
528
+ }
529
+ interface FormulaResult {
530
+ value: FormulaValue;
531
+ error?: string;
532
+ regex?: RegexInspection;
533
+ }
534
+ interface FormulaFunction {
535
+ name: string;
536
+ signature: string;
537
+ description: string;
538
+ min: number;
539
+ max: number;
540
+ result: FormulaType | "branch";
541
+ args: FormulaType[] | "any";
542
+ }
543
+ declare const FORMULA_FUNCTIONS: FormulaFunction[];
544
+ declare class FormulaError extends Error {
545
+ from: number;
546
+ to: number;
547
+ constructor(message: string, from?: number, to?: number);
548
+ }
549
+ declare function compileFormula(source: string, fields: readonly {
550
+ name: string;
551
+ type: FieldType;
552
+ }[], resolve?: (name: string) => FormulaPlan | undefined): FormulaPlan;
553
+ /** Self-contained runtime: serialized into the browser worker, never user code. */
554
+ declare function formulaRuntime(ast: FormulaNode, inputs: Record<string, FormulaValue>): FormulaResult;
555
+
556
+ /** Reserved field token namespace. Query state stores IDs, never formula source. */
557
+ declare const COMPUTED_PREFIX = "@computed/";
558
+ declare const computedFieldName: (id: string) => string;
559
+ declare const isComputedField: (name: string) => boolean;
560
+ interface ComputedColumn {
561
+ id: string;
562
+ label: string;
563
+ expression: {
564
+ language: "qt-expr";
565
+ version: 1;
566
+ source: string;
567
+ };
568
+ /** Optimistic concurrency token issued by the store. */
569
+ revision: string;
570
+ }
571
+ type ComputedColumnDraft = Omit<ComputedColumn, "revision">;
572
+ interface ComputedColumnStore {
573
+ /** Return the complete authorized catalogue for this dataset. */
574
+ list(dataset: string, signal?: AbortSignal): Promise<ComputedColumn[]>;
575
+ /** null creates a new ID; updates must match the current revision. */
576
+ save(dataset: string, column: ComputedColumnDraft, expectedRevision: string | null): Promise<ComputedColumn>;
577
+ /** Optional push invalidation, including changes from other sessions. */
578
+ subscribe?(dataset: string, listener: () => void): () => void;
579
+ }
580
+ declare function validateComputedColumn(input: unknown): ComputedColumn;
581
+ declare function memoryComputedColumnStore(seed?: Record<string, ComputedColumn[]>): ComputedColumnStore;
582
+ /** REST adapter. The implementing server owns authorization and DB persistence.
583
+ * GET base?dataset=… → ComputedColumn[]
584
+ * PUT base?dataset=… → {column, expectedRevision} → ComputedColumn
585
+ * Return HTTP 409 for a stale revision. No formula evaluation happens there. */
586
+ declare function httpComputedColumnStore(base: string, fetcher?: typeof fetch): ComputedColumnStore;
587
+ interface ComputedCellError {
588
+ computedError: string;
589
+ }
590
+ declare function isComputedCellError(value: unknown): value is ComputedCellError;
591
+ interface PreviewGroup {
592
+ inputs: FormulaValue[];
593
+ result: FormulaResult;
594
+ count: number;
595
+ }
596
+ interface ColumnPreview {
597
+ dependencies: string[];
598
+ groups: PreviewGroup[];
599
+ processed: number;
600
+ total: number;
601
+ nulls: number;
602
+ errors: number;
603
+ }
604
+ declare function groupPreview(dependencies: string[], inputs: Record<string, FormulaValue>[], results: FormulaResult[], total: number): ColumnPreview;
605
+
606
+ export { AGG_OPS_BY_TYPE, AGG_OPS_NEEDING_FIELD, type AggOp, type AggregateConfig, type AggregationBucket, type AggregationClause, type AggregationRequest, type AggregationResult, type AggregationResultEntry, type Align, type ApplyResult, type BackendField, COMPUTED_PREFIX, type CellRenderer, type ColumnPreview, type ComputedCellError, type ComputedColumn, type ComputedColumnDraft, type ComputedColumnStore, type DerivedField, type DistinctValuesQuery, type DistinctValuesResult, EMPTY_QUERY, FORMULA_FUNCTIONS, type FetchRowsResult, type FieldDef, type FieldSchema, type FieldSource, type FieldStats, type FieldType, type FilterConfig, type FilterOp, type FilterValues, type FormulaDiagnostic, FormulaError, type FormulaFunction, type FormulaNode, type FormulaPlan, type FormulaResult, type FormulaType, type FormulaValue, MAX_AGGREGATIONS, MAX_GROUP_BY_FIELDS, MAX_ORDER_BY_TERMS, MAX_QUERY_LIMIT, MAX_QUERY_OFFSET, MAX_QUERY_TOKEN_LENGTH, MAX_SELECT_COLUMNS, MAX_WHERE_CLAUSES, NULLARY_OPS, OPS_BY_TYPE, type OpChoice, type OpPair, type OrGroup, type OrderByClause, type PreviewGroup, type QueryState, type RegexInspection, type RowId, type SavedQuery, type SelectColumn, type SelectConfig, type ServerQuery, type SortConfig, type StorageAdapter, type Transport, type WhereClause, type WhereTerm, aggOpAllowedForType, aggOpNeedsField, aggOpsForField, applyAggregations, applyQuery, coerceValue, compileFormula, computedFieldName, decodeQuery, encodeQuery, filterValues, formulaRuntime, groupPreview, httpComputedColumnStore, indexFields, isComputedCellError, isComputedField, isFilterable, isGroupable, isMeasurable, isNegativePredicate, isOrGroup, isPushdownFilter, isSelectable, isSortable, loadSchema, localStorageAdapter, matchesClause, memoryComputedColumnStore, memoryStorageAdapter, negateClause, normalizeQueryState, opAllowedForType, opPairsForField, opsForField, predicatesOf, queriesEqual, readFieldValue, selectedFields, toAggregationQuery, toServerQuery, validateComputedColumn };