turbine-orm 0.67.0 → 0.70.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.
Files changed (56) hide show
  1. package/dist/cjs/cli/error-catalog.d.ts +77 -0
  2. package/dist/cjs/cli/error-catalog.js +388 -0
  3. package/dist/cjs/cli/index.js +3 -2
  4. package/dist/cjs/cli/mcp.d.ts +19 -3
  5. package/dist/cjs/cli/mcp.js +709 -22
  6. package/dist/cjs/cli/migrate.d.ts +19 -2
  7. package/dist/cjs/cli/observe.d.ts +2 -2
  8. package/dist/cjs/cli/observe.js +20 -2
  9. package/dist/cjs/cli/pii-predicate-guard.d.ts +6 -2
  10. package/dist/cjs/cli/pii-predicate-guard.js +6 -2
  11. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  12. package/dist/cjs/client.d.ts +30 -75
  13. package/dist/cjs/client.js +31 -11
  14. package/dist/cjs/introspect.d.ts +113 -17
  15. package/dist/cjs/introspect.js +229 -33
  16. package/dist/cjs/pg-types.d.ts +153 -0
  17. package/dist/cjs/pg-types.js +38 -0
  18. package/dist/cjs/pipeline.d.ts +3 -3
  19. package/dist/cjs/query/batched-loader.d.ts +2 -2
  20. package/dist/cjs/query/builder.d.ts +2 -2
  21. package/dist/cjs/query/builder.js +10 -1
  22. package/dist/cjs/query/deferred.d.ts +6 -6
  23. package/dist/cjs/query/filters.d.ts +13 -7
  24. package/dist/cjs/query/filters.js +13 -14
  25. package/dist/cjs/query/where.d.ts +2 -2
  26. package/dist/cjs/schema-sql.d.ts +18 -0
  27. package/dist/cjs/schema-sql.js +18 -0
  28. package/dist/cli/error-catalog.d.ts +77 -0
  29. package/dist/cli/error-catalog.js +383 -0
  30. package/dist/cli/index.js +3 -2
  31. package/dist/cli/mcp.d.ts +19 -3
  32. package/dist/cli/mcp.js +709 -23
  33. package/dist/cli/migrate.d.ts +19 -2
  34. package/dist/cli/observe.d.ts +2 -2
  35. package/dist/cli/observe.js +20 -2
  36. package/dist/cli/pii-predicate-guard.d.ts +6 -2
  37. package/dist/cli/pii-predicate-guard.js +6 -2
  38. package/dist/cli/studio-ui.generated.js +1 -1
  39. package/dist/client.d.ts +30 -75
  40. package/dist/client.js +31 -11
  41. package/dist/introspect.d.ts +113 -17
  42. package/dist/introspect.js +227 -33
  43. package/dist/pg-types.d.ts +153 -0
  44. package/dist/pg-types.js +37 -0
  45. package/dist/pipeline.d.ts +3 -3
  46. package/dist/query/batched-loader.d.ts +2 -2
  47. package/dist/query/builder.d.ts +2 -2
  48. package/dist/query/builder.js +10 -1
  49. package/dist/query/deferred.d.ts +6 -6
  50. package/dist/query/filters.d.ts +13 -7
  51. package/dist/query/filters.js +13 -13
  52. package/dist/query/where-compile.js +1 -1
  53. package/dist/query/where.d.ts +2 -2
  54. package/dist/schema-sql.d.ts +18 -0
  55. package/dist/schema-sql.js +18 -0
  56. package/package.json +19 -7
@@ -21,10 +21,10 @@
21
21
  * const users = db.table<User>('users');
