@@ -90,25 +90,75 @@ function assertStagePath(value: string): void {
9090 }
9191}
9292
93- const UNQUOTED_IDENTIFIER = / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ $ ] * $ /
93+ /** An unquoted identifier, or a double-quoted one with `""` escapes, as Execute SQL accepts. */
94+ const CONTEXT_IDENTIFIER = / ^ (?: [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ $ ] * | " (?: [ ^ " ] | " " ) + " ) $ /
95+
96+ /** Splits a qualified name on the dots outside double-quoted identifiers. */
97+ function splitQualifiedName ( name : string ) : string [ ] {
98+ const parts : string [ ] = [ ]
99+ let current = ''
100+ let quoted = false
101+ for ( const char of name ) {
102+ if ( char === '"' ) quoted = ! quoted
103+ if ( char === '.' && ! quoted ) {
104+ parts . push ( current )
105+ current = ''
106+ } else {
107+ current += char
108+ }
109+ }
110+ parts . push ( current )
111+ return parts
112+ }
113+
114+ /** Database and schema of a `DB.SCHEMA.VIEW` view name or an `@DB.SCHEMA.STAGE/file` stage path. */
115+ function sourceContext (
116+ kind : 'semantic_view' | 'semantic_model_file' ,
117+ value : string
118+ ) : { database : string ; schema : string } | null {
119+ const name = kind === 'semantic_view' ? value . trim ( ) : value . trim ( ) . slice ( 1 ) . split ( '/' ) [ 0 ]
120+ const parts = splitQualifiedName ( name )
121+ if ( parts . length !== 3 ) return null
122+ const [ database , schema ] = parts
123+ if ( ! CONTEXT_IDENTIFIER . test ( database ) || ! CONTEXT_IDENTIFIER . test ( schema ) ) return null
124+ return { database, schema }
125+ }
94126
95127/**
96- * Database and schema to run the generated SQL in, read from a fully qualified semantic view
97- * (`DB.SCHEMA.VIEW`) or stage path (`@DB.SCHEMA.STAGE/file.yaml`). Generated SQL can reference
98- * the semantic view unqualified, so it has to run in that view's schema. Quoted identifiers are
99- * left to the credential's default context rather than re-quoted.
128+ * Database and schema to run the generated SQL in. Generated SQL can reference the semantic
129+ * view unqualified, so it runs in the fully qualified view's (or staged model's) schema. With
130+ * several sources, Snowflake does not document how `semantic_model_selection` names the chosen
131+ * one, so the context is set only when every source shares the same database and schema;
132+ * otherwise the credential's default context applies.
100133 */
101134export function cortexAnalystSqlContext (
102- params : Pick < SnowflakeCortexAnalystAskParams , 'semanticView' | 'semanticModelFile' >
135+ params : Pick <
136+ SnowflakeCortexAnalystAskParams ,
137+ 'semanticView' | 'semanticModelFile' | 'semanticModels'
138+ >
103139) : { database ?: string ; schema ?: string } {
104- const name = hasValue ( params . semanticView )
105- ? String ( params . semanticView ) . trim ( )
140+ const contexts = hasValue ( params . semanticView )
141+ ? [ sourceContext ( 'semantic_view' , String ( params . semanticView ) ) ]
106142 : hasValue ( params . semanticModelFile )
107- ? String ( params . semanticModelFile ) . trim ( ) . slice ( 1 ) . split ( '/' ) [ 0 ]
108- : ''
109- const parts = name . split ( '.' )
110- if ( parts . length !== 3 || ! parts . every ( ( part ) => UNQUOTED_IDENTIFIER . test ( part ) ) ) return { }
111- return { database : parts [ 0 ] , schema : parts [ 1 ] }
143+ ? [ sourceContext ( 'semantic_model_file' , String ( params . semanticModelFile ) ) ]
144+ : hasValue ( params . semanticModels )
145+ ? parseSemanticModels ( params . semanticModels ) . map ( ( entry ) =>
146+ entry . semantic_view !== undefined
147+ ? sourceContext ( 'semantic_view' , entry . semantic_view )
148+ : sourceContext ( 'semantic_model_file' , entry . semantic_model_file )
149+ )
150+ : [ ]
151+ const [ first ] = contexts
152+ if (
153+ ! first ||
154+ contexts . some (
155+ ( context ) =>
156+ ! context || context . database !== first . database || context . schema !== first . schema
157+ )
158+ ) {
159+ return { }
160+ }
161+ return { database : first . database , schema : first . schema }
112162}
113163
114164/** The request's user turn, appended after any history. */
@@ -182,8 +232,10 @@ function stringArray(value: unknown): string[] {
182232 * `suggestion` and `suggestions`, and types the field as a string while its examples return an
183233 * array, so every form is read.
184234 *
185- * The analyst turn is echoed into `conversation` in the documented request shape (text blocks
186- * and `sql` statements only), so it can be replayed as history on the next ask.
235+ * The analyst turn is echoed into `conversation` so it can be replayed as history on the next
236+ * ask: text, `sql` statements without their confidence metadata, and suggestion lists, so a
237+ * follow-up can refer to a suggested question. Snowflake's own Cortex Analyst client replays
238+ * suggestion blocks the same way.
187239 */
188240export function mapCortexAnalystResponse (
189241 data : CortexAnalystResponsePayload ,
@@ -222,6 +274,10 @@ export function mapCortexAnalystResponse(
222274 if ( block . type === 'sql' && block . statement ) {
223275 return [ { type : 'sql' , statement : block . statement } ]
224276 }
277+ if ( block . type === 'suggestions' || block . type === 'suggestion' ) {
278+ const suggested = stringArray ( block . suggestions )
279+ return suggested . length > 0 ? [ { type : block . type , suggestions : suggested } ] : [ ]
280+ }
225281 return [ ]
226282 } ) ,
227283 }
0 commit comments