diff --git a/SqlScriptDom/Parser/TSql/Ast.xml b/SqlScriptDom/Parser/TSql/Ast.xml index bad570b2..d90c664a 100644 --- a/SqlScriptDom/Parser/TSql/Ast.xml +++ b/SqlScriptDom/Parser/TSql/Ast.xml @@ -794,6 +794,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -4658,6 +4682,28 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -4806,6 +4852,12 @@ + + + + + + diff --git a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs index e2712855..7bd0e484 100644 --- a/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs +++ b/SqlScriptDom/Parser/TSql/CodeGenerationSupporter.cs @@ -204,6 +204,7 @@ internal static class CodeGenerationSupporter internal const string ChangeTable = "CHANGETABLE"; internal const string ChangeTracking = "CHANGE_TRACKING"; internal const string ChangeTrackingContext = "CHANGE_TRACKING_CONTEXT"; + internal const string Chapter = "CHAPTER"; internal const string Char = "CHAR"; internal const string CharacterSet = "CHARACTER_SET"; internal const string CheckConstraints = "CHECK_CONSTRAINTS"; @@ -214,6 +215,7 @@ internal static class CodeGenerationSupporter internal const string ChecksumAgg = "CHECKSUM_AGG"; internal const string ChunkSize = "CHUNK_SIZE"; internal const string ChunkType = "CHUNK_TYPE"; + internal const string ChunkUsing = "CHUNK_USING"; internal const string ModularSum = "MODULAR_SUM"; internal const string Classifier = "CLASSIFIER"; internal const string Classification = "CLASSIFICATION"; @@ -394,6 +396,7 @@ internal static class CodeGenerationSupporter internal const string Extension = "EXTENSION"; internal const string External = "EXTERNAL"; internal const string ExternalAccess = "EXTERNAL_ACCESS"; + internal const string ExternalModel = "EXTERNAL_MODEL"; internal const string ExternalMonitor = "EXTERNAL_MONITOR"; internal const string Extract = "EXTRACT"; internal const string FailOperation = "FAIL_OPERATION"; @@ -465,6 +468,7 @@ internal static class CodeGenerationSupporter internal const string Full = "FULL"; internal const string FullScan = "FULLSCAN"; internal const string Fulltext = "FULLTEXT"; + internal const string FulltextStopList = "FULLTEXT_STOPLIST"; internal const string General = "GENERAL"; internal const string GenerateSeries = "GENERATE_SERIES"; internal const string Generated = "GENERATED"; @@ -499,6 +503,7 @@ internal static class CodeGenerationSupporter internal const string Hidden = "HIDDEN"; internal const string High = "HIGH"; internal const string Hint = "HINT"; + internal const string Hybrid = "HYBRID"; internal const string Histogram = "HISTOGRAM"; internal const string HistogramSteps = "HISTOGRAM_STEPS"; internal const string HistoryRetentionPeriod = "HISTORY_RETENTION_PERIOD"; @@ -536,6 +541,7 @@ internal static class CodeGenerationSupporter internal const string Intermediate = "INTERMEDIATE"; internal const string IntervalLengthMinutes = "INTERVAL_LENGTH_MINUTES"; internal const string Insensitive = "INSENSITIVE"; + internal const string InvokeExternalApi = "INVOKE_EXTERNAL_API"; internal const string IRowset = "IROWSET"; internal const string Isolation = "ISOLATION"; internal const string Job = "JOB"; @@ -755,6 +761,7 @@ internal static class CodeGenerationSupporter internal const string PageCount = "PAGECOUNT"; internal const string PageVerify = "PAGE_VERIFY"; internal const string PagLock = "PAGLOCK"; + internal const string Paragraph = "PARAGRAPH"; internal const string Param = "PARAM"; internal const string Parameter = "PARAMETER"; internal const string Parameters = "PARAMETERS"; @@ -926,6 +933,7 @@ internal static class CodeGenerationSupporter internal const string Scroll = "SCROLL"; internal const string ScrollLocks = "SCROLL_LOCKS"; internal const string Search = "SEARCH"; + internal const string SearchType = "SEARCH_TYPE"; internal const string Secondary = "SECONDARY"; internal const string SecondaryRole = "SECONDARY_ROLE"; internal const string Seconds = "SECONDS"; @@ -935,6 +943,8 @@ internal static class CodeGenerationSupporter internal const string SecurityLog = "SECURITY_LOG"; internal const string Selective = "SELECTIVE"; internal const string Self = "SELF"; + internal const string Semantic = "SEMANTIC"; + internal const string Sentence = "SENTENCE"; internal const string SemiColon = ";"; internal const string Send = "SEND"; internal const string Sensitivity = "SENSITIVITY"; @@ -1123,6 +1133,7 @@ internal static class CodeGenerationSupporter internal const string Varp = "VARP"; internal const string VDevNo = "VDEVNO"; internal const string Vector = "Vector"; + internal const string VectorIndex = "VECTOR_INDEX"; internal const string VectorSearch = "VECTOR_SEARCH"; internal const string Verbose = "VERBOSE"; internal const string VerboseLogging = "VerboseLogging"; diff --git a/SqlScriptDom/Parser/TSql/SemanticIndexChunkOptionKind.cs b/SqlScriptDom/Parser/TSql/SemanticIndexChunkOptionKind.cs new file mode 100644 index 00000000..c3a634ac --- /dev/null +++ b/SqlScriptDom/Parser/TSql/SemanticIndexChunkOptionKind.cs @@ -0,0 +1,24 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + + + +namespace Microsoft.SqlServer.TransactSql.ScriptDom +{ +#pragma warning disable 1591 + + /// + /// The possible values for semantic index chunk options. + /// + public enum SemanticIndexChunkOptionKind + { + Type = 0, + Size = 1, + Overlap = 2 + } + +#pragma warning restore 1591 +} diff --git a/SqlScriptDom/Parser/TSql/SemanticIndexSearchType.cs b/SqlScriptDom/Parser/TSql/SemanticIndexSearchType.cs new file mode 100644 index 00000000..d3d956fc --- /dev/null +++ b/SqlScriptDom/Parser/TSql/SemanticIndexSearchType.cs @@ -0,0 +1,25 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + + + +namespace Microsoft.SqlServer.TransactSql.ScriptDom +{ +#pragma warning disable 1591 + + /// + /// The possible values for semantic index search type. + /// + public enum SemanticIndexSearchType + { + NotSpecified = 0, + Vector = 1, + Fulltext = 2, + Hybrid = 3 + } + +#pragma warning restore 1591 +} diff --git a/SqlScriptDom/Parser/TSql/TSql180.g b/SqlScriptDom/Parser/TSql/TSql180.g index f52ab337..ae426b05 100644 --- a/SqlScriptDom/Parser/TSql/TSql180.g +++ b/SqlScriptDom/Parser/TSql/TSql180.g @@ -889,6 +889,9 @@ create2005Statements returns [TSqlStatement vResult = null] | {NextTokenMatches(CodeGenerationSupporter.Vector)}? vResult=createVectorIndexStatement[null, null] + | + {NextTokenMatches(CodeGenerationSupporter.Semantic)}? + vResult=createSemanticIndexStatement | {NextTokenMatches(CodeGenerationSupporter.Contract)}? vResult=createContractStatement @@ -17132,6 +17135,232 @@ createVectorIndexStatement [IToken tUnique, bool? isClustered] returns [CreateVe )? ; +createSemanticIndexStatement returns [CreateSemanticIndexStatement vResult = FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; + SchemaObjectName vSchemaObjectName; + SemanticIndexColumn vColumn; + FileGroupOrPartitionScheme vFileGroupOrPartitionScheme; +} + : tSemantic:Identifier tIndex:Index vIdentifier=identifier + { + Match(tSemantic, CodeGenerationSupporter.Semantic); + vResult.Name = vIdentifier; + } + tOn:On vSchemaObjectName=schemaObjectThreePartName + { + vResult.OnName = vSchemaObjectName; + } + LeftParenthesis + vColumn=semanticIndexColumnDefinition + { + AddAndUpdateTokenInfo(vResult, vResult.Columns, vColumn); + } + ( + Comma vColumn=semanticIndexColumnDefinition + { + AddAndUpdateTokenInfo(vResult, vResult.Columns, vColumn); + } + )* + tRParen:RightParenthesis + { + UpdateTokenInfo(vResult, tRParen); + } + ( + // Greedy due to conflict with withCommonTableExpressionsAndXmlNamespaces + options {greedy = true; } : + With LeftParenthesis + semanticIndexWithOptions[vResult] + RightParenthesis + )? + ( + On vFileGroupOrPartitionScheme=filegroupOrPartitionScheme + { + vResult.OnFileGroupOrPartitionScheme = vFileGroupOrPartitionScheme; + } + )? + { + // Validate that EXTERNAL_MODEL is specified for vector/hybrid columns + ValidateSemanticIndexExternalModel(vResult); + } + ; + +semanticIndexColumnDefinition returns [SemanticIndexColumn vResult = FragmentFactory.CreateFragment()] +{ + Identifier vIdentifier; + IdentifierOrValueExpression vValue; +} + : vIdentifier=identifier + { + vResult.ColumnName = vIdentifier; + } + ( + {NextTokenMatches(CodeGenerationSupporter.SearchType)}? + tSearchType:Identifier EqualsSign tSearchTypeValue:Identifier + { + Match(tSearchType, CodeGenerationSupporter.SearchType); + if (TryMatch(tSearchTypeValue, CodeGenerationSupporter.Vector)) + vResult.SearchType = SemanticIndexSearchType.Vector; + else if (TryMatch(tSearchTypeValue, CodeGenerationSupporter.Fulltext)) + vResult.SearchType = SemanticIndexSearchType.Fulltext; + else + { + Match(tSearchTypeValue, CodeGenerationSupporter.Hybrid); + vResult.SearchType = SemanticIndexSearchType.Hybrid; + } + } + )? + ( + {NextTokenMatches(CodeGenerationSupporter.Type)}? + Identifier Column vIdentifier=identifier + { + vResult.TypeColumnName = vIdentifier; + } + )? + ( + {NextTokenMatches(CodeGenerationSupporter.Language)}? + tLang:Identifier vValue=stringOrIdentifier + { + Match(tLang, CodeGenerationSupporter.Language); + vResult.Language = vValue; + } + )? + ( + {NextTokenMatches(CodeGenerationSupporter.ChunkUsing)}? + tChunkUsing:Identifier LeftParenthesis + { + Match(tChunkUsing, CodeGenerationSupporter.ChunkUsing); + } + semanticIndexChunkOptions[vResult] + RightParenthesis + )? + ; + +semanticIndexChunkOptions [SemanticIndexColumn vParent] +{ + SemanticIndexChunkOption vOption; +} + : vOption=semanticIndexChunkOption + { + AddAndUpdateTokenInfo(vParent, vParent.ChunkOptions, vOption); + } + ( + Comma vOption=semanticIndexChunkOption + { + AddAndUpdateTokenInfo(vParent, vParent.ChunkOptions, vOption); + } + )* + ; + +semanticIndexChunkOption returns [SemanticIndexChunkOption vResult = FragmentFactory.CreateFragment()] +{ + IdentifierOrValueExpression vValue; +} + : tOption:Identifier EqualsSign vValue=identifierOrInteger + { + if (TryMatch(tOption, CodeGenerationSupporter.Type)) + vResult.OptionKind = SemanticIndexChunkOptionKind.Type; + else if (TryMatch(tOption, CodeGenerationSupporter.Size)) + vResult.OptionKind = SemanticIndexChunkOptionKind.Size; + else + { + Match(tOption, CodeGenerationSupporter.Overlap); + vResult.OptionKind = SemanticIndexChunkOptionKind.Overlap; + } + vResult.Value = vValue; + } + ; + +semanticIndexWithOptions [CreateSemanticIndexStatement vParent] + : semanticIndexWithOption[vParent] + ( + Comma semanticIndexWithOption[vParent] + )* + ; + +semanticIndexWithOption [CreateSemanticIndexStatement vParent] +{ + IndexOption vIndexOption; + Identifier vIdentifier; + StringLiteral vStringLiteral; +} + : + ( + {NextTokenMatches(CodeGenerationSupporter.ExternalModel)}? + tExtModel:Identifier EqualsSign + { + Match(tExtModel, CodeGenerationSupporter.ExternalModel); + } + ( + LeftParenthesis vIdentifier=identifier RightParenthesis + { + vParent.ExternalModelName = vIdentifier; + } + | + vIdentifier=identifier + { + vParent.ExternalModelName = vIdentifier; + } + ) + ( + LeftParenthesis tParams:Identifier EqualsSign vStringLiteral=stringLiteral RightParenthesis + { + Match(tParams, CodeGenerationSupporter.Parameters); + vParent.ExternalModelParameters = vStringLiteral; + } + )? + | + {NextTokenMatches(CodeGenerationSupporter.VectorIndex)}? + tVecIdx:Identifier LeftParenthesis + { + Match(tVecIdx, CodeGenerationSupporter.VectorIndex); + } + vIndexOption=semanticIndexVectorOption + { + AddAndUpdateTokenInfo(vParent, vParent.VectorIndexOptions, vIndexOption); + } + ( + Comma vIndexOption=semanticIndexVectorOption + { + AddAndUpdateTokenInfo(vParent, vParent.VectorIndexOptions, vIndexOption); + } + )* + RightParenthesis + | + {NextTokenMatches(CodeGenerationSupporter.FulltextStopList)}? + tStoplist:Identifier EqualsSign + { + Match(tStoplist, CodeGenerationSupporter.FulltextStopList); + } + ( + tOff:Off + { + StopListFullTextIndexOption vStopOpt = FragmentFactory.CreateFragment(); + vStopOpt.OptionKind = FullTextIndexOptionKind.StopList; + vStopOpt.IsOff = true; + UpdateTokenInfo(vStopOpt, tStoplist); + UpdateTokenInfo(vStopOpt, tOff); + vParent.FulltextStoplistOption = vStopOpt; + } + | + vIdentifier=identifier + { + StopListFullTextIndexOption vStopOpt = FragmentFactory.CreateFragment(); + vStopOpt.OptionKind = FullTextIndexOptionKind.StopList; + vStopOpt.IsOff = false; + vStopOpt.StopListName = vIdentifier; + UpdateTokenInfo(vStopOpt, tStoplist); + vParent.FulltextStoplistOption = vStopOpt; + } + ) + | + vIndexOption=indexOption + { + AddAndUpdateTokenInfo(vParent, vParent.IndexOptions, vIndexOption); + } + ) + ; + indexKeyColumnList[CreateIndexStatement vParent] { ColumnWithSortOrder vColumnWithSortOrder; @@ -28283,6 +28512,15 @@ xmlCompressionOption returns [XmlCompressionOption vResult = FragmentFactory.Cre )? ; +semanticIndexVectorOption returns [IndexOption vResult = null] + : + {NextTokenMatches(CodeGenerationSupporter.Metric)}? + vResult=vectorMetricOption + | + {NextTokenMatches(CodeGenerationSupporter.Type)}? + vResult=vectorTypeOption + ; + vectorMetricOption returns [VectorMetricIndexOption vResult = FragmentFactory.CreateFragment()] : tMetric:Identifier EqualsSign tMetricValue:AsciiStringLiteral { diff --git a/SqlScriptDom/Parser/TSql/TSql180ParserBaseInternal.cs b/SqlScriptDom/Parser/TSql/TSql180ParserBaseInternal.cs index 89674e5d..5f3d827e 100644 --- a/SqlScriptDom/Parser/TSql/TSql180ParserBaseInternal.cs +++ b/SqlScriptDom/Parser/TSql/TSql180ParserBaseInternal.cs @@ -70,6 +70,27 @@ protected SecurityObjectKind ParseSecurityObjectKindTSql180(Identifier identifie return TSql170ParserBaseInternal.ParseSecurityObjectKind(identifier1, identifier2); } } + + /// + /// Validates that EXTERNAL_MODEL is specified when CREATE SEMANTIC INDEX contains vector or hybrid columns. + /// Vector and hybrid search types require an external model for embeddings. + /// NotSpecified defaults to Vector, so EXTERNAL_MODEL is required unless ALL columns are explicitly Fulltext. + /// + /// The CREATE SEMANTIC INDEX statement to validate. + protected static void ValidateSemanticIndexExternalModel(CreateSemanticIndexStatement statement) + { + // EXTERNAL_MODEL is NOT required only when ALL columns are explicitly FULLTEXT + // If any column is Vector, Hybrid, or NotSpecified (defaults to Vector), EXTERNAL_MODEL is required + bool allColumnsAreFulltext = statement.Columns.All(col => + col.SearchType == SemanticIndexSearchType.Fulltext); + + // EXTERNAL_MODEL is required unless all columns are explicitly fulltext + if (!allColumnsAreFulltext && statement.ExternalModelName == null) + { + ThrowParseErrorException("SQL46144", statement, + TSqlParserResource.SQL46144Message, "EXTERNAL_MODEL"); + } + } } } - + diff --git a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g index e4a7f24d..4ad40b85 100644 --- a/SqlScriptDom/Parser/TSql/TSqlFabricDW.g +++ b/SqlScriptDom/Parser/TSql/TSqlFabricDW.g @@ -730,7 +730,9 @@ lastStatement returns [TSqlStatement vResult = null] | vResult=createRuleStatement | vResult=createViewStatement | vResult=alterViewStatement + | (Create Function schemaObjectNonEmptyTwoPartName (LeftParenthesis (functionParameter (Comma functionParameter)*)? RightParenthesis)? (Identifier scalarDataType)? As External) => vResult=createExternalFunctionStatement | vResult=createFunctionStatement + | (Alter Function schemaObjectNonEmptyTwoPartName (LeftParenthesis (functionParameter (Comma functionParameter)*)? RightParenthesis)? (Identifier scalarDataType)? As External) => vResult=alterExternalFunctionStatement | vResult=alterFunctionStatement | vResult=createSchemaStatement | vResult=createIdentifierStatement @@ -770,7 +772,8 @@ alterIdentifierStatement returns [TSqlStatement vResult] createOrAlterStatements returns [TSqlStatement vResult] : tCreate:Create Or Alter ( - vResult = createOrAlterFunctionStatement + (Function schemaObjectNonEmptyTwoPartName (LeftParenthesis (functionParameter (Comma functionParameter)*)? RightParenthesis)? (Identifier scalarDataType)? As External) => vResult = createOrAlterExternalFunctionStatement + | vResult = createOrAlterFunctionStatement | vResult = createOrAlterProcedureStatement | vResult = createOrAlterTriggerStatement | vResult = createOrAlterViewStatement @@ -12996,6 +12999,83 @@ createOrAlterFunctionStatement returns [CreateOrAlterFunctionStatement vResult = } ; +createExternalFunctionStatement returns [CreateExternalFunctionStatement vResult = this.FragmentFactory.CreateFragment()] + : tCreate:Create + { + UpdateTokenInfo(vResult, tCreate); + } + externalFunctionStatementBody[vResult] + ; + +alterExternalFunctionStatement returns [AlterExternalFunctionStatement vResult = this.FragmentFactory.CreateFragment()] + : tAlter:Alter + { + UpdateTokenInfo(vResult, tAlter); + } + externalFunctionStatementBody[vResult] + ; + +createOrAlterExternalFunctionStatement returns [CreateOrAlterExternalFunctionStatement vResult = this.FragmentFactory.CreateFragment()] + : externalFunctionStatementBody[vResult] + ; + +externalFunctionStatementBody[ExternalFunctionStatement vResult] +{ + SchemaObjectName vName; + SchemaObjectName vExternalName; + DataTypeReference vReturnType; + ProcedureParameter vParameter; +} + : tFunction:Function + { + UpdateTokenInfo(vResult, tFunction); + } + vName=schemaObjectNonEmptyTwoPartName + { + vResult.Name = vName; + } + ( + LeftParenthesis + ( + vParameter=functionParameter + { + AddAndUpdateTokenInfo(vResult, vResult.Parameters, vParameter); + } + ( Comma vParameter=functionParameter + { + AddAndUpdateTokenInfo(vResult, vResult.Parameters, vParameter); + } + )* + )? + RightParenthesis + )? + ( + {NextTokenMatches(CodeGenerationSupporter.Returns)}? + tReturns:Identifier vReturnType=scalarDataType + { + Match(tReturns, CodeGenerationSupporter.Returns); + UpdateTokenInfo(vResult, tReturns); + vResult.ReturnType = vReturnType; + } + )? + tAs:As + { + UpdateTokenInfo(vResult, tAs); + } + tExternal:External + { + UpdateTokenInfo(vResult, tExternal); + } + tExternalFunction:Function + { + UpdateTokenInfo(vResult, tExternalFunction); + } + vExternalName=schemaObjectNonEmptyTwoPartName + { + vResult.ExternalName = vExternalName; + } + ; + functionStatementBody[FunctionStatementBody vResult, out bool vParseErrorOccurred] { SchemaObjectName vSchemaObjectName; @@ -31491,6 +31571,9 @@ expressionPrimary [ExpressionFlags expressionFlags] returns [PrimaryExpression v | {NextTokenMatches(CodeGenerationSupporter.AITranslate) && (LA(2) == LeftParenthesis)}? vResult=aiTranslateFunctionCall + | + {NextTokenMatches(CodeGenerationSupporter.InvokeExternalApi) && (LA(2) == LeftParenthesis)}? + vResult=invokeExternalApiFunctionCall | (Identifier LeftParenthesis)=> vResult=builtInFunctionCall @@ -32306,6 +32389,43 @@ aiTranslateFunctionCall returns [AITranslateFunctionCall vResult = this.Fragment RightParenthesis ; +// INVOKE_EXTERNAL_API('', '' [, [, ... ]]) +// functionSetName and functionName must be string literals. +// Remaining arguments are optional scalar expressions. +invokeExternalApiFunctionCall + returns [InvokeExternalApiFunctionCall vResult = this.FragmentFactory.CreateFragment()] +{ + StringLiteral vFunctionSetName; + StringLiteral vFunctionName; + ScalarExpression vArg; +} + : + tFunc:Identifier LeftParenthesis + { + Match(tFunc, CodeGenerationSupporter.InvokeExternalApi); + UpdateTokenInfo(vResult, tFunc); + } + vFunctionSetName=stringLiteral + { + vResult.FunctionSetName = vFunctionSetName; + } + Comma + vFunctionName=stringLiteral + { + vResult.FunctionName = vFunctionName; + } + ( + Comma vArg=expression + { + AddAndUpdateTokenInfo(vResult, vResult.Arguments, vArg); + } + )* + tRParen:RightParenthesis + { + UpdateTokenInfo(vResult, tRParen); + } + ; + // TODO, olegr: Add more checks for allowed functions here - there are quite some in SQL Server parser builtInFunctionCall returns [FunctionCall vResult = FragmentFactory.CreateFragment()] { diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateSemanticIndexStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateSemanticIndexStatement.cs new file mode 100644 index 00000000..c3333222 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.CreateSemanticIndexStatement.cs @@ -0,0 +1,208 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using System.Collections.Generic; +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + public override void ExplicitVisit(CreateSemanticIndexStatement node) + { + GenerateKeyword(TSqlTokenType.Create); + + GenerateSpaceAndIdentifier(CodeGenerationSupporter.Semantic); + + GenerateSpaceAndKeyword(TSqlTokenType.Index); + + // name + GenerateSpaceAndFragmentIfNotNull(node.Name); + + NewLineAndIndent(); + GenerateKeyword(TSqlTokenType.On); + GenerateSpaceAndFragmentIfNotNull(node.OnName); + + // Column definitions + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + for (int i = 0; i < node.Columns.Count; i++) + { + if (i > 0) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + } + GenerateFragmentIfNotNull(node.Columns[i]); + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + + // WITH clause + bool hasWithOption = node.ExternalModelName != null || + (node.VectorIndexOptions != null && node.VectorIndexOptions.Count > 0) || + node.FulltextStoplistOption != null || + (node.IndexOptions != null && node.IndexOptions.Count > 0); + + if (hasWithOption) + { + NewLineAndIndent(); + GenerateKeyword(TSqlTokenType.With); + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + bool first = true; + + if (node.ExternalModelName != null) + { + GenerateIdentifier(CodeGenerationSupporter.ExternalModel); + GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); + GenerateSpace(); + GenerateFragmentIfNotNull(node.ExternalModelName); + if (node.ExternalModelParameters != null) + { + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateIdentifier(CodeGenerationSupporter.Parameters); + GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); + GenerateSpace(); + GenerateFragmentIfNotNull(node.ExternalModelParameters); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + first = false; + } + + if (node.VectorIndexOptions != null && node.VectorIndexOptions.Count > 0) + { + if (!first) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + } + GenerateIdentifier(CodeGenerationSupporter.VectorIndex); + GenerateSpace(); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateCommaSeparatedList(node.VectorIndexOptions); + GenerateSymbol(TSqlTokenType.RightParenthesis); + first = false; + } + + if (node.FulltextStoplistOption != null) + { + if (!first) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + } + GenerateIdentifier(CodeGenerationSupporter.FulltextStopList); + GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); + GenerateSpace(); + if (node.FulltextStoplistOption.IsOff) + { + GenerateKeyword(TSqlTokenType.Off); + } + else + { + GenerateFragmentIfNotNull(node.FulltextStoplistOption.StopListName); + } + first = false; + } + + if (node.IndexOptions != null && node.IndexOptions.Count > 0) + { + foreach (var option in node.IndexOptions) + { + if (!first) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + } + GenerateFragmentIfNotNull(option); + first = false; + } + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + + if (node.OnFileGroupOrPartitionScheme != null) + { + NewLineAndIndent(); + GenerateKeyword(TSqlTokenType.On); + + GenerateSpaceAndFragmentIfNotNull(node.OnFileGroupOrPartitionScheme); + } + } + + public override void ExplicitVisit(SemanticIndexColumn node) + { + GenerateFragmentIfNotNull(node.ColumnName); + + if (node.SearchType != SemanticIndexSearchType.NotSpecified) + { + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.SearchType); + GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); + GenerateSpace(); + switch (node.SearchType) + { + case SemanticIndexSearchType.Vector: + GenerateIdentifier(CodeGenerationSupporter.Vector); + break; + case SemanticIndexSearchType.Fulltext: + GenerateIdentifier(CodeGenerationSupporter.Fulltext); + break; + case SemanticIndexSearchType.Hybrid: + GenerateIdentifier(CodeGenerationSupporter.Hybrid); + break; + } + } + + if (node.TypeColumnName != null) + { + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.Type); + GenerateSpaceAndKeyword(TSqlTokenType.Column); + GenerateSpaceAndFragmentIfNotNull(node.TypeColumnName); + } + + if (node.Language != null) + { + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.Language); + GenerateSpaceAndFragmentIfNotNull(node.Language); + } + + if (node.ChunkOptions != null && node.ChunkOptions.Count > 0) + { + GenerateSpace(); + GenerateIdentifier(CodeGenerationSupporter.ChunkUsing); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + GenerateCommaSeparatedList(node.ChunkOptions); + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } + + public override void ExplicitVisit(SemanticIndexChunkOption node) + { + switch (node.OptionKind) + { + case SemanticIndexChunkOptionKind.Type: + GenerateIdentifier(CodeGenerationSupporter.Type); + break; + case SemanticIndexChunkOptionKind.Size: + GenerateIdentifier(CodeGenerationSupporter.Size); + break; + case SemanticIndexChunkOptionKind.Overlap: + GenerateIdentifier(CodeGenerationSupporter.Overlap); + break; + } + GenerateSpaceAndSymbol(TSqlTokenType.EqualsSign); + GenerateSpace(); + GenerateFragmentIfNotNull(node.Value); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs new file mode 100644 index 00000000..8ab01c63 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.ExternalFunctionStatement.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + public override void ExplicitVisit(CreateExternalFunctionStatement node) + { + GenerateKeyword(TSqlTokenType.Create); + GenerateExternalFunctionStatementBody(node); + } + + public override void ExplicitVisit(AlterExternalFunctionStatement node) + { + GenerateKeyword(TSqlTokenType.Alter); + GenerateExternalFunctionStatementBody(node); + } + + public override void ExplicitVisit(CreateOrAlterExternalFunctionStatement node) + { + GenerateKeyword(TSqlTokenType.Create); + GenerateSpaceAndKeyword(TSqlTokenType.Or); + GenerateSpaceAndKeyword(TSqlTokenType.Alter); + GenerateExternalFunctionStatementBody(node); + } + + private void GenerateExternalFunctionStatementBody(ExternalFunctionStatement node) + { + GenerateSpaceAndKeyword(TSqlTokenType.Function); + GenerateSpaceAndFragmentIfNotNull(node.Name); + if (node.Parameters != null && node.Parameters.Count > 0) + { + GenerateSpace(); + GenerateParenthesisedCommaSeparatedList(node.Parameters); + } + if (node.ReturnType != null) + { + GenerateSpaceAndIdentifier(CodeGenerationSupporter.Returns); + GenerateSpaceAndFragmentIfNotNull(node.ReturnType); + } + GenerateSpaceAndKeyword(TSqlTokenType.As); + GenerateSpaceAndIdentifier(CodeGenerationSupporter.External); + GenerateSpaceAndKeyword(TSqlTokenType.Function); + GenerateSpaceAndFragmentIfNotNull(node.ExternalName); + } + } +} diff --git a/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InvokeExternalApiFunction.cs b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InvokeExternalApiFunction.cs new file mode 100644 index 00000000..8b6e8188 --- /dev/null +++ b/SqlScriptDom/ScriptDom/SqlServer/ScriptGenerator/SqlScriptGeneratorVisitor.InvokeExternalApiFunction.cs @@ -0,0 +1,56 @@ +//------------------------------------------------------------------------------ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +//------------------------------------------------------------------------------ + +using System; + +using Microsoft.SqlServer.TransactSql.ScriptDom; + +namespace Microsoft.SqlServer.TransactSql.ScriptDom.ScriptGenerator +{ + partial class SqlScriptGeneratorVisitor + { + /// + /// Emits an INVOKE_EXTERNAL_API function call like + /// INVOKE_EXTERNAL_API('FunctionSetName', 'FunctionName' [, arg1 [, arg2 ...]]). + /// FunctionSetName and FunctionName are required string literals; remaining + /// arguments are optional scalar expressions. + /// + /// Expression node to generate + public override void ExplicitVisit(InvokeExternalApiFunctionCall node) + { + if (node.FunctionSetName == null) + { + throw new InvalidOperationException("InvokeExternalApiFunctionCall.FunctionSetName is required."); + } + + if (node.FunctionName == null) + { + throw new InvalidOperationException("InvokeExternalApiFunctionCall.FunctionName is required."); + } + + GenerateIdentifier(CodeGenerationSupporter.InvokeExternalApi); + GenerateSymbol(TSqlTokenType.LeftParenthesis); + + GenerateFragmentIfNotNull(node.FunctionSetName); + + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.FunctionName); + + if (node.Arguments != null) + { + for (int i = 0; i < node.Arguments.Count; i++) + { + GenerateSymbol(TSqlTokenType.Comma); + GenerateSpace(); + GenerateFragmentIfNotNull(node.Arguments[i]); + } + } + + GenerateSymbol(TSqlTokenType.RightParenthesis); + } + } +} diff --git a/Test/SqlDom/Baselines180/CreateSemanticIndexTests180.sql b/Test/SqlDom/Baselines180/CreateSemanticIndexTests180.sql new file mode 100644 index 00000000..b853da18 --- /dev/null +++ b/Test/SqlDom/Baselines180/CreateSemanticIndexTests180.sql @@ -0,0 +1,78 @@ +CREATE SEMANTIC INDEX SI_Basic + ON dbo.Documents (content) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Vector + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Multi + ON dbo.books (summary SEARCH_TYPE = VECTOR, title SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Hybrid + ON dbo.books (content SEARCH_TYPE = HYBRID, title SEARCH_TYPE = FULLTEXT) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_TypeCol + ON dbo.Documents (content TYPE COLUMN content_type) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Lang + ON dbo.Documents (content LANGUAGE English) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Chunk + ON dbo.Documents (content CHUNK_USING(TYPE = paragraph, SIZE = 500, OVERLAP = 50)) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Full + ON dbo.Documents (content SEARCH_TYPE = VECTOR TYPE COLUMN content_type LANGUAGE English CHUNK_USING(TYPE = sentence, SIZE = 1000)) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_Model + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = OpenAIModel); + +CREATE SEMANTIC INDEX SI_ModelParen + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = MyModel); + +CREATE SEMANTIC INDEX SI_ModelParams + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = MyModel (PARAMETERS = '{"dimension": 1536}')); + +CREATE SEMANTIC INDEX SI_VecOpts + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = OpenAIModel, VECTOR_INDEX (METRIC = 'COSINE')); + +CREATE SEMANTIC INDEX SI_StopOff + ON dbo.books (content SEARCH_TYPE = FULLTEXT) + WITH (FULLTEXT_STOPLIST = OFF); + +CREATE SEMANTIC INDEX SI_StopSys + ON dbo.books (content SEARCH_TYPE = FULLTEXT) + WITH (FULLTEXT_STOPLIST = SYSTEM); + +CREATE SEMANTIC INDEX SI_StopName + ON dbo.books (content SEARCH_TYPE = FULLTEXT) + WITH (FULLTEXT_STOPLIST = MyStoplist); + +CREATE SEMANTIC INDEX SI_MaxDop + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = OpenAIModel, MAXDOP = 4); + +CREATE SEMANTIC INDEX SI_DropEx + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = OpenAIModel, DROP_EXISTING = ON); + +CREATE SEMANTIC INDEX SI_FG + ON dbo.books (summary SEARCH_TYPE = VECTOR) + WITH (EXTERNAL_MODEL = OpenAIModel) + ON [PRIMARY]; + +CREATE SEMANTIC INDEX SI_Complete + ON dbo.books (summary SEARCH_TYPE = VECTOR, title SEARCH_TYPE = FULLTEXT CHUNK_USING(TYPE = fixed, SIZE = 200, OVERLAP = 25)) + WITH (EXTERNAL_MODEL = OpenAIModel (PARAMETERS = '{"api_key": "test"}'), VECTOR_INDEX (METRIC = 'COSINE', TYPE = 'DISKANN'), FULLTEXT_STOPLIST = SYSTEM, MAXDOP = 8, DROP_EXISTING = OFF) + ON [PRIMARY]; + diff --git a/Test/SqlDom/BaselinesFabricDW/ExternalFunctionTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/ExternalFunctionTestsFabricDW.sql new file mode 100644 index 00000000..6920cc20 --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/ExternalFunctionTestsFabricDW.sql @@ -0,0 +1,49 @@ +CREATE FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.myFn; + +GO +ALTER FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.anotherFn; + +GO +CREATE OR ALTER FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.myFn; + +GO +CREATE FUNCTION MyFn AS EXTERNAL FUNCTION myFn; + +GO +CREATE FUNCTION dbo.MyExternalFn RETURNS MONEY AS EXTERNAL FUNCTION mySet.myFn; + +GO +ALTER FUNCTION dbo.MyExternalFn RETURNS NVARCHAR (100) AS EXTERNAL FUNCTION mySet.myFn; + +GO +CREATE OR ALTER FUNCTION dbo.MyExternalFn RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; + +GO +CREATE FUNCTION dbo.MyExternalFn (@x INT) AS EXTERNAL FUNCTION mySet.myFn; + +GO +CREATE FUNCTION dbo.MyExternalFn (@x INT, @y NVARCHAR (50)) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; + +GO +ALTER FUNCTION dbo.MyExternalFn (@x INT) RETURNS BIGINT AS EXTERNAL FUNCTION mySet.myFn; + +GO +CREATE OR ALTER FUNCTION dbo.MyExternalFn (@x INT) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; + +GO +DROP FUNCTION dbo.MyExternalFn; + +GO +SELECT dbo.MyExternalFn(); + +GO +SELECT dbo.MyExternalFn(42, N'hello'); + +GO +DECLARE @v AS INT = 1; + +SELECT dbo.MyExternalFn(@v, 'const'); + +GO +SELECT dbo.MyExternalFn(t.col1, t.col2) AS r +FROM dbo.t AS t; diff --git a/Test/SqlDom/BaselinesFabricDW/InvokeExternalApiTestsFabricDW.sql b/Test/SqlDom/BaselinesFabricDW/InvokeExternalApiTestsFabricDW.sql new file mode 100644 index 00000000..68422135 --- /dev/null +++ b/Test/SqlDom/BaselinesFabricDW/InvokeExternalApiTestsFabricDW.sql @@ -0,0 +1,27 @@ +SELECT INVOKE_EXTERNAL_API('myFunctionSet', 'myFunction'); + + +GO +SELECT INVOKE_EXTERNAL_API('myFunctionSet', 'myFunction', 42); + + +GO +DECLARE @v AS INT = 7; + +SELECT INVOKE_EXTERNAL_API(N'mySet', N'doStuff', @v, 'literal', 1 + 2, NULL); + + +GO +ALTER FUNCTION dbo.TestInvokeExternalApi +( ) +RETURNS NVARCHAR (MAX) +AS +BEGIN + RETURN (INVOKE_EXTERNAL_API('mySet', 'myFn', 'arg1')); +END + + +GO +SELECT 1 +FROM dbo.t +WHERE INVOKE_EXTERNAL_API('mySet', 'isAllowed', col1) = 1; diff --git a/Test/SqlDom/Only180SyntaxTests.cs b/Test/SqlDom/Only180SyntaxTests.cs index cd8746db..9be98076 100644 --- a/Test/SqlDom/Only180SyntaxTests.cs +++ b/Test/SqlDom/Only180SyntaxTests.cs @@ -14,7 +14,8 @@ public partial class SqlDomTests { new ParserTest180("AutomaticIndexCompactionTests180.sql", nErrors80: 2, nErrors90: 2, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 2, nErrors140: 2, nErrors150: 2, nErrors160: 2, nErrors170: 2), new ParserTest180("TrimFromReturnTests160.sql"), - new ParserTest180("PersistSamplePercentStatisticsTests130.sql") + new ParserTest180("PersistSamplePercentStatisticsTests130.sql"), + new ParserTest180("CreateSemanticIndexTests180.sql"), }; private static readonly ParserTest[] SqlAzure180_TestInfos = diff --git a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs index 8898e811..483e08ef 100644 --- a/Test/SqlDom/OnlyFabricDWSyntaxTests.cs +++ b/Test/SqlDom/OnlyFabricDWSyntaxTests.cs @@ -25,6 +25,8 @@ public partial class SqlDomTests new ParserTestFabricDW("AiGenerateResponseTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0), new ParserTestFabricDW("AiSummarizeTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0), new ParserTestFabricDW("AiTranslateTestsFabricDW.sql", nErrors80: 3, nErrors90: 1, nErrors100: 2, nErrors110: 2, nErrors120: 2, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0), + new ParserTestFabricDW("InvokeExternalApiTestsFabricDW.sql", nErrors80: 2, nErrors90: 1, nErrors100: 0, nErrors110: 0, nErrors120: 0, nErrors130: 0, nErrors140: 0, nErrors150: 0, nErrors160: 0, nErrors170: 0, nErrors180: 0), + new ParserTestFabricDW("ExternalFunctionTestsFabricDW.sql", nErrors80: 12, nErrors90: 4, nErrors100: 4, nErrors110: 4, nErrors120: 4, nErrors130: 11, nErrors140: 11, nErrors150: 11, nErrors160: 11, nErrors170: 11, nErrors180: 0), }; [TestMethod] diff --git a/Test/SqlDom/ParserErrorsTests.cs b/Test/SqlDom/ParserErrorsTests.cs index ce5ab88a..faeb5a68 100644 --- a/Test/SqlDom/ParserErrorsTests.cs +++ b/Test/SqlDom/ParserErrorsTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using Microsoft.SqlServer.TransactSql.ScriptDom; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -4632,6 +4633,82 @@ public void CreateJsonIndexStatementErrorTest() new ParserErrorInfo(26, "SQL46010", "(")); } + /// + /// Negative tests for CREATE SEMANTIC INDEX statement + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void CreateSemanticIndexStatementErrorTest() + { + // Semantic Index syntax should not be supported in SQL Server versions prior to 180 + ParserTestUtils.ErrorTest170("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest160("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest150("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest140("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest130("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest120("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + ParserTestUtils.ErrorTest110("CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(7, "SQL46010", "SEMANTIC")); + + // Test malformed syntax in TSql180 + TSql180Parser parser180 = new TSql180Parser(true); + + // Missing index name + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX ON dbo.t1 (col1)", + new ParserErrorInfo(22, "SQL46010", "ON")); + + // Missing table name + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON (col1)", + new ParserErrorInfo(30, "SQL46010", "(")); + + // Missing column list + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1", + new ParserErrorInfo(36, "SQL46029")); + + // Empty column list + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 ()", + new ParserErrorInfo(38, "SQL46010", ")")); + + // Invalid SEARCH_TYPE value + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE=invalid)", + new ParserErrorInfo(55, "SQL46005", "HYBRID", "invalid")); + + // Invalid CHUNK_USING option + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 CHUNK_USING(BADOPT = 100))", + new ParserErrorInfo(55, "SQL46005", "OVERLAP", "BADOPT")); + + // EXTERNAL_MODEL is required when SEARCH_TYPE is not specified (defaults to vector) + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + + // EXTERNAL_MODEL is required for vector columns + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE = VECTOR)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + + // EXTERNAL_MODEL is required for hybrid columns + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE = HYBRID)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + + // EXTERNAL_MODEL is required when at least one column is vector + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE = FULLTEXT, col2 SEARCH_TYPE = VECTOR)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + + // EXTERNAL_MODEL is required when at least one column has no SEARCH_TYPE (defaults to vector) + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE = FULLTEXT, col2)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + + // Multiple vector columns without EXTERNAL_MODEL + ParserTestUtils.ErrorTest(parser180, "CREATE SEMANTIC INDEX idx1 ON dbo.t1 (col1 SEARCH_TYPE = VECTOR, col2 SEARCH_TYPE = VECTOR)", + new ParserErrorInfo(22, "SQL46144", "EXTERNAL_MODEL")); + } + /// /// Check that the value of MAXDOP index option is within range /// @@ -7868,6 +7945,45 @@ public void AiTranslateNegativeTestsFabricDw() new ParserErrorInfo(28, "SQL46010", ",")); } + /// + /// Negative tests for CREATE/ALTER FUNCTION ... AS EXTERNAL FUNCTION syntax in Fabric DW. + /// + [TestMethod] + [Priority(0)] + [SqlStudioTestCategory(Category.UnitTest)] + public void ExternalFunctionNegativeTestsFabricDW() + { + // Missing external name after EXTERNAL FUNCTION + ParserTestUtils.ErrorTestFabricDW( + "CREATE FUNCTION dbo.f AS EXTERNAL FUNCTION", + new ParserErrorInfo(42, "SQL46029")); + + // Missing FUNCTION keyword after EXTERNAL + ParserTestUtils.ErrorTestFabricDW( + "CREATE FUNCTION dbo.f AS EXTERNAL mySet.myFn", + new ParserErrorInfo(34, "SQL46010", "mySet")); + + // Three-part external name not allowed + ParserTestUtils.ErrorTestFabricDW( + "CREATE FUNCTION dbo.f AS EXTERNAL FUNCTION db.mySet.myFn", + new ParserErrorInfo(51, "SQL46010", ".")); + + // RETURNS without a data type + ParserTestUtils.ErrorTestFabricDW( + "CREATE FUNCTION dbo.f RETURNS AS EXTERNAL FUNCTION mySet.myFn", + new ParserErrorInfo(22, "SQL46010", "RETURNS")); + + // ALTER: missing external name + ParserTestUtils.ErrorTestFabricDW( + "ALTER FUNCTION dbo.f AS EXTERNAL FUNCTION", + new ParserErrorInfo(41, "SQL46029")); + + // CREATE OR ALTER: missing FUNCTION keyword after EXTERNAL + ParserTestUtils.ErrorTestFabricDW( + "CREATE OR ALTER FUNCTION dbo.f AS EXTERNAL mySet.myFn", + new ParserErrorInfo(43, "SQL46010", "mySet")); + } + /// /// Negative test for OFFSET with FETCH APPROXIMATE - SQL46145 error /// OFFSET clause cannot be used with FETCH APPROXIMATE (introduced in SQL Server 2025) diff --git a/Test/SqlDom/TestScripts/CreateSemanticIndexTests180.sql b/Test/SqlDom/TestScripts/CreateSemanticIndexTests180.sql new file mode 100644 index 00000000..677e3487 --- /dev/null +++ b/Test/SqlDom/TestScripts/CreateSemanticIndexTests180.sql @@ -0,0 +1,56 @@ +-- Basic semantic index (defaults to SEARCH_TYPE=vector, requires EXTERNAL_MODEL) +CREATE SEMANTIC INDEX SI_Basic ON dbo.Documents (content) WITH (EXTERNAL_MODEL = MyModel); + +-- Semantic index with SEARCH_TYPE and EXTERNAL_MODEL (required for vector) +CREATE SEMANTIC INDEX SI_Vector ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = MyModel); + +-- Multiple columns with SEARCH_TYPE and EXTERNAL_MODEL (required for vector) +CREATE SEMANTIC INDEX SI_Multi ON dbo.books (summary SEARCH_TYPE=vector, title SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = MyModel); + +-- All search types (EXTERNAL_MODEL required for hybrid) +CREATE SEMANTIC INDEX SI_Hybrid ON dbo.books (content SEARCH_TYPE=hybrid, title SEARCH_TYPE=fulltext) WITH (EXTERNAL_MODEL = MyModel); + +-- With TYPE COLUMN (defaults to vector, requires EXTERNAL_MODEL) +CREATE SEMANTIC INDEX SI_TypeCol ON dbo.Documents (content TYPE COLUMN content_type) WITH (EXTERNAL_MODEL = MyModel); + +-- With LANGUAGE (defaults to vector, requires EXTERNAL_MODEL) +CREATE SEMANTIC INDEX SI_Lang ON dbo.Documents (content LANGUAGE English) WITH (EXTERNAL_MODEL = MyModel); + +-- With CHUNK_USING options (defaults to vector, requires EXTERNAL_MODEL) +CREATE SEMANTIC INDEX SI_Chunk ON dbo.Documents (content CHUNK_USING(TYPE = paragraph, SIZE = 500, OVERLAP = 50)) WITH (EXTERNAL_MODEL = MyModel); + +-- Full column definition with EXTERNAL_MODEL (required for vector) +CREATE SEMANTIC INDEX SI_Full ON dbo.Documents (content SEARCH_TYPE=vector TYPE COLUMN content_type LANGUAGE English CHUNK_USING(TYPE = sentence, SIZE = 1000)) WITH (EXTERNAL_MODEL = MyModel); + +-- WITH EXTERNAL_MODEL +CREATE SEMANTIC INDEX SI_Model ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = OpenAIModel); + +-- WITH EXTERNAL_MODEL in parentheses +CREATE SEMANTIC INDEX SI_ModelParen ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = (MyModel)); + +-- WITH EXTERNAL_MODEL and PARAMETERS +CREATE SEMANTIC INDEX SI_ModelParams ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = MyModel (PARAMETERS = '{"dimension": 1536}')); + +-- WITH VECTOR_INDEX options +CREATE SEMANTIC INDEX SI_VecOpts ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = OpenAIModel, VECTOR_INDEX (METRIC = 'cosine')); + +-- WITH FULLTEXT_STOPLIST OFF +CREATE SEMANTIC INDEX SI_StopOff ON dbo.books (content SEARCH_TYPE=fulltext) WITH (FULLTEXT_STOPLIST = OFF); + +-- WITH FULLTEXT_STOPLIST SYSTEM +CREATE SEMANTIC INDEX SI_StopSys ON dbo.books (content SEARCH_TYPE=fulltext) WITH (FULLTEXT_STOPLIST = SYSTEM); + +-- WITH FULLTEXT_STOPLIST name +CREATE SEMANTIC INDEX SI_StopName ON dbo.books (content SEARCH_TYPE=fulltext) WITH (FULLTEXT_STOPLIST = MyStoplist); + +-- WITH MAXDOP +CREATE SEMANTIC INDEX SI_MaxDop ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = OpenAIModel, MAXDOP = 4); + +-- WITH DROP_EXISTING +CREATE SEMANTIC INDEX SI_DropEx ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = OpenAIModel, DROP_EXISTING = ON); + +-- With ON filegroup +CREATE SEMANTIC INDEX SI_FG ON dbo.books (summary SEARCH_TYPE=vector) WITH (EXTERNAL_MODEL = OpenAIModel) ON [PRIMARY]; + +-- Complex example with all options +CREATE SEMANTIC INDEX SI_Complete ON dbo.books (summary SEARCH_TYPE=vector, title SEARCH_TYPE=fulltext CHUNK_USING(TYPE = fixed, SIZE = 200, OVERLAP = 25)) WITH (EXTERNAL_MODEL = OpenAIModel (PARAMETERS = '{"api_key": "test"}'), VECTOR_INDEX (METRIC = 'cosine', TYPE = 'DiskANN'), FULLTEXT_STOPLIST = SYSTEM, MAXDOP = 8, DROP_EXISTING = OFF) ON [PRIMARY]; diff --git a/Test/SqlDom/TestScripts/ExternalFunctionTestsFabricDW.sql b/Test/SqlDom/TestScripts/ExternalFunctionTestsFabricDW.sql new file mode 100644 index 00000000..43d8dc1c --- /dev/null +++ b/Test/SqlDom/TestScripts/ExternalFunctionTestsFabricDW.sql @@ -0,0 +1,62 @@ +-- Basic CREATE FUNCTION ... AS EXTERNAL FUNCTION +CREATE FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- ALTER FUNCTION ... AS EXTERNAL FUNCTION +ALTER FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.anotherFn; +GO + +-- CREATE OR ALTER FUNCTION ... AS EXTERNAL FUNCTION +CREATE OR ALTER FUNCTION dbo.MyExternalFn AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- Single-part external name +CREATE FUNCTION MyFn AS EXTERNAL FUNCTION myFn; +GO + +-- CREATE with RETURNS clause specifying explicit return type +CREATE FUNCTION dbo.MyExternalFn RETURNS MONEY AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- ALTER with RETURNS clause +ALTER FUNCTION dbo.MyExternalFn RETURNS NVARCHAR(100) AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- CREATE OR ALTER with RETURNS clause +CREATE OR ALTER FUNCTION dbo.MyExternalFn RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- CREATE with single parameter +CREATE FUNCTION dbo.MyExternalFn (@x INT) AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- CREATE with multiple parameters and RETURNS +CREATE FUNCTION dbo.MyExternalFn (@x INT, @y NVARCHAR(50)) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- ALTER with parameters and RETURNS +ALTER FUNCTION dbo.MyExternalFn (@x INT) RETURNS BIGINT AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- CREATE OR ALTER with parameters and RETURNS +CREATE OR ALTER FUNCTION dbo.MyExternalFn (@x INT) RETURNS INT AS EXTERNAL FUNCTION mySet.myFn; +GO + +-- DROP FUNCTION (existing syntax, no change needed) +DROP FUNCTION dbo.MyExternalFn; +GO + +-- DML: invoking the external function like a regular UDF +SELECT dbo.MyExternalFn(); +GO + +SELECT dbo.MyExternalFn(42, N'hello'); +GO + +DECLARE @v INT = 1; +SELECT dbo.MyExternalFn(@v, 'const'); +GO + +SELECT dbo.MyExternalFn(t.col1, t.col2) AS r +FROM dbo.t AS t; +GO diff --git a/Test/SqlDom/TestScripts/InvokeExternalApiTestsFabricDW.sql b/Test/SqlDom/TestScripts/InvokeExternalApiTestsFabricDW.sql new file mode 100644 index 00000000..ff0d704b --- /dev/null +++ b/Test/SqlDom/TestScripts/InvokeExternalApiTestsFabricDW.sql @@ -0,0 +1,27 @@ +-- Basic call with only required arguments. +SELECT INVOKE_EXTERNAL_API('myFunctionSet', 'myFunction'); +GO + +-- With a single optional argument. +SELECT INVOKE_EXTERNAL_API('myFunctionSet', 'myFunction', 42); +GO + +-- With multiple optional arguments of various expression kinds. +DECLARE @v INT = 7; +SELECT INVOKE_EXTERNAL_API(N'mySet', N'doStuff', @v, 'literal', 1 + 2, NULL); +GO + +-- Inside an ALTER FUNCTION RETURN statement (critical RETURN-context coverage). +ALTER FUNCTION dbo.TestInvokeExternalApi() +RETURNS NVARCHAR(MAX) +AS +BEGIN + RETURN (INVOKE_EXTERNAL_API('mySet', 'myFn', 'arg1')); +END; +GO + +-- Inside a WHERE clause. +SELECT 1 +FROM dbo.t +WHERE INVOKE_EXTERNAL_API('mySet', 'isAllowed', col1) = 1; +GO diff --git a/release-notes/180/180.37.3.md b/release-notes/180/180.37.3.md new file mode 100644 index 00000000..b7e7bac0 --- /dev/null +++ b/release-notes/180/180.37.3.md @@ -0,0 +1,30 @@ +# Release Notes + +## Microsoft.SqlServer.TransactSql.ScriptDom 180.37.3 +This update brings the following changes over the previous release: + +### Target Platform Support + +* .NET Framework 4.7.2 (Windows x86, Windows x64) +* .NET 8 (Windows x86, Windows x64, Linux, macOS) +* .NET Standard 2.0+ (Windows x86, Windows x64, Linux, macOS) + +### Dependencies +* Updates .NET SDK to latest patch version 8.0.420 + +#### .NET Framework +#### .NET Core + +### New Features +* Adds support for CREATE FUNCTION AS EXTERNAL FUNCTION in Warehouse in Microsoft Fabric. + +### Fixed +* Fix PERSIST_SAMPLE_PERCENT statistics option parsing [#207](https://github.com/microsoft/SqlScriptDOM/pull/207) +* Fixes the issue with AlterTableAddTableElementStatement omitted the separator between column-definitions/constraints and table indexes, producing 'NOT NULLINDEX ix_...' +* Fixes the issue with WindowDefinition omitted whitespace between the inherited window-name reference (RefWindowName) and a following PARTITION BY or ORDER BY, producing 'win2PARTITION ...' + +### Changes +* Improvements to how comments are preserved and emitted in the SQL Server script generator. + +### Known Issues +* None \ No newline at end of file