22
22
  * ```
23
23
  */
24
- import pg from 'pg';
25
24
  import { type Dialect } from './dialect.js';
26
25
  import { type ErrorMessageMode } from './errors.js';
27
26
  import { type ObserveConfig, type ObserveHandle } from './observe.js';
27
+ import type { PgCompatPool, PgCompatPoolClient } from './pg-types.js';
28
28
  import { type PipelineOptions, type PipelineResults } from './pipeline.js';
29
29
  import { type DeferredQuery, type GlobalFilters, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy, type TemporalInfinityReading } from './query/index.js';
30
30
  import { type NotificationHandler, type Subscription } from './realtime.js';
@@ -38,75 +38,13 @@ export interface RetryOptions {
38
38
  }
39
39
  export declare function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
40
40
  /**
41
- * Minimal pg-compatible query result.
42
- * `pg.Pool`, `@neondatabase/serverless` Pool, `@vercel/postgres` Pool and
43
- * any driver speaking the node-postgres API all satisfy this shape.
41
+ * The pg-compatible driver contract. Declared in `pg-types.ts` (a leaf module
42
+ * with no imports) rather than here, so `query/` can name the same interfaces
43
+ * without an import edge back to the client, and so no `@types/pg` name
44
+ * reaches the published declarations. Re-exported so every existing
45
+ * `from './client.js'` import path keeps working.
44
46
  */
45
- export interface PgCompatQueryResult<R = Record<string, unknown>> {
46
- rows: R[];
47
- rowCount: number | null;
48
- fields?: Array<{
49
- name: string;
50
- dataTypeID: number;
51
- }>;
52
- }
53
- /**
54
- * Minimal pg-compatible client used by TurbineClient for transactions.
55
- * `pg.PoolClient` satisfies this; so do Neon and Vercel's equivalents.
56
- */
57
- export interface PgCompatPoolClient {
58
- query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
59
- release(err?: Error | boolean): void;
60
- /**
61
- * Optional driver capability: `true` when `query()` may be called again on
62
- * this connection while earlier calls are still in flight, with replies
63
- * delivered to callers in FIFO submission order. Drivers that set this let
64
- * the batch `$transaction([...])` overload dispatch every statement in one
65
- * write burst (~1 network round trip plus server time) instead of awaiting
66
- * each reply before sending the next (N round trips). Leave unset for
67
- * drivers (node-postgres included) whose batch path must stay strictly
68
- * sequential.
69
- */
70
- readonly supportsPipelining?: boolean;
71
- /**
72
- * Optional engine seam: scope a transaction's user callback to its own
73
- * async subtree. When present, `TurbineClient.transaction` / `$transaction`
74
- * invoke the callback as `wrapTransactionCallback(() => fn(tx))` instead of
75
- * `fn(tx)` directly. Single-writer engines (PowDB) implement it with
76
- * `AsyncLocalStorage.run()` to plant their re-entrancy marker so that it
77
- * exists ONLY inside the callback's async subtree: a transaction opened
78
- * from inside the callback is detected as re-entrant (typed E017), while
79
- * the CALLER's context stays unmarked, so same-tick sibling transactions
80
- * queue FIFO instead of being falsely flagged. Absent on pg and every other
81
- * engine, in which case the callback runs unwrapped (zero behavior change).
82
- */
83
- wrapTransactionCallback?<R>(fn: () => Promise<R>): Promise<R>;
84
- }
85
- /**
86
- * Minimal pg-compatible pool. Pass any driver that satisfies this interface
87
- * via `TurbineConfig.pool`, lets Turbine run on Neon HTTP, Vercel Postgres,
88
- * Cloudflare Hyperdrive, or any other serverless Postgres driver.
89
- *
90
- * @example
91
- * ```ts
92
- * import { Pool } from '@neondatabase/serverless';
93
- * import { TurbineClient } from 'turbine-orm';
94
- *
95
- * const neonPool = new Pool({ connectionString: process.env.DATABASE_URL });
96
- * const db = new TurbineClient({ pool: neonPool }, schema);
97
- * ```
98
- */
99
- export interface PgCompatPool {
100
- query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
101
- connect(): Promise<PgCompatPoolClient>;
102
- end(): Promise<void>;
103
- /** Optional, pools that expose stats (pg.Pool does; Neon HTTP does not) */
104
- readonly totalCount?: number;
105
- readonly idleCount?: number;
106
- readonly waitingCount?: number;
107
- /** Optional, pg.Pool supports 'error' event; HTTP drivers typically do not */
108
- on?(event: 'error', listener: (err: Error) => void): this;
109
- }
47
+ export type { PgCompatPool, PgCompatPoolClient, PgCompatQueryConfig, PgCompatQueryResult, } from './pg-types.js';
110
48
  /**
111
49
  * Driver-neutral seam. Bundles a pg-compatible connection pool with the SQL
112
50
  * {@link Dialect} that owns every piece of SQL text varying across engines -
@@ -618,7 +556,7 @@ export declare class TransactionClient {
618
556
  private savepointCounter;
619
557
  /** Active SQL dialect, owns savepoint keywords and raw-SQL placeholders. */
620
558
  private readonly dialect;
621
- constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined,
559
+ constructor(client: PgCompatPoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined,
622
560
  /**
623
561
  * The parent pool this transaction runs on. Only its `readonly` and
624
562
  * `capabilities` are read (both PowDB-only flags), so the transaction-scoped
@@ -686,8 +624,19 @@ export declare class TransactionClient {
686
624
  private createTxPool;
687
625
  }
688
626
  export declare class TurbineClient {
689
- /** The underlying pg.Pool, exposed for escape hatches */
690
- readonly pool: pg.Pool;
627
+ /**
628
+ * The underlying connection pool, exposed for escape hatches.
629
+ *
630
+ * Typed as the driver-neutral {@link PgCompatPool} rather than `pg.Pool`,
631
+ * because it is only a `pg.Pool` when Turbine opened it: supply
632
+ * {@link TurbineConfig.pool} and this is whatever driver you passed (Neon,
633
+ * Vercel Postgres, Hyperdrive), and on the non-Postgres engines it is that
634
+ * engine's pool shim. `query` / `connect` / `end` are the members every
635
+ * driver has. For the pg-only surface (cursors, `copyFrom`/`copyTo`, pool
636
+ * events), import `Pool` from the pg package and cast:
637
+ * `db.pool as unknown as Pool`.
638
+ */
639
+ readonly pool: PgCompatPool;
691
640
  /** The schema metadata this client was built from */
692
641
  readonly schema: SchemaMetadata;
693
642
  private static int8ParserRegistered;
@@ -1052,8 +1001,14 @@ export declare class TurbineClient {
1052
1001
  */
1053
1002
  sql<T extends Record<string, unknown> = Record<string, unknown>>(strings: TemplateStringsArray, ...values: unknown[]): TypedSqlQuery<T>;
1054
1003
  /**
1055
- * Execute a function within a database transaction (raw pg.PoolClient).
1056
- * For the typed API, use `$transaction()` instead.
1004
+ * Execute a function within a database transaction on the raw driver
1005
+ * connection. For the typed API, use `$transaction()` instead.
1006
+ *
1007
+ * The client is typed as {@link PgCompatPoolClient}, the driver-neutral
1008
+ * contract: `query(text, values)` plus `release()`, which is all any
1009
+ * supported driver guarantees. For the pg-only surface (cursors, COPY
1010
+ * streams, the `QueryConfig` object form), import `PoolClient` from the pg
1011
+ * package and cast: `client as unknown as PoolClient`.
1057
1012
  *
1058
1013
  * @example
1059
1014
  * ```ts
