@query-farm/vgi 0.29.0 → 0.29.1

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 (90) hide show
  1. package/README.md +26 -1
  2. package/dist/arguments/argument-spec.d.ts +4 -2
  3. package/dist/catalog/composite.d.ts +13 -13
  4. package/dist/catalog/descriptors.d.ts +7 -3
  5. package/dist/catalog/interface.d.ts +67 -40
  6. package/dist/catalog/read-only.d.ts +32 -15
  7. package/dist/client/client.d.ts +36 -29
  8. package/dist/client/types.d.ts +2 -0
  9. package/dist/client-entry.d.ts +1 -1
  10. package/dist/client-entry.js +441 -279
  11. package/dist/client-entry.js.map +12 -11
  12. package/dist/filter-pushdown/capabilities.d.ts +3 -0
  13. package/dist/filter-pushdown/deserialize.d.ts +14 -15
  14. package/dist/filter-pushdown/evaluate.d.ts +14 -44
  15. package/dist/filter-pushdown/index.d.ts +2 -2
  16. package/dist/filter-pushdown/types.d.ts +120 -68
  17. package/dist/functions/aggregate.d.ts +4 -0
  18. package/dist/functions/registry.d.ts +5 -5
  19. package/dist/functions/scalar.d.ts +6 -1
  20. package/dist/functions/table-buffering.d.ts +9 -0
  21. package/dist/functions/table-in-out.d.ts +13 -0
  22. package/dist/functions/table.d.ts +9 -2
  23. package/dist/functions/types.d.ts +22 -2
  24. package/dist/generated/vgi-client.d.ts +56 -27
  25. package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
  26. package/dist/http/fetch.d.ts +7 -1
  27. package/dist/index.core.d.ts +2 -1
  28. package/dist/index.js +2317 -1354
  29. package/dist/index.js.map +43 -40
  30. package/dist/metadata/types.d.ts +15 -2
  31. package/dist/protocol/handlers/shared.d.ts +3 -1
  32. package/dist/protocol/serializers/splits.d.ts +1 -1
  33. package/dist/protocol/types.d.ts +9 -3
  34. package/dist/schema-path.d.ts +7 -0
  35. package/dist/serve-entry.d.ts +22 -1
  36. package/dist/serve-entry.js +412 -238
  37. package/dist/serve-entry.js.map +27 -26
  38. package/dist/split-token.d.ts +1 -1
  39. package/dist/types.d.ts +8 -0
  40. package/dist/worker-cf-entry.js +2030 -1201
  41. package/dist/worker-cf-entry.js.map +40 -36
  42. package/dist/worker.d.ts +1 -1
  43. package/package.json +3 -3
  44. package/src/arguments/argument-spec.ts +12 -4
  45. package/src/catalog/composite.ts +26 -26
  46. package/src/catalog/descriptors.ts +12 -3
  47. package/src/catalog/interface.ts +98 -48
  48. package/src/catalog/read-only.ts +124 -67
  49. package/src/client/client.ts +92 -72
  50. package/src/client/types.ts +2 -0
  51. package/src/client-entry.ts +2 -1
  52. package/src/filter-pushdown/capabilities.ts +13 -0
  53. package/src/filter-pushdown/collector.ts +22 -35
  54. package/src/filter-pushdown/deserialize.ts +657 -221
  55. package/src/filter-pushdown/evaluate.ts +410 -499
  56. package/src/filter-pushdown/index.ts +16 -2
  57. package/src/filter-pushdown/types.ts +88 -71
  58. package/src/functions/aggregate.ts +9 -1
  59. package/src/functions/registry.ts +24 -22
  60. package/src/functions/scalar.ts +26 -0
  61. package/src/functions/table-buffering.ts +46 -7
  62. package/src/functions/table-in-out.ts +187 -30
  63. package/src/functions/table.ts +58 -20
  64. package/src/functions/types.ts +24 -1
  65. package/src/generated/vgi-client.ts +61 -29
  66. package/src/generated/vgi-protocol-schemas.ts +225 -54
  67. package/src/http/fetch.ts +12 -0
  68. package/src/index.core.ts +2 -0
  69. package/src/metadata/resolve.ts +43 -1
  70. package/src/metadata/serialize.ts +43 -5
  71. package/src/metadata/types.ts +17 -1
  72. package/src/protocol/dispatch.ts +1 -1
  73. package/src/protocol/handlers/aggregate.ts +13 -7
  74. package/src/protocol/handlers/catalog/admin.ts +6 -6
  75. package/src/protocol/handlers/catalog/index_methods.ts +2 -2
  76. package/src/protocol/handlers/catalog/macro.ts +4 -4
  77. package/src/protocol/handlers/catalog/shared.ts +2 -0
  78. package/src/protocol/handlers/catalog/table.ts +16 -16
  79. package/src/protocol/handlers/catalog/view.ts +5 -5
  80. package/src/protocol/handlers/function.ts +2 -2
  81. package/src/protocol/handlers/shared.ts +12 -3
  82. package/src/protocol/handlers/table-buffering.ts +4 -4
  83. package/src/protocol/serializers/bind.ts +15 -6
  84. package/src/protocol/serializers/splits.ts +4 -2
  85. package/src/protocol/types.ts +9 -3
  86. package/src/schema-path.ts +25 -0
  87. package/src/serve-entry.ts +65 -3
  88. package/src/split-token.ts +2 -2
  89. package/src/types.ts +9 -0
  90. package/src/worker.ts +49 -4
