@query-farm/vgi 0.29.0 → 0.30.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 (91) 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 +442 -283
  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 +57 -31
  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 +2320 -1363
  29. package/dist/index.js.map +44 -41
  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 +414 -243
  37. package/dist/serve-entry.js.map +28 -27
  38. package/dist/split-token.d.ts +1 -1
  39. package/dist/types.d.ts +8 -0
  40. package/dist/worker-cf-entry.js +2033 -1210
  41. package/dist/worker-cf-entry.js.map +41 -37
  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 +125 -71
  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 +62 -33
  66. package/src/generated/vgi-protocol-schemas.ts +226 -58
  67. package/src/http/client-bundle.ts +1 -1
  68. package/src/http/fetch.ts +12 -0
  69. package/src/index.core.ts +2 -0
  70. package/src/metadata/resolve.ts +43 -1
  71. package/src/metadata/serialize.ts +43 -5
  72. package/src/metadata/types.ts +17 -1
  73. package/src/protocol/dispatch.ts +1 -1
  74. package/src/protocol/handlers/aggregate.ts +13 -7
  75. package/src/protocol/handlers/catalog/admin.ts +6 -6
  76. package/src/protocol/handlers/catalog/index_methods.ts +2 -2
  77. package/src/protocol/handlers/catalog/macro.ts +4 -4
  78. package/src/protocol/handlers/catalog/shared.ts +2 -0
  79. package/src/protocol/handlers/catalog/table.ts +16 -16
  80. package/src/protocol/handlers/catalog/view.ts +5 -5
  81. package/src/protocol/handlers/function.ts +2 -2
  82. package/src/protocol/handlers/shared.ts +12 -3
  83. package/src/protocol/handlers/table-buffering.ts +4 -4
  84. package/src/protocol/serializers/bind.ts +15 -6
  85. package/src/protocol/serializers/splits.ts +4 -2
  86. package/src/protocol/types.ts +9 -3
  87. package/src/schema-path.ts +25 -0
  88. package/src/serve-entry.ts +65 -3
  89. package/src/split-token.ts +2 -2
  90. package/src/types.ts +9 -0
  91. 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,17 +336,14 @@ 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,
318
343
  check_constraints: t.check ?? [],
319
344
  primary_key_constraints: pkIndices,
320
345
  foreign_key_constraints: fkBytes,
321
- supports_insert: false,
322
- supports_update: false,
323
- supports_delete: false,
324
- supports_returning: false,
346
+ write_result_modes: {},
325
347
  supports_column_statistics: t.statistics != null && Object.keys(t.statistics).length > 0,
326
348
  // Inline scan_function only for purely function-backed tables.
327
349
  // When explicit `columns` are also declared, the table is on the
@@ -329,7 +351,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
329
351
  // shadow the per-call dispatch and prevent workers from varying
330
352
  // the scan function (e.g. version-resolved time-travel).
331
353
  scan_function: t.function && !t.columns
332
- ? inlineScanFunction(t.function, t.arguments)
354
+ ? inlineScanFunction(t.function, t.arguments, this._schemaForFunction(t.function))
333
355
  : null,
334
356
  insert_function: null,
335
357
  update_function: null,
@@ -347,11 +369,11 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
347
369
 