@@ -1062,7 +1017,7 @@ export declare class TurbineClient {
1062
1017
  * });
1063
1018
  * ```
1064
1019
  */
1065
- transaction<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
1020
+ transaction<T>(fn: (client: PgCompatPoolClient) => Promise<T>): Promise<T>;
1066
1021
  /**
1067
1022
  * Execute a function within a database transaction with full typed table accessors.
1068
1023
  *
@@ -585,8 +585,8 @@ class TransactionClient {
585
585
  return await client.query(textOrConfig, values);
586
586
  }
587
587
  // Object form for prepared statements: { name, text, values }
588
- // pg.PoolClient.query accepts QueryConfig but the overloads make TS
589
- // unhappy with the union, so we cast through unknown.
588
+ // A driver client accepts it, but the two-argument `query` in the
589
+ // PgCompatPoolClient contract does not describe it, so we cast.
590
590
  return await client.query(textOrConfig);
591
591
  }
592
592
  catch (err) {
@@ -610,7 +610,18 @@ exports.TransactionClient = TransactionClient;
610
610
  // TurbineClient
611
611
  // ---------------------------------------------------------------------------
612
612
  class TurbineClient {
613
- /** The underlying pg.Pool, exposed for escape hatches */
613
+ /**
614
+ * The underlying connection pool, exposed for escape hatches.
615
+ *
616
+ * Typed as the driver-neutral {@link PgCompatPool} rather than `pg.Pool`,
617
+ * because it is only a `pg.Pool` when Turbine opened it: supply
618
+ * {@link TurbineConfig.pool} and this is whatever driver you passed (Neon,
619
+ * Vercel Postgres, Hyperdrive), and on the non-Postgres engines it is that
620
+ * engine's pool shim. `query` / `connect` / `end` are the members every
621
+ * driver has. For the pg-only surface (cursors, `copyFrom`/`copyTo`, pool
622
+ * events), import `Pool` from the pg package and cast:
623
+ * `db.pool as unknown as Pool`.
624
+ */
614
625
  pool;
615
626
  /** The schema metadata this client was built from */
616
627
  schema;
@@ -953,11 +964,15 @@ class TurbineClient {
953
964
  if (config.ssl !== undefined) {
954
965
  poolConfig.ssl = config.ssl;
955
966
  }
956
- this.pool = new pg_1.default.Pool(TurbineClient.withPlanCacheMode(poolConfig, this.planCacheMode));
957
- this.ownsPool = true;
958
- this.pool.on('error', (err) => {
967
+ // Held as a `pg.Pool` for the length of this block: `PgCompatPool.on` is
968
+ // optional (HTTP drivers have no pool events), and here we know we own a
969
+ // real pg pool that has one. Same shape as the replica loop below.
970
+ const ownPool = new pg_1.default.Pool(TurbineClient.withPlanCacheMode(poolConfig, this.planCacheMode));
971
+ ownPool.on('error', (err) => {
959
972
  console.error('[turbine] Unexpected pool error:', err.message);
960
973
  });
974
+ this.pool = ownPool;
975
+ this.ownsPool = true;
961
976
  if (this.logging) {
962
977
  console.log(`[turbine] Pool created, max ${poolConfig.max} connections, ${Object.keys(schema.tables).length} tables`);
963
978
  }
@@ -1413,10 +1428,9 @@ class TurbineClient {
1413
1428
  }
1414
1429
  /** Construct a QueryInterface bound to `pool` (honoring any injected factory). */
1415
1430
  buildTableQI(pool, name) {
1416
- const asPgPool = pool;
1417
1431
  return this.queryOptions?.queryInterfaceFactory
1418
- ? this.queryOptions.queryInterfaceFactory(asPgPool, name, this.schema, this.middlewares, this.queryOptions)
1419
- : new index_js_1.QueryInterface(asPgPool, name, this.schema, this.middlewares, this.queryOptions);
1432
+ ? this.queryOptions.queryInterfaceFactory(pool, name, this.schema, this.middlewares, this.queryOptions)
1433
+ : new index_js_1.QueryInterface(pool, name, this.schema, this.middlewares, this.queryOptions);
1420
1434
  }
1421
1435
  /**
1422
1436
  * Build the read/write routing proxy for a table. The proxy targets the
@@ -1596,8 +1610,14 @@ class TurbineClient {
1596
1610
  // Transaction support (raw, legacy)
1597
1611
  // -------------------------------------------------------------------------
1598
1612
  /**
1599
- * Execute a function within a database transaction (raw pg.PoolClient).
1600
- * For the typed API, use `$transaction()` instead.
1613
+ * Execute a function within a database transaction on the raw driver
1614
+ * connection. For the typed API, use `$transaction()` instead.
1615
+ *
1616
+ * The client is typed as {@link PgCompatPoolClient}, the driver-neutral
1617
+ * contract: `query(text, values)` plus `release()`, which is all any
1618
+ * supported driver guarantees. For the pg-only surface (cursors, COPY
1619
+ * streams, the `QueryConfig` object form), import `PoolClient` from the pg
1620
+ * package and cast: `client as unknown as PoolClient`.
1601
1621
  *
1602
1622
  * @example
1603
1623
  * ```ts
