@@ -15,6 +15,7 @@ import {
1515} from "../local-relation-site.js" ;
1616import {
1717 POSTGRESQL_SQL_RELATION_DIALECT ,
18+ type SqlRelationDialectRuntime ,
1819} from "../relation-dialect.js" ;
1920import {
2021 createIdentitySqlSource ,
@@ -28,6 +29,7 @@ import type {
2829 SqlCatalogRelationKind ,
2930 SqlCatalogReadyCoverage ,
3031 SqlCompletionList ,
32+ SqlRelationCompletionDialectRuntime ,
3133} from "../relation-completion-types.js" ;
3234import type {
3335 SqlTextRange ,
@@ -112,6 +114,7 @@ function relationPath(
112114function catalogRelation ( input : {
113115 readonly completionPathStart ?: number ;
114116 readonly containers ?: readonly string [ ] ;
117+ readonly detail ?: string ;
115118 readonly entityId : string ;
116119 readonly kind ?: SqlCatalogRelationKind ;
117120 readonly match ?: "equivalent" | "exact" ;
@@ -133,7 +136,7 @@ function catalogRelation(input: {
133136 completionPathStart === 0
134137 ? path
135138 : relationPath ( [ ] , input . name ) ;
136- return Object . freeze ( {
139+ const relation = {
137140 canonicalPath : path ,
138141 completionPath,
139142 completionPathStart,
@@ -143,7 +146,12 @@ function catalogRelation(input: {
143146 entityId : input . entityId ,
144147 matchQuality : input . match ?? "exact" ,
145148 relationKind : input . kind ?? "table" ,
146- } ) ;
149+ } ;
150+ return Object . freeze (
151+ input . detail === undefined
152+ ? relation
153+ : { ...relation , detail : input . detail } ,
154+ ) ;
147155}
148156
149157function readyResponse (
@@ -363,6 +371,263 @@ describe("relation completion composition", () => {
363371 } ,
364372 ) ;
365373
374+ it ( "composes terminal loading and failed provider evidence" , ( ) => {
375+ const loading = compose (
376+ "SELECT * FROM |" ,
377+ usable (
378+ Object . freeze ( {
379+ epoch : Object . freeze ( {
380+ generation : 2 ,
381+ token : "loading" ,
382+ } ) ,
383+ status : "loading" ,
384+ } ) ,
385+ ) ,
386+ ) ;
387+ expect ( loading . issues ) . toEqual ( [
388+ {
389+ reason : "catalog-loading" ,
390+ remainingIntentLeaseMs : 25 ,
391+ } ,
392+ ] ) ;
393+
394+ const local = markedLocalSite ( "SELECT * FROM |" ) ;
395+ const failed = composeSqlRelationCompletion ( {
396+ catalogOutcome : usable (
397+ Object . freeze ( {
398+ code : "authentication" ,
399+ epoch : Object . freeze ( {
400+ generation : 2 ,
401+ token : "failed" ,
402+ } ) ,
403+ retry : "after-invalidation" ,
404+ status : "failed" ,
405+ } ) ,
406+ ) ,
407+ dialect : POSTGRESQL_SQL_RELATION_DIALECT ,
408+ localSite : local . localSite ,
409+ providerId : "catalog" ,
410+ remainingIntentLeaseMs : 0 ,
411+ replacementRange : local . replacementRange ,
412+ statementOffset : local . statementOffset ,
413+ } ) ;
414+ expect ( failed . value . issues ) . toEqual ( [
415+ { reason : "catalog-failed" } ,
416+ ] ) ;
417+ expect ( failed . sources ) . toEqual ( [
418+ {
419+ code : "authentication" ,
420+ feature : "relation-catalog" ,
421+ outcome : "failed" ,
422+ providerId : "catalog" ,
423+ retry : "after-invalidation" ,
424+ } ,
425+ ] ) ;
426+ } ) ;
427+
428+ it ( "fails closed for uncertain and hostile CTE prefix policy" , ( ) => {
429+ const local = markedLocalSite (
430+ "WITH local_table AS (SELECT 1) SELECT * FROM |" ,
431+ ) ;
432+ const complete = (
433+ cteIdentifierMatchesPrefix :
434+ SqlRelationCompletionDialectRuntime [ "cteIdentifierMatchesPrefix" ] ,
435+ ) : SqlCompletionList => {
436+ const completion = Object . freeze ( {
437+ ...POSTGRESQL_SQL_RELATION_DIALECT . completion ,
438+ cteIdentifierMatchesPrefix,
439+ } ) ;
440+ const dialect : SqlRelationDialectRuntime = Object . freeze ( {
441+ ...POSTGRESQL_SQL_RELATION_DIALECT ,
442+ completion,
443+ } ) ;
444+ return composeSqlRelationCompletion ( {
445+ catalogOutcome : null ,
446+ dialect,
447+ localSite : local . localSite ,
448+ providerId : null ,
449+ remainingIntentLeaseMs : 0 ,
450+ replacementRange : local . replacementRange ,
451+ statementOffset : local . statementOffset ,
452+ } ) . value ;
453+ } ;
454+
455+ const unknown = complete ( ( ) => "unknown" ) ;
456+ expect ( unknown . items ) . toEqual ( [ ] ) ;
457+ expect ( unknown . issues ) . toContainEqual ( {
458+ reason : "cte-scope-uncertainty" ,
459+ } ) ;
460+
461+ const throwing = complete ( ( ) => {
462+ throw new Error ( "hostile prefix policy" ) ;
463+ } ) ;
464+ expect ( throwing . items ) . toEqual ( [ ] ) ;
465+ expect ( throwing . issues ) . toContainEqual ( {
466+ reason : "cte-scope-uncertainty" ,
467+ } ) ;
468+
469+ const noMatch = complete ( ( ) => "no-match" ) ;
470+ expect ( noMatch . items ) . toEqual ( [ ] ) ;
471+ expect ( noMatch . isIncomplete ) . toBe ( false ) ;
472+ } ) ;
473+
474+ it ( "retains catalog evidence when CTE shadow comparison is uncertain" , ( ) => {
475+ const local = markedLocalSite (
476+ "WITH users AS (SELECT 1) SELECT * FROM |" ,
477+ ) ;
478+ const completion = Object . freeze ( {
479+ ...POSTGRESQL_SQL_RELATION_DIALECT . completion ,
480+ compareCteIdentifiers : ( ) => {
481+ throw new Error ( "hostile comparison policy" ) ;
482+ } ,
483+ renderRelationPath : ( ) =>
484+ Object . freeze ( {
485+ reason : "illegal-role-sequence" as const ,
486+ status : "unsupported" as const ,
487+ } ) ,
488+ } ) ;
489+ const dialect : SqlRelationDialectRuntime = Object . freeze ( {
490+ ...POSTGRESQL_SQL_RELATION_DIALECT ,
491+ completion,
492+ } ) ;
493+ const result = composeSqlRelationCompletion ( {
494+ catalogOutcome : usable (
495+ readyResponse ( [
496+ catalogRelation ( {
497+ detail : "A catalog relation" ,
498+ entityId : "users" ,
499+ name : "users" ,
500+ } ) ,
501+ ] ) ,
502+ ) ,
503+ dialect,
504+ localSite : local . localSite ,
505+ providerId : "catalog" ,
506+ remainingIntentLeaseMs : 0 ,
507+ replacementRange : local . replacementRange ,
508+ statementOffset : local . statementOffset ,
509+ } ) ;
510+
511+ expect ( result . value . items ) . toHaveLength ( 2 ) ;
512+ expect ( result . value . items [ 1 ] ) . toMatchObject ( {
513+ detail : "A catalog relation" ,
514+ label : "users" ,
515+ provenance : { entityId : "users" } ,
516+ } ) ;
517+ expect ( result . value . issues ) . toContainEqual ( {
518+ reason : "cte-scope-uncertainty" ,
519+ } ) ;
520+
521+ if ( local . localSite . local . kind !== "unqualified" ) {
522+ throw new Error ( "Expected an unqualified local site" ) ;
523+ }
524+ const uncertainSite : typeof local . localSite = Object . freeze ( {
525+ ...local . localSite ,
526+ local : Object . freeze ( {
527+ ...local . localSite . local ,
528+ cteVisibility : Object . freeze ( {
529+ ...local . localSite . local . cteVisibility ,
530+ quality : "recovered" as const ,
531+ shadowing : Object . freeze ( {
532+ coverage : "unknown" as const ,
533+ } ) ,
534+ } ) ,
535+ } ) ,
536+ } ) ;
537+ const unknownCoverage = composeSqlRelationCompletion ( {
538+ catalogOutcome : usable (
539+ readyResponse ( [
540+ catalogRelation ( {
541+ entityId : "users" ,
542+ name : "users" ,
543+ } ) ,
544+ ] ) ,
545+ ) ,
546+ dialect : POSTGRESQL_SQL_RELATION_DIALECT ,
547+ localSite : uncertainSite ,
548+ providerId : "catalog" ,
549+ remainingIntentLeaseMs : 0 ,
550+ replacementRange : local . replacementRange ,
551+ statementOffset : local . statementOffset ,
552+ } ) ;
553+ expect ( unknownCoverage . value . items ) . toHaveLength ( 2 ) ;
554+ expect ( unknownCoverage . value . issues ) . toContainEqual ( {
555+ reason : "cte-scope-uncertainty" ,
556+ } ) ;
557+ } ) ;
558+
559+ it ( "surfaces recursive CTE and opaque query-site recovery" , ( ) => {
560+ const recursive = compose (
561+ "WITH RECURSIVE local_table AS (SELECT * FROM |) SELECT * FROM local_table" ,
562+ null ,
563+ ) ;
564+ expect ( recursive . issues . map ( ( issue ) => issue . reason ) ) . toEqual ( [
565+ "cte-scope-uncertainty" ,
566+ "recursive-cte-uncertainty" ,
567+ ] ) ;
568+
569+ const local = markedLocalSite ( "SELECT * FROM |" ) ;
570+ const opaqueSite : typeof local . localSite = Object . freeze ( {
571+ ...local . localSite ,
572+ querySite : Object . freeze ( {
573+ ...local . localSite . querySite ,
574+ recognition : Object . freeze ( {
575+ issues : Object . freeze ( [
576+ "opaque-template-context" ,
577+ ] as const ) ,
578+ quality : "recovered" as const ,
579+ } ) ,
580+ } ) ,
581+ } ) ;
582+ const opaque = composeSqlRelationCompletion ( {
583+ catalogOutcome : null ,
584+ dialect : POSTGRESQL_SQL_RELATION_DIALECT ,
585+ localSite : opaqueSite ,
586+ providerId : null ,
587+ remainingIntentLeaseMs : 0 ,
588+ replacementRange : local . replacementRange ,
589+ statementOffset : local . statementOffset ,
590+ } ) . value ;
591+ expect ( opaque . issues ) . toContainEqual ( {
592+ reason : "opaque-template-context" ,
593+ } ) ;
594+ } ) ;
595+
596+ it ( "uses path and entity IDs as deterministic final catalog ties" , ( ) => {
597+ const relations = [
598+ catalogRelation ( {
599+ completionPathStart : 0 ,
600+ containers : [ "zeta" ] ,
601+ entityId : "z-last" ,
602+ name : "users" ,
603+ } ) ,
604+ catalogRelation ( {
605+ completionPathStart : 0 ,
606+ containers : [ "alpha" ] ,
607+ entityId : "b" ,
608+ name : "users" ,
609+ } ) ,
610+ catalogRelation ( {
611+ completionPathStart : 0 ,
612+ containers : [ "alpha" ] ,
613+ entityId : "a" ,
614+ name : "users" ,
615+ } ) ,
616+ ] ;
617+ const value = compose (
618+ "SELECT * FROM |" ,
619+ usable ( readyResponse ( relations ) ) ,
620+ ) ;
621+ expect (
622+ value . items . map (
623+ ( item ) =>
624+ item . provenance . kind === "catalog"
625+ ? item . provenance . entityId
626+ : "cte" ,
627+ ) ,
628+ ) . toEqual ( [ "a" , "b" , "z-last" ] ) ;
629+ } ) ;
630+
366631 it ( "applies the result cap after ranking and returns deeply frozen output" , ( ) => {
367632 const declarations = Array . from (
368633 { length : MAX_RELATION_COMPLETION_RESULTS + 1 } ,
0 commit comments