@@ -2,11 +2,15 @@ import Int64 from 'node-int64';
22import IBackend from '../contracts/IBackend' ;
33import ISessionBackend from '../contracts/ISessionBackend' ;
44import IClientContext from '../contracts/IClientContext' ;
5- import { OpenSessionRequest } from '../contracts/IDBSQLClient' ;
5+ import { ConnectionOptions , OpenSessionRequest } from '../contracts/IDBSQLClient' ;
66import { TProtocolVersion } from '../../thrift/TCLIService_types' ;
77import Status from '../dto/Status' ;
88import { definedOrError , serializeQueryTags } from '../utils' ;
99import ThriftSessionBackend from './ThriftSessionBackend' ;
10+ import StatusError from '../errors/StatusError' ;
11+ import reydenCache from '../ReydenWarehouseCache' ;
12+ import KernelBackend from '../kernel/KernelBackend' ;
13+ import { LogLevel } from '../contracts/IDBSQLLogger' ;
1014
1115function getInitialNamespaceOptions ( catalogName ?: string , schemaName ?: string ) {
1216 if ( ! catalogName && ! schemaName ) {
@@ -31,12 +35,36 @@ export default class ThriftBackend implements IBackend {
3135
3236 private readonly onConnectionEvent : ThriftBackendOptions [ 'onConnectionEvent' ] ;
3337
38+ private connectionOptions ?: ConnectionOptions ;
39+
3440 constructor ( { context, onConnectionEvent } : ThriftBackendOptions ) {
3541 this . context = context ;
3642 this . onConnectionEvent = onConnectionEvent ;
3743 }
3844
39- public async connect ( ) : Promise < void > {
45+ /**
46+ * Extracts warehouse/endpoint ID from the HTTP path.
47+ * Matches patterns like `/sql/1.0/warehouses/<id>` or `/sql/1.0/endpoints/<id>`.
48+ * Returns undefined if no ID can be extracted.
49+ */
50+ private static extractWarehouseId ( httpPath : string | undefined ) : string | undefined {
51+ if ( ! httpPath ) {
52+ return undefined ;
53+ }
54+
55+ // Stop at query string
56+ const pathOnly = httpPath . split ( '?' ) [ 0 ] ;
57+
58+ // Match `/warehouses/<id>` or `/endpoints/<id>`
59+ // Stop at `/` or end of string
60+ const match = pathOnly . match ( / \/ ( w a r e h o u s e s | e n d p o i n t s ) \/ ( [ ^ / ] + ) / ) ;
61+ return match ? match [ 2 ] : undefined ;
62+ }
63+
64+ public async connect ( options : ConnectionOptions ) : Promise < void > {
65+ // Store connection options for warehouse ID extraction in openSession
66+ this . connectionOptions = options ;
67+
4068 // The connection provider is owned by DBSQLClient (it implements IClientContext).
4169 // We only need to wire the EventEmitter listeners through this backend.
4270 const connectionProvider = await this . context . getConnectionProvider ( ) ;
@@ -60,6 +88,57 @@ export default class ThriftBackend implements IBackend {
6088 }
6189
6290 public async openSession ( request : OpenSessionRequest ) : Promise < ISessionBackend > {
91+ const logger = this . context . getLogger ( ) ;
92+
93+ // Extract warehouse ID for cache lookups
94+ const warehouseId = ThriftBackend . extractWarehouseId ( this . connectionOptions ?. path ) ;
95+ const host = this . connectionOptions ?. host ;
96+
97+ // Check if this warehouse is known to be Reyden (requires SEA backend)
98+ if ( host && warehouseId && reydenCache . isKnownReyden ( host , warehouseId ) ) {
99+ logger . log ( LogLevel . debug , `Reyden: warehouse ${ warehouseId } is known to require SEA fallback; skipping Thrift` ) ;
100+ return this . openSessionWithKernelBackend ( request ) ;
101+ }
102+
103+ // Try Thrift first (default path).
104+ try {
105+ return await this . openSessionWithThrift ( request ) ;
106+ } catch ( error ) {
107+ // Only a Reyden KP001 rejection triggers fallback. Every other error
108+ // propagates unchanged — note StatusError is NOT an Error subclass
109+ // (it only `implements Error`), so it must be re-thrown as-is rather
110+ // than normalized, or its sqlState/message would be lost.
111+ if ( error instanceof StatusError && error . sqlState === 'KP001' ) {
112+ logger . log ( LogLevel . debug , `Reyden: detected KP001 on warehouse ${ warehouseId } ; falling back to SEA backend` ) ;
113+
114+ // Mark this warehouse as Reyden for future connections.
115+ if ( host && warehouseId ) {
116+ reydenCache . markReyden ( host , warehouseId ) ;
117+ }
118+
119+ // Fall back to the kernel (SEA) backend exactly once. If it also fails,
120+ // surface the kernel error but keep the original Thrift rejection as its
121+ // cause for diagnosis.
122+ try {
123+ return await this . openSessionWithKernelBackend ( request ) ;
124+ } catch ( kernelError ) {
125+ if ( kernelError && typeof kernelError === 'object' ) {
126+ ( kernelError as { cause ?: unknown } ) . cause = error ;
127+ }
128+ logger . log ( LogLevel . error , 'Reyden: both Thrift (KP001) and SEA fallback failed' ) ;
129+ throw kernelError ;
130+ }
131+ }
132+
133+ // Not a Reyden rejection — surface the original error unchanged.
134+ throw error ;
135+ }
136+ }
137+
138+ /**
139+ * Opens a session using the Thrift backend.
140+ */
141+ private async openSessionWithThrift ( request : OpenSessionRequest ) : Promise < ISessionBackend > {
63142 const driver = await this . context . getDriver ( ) ;
64143 const config = this . context . getConfig ( ) ;
65144
@@ -93,6 +172,24 @@ export default class ThriftBackend implements IBackend {
93172 } ) ;
94173 }
95174
175+ /**
176+ * Opens a session using the KernelBackend (SEA).
177+ * Called as a fallback when Thrift returns KP001 (Reyden rejection).
178+ */
179+ private async openSessionWithKernelBackend ( request : OpenSessionRequest ) : Promise < ISessionBackend > {
180+ if ( ! this . connectionOptions ) {
181+ throw new Error ( 'KernelBackend fallback: connection options not available' ) ;
182+ }
183+
184+ const logger = this . context . getLogger ( ) ;
185+ logger . log ( LogLevel . debug , 'Reyden: opening session via KernelBackend (SEA)' ) ;
186+
187+ // Create a new KernelBackend instance and connect/open
188+ const kernelBackend = new KernelBackend ( { context : this . context } ) ;
189+ await kernelBackend . connect ( this . connectionOptions ) ;
190+ return kernelBackend . openSession ( request ) ;
191+ }
192+
96193 public async close ( ) : Promise < void > {
97194 // DBSQLClient owns the connection lifecycle and clears its own state
98195 // (connectionProvider, authProvider, thrift client) after this returns.
0 commit comments