348
370
  override tableColumnStatisticsGet(
349
371
  attachOpaqueData: AttachOpaqueData,
350
- schemaName: string,
372
+ schemaPath: string[],
351
373
  name: string,
352
374
  transactionOpaqueData?: TransactionOpaqueData,
353
375
  ): { bytes: Uint8Array; cacheMaxAgeSeconds: number | null } | null {
354
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
376
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
355
377
  if (!schema || !schema.tables) return null;
356
378
  const table = schema.tables.find((t) => t.name === name);
357
379
  if (!table || !table.statistics) return null;
@@ -365,17 +387,17 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
365
387
 
366
388
  override schemaContentsViews(
367
389
  attachOpaqueData: AttachOpaqueData,
368
- name: string,
390
+ path: string[],
369
391
  transactionOpaqueData?: TransactionOpaqueData
370
392
  ): ViewInfo[] {
371
- const schema = this._descriptor.schemas.find((s) => s.name === name);
393
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
372
394
  if (!schema || !schema.views) return [];
373
395
 
374
396
  return schema.views.map((v) => ({
375
397
  comment: v.comment ?? null,
376
398
  tags: v.tags ?? {},
377
399
  name: v.name,
378
- schema_name: name,
400
+ schema_path: normalizeSchemaPath(path),
379
401
  definition: v.definition,
380
402
  column_comments: v.columnComments ?? {},
381
403
  }));
@@ -383,11 +405,11 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
383
405
 
384
406
  override schemaContentsFunctions(
385
407
  attachOpaqueData: AttachOpaqueData,
386
- name: string,
408
+ path: string[],
387
409
  type: string,
388
410
  transactionOpaqueData?: TransactionOpaqueData
389
411
  ): FunctionInfo[] {
390
- const schema = this._descriptor.schemas.find((s) => s.name === name);
412
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
391
413
  if (!schema || !schema.functions) return [];
392
414
 
393
415
  return schema.functions
@@ -401,21 +423,44 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
401
423
  if (t === "AGGREGATE_FUNCTION") return (f.kind as string) === "aggregate";
402
424
  return true;
403
425
  })
404
- .map((f) => this._functionToInfo(f, name));
426
+ .map((f) => this._functionToInfo(f, path));
405
427
  }
406
428
 
407
429
  /**
408
430
  * Build the wire `FunctionInfo` for one declared function.
409
431
  *
410
- * `schemaName` is the schema the function actually lives in — it is the
432
+ * `schemaPath` is the schema the function actually lives in — it is the
411
433
  * bind-dispatch key, so a function advertised through more than one route
412
434
  * (schema contents *and* `global_functions`) must carry the same value in
413
435
  * both. Shared by `schemaContentsFunctions` and `_globalFunctionInfos`.
414
436
  */