@@ -135,21 +135,98 @@ export declare function applyRelationRenames(schema: SchemaMetadata, renames: Re
135
135
  */
136
136
  export declare function introspectPostgresCatalog(options: IntrospectOptions): Promise<SchemaMetadata>;
137
137
  /**
138
- * Parse the indexed column names out of a `pg_indexes.indexdef` string.
138
+ * The index-definition key-list SCANNER, and the only one that reads an
139
+ * `indexdef` character by character.
139
140
  *
140
- * `indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING method
141
- * (col, ...) [WHERE predicate]`. We anchor on the `USING` clause's parenthesised
142
- * column list (the same precedent as `describeIndexDefMismatch` in
143
- * schema-sql.ts) so a PARTIAL index's trailing `WHERE (...)` parentheses are
144
- * never mistaken for the column list. The older greedy `/\((.+)\)/` swallowed
145
- * `) WHERE (` and spliced a raw predicate fragment into the column names, which
146
- * then leaked into generated compound-unique selector names.
141
+ * `pg_indexes.indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING
142
+ * method (key, ...) [INCLUDE (col, ...)] [WITH (...)] [TABLESPACE ts]
143
+ * [WHERE predicate]`. This returns the raw entries of the KEY LIST only, one per
144
+ * top-level comma, expression entries included and verbatim.
147
145
  *
148
- * Each column is de-quoted (Postgres quotes non-lowercase identifiers such as a
149
- * Prisma implicit m2m junction's `"A"` / `"B"`), so the names match the
150
- * unquoted column names carried elsewhere in the metadata. Expression columns
151
- * (anything containing a parenthesis) are dropped conservatively: a functional
152
- * index does not name a plain column.
146
+ * ## THREE indexdef parsers coexist in this repo. This is one of them.
147
+ *
148
+ * An earlier version of this comment said there was exactly one. There is not,
149
+ * and pretending otherwise is how hand-synced parsers drift here, so each of the
150
+ * three names the other two:
151
+ *
152
+ * 1. THIS scanner (with {@link indexKeyColumn} / {@link parseIndexColumns}).
153
+ * Safe for any `indexdef` pg emits, including expression keys, quoted
154
+ * identifiers holding commas or parens, string literals, INCLUDE lists and
155
+ * partial predicates. Feeds generated metadata, compound-unique selectors,
156
+ * the FK-index advisor and m2m detection, plus `cli/mcp.ts`.
157
+ * 2. {@link parsePlainUniqueIndexColumns}, below. Still the old
158
+ * `USING \w+ \(([^)]*)\)` regex. It answers a NARROWER question ("is this a
159
+ * plain, whole-table unique index over these exact columns") and returns
160
+ * `null` on everything it cannot read, so its regex's known weaknesses cost
161
+ * a missed hasOne flip rather than a wrong answer.
162
+ * 3. `describeIndexDefMismatch` in `schema-sql.ts`. Same old regex, same
163
+ * fail-toward-a-warning posture.
164
+ *
165
+ * Unifying them is a separate change with its own risk: (2) and (3) both treat
166
+ * "cannot read this" as a safe refusal, and swapping in a parser that reads MORE
167
+ * turns some of those refusals into answers. Until then, prefer this scanner for
168
+ * any new caller, and do not assume a fix here reaches the other two.
169
+ *
170
+ * ## Why the mcp.ts copy is gone
171
+ *
172
+ * `cli/mcp.ts` kept its own weaker copy of this, and the two drifted: on
173
+ * `USING btree (id) INCLUDE (email)` the copy answered `['id) INCLUDE (email']`
174
+ * while this one answered `['id']`. Those columns feed `deriveCatalogRelations`,
175
+ * which decides hasOne-vs-hasMany and auto-m2m, so a UNIQUE index with INCLUDE
176
+ * columns was visible to `turbine generate` and invisible to the MCP server, and
177
+ * the two surfaces disagreed about which relations the schema has. The
178
+ * duplication is also what produced the predicate leak that
179
+ * {@link parseIndexColumns}'s own history records. So mcp.ts consumes this
180
+ * function now, and the split between the two callers is expressed as the two
181
+ * exports below rather than as two implementations.
182
+ *
183
+ * ## Why a scanner rather than a regex
184
+ *
185
+ * The regex this replaces (`/USING\s+\w+\s*\(([^)]*)\)/`) stops at the FIRST
186
+ * `)`, which is the wrong paren for any expression key (`lower(email)`), and a
187
+ * plain `.split(',')` cuts `coalesce(a, b)` in half. The scan tracks paren depth
188
+ * and single-quoted literals, so a comma or a paren inside an expression or a
189
+ * literal is not a boundary.
190
+ *
191
+ * The anchor requires `USING <method> (`, which no predicate, INCLUDE list, WITH
192
+ * list or literal can spell, so the key list is found positionally rather than
193
+ * by hoping the first paren is the right one. When the anchor is absent (not a
194
+ * shape pg emits) it falls back to the first parenthesised group, matching the
195
+ * previous behaviour.
196
+ */
197
+ export declare function parseIndexKeyEntries(indexdef: string): string[];
198
+ /**
199
+ * The plain COLUMN NAME an index key entry indexes, or `null` when the entry is
200
+ * an expression rather than a column.
201
+ *
202
+ * A key entry is `{ column | (expression) } [COLLATE c] [opclass [(params)]]
203
+ * [ASC|DESC] [NULLS FIRST|LAST]`, so the column is the LEADING token and
204
+ * everything after it is a modifier. Reading it that way is what makes
205
+ * `email COLLATE "C" text_pattern_ops` resolve to `email`; the previous
206
+ * suffix-stripping (`ASC`/`DESC` only) returned the whole entry verbatim as a
207
+ * "column name", which matches no real column.
208
+ *
209
+ * SCOPE OF THAT FIX, stated exactly because an earlier version of this comment
210
+ * overstated it: what changes is what {@link parseIndexColumns} reports, and so
211
+ * what its consumers see. Those are the generated `metadata.ts` index lists,
212
+ * the compound-unique selector derivation, the FK-index advisor's leading-column
213
+ * check, and m2m junction detection. Relation CARDINALITY is NOT among them: the
214
+ * `hasMany`/`hasOne` flip reads {@link parsePlainUniqueIndexColumns}, a separate
215
+ * parser this does not feed, and that one still answers `null` for an opclass'd
216
+ * UNIQUE index exactly as it did before.
217
+ *
218
+ * The name is de-quoted (Postgres quotes non-lowercase identifiers such as a
219
+ * Prisma implicit m2m junction's `"A"` / `"B"`) so it matches the unquoted names
220
+ * carried elsewhere in the metadata.
221
+ */
222
+ export declare function indexKeyColumn(entry: string): string | null;
223
+ /**
224
+ * Parse the indexed COLUMN names out of a `pg_indexes.indexdef` string.
225
+ *
226
+ * Expression keys are dropped, conservatively: a functional index does not name
227
+ * a plain column, and generated metadata has nowhere to say "there was a key
228
+ * here that is not a column". {@link parseIndexKeyEntries} is the variant that
229
+ * keeps them, for the one caller (`cli/mcp.ts`) that reports their presence.
153
230
  */
154
231
  export declare function parseIndexColumns(indexdef: string): string[];
155
232
  /**
@@ -198,10 +275,29 @@ export declare function isUnknownTsType(tsType: string): boolean;
198
275
  * expression, not the raw FK column set.
199
276
  *
200
277
  * Anchors on the `USING <method> (` clause the same way
201
- * {@link describeIndexDefMismatch} does, so a partial index's `WHERE (...)`
202
- * parentheses are never mistaken for the column list. Every column token must be
203
- * a bare or double-quoted identifier; anything else (a function call, an
204
- * operator expression) fails the check and yields `null`.
278
+ * `describeIndexDefMismatch` (schema-sql.ts) does, so a partial index's
279
+ * `WHERE (...)` parentheses are never mistaken for the column list. Every column
280
+ * token must be a bare or double-quoted identifier; anything else (a function
281
+ * call, an operator expression) fails the check and yields `null`.
282
+ *
283
+ * ## Parser 2 of 3, and what it is safe for
284
+ *
285
+ * This is the second of the three indexdef parsers catalogued on
286
+ * {@link parseIndexKeyEntries}; the third is `describeIndexDefMismatch` in
287
+ * schema-sql.ts. It is still the `USING \w+ \(([^)]*)\)` regex that the scanner
288
+ * up there was written to replace, so it inherits that regex's weaknesses: it
289
+ * stops at the FIRST `)`, and it splits on every comma. On an expression key
290
+ * (`lower(email)`), on a quoted identifier containing a comma or a paren, and on
291
+ * an opclass'd key (`email text_pattern_ops`) it therefore reads a token that is
292
+ * not a bare identifier and returns `null`.
293
+ *
294
+ * That is SAFE HERE and only here, because `null` is this function's "I cannot
295
+ * vouch for this index" answer and its single consumer
296
+ * ({@link detectUniqueForeignKeySets}) treats it as "this index does not prove
297
+ * uniqueness". The cost of every misread is a relation left as `hasMany` that
298
+ * could have been `hasOne`, never a uniqueness claim the database does not back.
299
+ * Do NOT reuse it anywhere a wrong-but-plausible column list would be acted on;
300
+ * use the scanner for that.
205
301
  */
206
302
  export declare function parsePlainUniqueIndexColumns(indexdef: string): string[] | null;
207
303
  /**