@@ -1,7 +1,7 @@
1
1
  // Copyright 2025, 2026 Query Farm LLC - https://query.farm
2
2
  // ReadOnlyCatalogInterface: derives catalog from registered functions + descriptors.
3
3
 
4
- import { type VgiSchema, schema as schema_, type VgiField, field, type VgiDataType, int64, utf8, binary, bool, nullType, list, isInt } from "../arrow/index.js";
4
+ import { type VgiSchema, schema as schema_, type VgiField, field, type VgiDataType, int64, utf8, binary, bool, nullType, list, isInt, typeSignature } from "../arrow/index.js";
5
5
  import {
6
6
  CatalogInterface,
7
7
  type AttachOpaqueData,
@@ -20,7 +20,7 @@ import {
20
20
  encodeScanFunctionResult,
21
21
  encodeFunctionInfo,
22
22
  } from "./interface.js";
23
- import type { CatalogDescriptor, SchemaDescriptor, TableDescriptor, ViewDescriptor, MacroDescriptor, SettingDescriptor, SecretTypeDescriptor, ForeignKeyDef, DefaultValue } from "./descriptors.js";
23
+ import { schemaDescriptorPath, type CatalogDescriptor, type SchemaDescriptor, type TableDescriptor, type ViewDescriptor, type MacroDescriptor, type SettingDescriptor, type SecretTypeDescriptor, type ForeignKeyDef, type DefaultValue } from "./descriptors.js";
24
24
  import { serializeColumnStatistics } from "../util/statistics.js";
25
25
  import type { VgiFunction } from "../functions/types.js";
26
26
  import type { FunctionRegistry } from "../functions/registry.js";
@@ -30,6 +30,8 @@ import { resolveMetadata } from "../metadata/resolve.js";
30
30
  import { type AttachOptionSpec, validateRequiredAttachOptions } from "./attach-option.js";
31
31
  import { FunctionStability, NullHandling, OrderPreservation, DEFAULT_MAX_WORKERS } from "../types.js";
32
32
  import { Arguments } from "../arguments/arguments.js";
33
+ import { normalizeSchemaPath, schemaPathDisplay, schemaPathsEqual } from "../schema-path.js";
34
+ import { ForeignKeyInfoSchema } from "../generated/vgi-protocol-schemas.js";
33
35
 
34
36
  export class ReadOnlyCatalogInterface extends CatalogInterface {
35
37
  private _descriptor: CatalogDescriptor;
@@ -46,16 +48,39 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
46
48
  // is declared in more than one schema. Table-backing functions are indexed
47
49
  // under their table's schema for the same reason.
48
50
  for (const sch of descriptor.schemas ?? []) {
49
- for (const fn of sch.functions ?? []) registry.registerInSchema(fn, sch.name, descriptor.name);
51
+ for (const fn of sch.functions ?? []) registry.registerInSchema(fn, schemaDescriptorPath(sch), descriptor.name);
50
52
  for (const tbl of sch.tables ?? []) {
51
53
  for (const key of ["function", "insertFunction", "updateFunction", "deleteFunction"] as const) {
52
54
  const fn = (tbl as any)[key];
53
- if (fn) registry.registerInSchema(fn, sch.name, descriptor.name);
55
+ if (fn) registry.registerInSchema(fn, schemaDescriptorPath(sch), descriptor.name);
54
56
  }
55
57
  }
56
58
  }
57
59
  }
58
60
 
61
+ /**
62
+ * Which schema declares `fn`, by object identity, or null if no schema does.
63
+ *
64
+ * A table's backing function is NOT necessarily declared in the table's own
65
+ * schema — `data.large_sequence` scans via `sequence`, which the example
66
+ * catalog declares in `main`. Populating `ScanFunctionResult.schemaPath` /
67
+ * `ScanBranch.schemaPath` (protocol 2.0.0) correctly needs this reverse
68
+ * lookup, not the `schemaPath` parameter of the call being served.
69
+ *
70
+ * Identity, not name: the whole point of the field is to disambiguate one
71
+ * name declared in several schemas, so a name match would answer the very
72
+ * question being asked. Null when the function is deliberately unadvertised
73
+ * (`cache_multicol`) — it has no schema to report and the client falls back
74
+ * to its own heuristic, which is exactly the pre-1.5.0 behaviour.
75
+ * Mirrors vgi-python's `ReadOnlyCatalogInterface._schema_for_function`.
76
+ */
77
+ private _schemaForFunction(fn: VgiFunction): string[] | null {
78
+ for (const sch of this._descriptor.schemas ?? []) {
79
+ if ((sch.functions ?? []).includes(fn)) return schemaDescriptorPath(sch);
80
+ }
81
+ return null;
82
+ }
83
+
59
84
  catalogs(): string[] {
60
85
  return [this._descriptor.name];
61
86
  }
@@ -168,7 +193,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
168
193
  schemas(attachOpaqueData: AttachOpaqueData, transactionOpaqueData?: TransactionOpaqueData): SchemaInfo[] {
169
194
  return this._descriptor.schemas.map((s) => ({
170
195
  attach_opaque_data: attachOpaqueData,
171
- name: s.name,
196
+ path: schemaDescriptorPath(s),
172
197
  comment: s.comment ?? null,
173
198
  tags: s.tags ?? {},
174
199
  estimated_object_count: this._estimatedObjectCount(s),
@@ -199,14 +224,14 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
199
224
 
200
225
  override schemaGet(
201
226
  attachOpaqueData: AttachOpaqueData,
202
- name: string,
227
+ path: string[],
203
228
  transactionOpaqueData?: TransactionOpaqueData
204
229
  ): SchemaInfo | null {
205
- const desc = this._descriptor.schemas.find((s) => s.name === name);
230
+ const desc = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
206
231
  if (!desc) return null;
207
232
  return {
208
233
  attach_opaque_data: attachOpaqueData,
209
- name: desc.name,
234
+ path: schemaDescriptorPath(desc),
210
235
  comment: desc.comment ?? null,
211
236
  tags: desc.tags ?? {},
212
237
  estimated_object_count: this._estimatedObjectCount(desc),
@@ -215,10 +240,10 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
215
240
 
216
241
  override async schemaContentsTables(
217
242
  attachOpaqueData: AttachOpaqueData,
218
- name: string,
243
+ path: string[],
219
244
  transactionOpaqueData?: TransactionOpaqueData
220
245
  ): Promise<TableInfo[]> {
221
- const schema = this._descriptor.schemas.find((s) => s.name === name);
246
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
222
247
  if (!schema || !schema.tables) return [];
223
248
 
224
249
  return await Promise.all(schema.tables.map(async (t) => {
@@ -273,7 +298,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
273
298
  const notNullIndices = resolveIndices(colSchema, t.notNull);
274
299
  const uniqueIndices = resolveIndexGroups(colSchema, t.unique);
275
300
  const pkIndices = resolveIndexGroups(colSchema, t.primaryKey);
276
- const fkBytes = serializeForeignKeys(t.foreignKey, name);
301
+ const fkBytes = serializeForeignKeys(t.foreignKey, path);
277
302
 
278
303
  // Validate required_filters: an AND (outer list) of OR-groups (inner
279
304
  // lists). Each OR-group must be non-empty, and the leading dotted segment
@@ -311,7 +336,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
311
336
  comment: t.comment ?? null,
312
337
  tags: t.tags ?? {},
313
338
  name: t.name,
314
- schema_name: name,
339
+ schema_path: normalizeSchemaPath(path),
315
340
  columns,
316
341
  not_null_constraints: notNullIndices,
317
342
  unique_constraints: uniqueIndices,
@@ -329,7 +354,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
329
354
  // shadow the per-call dispatch and prevent workers from varying
330
355
  // the scan function (e.g. version-resolved time-travel).
331
356
  scan_function: t.function && !t.columns
332
- ? inlineScanFunction(t.function, t.arguments)
357
+ ? inlineScanFunction(t.function, t.arguments, this._schemaForFunction(t.function))
333
358
  : null,
334
359
  insert_function: null,
335
360
  update_function: null,
@@ -347,11 +372,11 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
347
372
 
348
373
  override tableColumnStatisticsGet(
349
374
  attachOpaqueData: AttachOpaqueData,
350
- schemaName: string,
375
+ schemaPath: string[],
351
376
  name: string,
352
377
  transactionOpaqueData?: TransactionOpaqueData,
353
378
  ): { bytes: Uint8Array; cacheMaxAgeSeconds: number | null } | null {
354
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
379
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
355
380
  if (!schema || !schema.tables) return null;
356
381
  const table = schema.tables.find((t) => t.name === name);
357
382
  if (!table || !table.statistics) return null;
@@ -365,17 +390,17 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
365
390
 
366
391
  override schemaContentsViews(
367
392
  attachOpaqueData: AttachOpaqueData,
368
- name: string,
393
+ path: string[],
369
394
  transactionOpaqueData?: TransactionOpaqueData
370
395
  ): ViewInfo[] {
371
- const schema = this._descriptor.schemas.find((s) => s.name === name);
396
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
372
397
  if (!schema || !schema.views) return [];
373
398
 
374
399
  return schema.views.map((v) => ({
375
400
  comment: v.comment ?? null,
376
401
  tags: v.tags ?? {},
377
402
  name: v.name,
378
- schema_name: name,
403
+ schema_path: normalizeSchemaPath(path),
379
404
  definition: v.definition,
380
405
  column_comments: v.columnComments ?? {},
381
406
  }));
@@ -383,11 +408,11 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
383
408
 
384
409
  override schemaContentsFunctions(
385
410
  attachOpaqueData: AttachOpaqueData,
386
- name: string,
411
+ path: string[],
387
412
  type: string,
388
413
  transactionOpaqueData?: TransactionOpaqueData
389
414
  ): FunctionInfo[] {
390
- const schema = this._descriptor.schemas.find((s) => s.name === name);
415
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
391
416
  if (!schema || !schema.functions) return [];
392
417
 
393
418
  return schema.functions
@@ -401,21 +426,44 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
401
426
  if (t === "AGGREGATE_FUNCTION") return (f.kind as string) === "aggregate";
402
427
  return true;
403
428
  })
404
- .map((f) => this._functionToInfo(f, name));
429
+ .map((f) => this._functionToInfo(f, path));
405
430
  }
406
431
 
407
432
  /**
408
433
  * Build the wire `FunctionInfo` for one declared function.
409
434
  *
410
- * `schemaName` is the schema the function actually lives in — it is the
435
+ * `schemaPath` is the schema the function actually lives in — it is the
411
436
  * bind-dispatch key, so a function advertised through more than one route
412
437
  * (schema contents *and* `global_functions`) must carry the same value in
413
438
  * both. Shared by `schemaContentsFunctions` and `_globalFunctionInfos`.
414
439
  */
415
- private _functionToInfo(f: VgiFunction, name: string): FunctionInfo {
440
+ private _functionToInfo(f: VgiFunction, path: string[]): FunctionInfo {
416
441
  const meta = resolveMetadata(f);
417
442
  const argSchema = argumentSpecsToSchema(f.argumentSpecs);
418
443
  const argBytes = serializeSchema(argSchema);
444
+ const defaults = f.meta.parameterDefaultValues ?? null;
445
+ if (defaults) {
446
+ if (defaults.numRows !== 1) {
447
+ throw new Error(`parameterDefaultValues must contain exactly one row, got ${defaults.numRows}`);
448
+ }
449
+ let argumentIndex = 0;
450
+ for (const defaultField of defaults.schema.fields) {
451
+ while (argumentIndex < argSchema.fields.length && argSchema.fields[argumentIndex].name !== defaultField.name) {
452
+ argumentIndex++;
453
+ }
454
+ if (argumentIndex === argSchema.fields.length) {
455
+ throw new Error(`parameterDefaultValues field '${defaultField.name}' is not in argument signature order`);
456
+ }
457
+ const argumentField = argSchema.fields[argumentIndex];
458
+ if (typeSignature(argumentField.type) !== typeSignature(defaultField.type)) {
459
+ throw new Error(
460
+ `parameterDefaultValues field '${defaultField.name}' has type ${typeSignature(defaultField.type)}, ` +
461
+ `expected ${typeSignature(argumentField.type)}`,
462
+ );
463
+ }
464
+ argumentIndex++;
465
+ }
466
+ }
419
467
 
420
468
  // Use the function's default output schema if available,
421
469
  // otherwise empty schema (table functions determine output at bind time)
@@ -443,12 +491,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
443
491
  comment: null,
444
492
  tags,
445
493
  name: f.meta.name,
446
- schema_name: name,
494
+ schema_path: normalizeSchemaPath(path),
447
495
  function_type: meta.functionType.toUpperCase() as "SCALAR" | "TABLE" | "TABLE_BUFFERING" | "AGGREGATE",
448
496
  arguments: argBytes,
449
497
  output_schema: outputSchemaBytes,
498
+ parameter_default_values: defaults
499
+ ? serializeBatch(defaults)
500
+ : null,
450
501
  stability: meta.stability as any,
451
502
  null_handling: meta.nullHandling as any,
503
+ argument_monotonicity: meta.argumentMonotonicity,
452
504
  description: meta.description,
453
505
  examples: meta.examples.map((e) => ({
454
506
  sql: e.sql,
@@ -463,7 +515,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
463
515
  // The C++ extension only honours this for rowid-bearing tables.
464
516
  late_materialization:
465
517
  (f.kind as string) === "table" && f.meta.lateMaterialization === true ? true : null,
466
- supported_expression_filters: meta.supportedExpressionFilters,
518
+ filter_semantic_profiles: meta.filterSemanticProfiles,
519
+ additional_filter_functions: meta.additionalFilterFunctions.map((value) => ({
520
+ namespace: value.namespace, name: value.name, version: BigInt(value.version) as any,
521
+ })),
522
+ runtime_filter_algorithms: meta.runtimeFilterAlgorithms.map((value) => ({
523
+ namespace: value.namespace, name: value.name, version: BigInt(value.version) as any,
524
+ })),
525
+ filter_evaluation_contexts: meta.filterEvaluationContexts.map((value) => ({
526
+ profile: value.profile, provider_fingerprint: value.providerFingerprint,
527
+ })),
467
528
  order_preservation: meta.preservesOrder as any,
468
529
  max_workers: meta.maxWorkers ?? DEFAULT_MAX_WORKERS,
469
530
  // Opt-in features the read-only catalog's functions do not use; the
@@ -533,7 +594,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
533
594
  /**
534
595
  * Build `FunctionInfo` records for `CatalogDescriptor.globalFunctions`.
535
596
  *
536
- * Each entry's `schema_name` is the schema it actually lives in — the
597
+ * Each entry's `schema_path` is the schema it actually lives in — the
537
598
  * globally visible name is derived client-side from
538
599
  * `global_function_prefix`, so `name` stays unprefixed here. An entry not
539
600
  * declared in any schema is a descriptor bug, so it throws rather than
@@ -544,32 +605,32 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
544
605
  const globals = this._descriptor.globalFunctions ?? [];
545
606
  if (globals.length === 0) return [];
546
607
 
547
- const schemaOf = new Map<VgiFunction, string>();
608
+ const schemaOf = new Map<VgiFunction, string[]>();
548
609
  for (const sch of this._descriptor.schemas) {
549
610
  for (const fn of sch.functions ?? []) {
550
- if (!schemaOf.has(fn)) schemaOf.set(fn, sch.name);
611
+ if (!schemaOf.has(fn)) schemaOf.set(fn, schemaDescriptorPath(sch));
551
612
  }
552
613
  }
553
614
 
554
615
  return globals.map((fn) => {
555
- const schemaName = schemaOf.get(fn);
556
- if (schemaName === undefined) {
616
+ const schemaPath = schemaOf.get(fn);
617
+ if (schemaPath === undefined) {
557
618
  throw new Error(
558
619
  `Catalog '${this._descriptor.name}': '${fn.meta.name}' is listed in ` +
559
620
  `globalFunctions but is not declared in any schema.`,
560
621
  );
561
622
  }
562
- return this._functionToInfo(fn, schemaName);
623
+ return this._functionToInfo(fn, schemaPath);
563
624
  });
564
625
  }
565
626
 
566
627
  override schemaContentsMacros(
567
628
  attachOpaqueData: AttachOpaqueData,
568
- name: string,
629
+ path: string[],
569
630
  type: string,
570
631
  transactionOpaqueData?: TransactionOpaqueData
571
632
  ): MacroInfo[] {
572
- const schema = this._descriptor.schemas.find((s) => s.name === name);
633
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
573
634
  if (!schema || !schema.macros) return [];
574
635
 
575
636
  return schema.macros
@@ -584,7 +645,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
584
645
  comment: m.comment ?? null,
585
646
  tags: m.tags ?? {},
586
647
  name: m.name,
587
- schema_name: name,
648
+ schema_path: normalizeSchemaPath(path),
588
649
  macro_type: (m.macroType === "scalar" ? "SCALAR" : "TABLE") as "SCALAR" | "TABLE",
589
650
  parameters: m.parameters,
590
651
  parameter_default_values: m.parameterDefaultValues ?? new Uint8Array(0),
@@ -601,16 +662,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
601
662
 
602
663
  override schemaContentsIndexes(
603
664
  attachOpaqueData: AttachOpaqueData,
604
- name: string,
665
+ path: string[],
605
666
  transactionOpaqueData?: TransactionOpaqueData
606
667
  ): IndexInfo[] {
607
- const schema = this._descriptor.schemas.find((s) => s.name === name);
668
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
608
669
  if (!schema || !schema.indexes) return [];
609
670
  return schema.indexes.map((idx) => ({
610
671
  comment: idx.comment ?? null,
611
672
  tags: idx.tags ?? {},
612
673
  name: idx.name,
613
- schema_name: name,
674
+ schema_path: normalizeSchemaPath(path),
614
675
  table_name: idx.tableName,
615
676
  index_type: idx.indexType ?? "ART",
616
677
  constraint_type: (idx.constraintType ?? "NONE") as IndexConstraintType,
@@ -621,29 +682,29 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
621
682
 
622
683
  override indexGet(
623
684
  attachOpaqueData: AttachOpaqueData,
624
- schemaName: string,
685
+ schemaPath: string[],
625
686
  name: string,
626
687
  transactionOpaqueData?: TransactionOpaqueData
627
688
  ): IndexInfo | null {
628
- const all = this.schemaContentsIndexes(attachOpaqueData, schemaName, transactionOpaqueData);
689
+ const all = this.schemaContentsIndexes(attachOpaqueData, schemaPath, transactionOpaqueData);
629
690
  return all.find((i) => i.name === name) ?? null;
630
691
  }
631
692
 
632
693
  override macroGet(
633
694
  attachOpaqueData: AttachOpaqueData,
634
- schemaName: string,
695
+ schemaPath: string[],
635
696
  name: string,
636
697
  transactionOpaqueData?: TransactionOpaqueData
637
698
  ): MacroInfo | null {
638
- const macros = this.schemaContentsMacros(attachOpaqueData, schemaName, "SCALAR_MACRO", transactionOpaqueData);
639
- const tableMacros = this.schemaContentsMacros(attachOpaqueData, schemaName, "TABLE_MACRO", transactionOpaqueData);
699
+ const macros = this.schemaContentsMacros(attachOpaqueData, schemaPath, "SCALAR_MACRO", transactionOpaqueData);
700
+ const tableMacros = this.schemaContentsMacros(attachOpaqueData, schemaPath, "TABLE_MACRO", transactionOpaqueData);
640
701
  const all = [...macros, ...tableMacros];
641
702
  return all.find((m) => m.name.toLowerCase() === name.toLowerCase()) ?? null;
642
703
  }
643
704
 
644
705
  override tableScanFunctionGet(
645
706
  attachOpaqueData: AttachOpaqueData,
646
- schemaName: string,
707
+ schemaPath: string[],
647
708
  name: string,
648
709
  atUnit?: string,
649
710
  atValue?: string,
@@ -652,18 +713,18 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
652
713
  validateAtParams(atUnit, atValue);
653
714
 
654
715
  // Find the table descriptor
655
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
716
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
656
717
  if (!schema || !schema.tables) {
657
- throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
718
+ throw new Error(`Table '${name}' not found in schema ${schemaPathDisplay(schemaPath)}`);
658
719
  }
659
720
  const table = schema.tables.find((t) => t.name === name);
660
721
  if (!table) {
661
- throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
722
+ throw new Error(`Table '${name}' not found in schema ${schemaPathDisplay(schemaPath)}`);
662
723
  }
663
724
 
664
725
  // Reject AT clause on tables that don't support time travel
665
726
  if (atUnit && !table.supportsTimeTravel) {
666
- throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
727
+ throw new Error(`Table ${schemaPathDisplay(schemaPath)}.${name} does not support time travel queries`);
667
728
  }
668
729
 
669
730
  if (table.function) {
@@ -675,6 +736,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
675
736
  function_name: table.function.meta.name,
676
737
  arguments: argBytes,
677
738
  required_extensions: [],
739
+ schema_path: this._schemaForFunction(table.function),
678
740
  };
679
741
  }
680
742
 
@@ -683,7 +745,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
683
745
 
684
746
  override async tableGet(
685
747
  attachOpaqueData: AttachOpaqueData,
686
- schemaName: string,
748
+ schemaPath: string[],
687
749
  name: string,
688
750
  atUnit?: string,
689
751
  atValue?: string,
@@ -692,25 +754,25 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
692
754
  validateAtParams(atUnit, atValue);
693
755
 
694
756
  // Find the table descriptor to check supports_time_travel
695
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
757
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
696
758
  if (schema?.tables) {
697
759
  const tableDesc = schema.tables.find((t) => t.name === name);
698
760
  if (tableDesc && atUnit && !tableDesc.supportsTimeTravel) {
699
- throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
761
+ throw new Error(`Table ${schemaPathDisplay(schemaPath)}.${name} does not support time travel queries`);
700
762
  }
701
763
  }
702
764
 
703
- const tables = await this.schemaContentsTables(attachOpaqueData, schemaName, transactionOpaqueData);
765
+ const tables = await this.schemaContentsTables(attachOpaqueData, schemaPath, transactionOpaqueData);
704
766
  return tables.find((t) => t.name === name) ?? null;
705
767
  }
706
768
 
707
769
  override viewGet(
708
770
  attachOpaqueData: AttachOpaqueData,
709
- schemaName: string,
771
+ schemaPath: string[],
710
772
  name: string,
711
773
  transactionOpaqueData?: TransactionOpaqueData
712
774
  ): ViewInfo | null {
713
- const views = this.schemaContentsViews(attachOpaqueData, schemaName, transactionOpaqueData);
775
+ const views = this.schemaContentsViews(attachOpaqueData, schemaPath, transactionOpaqueData);
714
776
  return views.find((v) => v.name === name) ?? null;
715
777
  }
716
778
 
@@ -786,14 +848,7 @@ function resolveIndexGroups(schema: VgiSchema, groups?: string[][]): number[][]
786
848
  return groups.map((group) => resolveIndices(schema, group));
787
849
  }
788
850
 
789
- const FK_SCHEMA = schema_([
790
- field("fk_columns", list(field("item", utf8(), true)), false),
791
- field("pk_columns", list(field("item", utf8(), true)), false),
792
- field("referenced_table", utf8(), false),
793
- field("referenced_schema", utf8(), false),
794
- ]);
795
-
796
- function serializeForeignKeys(fks?: ForeignKeyDef[], schemaName?: string): Uint8Array[] {
851
+ function serializeForeignKeys(fks?: ForeignKeyDef[], schemaPath?: string[]): Uint8Array[] {
797
852
  if (!fks || fks.length === 0) return [];
798
853
  return fks.map((fk) =>
799
854
  serializeBatch(
@@ -802,9 +857,9 @@ function serializeForeignKeys(fks?: ForeignKeyDef[], schemaName?: string): Uint8
802
857
  fk_columns: [fk.columns],
803
858
  pk_columns: [fk.referencedColumns],
804
859
  referenced_table: [fk.referencedTable],
805
- referenced_schema: [fk.referencedSchema ?? schemaName ?? "main"],
860
+ referenced_schema_path: [fk.referencedSchemaPath ?? schemaPath ?? ["main"]],
806
861
  },
807
- FK_SCHEMA
862
+ ForeignKeyInfoSchema
808
863
  )
809
864
  )
810
865
  );
@@ -1001,14 +1056,16 @@ function inferScalarType(val: any): VgiDataType {
1001
1056
 
1002
1057
  // Build the inlined `scan_function` payload for a function-backed table.
1003
1058
  // Mirrors python's _inline_function_result: a serialized one-row batch of
1004
- // ScanFunctionResultSchema (function_name, arguments, required_extensions).
1005
- // The C++ extension uses these bytes verbatim and skips the
1006
- // catalog_table_scan_function_get RPC.
1059
+ // ScanFunctionResultSchema (function_name, arguments, required_extensions,
1060
+ // schema_path). The C++ extension uses these bytes verbatim and skips the
1061
+ // catalog_table_scan_function_get RPC, so the schema this inlines has to agree
1062
+ // with what that RPC would have answered.
1007
1063
  function inlineScanFunction(
1008
1064
  func: VgiFunction,
1009
1065
  args: Arguments | undefined,
1066
+ schemaPath: string[] | null,
1010
1067
  ): Uint8Array {
1011
1068
  const argSchema = argumentSpecsToSchema(func.argumentSpecs);
1012
1069
  const argBytes = serializeArgsBatch(args ?? new Arguments(), argSchema);
1013
- return encodeScanFunctionResult(func.meta.name, argBytes);
1070
+ return encodeScanFunctionResult(func.meta.name, argBytes, [], schemaPath);
1014
1071
  }