415
- private _functionToInfo(f: VgiFunction, name: string): FunctionInfo {
437
+ private _functionToInfo(f: VgiFunction, path: string[]): FunctionInfo {
416
438
  const meta = resolveMetadata(f);
417
439
  const argSchema = argumentSpecsToSchema(f.argumentSpecs);
418
440
  const argBytes = serializeSchema(argSchema);
441
+ const defaults = f.meta.parameterDefaultValues ?? null;
442
+ if (defaults) {
443
+ if (defaults.numRows !== 1) {
444
+ throw new Error(`parameterDefaultValues must contain exactly one row, got ${defaults.numRows}`);
445
+ }
446
+ let argumentIndex = 0;
447
+ for (const defaultField of defaults.schema.fields) {
448
+ while (argumentIndex < argSchema.fields.length && argSchema.fields[argumentIndex].name !== defaultField.name) {
449
+ argumentIndex++;
450
+ }
451
+ if (argumentIndex === argSchema.fields.length) {
452
+ throw new Error(`parameterDefaultValues field '${defaultField.name}' is not in argument signature order`);
453
+ }
454
+ const argumentField = argSchema.fields[argumentIndex];
455
+ if (typeSignature(argumentField.type) !== typeSignature(defaultField.type)) {
456
+ throw new Error(
457
+ `parameterDefaultValues field '${defaultField.name}' has type ${typeSignature(defaultField.type)}, ` +
458
+ `expected ${typeSignature(argumentField.type)}`,
459
+ );
460
+ }
461
+ argumentIndex++;
462
+ }
463
+ }
419
464
 
420
465
  // Use the function's default output schema if available,
421
466
  // otherwise empty schema (table functions determine output at bind time)
@@ -443,12 +488,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
443
488
  comment: null,
444
489
  tags,
445
490
  name: f.meta.name,
446
- schema_name: name,
491
+ schema_path: normalizeSchemaPath(path),
447
492
  function_type: meta.functionType.toUpperCase() as "SCALAR" | "TABLE" | "TABLE_BUFFERING" | "AGGREGATE",
448
493
  arguments: argBytes,
449
494
  output_schema: outputSchemaBytes,
495
+ parameter_default_values: defaults
496
+ ? serializeBatch(defaults)
497
+ : null,
450
498
  stability: meta.stability as any,
451
499
  null_handling: meta.nullHandling as any,
500
+ argument_monotonicity: meta.argumentMonotonicity,
452
501
  description: meta.description,
453
502
  examples: meta.examples.map((e) => ({
454
503
  sql: e.sql,
@@ -463,7 +512,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
463
512
  // The C++ extension only honours this for rowid-bearing tables.
464
513
  late_materialization:
465
514
  (f.kind as string) === "table" && f.meta.lateMaterialization === true ? true : null,
466
- supported_expression_filters: meta.supportedExpressionFilters,
515
+ filter_semantic_profiles: meta.filterSemanticProfiles,
516
+ additional_filter_functions: meta.additionalFilterFunctions.map((value) => ({
517
+ namespace: value.namespace, name: value.name, version: BigInt(value.version) as any,
518
+ })),
519
+ runtime_filter_algorithms: meta.runtimeFilterAlgorithms.map((value) => ({
520
+ namespace: value.namespace, name: value.name, version: BigInt(value.version) as any,
521
+ })),
522
+ filter_evaluation_contexts: meta.filterEvaluationContexts.map((value) => ({
523
+ profile: value.profile, provider_fingerprint: value.providerFingerprint,
524
+ })),
467
525
  order_preservation: meta.preservesOrder as any,
468
526
  max_workers: meta.maxWorkers ?? DEFAULT_MAX_WORKERS,
469
527
  // Opt-in features the read-only catalog's functions do not use; the
@@ -533,7 +591,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
533
591
  /**
534
592
  * Build `FunctionInfo` records for `CatalogDescriptor.globalFunctions`.
535
593
  *
536
- * Each entry's `schema_name` is the schema it actually lives in — the
594
+ * Each entry's `schema_path` is the schema it actually lives in — the
537
595
  * globally visible name is derived client-side from
538
596
  * `global_function_prefix`, so `name` stays unprefixed here. An entry not
539
597
  * declared in any schema is a descriptor bug, so it throws rather than
@@ -544,32 +602,32 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
544
602
  const globals = this._descriptor.globalFunctions ?? [];
545
603
  if (globals.length === 0) return [];
546
604
 
547
- const schemaOf = new Map<VgiFunction, string>();
605
+ const schemaOf = new Map<VgiFunction, string[]>();
548
606
  for (const sch of this._descriptor.schemas) {
549
607
  for (const fn of sch.functions ?? []) {
550
- if (!schemaOf.has(fn)) schemaOf.set(fn, sch.name);
608
+ if (!schemaOf.has(fn)) schemaOf.set(fn, schemaDescriptorPath(sch));
551
609
  }
552
610
  }
553
611
 
554
612
  return globals.map((fn) => {
555
- const schemaName = schemaOf.get(fn);
556
- if (schemaName === undefined) {
613
+ const schemaPath = schemaOf.get(fn);
614
+ if (schemaPath === undefined) {
557
615
  throw new Error(
558
616
  `Catalog '${this._descriptor.name}': '${fn.meta.name}' is listed in ` +
559
617
  `globalFunctions but is not declared in any schema.`,
560
618
  );
561
619
  }
562
- return this._functionToInfo(fn, schemaName);
620
+ return this._functionToInfo(fn, schemaPath);
563
621
  });
564
622
  }
565
623
 
566
624
  override schemaContentsMacros(
567
625
  attachOpaqueData: AttachOpaqueData,
568
- name: string,
626
+ path: string[],
569
627
  type: string,
570
628
  transactionOpaqueData?: TransactionOpaqueData
571
629
  ): MacroInfo[] {
572
- const schema = this._descriptor.schemas.find((s) => s.name === name);
630
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
573
631
  if (!schema || !schema.macros) return [];
574
632
 
575
633
  return schema.macros
@@ -584,7 +642,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
584
642
  comment: m.comment ?? null,
585
643
  tags: m.tags ?? {},
586
644
  name: m.name,
587
- schema_name: name,
645
+ schema_path: normalizeSchemaPath(path),
588
646
  macro_type: (m.macroType === "scalar" ? "SCALAR" : "TABLE") as "SCALAR" | "TABLE",
589
647
  parameters: m.parameters,
590
648
  parameter_default_values: m.parameterDefaultValues ?? new Uint8Array(0),
@@ -601,16 +659,16 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
601
659
 
602
660
  override schemaContentsIndexes(
603
661
  attachOpaqueData: AttachOpaqueData,
604
- name: string,
662
+ path: string[],
605
663
  transactionOpaqueData?: TransactionOpaqueData
606
664
  ): IndexInfo[] {
607
- const schema = this._descriptor.schemas.find((s) => s.name === name);
665
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), path));
608
666
  if (!schema || !schema.indexes) return [];
609
667
  return schema.indexes.map((idx) => ({
610
668
  comment: idx.comment ?? null,
611
669
  tags: idx.tags ?? {},
612
670
  name: idx.name,
613
- schema_name: name,
671
+ schema_path: normalizeSchemaPath(path),
614
672
  table_name: idx.tableName,
615
673
  index_type: idx.indexType ?? "ART",
616
674
  constraint_type: (idx.constraintType ?? "NONE") as IndexConstraintType,
@@ -621,29 +679,29 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
621
679
 
622
680
  override indexGet(
623
681
  attachOpaqueData: AttachOpaqueData,
624
- schemaName: string,
682
+ schemaPath: string[],
625
683
  name: string,
626
684
  transactionOpaqueData?: TransactionOpaqueData
627
685
  ): IndexInfo | null {
628
- const all = this.schemaContentsIndexes(attachOpaqueData, schemaName, transactionOpaqueData);
686
+ const all = this.schemaContentsIndexes(attachOpaqueData, schemaPath, transactionOpaqueData);
629
687
  return all.find((i) => i.name === name) ?? null;
630
688
  }
631
689
 
632
690
  override macroGet(
633
691
  attachOpaqueData: AttachOpaqueData,
634
- schemaName: string,
692
+ schemaPath: string[],
635
693
  name: string,
636
694
  transactionOpaqueData?: TransactionOpaqueData
637
695
  ): MacroInfo | null {
638
- const macros = this.schemaContentsMacros(attachOpaqueData, schemaName, "SCALAR_MACRO", transactionOpaqueData);
639
- const tableMacros = this.schemaContentsMacros(attachOpaqueData, schemaName, "TABLE_MACRO", transactionOpaqueData);
696
+ const macros = this.schemaContentsMacros(attachOpaqueData, schemaPath, "SCALAR_MACRO", transactionOpaqueData);
697
+ const tableMacros = this.schemaContentsMacros(attachOpaqueData, schemaPath, "TABLE_MACRO", transactionOpaqueData);
640
698
  const all = [...macros, ...tableMacros];
641
699
  return all.find((m) => m.name.toLowerCase() === name.toLowerCase()) ?? null;
642
700
  }
643
701
 
644
702
  override tableScanFunctionGet(
645
703
  attachOpaqueData: AttachOpaqueData,
646
- schemaName: string,
704
+ schemaPath: string[],
647
705
  name: string,
648
706
  atUnit?: string,
649
707
  atValue?: string,
@@ -652,18 +710,18 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
652
710
  validateAtParams(atUnit, atValue);
653
711
 
654
712
  // Find the table descriptor
655
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
713
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
656
714
  if (!schema || !schema.tables) {
657
- throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
715
+ throw new Error(`Table '${name}' not found in schema ${schemaPathDisplay(schemaPath)}`);
658
716
  }
659
717
  const table = schema.tables.find((t) => t.name === name);
660
718
  if (!table) {
661
- throw new Error(`Table '${name}' not found in schema '${schemaName}'`);
719
+ throw new Error(`Table '${name}' not found in schema ${schemaPathDisplay(schemaPath)}`);
662
720
  }
663
721
 
664
722
  // Reject AT clause on tables that don't support time travel
665
723
  if (atUnit && !table.supportsTimeTravel) {
666
- throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
724
+ throw new Error(`Table ${schemaPathDisplay(schemaPath)}.${name} does not support time travel queries`);
667
725
  }
668
726
 
669
727
  if (table.function) {
@@ -675,6 +733,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
675
733
  function_name: table.function.meta.name,
676
734
  arguments: argBytes,
677
735
  required_extensions: [],
736
+ schema_path: this._schemaForFunction(table.function),
678
737
  };
679
738
  }
680
739
 
@@ -683,7 +742,7 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
683
742
 
684
743
  override async tableGet(
685
744
  attachOpaqueData: AttachOpaqueData,
686
- schemaName: string,
745
+ schemaPath: string[],
687
746
  name: string,
688
747
  atUnit?: string,
689
748
  atValue?: string,
@@ -692,25 +751,25 @@ export class ReadOnlyCatalogInterface extends CatalogInterface {
692
751
  validateAtParams(atUnit, atValue);
693
752
 
694
753
  // Find the table descriptor to check supports_time_travel
695
- const schema = this._descriptor.schemas.find((s) => s.name === schemaName);
754
+ const schema = this._descriptor.schemas.find((s) => schemaPathsEqual(schemaDescriptorPath(s), schemaPath));
696
755
  if (schema?.tables) {
697
756
  const tableDesc = schema.tables.find((t) => t.name === name);
698
757
  if (tableDesc && atUnit && !tableDesc.supportsTimeTravel) {
699
- throw new Error(`Table '${schemaName}.${name}' does not support time travel queries`);
758
+ throw new Error(`Table ${schemaPathDisplay(schemaPath)}.${name} does not support time travel queries`);
700
759
  }
701
760
  }
702
761
 
703
- const tables = await this.schemaContentsTables(attachOpaqueData, schemaName, transactionOpaqueData);
762
+ const tables = await this.schemaContentsTables(attachOpaqueData, schemaPath, transactionOpaqueData);
704
763
  return tables.find((t) => t.name === name) ?? null;
705
764
  }
706
765
 
707
766
  override viewGet(
708
767
  attachOpaqueData: AttachOpaqueData,
709
- schemaName: string,
768
+ schemaPath: string[],
710
769
  name: string,
711
770
  transactionOpaqueData?: TransactionOpaqueData
712
771
  ): ViewInfo | null {
713
- const views = this.schemaContentsViews(attachOpaqueData, schemaName, transactionOpaqueData);
772
+ const views = this.schemaContentsViews(attachOpaqueData, schemaPath, transactionOpaqueData);
714
773
  return views.find((v) => v.name === name) ?? null;
715
774
  }
716
775
 
@@ -786,14 +845,7 @@ function resolveIndexGroups(schema: VgiSchema, groups?: string[][]): number[][]
786
845
  return groups.map((group) => resolveIndices(schema, group));
787
846
  }
788
847
 
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[] {
848
+ function serializeForeignKeys(fks?: ForeignKeyDef[], schemaPath?: string[]): Uint8Array[] {
797
849
  if (!fks || fks.length === 0) return [];
798
850
  return fks.map((fk) =>
799
851
  serializeBatch(
@@ -802,9 +854,9 @@ function serializeForeignKeys(fks?: ForeignKeyDef[], schemaName?: string): Uint8
802
854
  fk_columns: [fk.columns],
803
855
  pk_columns: [fk.referencedColumns],
804
856
  referenced_table: [fk.referencedTable],
805
- referenced_schema: [fk.referencedSchema ?? schemaName ?? "main"],
857
+ referenced_schema_path: [fk.referencedSchemaPath ?? schemaPath ?? ["main"]],
806
858
  },
807
- FK_SCHEMA
859
+ ForeignKeyInfoSchema
808
860
  )
809
861
  )
810
862
  );
@@ -1001,14 +1053,16 @@ function inferScalarType(val: any): VgiDataType {
1001
1053
 
1002
1054
  // Build the inlined `scan_function` payload for a function-backed table.
1003
1055
  // 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.
1056
+ // ScanFunctionResultSchema (function_name, arguments, required_extensions,
1057
+ // schema_path). The C++ extension uses these bytes verbatim and skips the
1058
+ // catalog_table_scan_function_get RPC, so the schema this inlines has to agree
1059
+ // with what that RPC would have answered.
1007
1060
  function inlineScanFunction(
1008
1061
  func: VgiFunction,
1009
1062
  args: Arguments | undefined,
1063
+ schemaPath: string[] | null,
1010
1064
  ): Uint8Array {
1011
1065
  const argSchema = argumentSpecsToSchema(func.argumentSpecs);
1012
1066
  const argBytes = serializeArgsBatch(args ?? new Arguments(), argSchema);
1013
- return encodeScanFunctionResult(func.meta.name, argBytes);
1067
+ return encodeScanFunctionResult(func.meta.name, argBytes, [], schemaPath);
1014
1068
  }