diff --git a/ast/begin_dialog_statement.go b/ast/begin_dialog_statement.go new file mode 100644 index 00000000..04c572fa --- /dev/null +++ b/ast/begin_dialog_statement.go @@ -0,0 +1,45 @@ +package ast + +// BeginDialogStatement represents a BEGIN DIALOG statement for SQL Server Service Broker. +type BeginDialogStatement struct { + IsConversation bool `json:"IsConversation,omitempty"` + Handle ScalarExpression `json:"Handle,omitempty"` + InitiatorServiceName *IdentifierOrValueExpression `json:"InitiatorServiceName,omitempty"` + TargetServiceName ScalarExpression `json:"TargetServiceName,omitempty"` + ContractName *IdentifierOrValueExpression `json:"ContractName,omitempty"` + InstanceSpec ScalarExpression `json:"InstanceSpec,omitempty"` + Options []DialogOption `json:"Options,omitempty"` +} + +func (s *BeginDialogStatement) node() {} +func (s *BeginDialogStatement) statement() {} + +// BeginConversationTimerStatement represents a BEGIN CONVERSATION TIMER statement. +type BeginConversationTimerStatement struct { + Handle ScalarExpression `json:"Handle,omitempty"` + Timeout ScalarExpression `json:"Timeout,omitempty"` +} + +func (s *BeginConversationTimerStatement) node() {} +func (s *BeginConversationTimerStatement) statement() {} + +// DialogOption is an interface for dialog options. +type DialogOption interface { + dialogOption() +} + +// ScalarExpressionDialogOption represents a dialog option with a scalar expression value. +type ScalarExpressionDialogOption struct { + Value ScalarExpression `json:"Value,omitempty"` + OptionKind string `json:"OptionKind,omitempty"` // RelatedConversation, RelatedConversationGroup, Lifetime +} + +func (o *ScalarExpressionDialogOption) dialogOption() {} + +// OnOffDialogOption represents a dialog option with an ON/OFF value. +type OnOffDialogOption struct { + OptionState string `json:"OptionState,omitempty"` // On, Off + OptionKind string `json:"OptionKind,omitempty"` // Encryption +} + +func (o *OnOffDialogOption) dialogOption() {} diff --git a/ast/bulk_insert_statement.go b/ast/bulk_insert_statement.go index a9780cbf..a0803e2f 100644 --- a/ast/bulk_insert_statement.go +++ b/ast/bulk_insert_statement.go @@ -28,8 +28,9 @@ type InsertBulkColumnDefinition struct { // ColumnDefinitionBase represents a basic column definition. type ColumnDefinitionBase struct { - ColumnIdentifier *Identifier `json:"ColumnIdentifier,omitempty"` + ColumnIdentifier *Identifier `json:"ColumnIdentifier,omitempty"` DataType DataTypeReference `json:"DataType,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } // BulkInsertOption is the interface for bulk insert options. diff --git a/ast/column_reference_expression.go b/ast/column_reference_expression.go index e4db7e6c..d666700a 100644 --- a/ast/column_reference_expression.go +++ b/ast/column_reference_expression.go @@ -4,6 +4,7 @@ package ast type ColumnReferenceExpression struct { ColumnType string `json:"ColumnType,omitempty"` MultiPartIdentifier *MultiPartIdentifier `json:"MultiPartIdentifier,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } func (*ColumnReferenceExpression) node() {} diff --git a/ast/create_columnstore_index_statement.go b/ast/create_columnstore_index_statement.go index e752d0fc..42e8d705 100644 --- a/ast/create_columnstore_index_statement.go +++ b/ast/create_columnstore_index_statement.go @@ -2,15 +2,16 @@ package ast // CreateColumnStoreIndexStatement represents a CREATE COLUMNSTORE INDEX statement type CreateColumnStoreIndexStatement struct { - Name *Identifier - Clustered bool - ClusteredExplicit bool // true if CLUSTERED or NONCLUSTERED was explicitly specified - OnName *SchemaObjectName - Columns []*ColumnReferenceExpression - OrderedColumns []*ColumnReferenceExpression - IndexOptions []IndexOption - FilterClause BooleanExpression - OnPartition *PartitionSpecifier + Name *Identifier + Clustered bool + ClusteredExplicit bool // true if CLUSTERED or NONCLUSTERED was explicitly specified + OnName *SchemaObjectName + Columns []*ColumnReferenceExpression + OrderedColumns []*ColumnReferenceExpression + IndexOptions []IndexOption + FilterClause BooleanExpression + OnPartition *PartitionSpecifier + OnFileGroupOrPartitionScheme *FileGroupOrPartitionScheme } func (s *CreateColumnStoreIndexStatement) statement() {} diff --git a/ast/create_procedure_statement.go b/ast/create_procedure_statement.go index ed020c4d..919c86b0 100644 --- a/ast/create_procedure_statement.go +++ b/ast/create_procedure_statement.go @@ -13,6 +13,19 @@ type CreateProcedureStatement struct { func (c *CreateProcedureStatement) node() {} func (c *CreateProcedureStatement) statement() {} +// CreateOrAlterProcedureStatement represents a CREATE OR ALTER PROCEDURE statement. +type CreateOrAlterProcedureStatement struct { + ProcedureReference *ProcedureReference + Parameters []*ProcedureParameter + StatementList *StatementList + IsForReplication bool + Options []ProcedureOptionBase + MethodSpecifier *MethodSpecifier +} + +func (c *CreateOrAlterProcedureStatement) node() {} +func (c *CreateOrAlterProcedureStatement) statement() {} + // ProcedureParameter represents a parameter in a procedure definition. type ProcedureParameter struct { VariableName *Identifier diff --git a/ast/create_simple_statements.go b/ast/create_simple_statements.go index ead90b2e..d091be8b 100644 --- a/ast/create_simple_statements.go +++ b/ast/create_simple_statements.go @@ -122,12 +122,44 @@ func (s *CreateAssemblyStatement) statement() {} // CreateCertificateStatement represents a CREATE CERTIFICATE statement. type CreateCertificateStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + Owner *Identifier `json:"Owner,omitempty"` + CertificateSource EncryptionSource `json:"CertificateSource,omitempty"` + ActiveForBeginDialog string `json:"ActiveForBeginDialog,omitempty"` // "On", "Off", "NotSet" + PrivateKeyPath *StringLiteral `json:"PrivateKeyPath,omitempty"` + EncryptionPassword *StringLiteral `json:"EncryptionPassword,omitempty"` + DecryptionPassword *StringLiteral `json:"DecryptionPassword,omitempty"` + CertificateOptions []*CertificateOption `json:"CertificateOptions,omitempty"` } func (s *CreateCertificateStatement) node() {} func (s *CreateCertificateStatement) statement() {} +// CertificateOption represents an option in a CREATE CERTIFICATE statement. +type CertificateOption struct { + Kind string `json:"Kind,omitempty"` // "Subject", "StartDate", "ExpiryDate" + Value *StringLiteral `json:"Value,omitempty"` +} + +func (o *CertificateOption) node() {} + +// AssemblyEncryptionSource represents a certificate source from an assembly. +type AssemblyEncryptionSource struct { + Assembly *Identifier `json:"Assembly,omitempty"` +} + +func (s *AssemblyEncryptionSource) node() {} +func (s *AssemblyEncryptionSource) encryptionSource() {} + +// FileEncryptionSource represents a certificate source from a file. +type FileEncryptionSource struct { + IsExecutable bool `json:"IsExecutable,omitempty"` + File *StringLiteral `json:"File,omitempty"` +} + +func (s *FileEncryptionSource) node() {} +func (s *FileEncryptionSource) encryptionSource() {} + // CreateAsymmetricKeyStatement represents a CREATE ASYMMETRIC KEY statement. type CreateAsymmetricKeyStatement struct { Name *Identifier `json:"Name,omitempty"` diff --git a/ast/create_table_statement.go b/ast/create_table_statement.go index 6699e2ec..010ef69e 100644 --- a/ast/create_table_statement.go +++ b/ast/create_table_statement.go @@ -49,10 +49,19 @@ type ColumnDefinition struct { IsHidden bool IsMasked bool Nullable *NullableConstraintDefinition + StorageOptions *ColumnStorageOptions } func (c *ColumnDefinition) node() {} +// ColumnStorageOptions represents storage options for a column (SPARSE, FILESTREAM) +type ColumnStorageOptions struct { + IsFileStream bool // true if FILESTREAM specified + SparseOption string // "None", "Sparse", "ColumnSetForAllSparseColumns" +} + +func (c *ColumnStorageOptions) node() {} + // DataTypeReference is an interface for data type references type DataTypeReference interface { Node @@ -166,3 +175,4 @@ type ForeignKeyConstraintDefinition struct { func (f *ForeignKeyConstraintDefinition) node() {} func (f *ForeignKeyConstraintDefinition) tableConstraint() {} +func (f *ForeignKeyConstraintDefinition) constraintDefinition() {} diff --git a/ast/create_trigger_statement.go b/ast/create_trigger_statement.go index 00543360..60ede459 100644 --- a/ast/create_trigger_statement.go +++ b/ast/create_trigger_statement.go @@ -16,6 +16,22 @@ type CreateTriggerStatement struct { func (s *CreateTriggerStatement) statement() {} func (s *CreateTriggerStatement) node() {} +// CreateOrAlterTriggerStatement represents a CREATE OR ALTER TRIGGER statement +type CreateOrAlterTriggerStatement struct { + Name *SchemaObjectName + TriggerObject *TriggerObject + TriggerType string // "For", "After", "InsteadOf" + TriggerActions []*TriggerAction + Options []TriggerOptionType + WithAppend bool + IsNotForReplication bool + MethodSpecifier *MethodSpecifier + StatementList *StatementList +} + +func (s *CreateOrAlterTriggerStatement) statement() {} +func (s *CreateOrAlterTriggerStatement) node() {} + // EventTypeContainer represents an event type container type EventTypeContainer struct { EventType string `json:"EventType,omitempty"` diff --git a/ast/create_view_statement.go b/ast/create_view_statement.go index 951a1ef4..480c7a2a 100644 --- a/ast/create_view_statement.go +++ b/ast/create_view_statement.go @@ -13,7 +13,48 @@ type CreateViewStatement struct { func (c *CreateViewStatement) node() {} func (c *CreateViewStatement) statement() {} -// ViewOption represents a view option like SCHEMABINDING. -type ViewOption struct { +// CreateOrAlterViewStatement represents a CREATE OR ALTER VIEW statement. +type CreateOrAlterViewStatement struct { + SchemaObjectName *SchemaObjectName `json:"SchemaObjectName,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` + SelectStatement *SelectStatement `json:"SelectStatement,omitempty"` + WithCheckOption bool `json:"WithCheckOption"` + ViewOptions []ViewOption `json:"ViewOptions,omitempty"` + IsMaterialized bool `json:"IsMaterialized"` +} + +func (c *CreateOrAlterViewStatement) node() {} +func (c *CreateOrAlterViewStatement) statement() {} + +// ViewOption is an interface for different view option types. +type ViewOption interface { + viewOption() +} + +// ViewStatementOption represents a simple view option like SCHEMABINDING. +type ViewStatementOption struct { OptionKind string `json:"OptionKind,omitempty"` } + +func (v *ViewStatementOption) viewOption() {} + +// ViewDistributionOption represents a DISTRIBUTION option for materialized views. +type ViewDistributionOption struct { + OptionKind string `json:"OptionKind,omitempty"` + Value *ViewHashDistributionPolicy `json:"Value,omitempty"` +} + +func (v *ViewDistributionOption) viewOption() {} + +// ViewHashDistributionPolicy represents the hash distribution policy for materialized views. +type ViewHashDistributionPolicy struct { + DistributionColumn *Identifier `json:"DistributionColumn,omitempty"` + DistributionColumns []*Identifier `json:"DistributionColumns,omitempty"` +} + +// ViewForAppendOption represents the FOR_APPEND option for materialized views. +type ViewForAppendOption struct { + OptionKind string `json:"OptionKind,omitempty"` +} + +func (v *ViewForAppendOption) viewOption() {} diff --git a/ast/delete_statement.go b/ast/delete_statement.go index 392e4647..d1a2b665 100644 --- a/ast/delete_statement.go +++ b/ast/delete_statement.go @@ -12,7 +12,10 @@ func (d *DeleteStatement) statement() {} // DeleteSpecification contains the details of a DELETE. type DeleteSpecification struct { - Target TableReference `json:"Target,omitempty"` - FromClause *FromClause `json:"FromClause,omitempty"` - WhereClause *WhereClause `json:"WhereClause,omitempty"` + Target TableReference `json:"Target,omitempty"` + FromClause *FromClause `json:"FromClause,omitempty"` + WhereClause *WhereClause `json:"WhereClause,omitempty"` + TopRowFilter *TopRowFilter `json:"TopRowFilter,omitempty"` + OutputClause *OutputClause `json:"OutputClause,omitempty"` + OutputIntoClause *OutputIntoClause `json:"OutputIntoClause,omitempty"` } diff --git a/ast/drop_statements.go b/ast/drop_statements.go index 6dea509c..5c64335c 100644 --- a/ast/drop_statements.go +++ b/ast/drop_statements.go @@ -107,6 +107,37 @@ type FileStreamOnDropIndexOption struct { func (o *FileStreamOnDropIndexOption) node() {} func (o *FileStreamOnDropIndexOption) dropIndexOption() {} +// WaitAtLowPriorityOption represents the WAIT_AT_LOW_PRIORITY option +type WaitAtLowPriorityOption struct { + Options []LowPriorityLockWaitOption + OptionKind string // WaitAtLowPriority +} + +func (o *WaitAtLowPriorityOption) node() {} +func (o *WaitAtLowPriorityOption) dropIndexOption() {} + +// LowPriorityLockWaitOption is the interface for options within WAIT_AT_LOW_PRIORITY +type LowPriorityLockWaitOption interface { + lowPriorityLockWaitOption() +} + +// LowPriorityLockWaitMaxDurationOption represents MAX_DURATION option +type LowPriorityLockWaitMaxDurationOption struct { + MaxDuration *IntegerLiteral + Unit string // Minutes or Seconds + OptionKind string // MaxDuration +} + +func (o *LowPriorityLockWaitMaxDurationOption) lowPriorityLockWaitOption() {} + +// LowPriorityLockWaitAbortAfterWaitOption represents ABORT_AFTER_WAIT option +type LowPriorityLockWaitAbortAfterWaitOption struct { + AbortAfterWait string // None, Self, Blockers + OptionKind string // AbortAfterWait +} + +func (o *LowPriorityLockWaitAbortAfterWaitOption) lowPriorityLockWaitOption() {} + // DropStatisticsStatement represents a DROP STATISTICS statement type DropStatisticsStatement struct { Objects []*SchemaObjectName diff --git a/ast/end_conversation_statement.go b/ast/end_conversation_statement.go new file mode 100644 index 00000000..c0bb123b --- /dev/null +++ b/ast/end_conversation_statement.go @@ -0,0 +1,12 @@ +package ast + +// EndConversationStatement represents END CONVERSATION statement +type EndConversationStatement struct { + Conversation ScalarExpression // The conversation handle + WithCleanup bool // true if WITH CLEANUP specified + ErrorCode ScalarExpression // optional error code with WITH ERROR + ErrorDescription ScalarExpression // optional error description with WITH ERROR +} + +func (s *EndConversationStatement) statement() {} +func (s *EndConversationStatement) node() {} diff --git a/ast/event_statements.go b/ast/event_statements.go index d2751cff..e1dd67c5 100644 --- a/ast/event_statements.go +++ b/ast/event_statements.go @@ -2,11 +2,11 @@ package ast // CreateEventSessionStatement represents CREATE EVENT SESSION statement type CreateEventSessionStatement struct { - Name *Identifier - ServerName *Identifier - Events []*EventDeclaration - Targets []*EventTarget - Options []*EventSessionOption + Name *Identifier + SessionScope string // "Server" or "Database" + EventDeclarations []*EventDeclaration + TargetDeclarations []*TargetDeclaration + SessionOptions []SessionOption } func (s *CreateEventSessionStatement) node() {} @@ -14,32 +14,101 @@ func (s *CreateEventSessionStatement) statement() {} // EventDeclaration represents an event in the event session type EventDeclaration struct { - PackageName *Identifier - EventName *Identifier - Actions []*EventAction - WhereClause ScalarExpression + ObjectName *EventSessionObjectName + EventDeclarationActionParameters []*EventSessionObjectName + EventDeclarationPredicateParameter BooleanExpression } -// EventAction represents an action for an event +// Note: EventSessionObjectName is defined in server_audit_statement.go + +// TargetDeclaration represents a target for the event session +type TargetDeclaration struct { + ObjectName *EventSessionObjectName + TargetDeclarationParameters []*EventDeclarationSetParameter +} + +// EventDeclarationSetParameter represents a SET parameter +type EventDeclarationSetParameter struct { + EventField *Identifier + EventValue ScalarExpression +} + +// SessionOption interface for event session options +type SessionOption interface { + sessionOption() +} + +// LiteralSessionOption represents a literal session option like MAX_MEMORY +type LiteralSessionOption struct { + OptionKind string + Value ScalarExpression + Unit string +} + +func (o *LiteralSessionOption) sessionOption() {} + +// OnOffSessionOption represents an ON/OFF session option +type OnOffSessionOption struct { + OptionKind string + OptionState string // "On" or "Off" +} + +func (o *OnOffSessionOption) sessionOption() {} + +// EventRetentionSessionOption represents EVENT_RETENTION_MODE option +type EventRetentionSessionOption struct { + OptionKind string + Value string // e.g. "AllowSingleEventLoss" +} + +func (o *EventRetentionSessionOption) sessionOption() {} + +// MaxDispatchLatencySessionOption represents MAX_DISPATCH_LATENCY option +type MaxDispatchLatencySessionOption struct { + OptionKind string + Value ScalarExpression + IsInfinite bool +} + +func (o *MaxDispatchLatencySessionOption) sessionOption() {} + +// MemoryPartitionSessionOption represents MEMORY_PARTITION_MODE option +type MemoryPartitionSessionOption struct { + OptionKind string + Value string // e.g. "None" +} + +func (o *MemoryPartitionSessionOption) sessionOption() {} + +// EventDeclarationCompareFunctionParameter for function calls in WHERE clause +type EventDeclarationCompareFunctionParameter struct { + Name *EventSessionObjectName + SourceDeclaration *SourceDeclaration + EventValue ScalarExpression +} + +func (e *EventDeclarationCompareFunctionParameter) node() {} +func (e *EventDeclarationCompareFunctionParameter) booleanExpression() {} + +// Note: SourceDeclaration is defined in server_audit_statement.go + +// Legacy fields for backwards compatibility type EventAction struct { PackageName *Identifier ActionName *Identifier } -// EventTarget represents a target for the event session type EventTarget struct { PackageName *Identifier TargetName *Identifier Options []*EventTargetOption } -// EventTargetOption represents an option for an event target type EventTargetOption struct { Name *Identifier Value ScalarExpression } -// EventSessionOption represents an option for the event session type EventSessionOption struct { OptionKind string Value ScalarExpression diff --git a/ast/execute_statement.go b/ast/execute_statement.go index bcfb8c5e..3f90e878 100644 --- a/ast/execute_statement.go +++ b/ast/execute_statement.go @@ -81,6 +81,7 @@ type ExecutableEntity interface { type ExecutableProcedureReference struct { ProcedureReference *ProcedureReferenceName `json:"ProcedureReference,omitempty"` Parameters []*ExecuteParameter `json:"Parameters,omitempty"` + AdHocDataSource *AdHocDataSource `json:"AdHocDataSource,omitempty"` } func (e *ExecutableProcedureReference) executableEntity() {} @@ -102,12 +103,19 @@ type ProcedureReferenceName struct { // ProcedureReference references a stored procedure by name. type ProcedureReference struct { - Name *SchemaObjectName `json:"Name,omitempty"` + Name *SchemaObjectName `json:"Name,omitempty"` + Number *IntegerLiteral `json:"Number,omitempty"` } // ExecuteParameter represents a parameter to an EXEC call. type ExecuteParameter struct { - ParameterValue ScalarExpression `json:"ParameterValue,omitempty"` + ParameterValue ScalarExpression `json:"ParameterValue,omitempty"` Variable *VariableReference `json:"Variable,omitempty"` - IsOutput bool `json:"IsOutput"` + IsOutput bool `json:"IsOutput"` +} + +// AdHocDataSource represents an OPENDATASOURCE or OPENROWSET call for ad-hoc data access. +type AdHocDataSource struct { + ProviderName *StringLiteral `json:"ProviderName,omitempty"` + InitString *StringLiteral `json:"InitString,omitempty"` } diff --git a/ast/external_statements.go b/ast/external_statements.go index b73b3ddc..f166fbbf 100644 --- a/ast/external_statements.go +++ b/ast/external_statements.go @@ -50,18 +50,28 @@ func (o *ExternalFileFormatLiteralOption) externalFileFormatOption() {} // CreateExternalTableStatement represents CREATE EXTERNAL TABLE statement type CreateExternalTableStatement struct { - SchemaObjectName *SchemaObjectName - Definition *TableDefinition - DataSource *Identifier - Location ScalarExpression - FileFormat *Identifier - Options []*ExternalTableOption + SchemaObjectName *SchemaObjectName + ColumnDefinitions []*ExternalTableColumnDefinition + DataSource *Identifier + ExternalTableOptions []*ExternalTableLiteralOrIdentifierOption } func (s *CreateExternalTableStatement) node() {} func (s *CreateExternalTableStatement) statement() {} -// ExternalTableOption represents an option for external table +// ExternalTableColumnDefinition represents a column definition in an external table +type ExternalTableColumnDefinition struct { + ColumnDefinition *ColumnDefinitionBase + NullableConstraint *NullableConstraintDefinition +} + +// ExternalTableLiteralOrIdentifierOption represents an option for external table +type ExternalTableLiteralOrIdentifierOption struct { + OptionKind string + Value *IdentifierOrValueExpression +} + +// ExternalTableOption represents a simple option for external table (legacy) type ExternalTableOption struct { OptionKind string Value ScalarExpression diff --git a/ast/for_clause.go b/ast/for_clause.go new file mode 100644 index 00000000..bea8432a --- /dev/null +++ b/ast/for_clause.go @@ -0,0 +1,43 @@ +package ast + +// ForClause is an interface for different types of FOR clauses. +type ForClause interface { + Node + forClause() +} + +// BrowseForClause represents a FOR BROWSE clause. +type BrowseForClause struct{} + +func (*BrowseForClause) node() {} +func (*BrowseForClause) forClause() {} + +// ReadOnlyForClause represents a FOR READ ONLY clause. +type ReadOnlyForClause struct{} + +func (*ReadOnlyForClause) node() {} +func (*ReadOnlyForClause) forClause() {} + +// UpdateForClause represents a FOR UPDATE [OF columns] clause. +type UpdateForClause struct { + Columns []*ColumnReferenceExpression `json:"Columns,omitempty"` +} + +func (*UpdateForClause) node() {} +func (*UpdateForClause) forClause() {} + +// XmlForClause represents a FOR XML clause with its options. +type XmlForClause struct { + Options []*XmlForClauseOption `json:"Options,omitempty"` +} + +func (*XmlForClause) node() {} +func (*XmlForClause) forClause() {} + +// XmlForClauseOption represents an option in a FOR XML clause. +type XmlForClauseOption struct { + OptionKind string `json:"OptionKind,omitempty"` + Value *StringLiteral `json:"Value,omitempty"` +} + +func (*XmlForClauseOption) node() {} diff --git a/ast/function_call.go b/ast/function_call.go index f55bdcdf..ff3cd9fc 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -28,7 +28,8 @@ func (*UserDefinedTypeCallTarget) callTarget() {} // OverClause represents an OVER clause for window functions. type OverClause struct { - // Add partition by, order by, and window frame as needed + Partitions []ScalarExpression `json:"Partitions,omitempty"` + OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` } // WithinGroupClause represents a WITHIN GROUP clause for ordered set aggregate functions. @@ -49,6 +50,7 @@ type FunctionCall struct { OverClause *OverClause `json:"OverClause,omitempty"` IgnoreRespectNulls []*Identifier `json:"IgnoreRespectNulls,omitempty"` WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` } func (*FunctionCall) node() {} @@ -95,3 +97,13 @@ type TryConvertCall struct { func (*TryConvertCall) node() {} func (*TryConvertCall) scalarExpression() {} + +// IdentityFunctionCall represents an IDENTITY function call: IDENTITY(data_type [, seed, increment]) +type IdentityFunctionCall struct { + DataType DataTypeReference `json:"DataType,omitempty"` + Seed ScalarExpression `json:"Seed,omitempty"` + Increment ScalarExpression `json:"Increment,omitempty"` +} + +func (*IdentityFunctionCall) node() {} +func (*IdentityFunctionCall) scalarExpression() {} diff --git a/ast/query_specification.go b/ast/query_specification.go index df4b748a..1f979b53 100644 --- a/ast/query_specification.go +++ b/ast/query_specification.go @@ -10,6 +10,7 @@ type QuerySpecification struct { GroupByClause *GroupByClause `json:"GroupByClause,omitempty"` HavingClause *HavingClause `json:"HavingClause,omitempty"` OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` + ForClause ForClause `json:"ForClause,omitempty"` } func (*QuerySpecification) node() {} diff --git a/ast/server_audit_statement.go b/ast/server_audit_statement.go index cf28fa2b..c396abb6 100644 --- a/ast/server_audit_statement.go +++ b/ast/server_audit_statement.go @@ -84,8 +84,9 @@ type SourceDeclaration struct { Value *EventSessionObjectName } -func (s *SourceDeclaration) node() {} -func (s *SourceDeclaration) scalarExpression() {} +func (s *SourceDeclaration) node() {} +func (s *SourceDeclaration) scalarExpression() {} +func (s *SourceDeclaration) booleanExpression() {} // EventSessionObjectName represents an event session object name type EventSessionObjectName struct { diff --git a/parser/lexer.go b/parser/lexer.go index 9686f54d..507f9455 100644 --- a/parser/lexer.go +++ b/parser/lexer.go @@ -1,8 +1,10 @@ package parser import ( + "encoding/binary" "strings" "unicode" + "unicode/utf16" ) // TokenType represents the type of a token. @@ -185,6 +187,7 @@ const ( TokenColonColon TokenMove TokenConversation + TokenDialog TokenGet TokenUse TokenKill @@ -242,6 +245,10 @@ type Lexer struct { // NewLexer creates a new Lexer for the given input. func NewLexer(input string) *Lexer { + // Handle UTF-16 LE BOM (0xFF 0xFE) - convert to UTF-8 + if len(input) >= 2 && input[0] == 0xFF && input[1] == 0xFE { + input = utf16LEToUTF8(input[2:]) + } // Skip UTF-8 BOM if present if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF { input = input[3:] @@ -251,6 +258,21 @@ func NewLexer(input string) *Lexer { return l } +// utf16LEToUTF8 converts a UTF-16 LE string to UTF-8 +func utf16LEToUTF8(data string) string { + // Convert byte string to []uint16 + if len(data)%2 != 0 { + data = data[:len(data)-1] // Truncate odd byte + } + u16s := make([]uint16, len(data)/2) + for i := 0; i < len(u16s); i++ { + u16s[i] = binary.LittleEndian.Uint16([]byte(data[i*2 : i*2+2])) + } + // Decode UTF-16 to runes + runes := utf16.Decode(u16s) + return string(runes) +} + func (l *Lexer) readChar() { if l.readPos >= len(l.input) { l.ch = 0 @@ -925,6 +947,7 @@ var keywords = map[string]TokenType{ "TRUNCATE": TokenTruncate, "MOVE": TokenMove, "CONVERSATION": TokenConversation, + "DIALOG": TokenDialog, "GET": TokenGet, "USE": TokenUse, "KILL": TokenKill, diff --git a/parser/marshal.go b/parser/marshal.go index b41c6acd..d7699ca9 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -72,12 +72,20 @@ func statementToJSON(stmt ast.Statement) jsonNode { return beginEndBlockStatementToJSON(s) case *ast.BeginEndAtomicBlockStatement: return beginEndAtomicBlockStatementToJSON(s) + case *ast.BeginDialogStatement: + return beginDialogStatementToJSON(s) + case *ast.BeginConversationTimerStatement: + return beginConversationTimerStatementToJSON(s) case *ast.CreateViewStatement: return createViewStatementToJSON(s) + case *ast.CreateOrAlterViewStatement: + return createOrAlterViewStatementToJSON(s) case *ast.CreateSchemaStatement: return createSchemaStatementToJSON(s) case *ast.CreateProcedureStatement: return createProcedureStatementToJSON(s) + case *ast.CreateOrAlterProcedureStatement: + return createOrAlterProcedureStatementToJSON(s) case *ast.AlterProcedureStatement: return alterProcedureStatementToJSON(s) case *ast.CreateRoleStatement: @@ -376,8 +384,12 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterTriggerStatementToJSON(s) case *ast.CreateTriggerStatement: return createTriggerStatementToJSON(s) + case *ast.CreateOrAlterTriggerStatement: + return createOrAlterTriggerStatementToJSON(s) case *ast.EnableDisableTriggerStatement: return enableDisableTriggerStatementToJSON(s) + case *ast.EndConversationStatement: + return endConversationStatementToJSON(s) case *ast.CreateDatabaseStatement: return createDatabaseStatementToJSON(s) case *ast.CreateDatabaseEncryptionKeyStatement: @@ -1241,6 +1253,51 @@ func querySpecificationToJSON(q *ast.QuerySpecification) jsonNode { if q.OrderByClause != nil { node["OrderByClause"] = orderByClauseToJSON(q.OrderByClause) } + if q.ForClause != nil { + node["ForClause"] = forClauseToJSON(q.ForClause) + } + return node +} + +func forClauseToJSON(fc ast.ForClause) jsonNode { + switch f := fc.(type) { + case *ast.BrowseForClause: + return jsonNode{"$type": "BrowseForClause"} + case *ast.ReadOnlyForClause: + return jsonNode{"$type": "ReadOnlyForClause"} + case *ast.UpdateForClause: + node := jsonNode{"$type": "UpdateForClause"} + if len(f.Columns) > 0 { + cols := make([]jsonNode, len(f.Columns)) + for i, col := range f.Columns { + cols[i] = columnReferenceExpressionToJSON(col) + } + node["Columns"] = cols + } + return node + case *ast.XmlForClause: + node := jsonNode{"$type": "XmlForClause"} + if len(f.Options) > 0 { + opts := make([]jsonNode, len(f.Options)) + for i, opt := range f.Options { + opts[i] = xmlForClauseOptionToJSON(opt) + } + node["Options"] = opts + } + return node + default: + return jsonNode{"$type": "UnknownForClause"} + } +} + +func xmlForClauseOptionToJSON(opt *ast.XmlForClauseOption) jsonNode { + node := jsonNode{"$type": "XmlForClauseOption"} + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + if opt.Value != nil { + node["Value"] = stringLiteralToJSON(opt.Value) + } return node } @@ -1314,6 +1371,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { if e.MultiPartIdentifier != nil { node["MultiPartIdentifier"] = multiPartIdentifierToJSON(e.MultiPartIdentifier) } + if e.Collation != nil { + node["Collation"] = identifierToJSON(e.Collation) + } return node case *ast.IntegerLiteral: node := jsonNode{ @@ -1389,9 +1449,7 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["WithinGroupClause"] = withinGroupClauseToJSON(e.WithinGroupClause) } if e.OverClause != nil { - node["OverClause"] = jsonNode{ - "$type": "OverClause", - } + node["OverClause"] = overClauseToJSON(e.OverClause) } if len(e.IgnoreRespectNulls) > 0 { idents := make([]jsonNode, len(e.IgnoreRespectNulls)) @@ -1401,6 +1459,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["IgnoreRespectNulls"] = idents } node["WithArrayWrapper"] = e.WithArrayWrapper + if e.Collation != nil { + node["Collation"] = identifierToJSON(e.Collation) + } return node case *ast.UserDefinedTypePropertyAccess: node := jsonNode{ @@ -1478,6 +1539,20 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["Collation"] = identifierToJSON(e.Collation) } return node + case *ast.IdentityFunctionCall: + node := jsonNode{ + "$type": "IdentityFunctionCall", + } + if e.DataType != nil { + node["DataType"] = dataTypeReferenceToJSON(e.DataType) + } + if e.Seed != nil { + node["Seed"] = scalarExpressionToJSON(e.Seed) + } + if e.Increment != nil { + node["Increment"] = scalarExpressionToJSON(e.Increment) + } + return node case *ast.BinaryExpression: node := jsonNode{ "$type": "BinaryExpression", @@ -1697,6 +1772,16 @@ func eventSessionObjectNameToJSON(e *ast.EventSessionObjectName) jsonNode { return node } +func sourceDeclarationToJSON(s *ast.SourceDeclaration) jsonNode { + node := jsonNode{ + "$type": "SourceDeclaration", + } + if s.Value != nil { + node["Value"] = eventSessionObjectNameToJSON(s.Value) + } + return node +} + func identifierOrValueExpressionToJSON(iove *ast.IdentifierOrValueExpression) jsonNode { node := jsonNode{ "$type": "IdentifierOrValueExpression", @@ -2036,6 +2121,22 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode { node["ThirdExpression"] = scalarExpressionToJSON(e.ThirdExpression) } return node + case *ast.EventDeclarationCompareFunctionParameter: + node := jsonNode{ + "$type": "EventDeclarationCompareFunctionParameter", + } + if e.Name != nil { + node["Name"] = eventSessionObjectNameToJSON(e.Name) + } + if e.SourceDeclaration != nil { + node["SourceDeclaration"] = sourceDeclarationToJSON(e.SourceDeclaration) + } + if e.EventValue != nil { + node["EventValue"] = scalarExpressionToJSON(e.EventValue) + } + return node + case *ast.SourceDeclaration: + return sourceDeclarationToJSON(e) default: return jsonNode{"$type": "UnknownBooleanExpression"} } @@ -2125,6 +2226,23 @@ func withinGroupClauseToJSON(wg *ast.WithinGroupClause) jsonNode { return node } +func overClauseToJSON(oc *ast.OverClause) jsonNode { + node := jsonNode{ + "$type": "OverClause", + } + if len(oc.Partitions) > 0 { + partitions := make([]jsonNode, len(oc.Partitions)) + for i, p := range oc.Partitions { + partitions[i] = scalarExpressionToJSON(p) + } + node["Partitions"] = partitions + } + if oc.OrderByClause != nil { + node["OrderByClause"] = orderByClauseToJSON(oc.OrderByClause) + } + return node +} + // ======================= New Statement JSON Functions ======================= func tableHintToJSON(h ast.TableHintType) jsonNode { @@ -2333,6 +2451,9 @@ func executableEntityToJSON(entity ast.ExecutableEntity) jsonNode { } node["Parameters"] = params } + if e.AdHocDataSource != nil { + node["AdHocDataSource"] = adHocDataSourceToJSON(e.AdHocDataSource) + } return node case *ast.ExecutableStringList: node := jsonNode{ @@ -2378,6 +2499,9 @@ func procedureReferenceToJSON(pr *ast.ProcedureReference) jsonNode { if pr.Name != nil { node["Name"] = schemaObjectNameToJSON(pr.Name) } + if pr.Number != nil { + node["Number"] = scalarExpressionToJSON(pr.Number) + } return node } @@ -2395,6 +2519,19 @@ func executeParameterToJSON(ep *ast.ExecuteParameter) jsonNode { return node } +func adHocDataSourceToJSON(ds *ast.AdHocDataSource) jsonNode { + node := jsonNode{ + "$type": "AdHocDataSource", + } + if ds.ProviderName != nil { + node["ProviderName"] = scalarExpressionToJSON(ds.ProviderName) + } + if ds.InitString != nil { + node["InitString"] = scalarExpressionToJSON(ds.InitString) + } + return node +} + func updateStatementToJSON(s *ast.UpdateStatement) jsonNode { node := jsonNode{ "$type": "UpdateStatement", @@ -2549,6 +2686,15 @@ func deleteSpecificationToJSON(spec *ast.DeleteSpecification) jsonNode { if spec.Target != nil { node["Target"] = tableReferenceToJSON(spec.Target) } + if spec.TopRowFilter != nil { + node["TopRowFilter"] = topRowFilterToJSON(spec.TopRowFilter) + } + if spec.OutputClause != nil { + node["OutputClause"] = outputClauseToJSON(spec.OutputClause) + } + if spec.OutputIntoClause != nil { + node["OutputIntoClause"] = outputIntoClauseToJSON(spec.OutputIntoClause) + } return node } @@ -2803,6 +2949,71 @@ func statementListToJSON(sl *ast.StatementList) jsonNode { return node } +func beginDialogStatementToJSON(s *ast.BeginDialogStatement) jsonNode { + node := jsonNode{ + "$type": "BeginDialogStatement", + "IsConversation": s.IsConversation, + } + if s.Handle != nil { + node["Handle"] = scalarExpressionToJSON(s.Handle) + } + if s.InitiatorServiceName != nil { + node["InitiatorServiceName"] = identifierOrValueExpressionToJSON(s.InitiatorServiceName) + } + if s.TargetServiceName != nil { + node["TargetServiceName"] = scalarExpressionToJSON(s.TargetServiceName) + } + if s.ContractName != nil { + node["ContractName"] = identifierOrValueExpressionToJSON(s.ContractName) + } + if s.InstanceSpec != nil { + node["InstanceSpec"] = scalarExpressionToJSON(s.InstanceSpec) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = dialogOptionToJSON(o) + } + node["Options"] = options + } + return node +} + +func dialogOptionToJSON(o ast.DialogOption) jsonNode { + switch opt := o.(type) { + case *ast.ScalarExpressionDialogOption: + node := jsonNode{ + "$type": "ScalarExpressionDialogOption", + "OptionKind": opt.OptionKind, + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + return node + case *ast.OnOffDialogOption: + return jsonNode{ + "$type": "OnOffDialogOption", + "OptionState": opt.OptionState, + "OptionKind": opt.OptionKind, + } + default: + return jsonNode{"$type": "UnknownDialogOption"} + } +} + +func beginConversationTimerStatementToJSON(s *ast.BeginConversationTimerStatement) jsonNode { + node := jsonNode{ + "$type": "BeginConversationTimerStatement", + } + if s.Handle != nil { + node["Handle"] = scalarExpressionToJSON(s.Handle) + } + if s.Timeout != nil { + node["Timeout"] = scalarExpressionToJSON(s.Timeout) + } + return node +} + func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { node := jsonNode{ "$type": "CreateViewStatement", @@ -2817,6 +3028,42 @@ func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { } node["Columns"] = cols } + if len(s.ViewOptions) > 0 { + opts := make([]jsonNode, len(s.ViewOptions)) + for i, opt := range s.ViewOptions { + opts[i] = viewOptionToJSON(opt) + } + node["ViewOptions"] = opts + } + if s.SelectStatement != nil { + node["SelectStatement"] = selectStatementToJSON(s.SelectStatement) + } + node["WithCheckOption"] = s.WithCheckOption + node["IsMaterialized"] = s.IsMaterialized + return node +} + +func createOrAlterViewStatementToJSON(s *ast.CreateOrAlterViewStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterViewStatement", + } + if s.SchemaObjectName != nil { + node["SchemaObjectName"] = schemaObjectNameToJSON(s.SchemaObjectName) + } + if len(s.Columns) > 0 { + cols := make([]jsonNode, len(s.Columns)) + for i, c := range s.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if len(s.ViewOptions) > 0 { + opts := make([]jsonNode, len(s.ViewOptions)) + for i, opt := range s.ViewOptions { + opts[i] = viewOptionToJSON(opt) + } + node["ViewOptions"] = opts + } if s.SelectStatement != nil { node["SelectStatement"] = selectStatementToJSON(s.SelectStatement) } @@ -2825,6 +3072,50 @@ func createViewStatementToJSON(s *ast.CreateViewStatement) jsonNode { return node } +func viewOptionToJSON(opt ast.ViewOption) jsonNode { + switch o := opt.(type) { + case *ast.ViewStatementOption: + return jsonNode{ + "$type": "ViewOption", + "OptionKind": o.OptionKind, + } + case *ast.ViewDistributionOption: + node := jsonNode{ + "$type": "ViewDistributionOption", + "OptionKind": o.OptionKind, + } + if o.Value != nil { + valueNode := jsonNode{ + "$type": "ViewHashDistributionPolicy", + } + if o.Value.DistributionColumn != nil { + valueNode["DistributionColumn"] = identifierToJSON(o.Value.DistributionColumn) + } + if len(o.Value.DistributionColumns) > 0 { + cols := make([]jsonNode, len(o.Value.DistributionColumns)) + for i, c := range o.Value.DistributionColumns { + // First column is same as DistributionColumn, use $ref + if i == 0 && o.Value.DistributionColumn != nil { + cols[i] = jsonNode{"$ref": "Identifier"} + } else { + cols[i] = identifierToJSON(c) + } + } + valueNode["DistributionColumns"] = cols + } + node["Value"] = valueNode + } + return node + case *ast.ViewForAppendOption: + return jsonNode{ + "$type": "ViewForAppendOption", + "OptionKind": o.OptionKind, + } + default: + return jsonNode{"$type": "UnknownViewOption"} + } +} + func createSchemaStatementToJSON(s *ast.CreateSchemaStatement) jsonNode { node := jsonNode{ "$type": "CreateSchemaStatement", @@ -3062,14 +3353,12 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) upperLit := strings.ToUpper(p.curTok.Literal) if p.curTok.Type == TokenOn { p.nextToken() // consume ON - // Parse filegroup identifier - ident := p.parseIdentifier() - stmt.OnFileGroupOrPartitionScheme = &ast.FileGroupOrPartitionScheme{ - Name: &ast.IdentifierOrValueExpression{ - Value: ident.Value, - Identifier: ident, - }, + // Parse filegroup or partition scheme with optional columns + fg, err := p.parseFileGroupOrPartitionScheme() + if err != nil { + return nil, err } + stmt.OnFileGroupOrPartitionScheme = fg } else if upperLit == "TEXTIMAGE_ON" { p.nextToken() // consume TEXTIMAGE_ON // Parse filegroup identifier or string literal @@ -3140,6 +3429,20 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) DataCompressionOption: opt, OptionKind: "DataCompression", }) + } else if optionName == "MEMORY_OPTIMIZED" { + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + stateUpper := strings.ToUpper(p.curTok.Literal) + state := "On" + if stateUpper == "OFF" { + state = "Off" + } + p.nextToken() // consume ON/OFF + stmt.Options = append(stmt.Options, &ast.MemoryOptimizedTableOption{ + OptionKind: "MemoryOptimized", + OptionState: state, + }) } else { // Skip unknown option value if p.curTok.Type == TokenEquals { @@ -3262,10 +3565,9 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { col.IsPersisted = true p.nextToken() // consume PERSISTED } - return col, nil - } - - // Parse data type - be lenient if no data type is provided + // Fall through to parse constraints (NOT NULL, CHECK, FOREIGN KEY, etc.) + } else { + // Parse data type - be lenient if no data type is provided dataType, err := p.parseDataTypeReference() if err != nil { // Lenient: return column definition without data type @@ -3322,7 +3624,8 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { } col.IdentityOptions = identityOpts - } + } + } // end of else block for non-computed columns // Parse column constraints (NULL, NOT NULL, UNIQUE, PRIMARY KEY, DEFAULT, CHECK, CONSTRAINT) var constraintName *ast.Identifier @@ -3352,8 +3655,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } } // Parse WITH (index_options) if strings.ToUpper(p.curTok.Literal) == "WITH" { @@ -3384,8 +3693,14 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } } // Parse WITH (index_options) if strings.ToUpper(p.curTok.Literal) == "WITH" { @@ -3425,6 +3740,15 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { CheckCondition: cond, }) } + } else if upperLit == "FOREIGN" { + // Parse FOREIGN KEY constraint for column + constraint, err := p.parseForeignKeyConstraint() + if err != nil { + return nil, err + } + constraint.ConstraintIdentifier = constraintName + constraintName = nil + col.Constraints = append(col.Constraints, constraint) } else if upperLit == "CONSTRAINT" { p.nextToken() // consume CONSTRAINT // Parse and save constraint name for next constraint @@ -3439,11 +3763,133 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { indexDef := &ast.IndexDefinition{ IndexType: &ast.IndexType{}, } - // Parse index name - if p.curTok.Type == TokenIdent { + // Parse index name (skip if it's CLUSTERED/NONCLUSTERED) + idxUpper := strings.ToUpper(p.curTok.Literal) + if p.curTok.Type == TokenIdent && idxUpper != "CLUSTERED" && idxUpper != "NONCLUSTERED" && p.curTok.Type != TokenLParen { indexDef.Name = p.parseIdentifier() } + // Parse optional CLUSTERED/NONCLUSTERED + if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { + indexDef.IndexType.IndexTypeKind = "Clustered" + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { + indexDef.IndexType.IndexTypeKind = "NonClustered" + p.nextToken() + } + // Parse optional column list: (col1 [ASC|DESC], ...) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colWithSort := &ast.ColumnWithSortOrder{ + SortOrder: ast.SortOrderNotSpecified, + } + // Parse column name + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{p.parseIdentifier()}, + }, + } + colRef.MultiPartIdentifier.Count = len(colRef.MultiPartIdentifier.Identifiers) + colWithSort.Column = colRef + + // Parse optional ASC/DESC + if strings.ToUpper(p.curTok.Literal) == "ASC" { + colWithSort.SortOrder = ast.SortOrderAscending + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "DESC" { + colWithSort.SortOrder = ast.SortOrderDescending + p.nextToken() + } + indexDef.Columns = append(indexDef.Columns, colWithSort) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + // Parse optional INCLUDE clause + if strings.ToUpper(p.curTok.Literal) == "INCLUDE" { + p.nextToken() // consume INCLUDE + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{p.parseIdentifier()}, + }, + } + colRef.MultiPartIdentifier.Count = len(colRef.MultiPartIdentifier.Identifiers) + indexDef.IncludeColumns = append(indexDef.IncludeColumns, colRef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } col.Index = indexDef + } else if upperLit == "SPARSE" { + p.nextToken() // consume SPARSE + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.SparseOption = "Sparse" + } else if upperLit == "FILESTREAM" { + p.nextToken() // consume FILESTREAM + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.IsFileStream = true + } else if upperLit == "COLUMN_SET" { + p.nextToken() // consume COLUMN_SET + // Expect FOR ALL_SPARSE_COLUMNS + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + if strings.ToUpper(p.curTok.Literal) == "ALL_SPARSE_COLUMNS" { + p.nextToken() // consume ALL_SPARSE_COLUMNS + if col.StorageOptions == nil { + col.StorageOptions = &ast.ColumnStorageOptions{} + } + col.StorageOptions.SparseOption = "ColumnSetForAllSparseColumns" + } + } + } else if upperLit == "ROWGUIDCOL" { + p.nextToken() // consume ROWGUIDCOL + col.IsRowGuidCol = true + } else if upperLit == "HIDDEN" { + p.nextToken() // consume HIDDEN + col.IsHidden = true + } else if upperLit == "MASKED" { + p.nextToken() // consume MASKED + col.IsMasked = true + // Skip optional WITH clause + if strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() + if p.curTok.Type == TokenLParen { + depth := 1 + p.nextToken() + for depth > 0 && p.curTok.Type != TokenEOF { + if p.curTok.Type == TokenLParen { + depth++ + } else if p.curTok.Type == TokenRParen { + depth-- + } + p.nextToken() + } + } + } } else { break } @@ -4721,6 +5167,9 @@ func columnDefinitionToJSON(c *ast.ColumnDefinition) jsonNode { "IsMasked": c.IsMasked, "ColumnIdentifier": identifierToJSON(c.ColumnIdentifier), } + if c.StorageOptions != nil { + node["StorageOptions"] = columnStorageOptionsToJSON(c.StorageOptions) + } if c.ComputedColumnExpression != nil { node["ComputedColumnExpression"] = scalarExpressionToJSON(c.ComputedColumnExpression) } @@ -4749,6 +5198,18 @@ func columnDefinitionToJSON(c *ast.ColumnDefinition) jsonNode { return node } +func columnStorageOptionsToJSON(o *ast.ColumnStorageOptions) jsonNode { + sparseOption := o.SparseOption + if sparseOption == "" { + sparseOption = "None" + } + return jsonNode{ + "$type": "ColumnStorageOptions", + "IsFileStream": o.IsFileStream, + "SparseOption": sparseOption, + } +} + func defaultConstraintToJSON(d *ast.DefaultConstraintDefinition) jsonNode { node := jsonNode{ "$type": "DefaultConstraintDefinition", @@ -4788,6 +5249,8 @@ func constraintDefinitionToJSON(c ast.ConstraintDefinition) jsonNode { return uniqueConstraintToJSON(constraint) case *ast.CheckConstraintDefinition: return checkConstraintToJSON(constraint) + case *ast.ForeignKeyConstraintDefinition: + return foreignKeyConstraintToJSON(constraint) default: return jsonNode{"$type": "UnknownConstraint"} } @@ -4798,8 +5261,10 @@ func uniqueConstraintToJSON(c *ast.UniqueConstraintDefinition) jsonNode { "$type": "UniqueConstraintDefinition", "IsPrimaryKey": c.IsPrimaryKey, } - // Output Clustered if it's true, or if IndexType is set (meaning NONCLUSTERED was explicitly specified) - if c.Clustered || c.IndexType != nil { + // Output Clustered if it's true, or if IndexType is NonClustered (not Hash variants) + if c.Clustered { + node["Clustered"] = c.Clustered + } else if c.IndexType != nil && (c.IndexType.IndexTypeKind == "NonClustered" || c.IndexType.IndexTypeKind == "Clustered") { node["Clustered"] = c.Clustered } // Output IsEnforced if it's explicitly set @@ -4863,6 +5328,13 @@ func userDataTypeReferenceToJSON(dt *ast.UserDataTypeReference) jsonNode { node := jsonNode{ "$type": "UserDataTypeReference", } + if len(dt.Parameters) > 0 { + params := make([]jsonNode, len(dt.Parameters)) + for i, p := range dt.Parameters { + params[i] = scalarExpressionToJSON(p) + } + node["Parameters"] = params + } if dt.Name != nil { node["Name"] = schemaObjectNameToJSON(dt.Name) } @@ -5388,6 +5860,9 @@ func columnReferenceExpressionToJSON(c *ast.ColumnReferenceExpression) jsonNode if c.MultiPartIdentifier != nil { node["MultiPartIdentifier"] = multiPartIdentifierToJSON(c.MultiPartIdentifier) } + if c.Collation != nil { + node["Collation"] = identifierToJSON(c.Collation) + } return node } @@ -5979,6 +6454,37 @@ func createProcedureStatementToJSON(s *ast.CreateProcedureStatement) jsonNode { return node } +func createOrAlterProcedureStatementToJSON(s *ast.CreateOrAlterProcedureStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterProcedureStatement", + "IsForReplication": s.IsForReplication, + } + if s.ProcedureReference != nil { + node["ProcedureReference"] = procedureReferenceToJSON(s.ProcedureReference) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + options[i] = procedureOptionToJSON(opt) + } + node["Options"] = options + } + if len(s.Parameters) > 0 { + params := make([]jsonNode, len(s.Parameters)) + for i, p := range s.Parameters { + params[i] = procedureParameterToJSON(p) + } + node["Parameters"] = params + } + if s.MethodSpecifier != nil { + node["MethodSpecifier"] = methodSpecifierToJSON(s.MethodSpecifier) + } + if s.StatementList != nil { + node["StatementList"] = statementListToJSON(s.StatementList) + } + return node +} + func procedureOptionToJSON(opt ast.ProcedureOptionBase) jsonNode { switch o := opt.(type) { case *ast.ProcedureOption: @@ -6939,8 +7445,12 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI } stmt.IndexOptions = append(stmt.IndexOptions, opt) - case "SORT_IN_TEMPDB": - p.nextToken() // consume SORT_IN_TEMPDB + case "SORT_IN_TEMPDB", "DROP_EXISTING": + optKind := "SortInTempDB" + if optName == "DROP_EXISTING" { + optKind = "DropExisting" + } + p.nextToken() // consume option name if p.curTok.Type == TokenEquals { p.nextToken() // consume = } @@ -6952,9 +7462,23 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI state = "Off" p.nextToken() } - stmt.IndexOptions = append(stmt.IndexOptions, &ast.IndexStateOption{ - OptionKind: "SortInTempDB", - OptionState: state, + stmt.IndexOptions = append(stmt.IndexOptions, &ast.IndexStateOption{ + OptionKind: optKind, + OptionState: state, + }) + + case "MAXDOP": + p.nextToken() // consume MAXDOP + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.IndexOptions = append(stmt.IndexOptions, &ast.IndexExpressionOption{ + OptionKind: "MaxDop", + Expression: expr, }) case "ORDER": @@ -7001,13 +7525,31 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI } } - // Skip optional ON partition clause + // Parse optional ON filegroup/partition scheme if p.curTok.Type == TokenOn { - p.nextToken() - // Skip to semicolon - for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { - p.nextToken() + p.nextToken() // consume ON + fgps := &ast.FileGroupOrPartitionScheme{ + Name: &ast.IdentifierOrValueExpression{ + Identifier: p.parseIdentifier(), + }, + } + fgps.Name.Value = fgps.Name.Identifier.Value + // Check for partition columns + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + fgps.PartitionSchemeColumns = append(fgps.PartitionSchemeColumns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } } + stmt.OnFileGroupOrPartitionScheme = fgps } // Skip optional semicolon @@ -7441,19 +7983,45 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { if p.curTok.Type == TokenEquals { p.nextToken() - valueStr := strings.ToUpper(p.curTok.Literal) + valueStr := p.curTok.Literal + valueUpper := strings.ToUpper(valueStr) p.nextToken() - if valueStr == "ON" || valueStr == "OFF" { - opt := &ast.IndexStateOption{ - OptionKind: p.getIndexOptionKind(optionName), - OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + if optionName == "COMPRESSION_DELAY" { + // Parse COMPRESSION_DELAY = value [MINUTE|MINUTES] + timeUnit := "Unitless" + nextUpper := strings.ToUpper(p.curTok.Literal) + if nextUpper == "MINUTE" { + timeUnit = "Minute" + p.nextToken() + } else if nextUpper == "MINUTES" { + timeUnit = "Minutes" + p.nextToken() + } + opt := &ast.CompressionDelayIndexOption{ + OptionKind: "CompressionDelay", + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, + TimeUnit: timeUnit, } stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else if valueUpper == "ON" || valueUpper == "OFF" { + if optionName == "IGNORE_DUP_KEY" { + opt := &ast.IgnoreDupKeyIndexOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else { + opt := &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueUpper)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } } else { opt := &ast.IndexExpressionOption{ OptionKind: p.getIndexOptionKind(optionName), - Expression: &ast.IntegerLiteral{Value: valueStr}, + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, } stmt.IndexOptions = append(stmt.IndexOptions, opt) } @@ -7519,16 +8087,24 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { // Determine if it's a state option (ON/OFF) or expression option if valueStr == "ON" || valueStr == "OFF" { - opt := &ast.IndexStateOption{ - OptionKind: p.getIndexOptionKind(optionName), - OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + if optionName == "IGNORE_DUP_KEY" { + opt := &ast.IgnoreDupKeyIndexOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) + } else { + opt := &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + } + stmt.IndexOptions = append(stmt.IndexOptions, opt) } - stmt.IndexOptions = append(stmt.IndexOptions, opt) } else { // Expression option like FILLFACTOR = 80 opt := &ast.IndexExpressionOption{ OptionKind: p.getIndexOptionKind(optionName), - Expression: &ast.IntegerLiteral{Value: valueStr}, + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueStr}, } stmt.IndexOptions = append(stmt.IndexOptions, opt) } @@ -7554,6 +8130,7 @@ func (p *Parser) parseAlterIndexStatement() (*ast.AlterIndexStatement, error) { func (p *Parser) getIndexOptionKind(optionName string) string { optionMap := map[string]string{ + "BUCKET_COUNT": "BucketCount", "PAD_INDEX": "PadIndex", "FILLFACTOR": "FillFactor", "SORT_IN_TEMPDB": "SortInTempDB", @@ -7569,6 +8146,9 @@ func (p *Parser) getIndexOptionKind(optionName string) string { "MAX_DURATION": "MaxDuration", "WAIT_AT_LOW_PRIORITY": "WaitAtLowPriority", "OPTIMIZE_FOR_SEQUENTIAL_KEY": "OptimizeForSequentialKey", + "COMPRESS_ALL_ROW_GROUPS": "CompressAllRowGroups", + "COMPRESSION_DELAY": "CompressionDelay", + "LOB_COMPACTION": "LobCompaction", } if kind, ok := optionMap[optionName]; ok { return kind @@ -8119,21 +8699,57 @@ func (p *Parser) parseCreateOrAlterFunctionStatement() (*ast.CreateOrAlterFuncti } // parseCreateOrAlterProcedureStatement parses a CREATE OR ALTER PROCEDURE statement -func (p *Parser) parseCreateOrAlterProcedureStatement() (*ast.CreateProcedureStatement, error) { - // For now, delegate to regular CREATE PROCEDURE parsing - return p.parseCreateProcedureStatement() +func (p *Parser) parseCreateOrAlterProcedureStatement() (*ast.CreateOrAlterProcedureStatement, error) { + // Parse as regular CREATE PROCEDURE, then convert to CreateOrAlter type + stmt, err := p.parseCreateProcedureStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterProcedureStatement{ + ProcedureReference: stmt.ProcedureReference, + Parameters: stmt.Parameters, + StatementList: stmt.StatementList, + IsForReplication: stmt.IsForReplication, + Options: stmt.Options, + MethodSpecifier: stmt.MethodSpecifier, + }, nil } // parseCreateOrAlterViewStatement parses a CREATE OR ALTER VIEW statement -func (p *Parser) parseCreateOrAlterViewStatement() (*ast.CreateViewStatement, error) { - // For now, delegate to regular CREATE VIEW parsing - return p.parseCreateViewStatement() +func (p *Parser) parseCreateOrAlterViewStatement() (*ast.CreateOrAlterViewStatement, error) { + // Parse as regular CREATE VIEW, then convert to CreateOrAlter type + stmt, err := p.parseCreateViewStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterViewStatement{ + SchemaObjectName: stmt.SchemaObjectName, + Columns: stmt.Columns, + SelectStatement: stmt.SelectStatement, + WithCheckOption: stmt.WithCheckOption, + ViewOptions: stmt.ViewOptions, + IsMaterialized: stmt.IsMaterialized, + }, nil } // parseCreateOrAlterTriggerStatement parses a CREATE OR ALTER TRIGGER statement -func (p *Parser) parseCreateOrAlterTriggerStatement() (*ast.CreateTriggerStatement, error) { - // For now, delegate to regular CREATE TRIGGER parsing - return p.parseCreateTriggerStatement() +func (p *Parser) parseCreateOrAlterTriggerStatement() (*ast.CreateOrAlterTriggerStatement, error) { + // Parse as regular CREATE TRIGGER, then convert to CreateOrAlter type + stmt, err := p.parseCreateTriggerStatement() + if err != nil { + return nil, err + } + return &ast.CreateOrAlterTriggerStatement{ + Name: stmt.Name, + TriggerObject: stmt.TriggerObject, + TriggerType: stmt.TriggerType, + TriggerActions: stmt.TriggerActions, + Options: stmt.Options, + WithAppend: stmt.WithAppend, + IsNotForReplication: stmt.IsNotForReplication, + MethodSpecifier: stmt.MethodSpecifier, + StatementList: stmt.StatementList, + }, nil } // parseCreateTriggerStatement parses a CREATE TRIGGER statement @@ -8752,6 +9368,9 @@ func createColumnStoreIndexStatementToJSON(s *ast.CreateColumnStoreIndexStatemen } node["OrderedColumns"] = cols } + if s.OnFileGroupOrPartitionScheme != nil { + node["OnFileGroupOrPartitionScheme"] = fileGroupOrPartitionSchemeToJSON(s.OnFileGroupOrPartitionScheme) + } return node } @@ -8786,6 +9405,15 @@ func columnStoreIndexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionKind": o.OptionKind, "OptionState": o.OptionState, } + case *ast.IndexExpressionOption: + node := jsonNode{ + "$type": "IndexExpressionOption", + "OptionKind": o.OptionKind, + } + if o.Expression != nil { + node["Expression"] = scalarExpressionToJSON(o.Expression) + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } @@ -9134,6 +9762,39 @@ func createTriggerStatementToJSON(s *ast.CreateTriggerStatement) jsonNode { return node } +func createOrAlterTriggerStatementToJSON(s *ast.CreateOrAlterTriggerStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterTriggerStatement", + "TriggerType": s.TriggerType, + "WithAppend": s.WithAppend, + "IsNotForReplication": s.IsNotForReplication, + } + if s.Name != nil { + node["Name"] = schemaObjectNameToJSON(s.Name) + } + if s.TriggerObject != nil { + node["TriggerObject"] = triggerObjectToJSON(s.TriggerObject) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = triggerOptionTypeToJSON(o) + } + node["Options"] = options + } + if len(s.TriggerActions) > 0 { + actions := make([]jsonNode, len(s.TriggerActions)) + for i, a := range s.TriggerActions { + actions[i] = triggerActionToJSON(a) + } + node["TriggerActions"] = actions + } + if s.StatementList != nil { + node["StatementList"] = statementListToJSON(s.StatementList) + } + return node +} + func triggerOptionTypeToJSON(o ast.TriggerOptionType) jsonNode { switch opt := o.(type) { case *ast.TriggerOption: @@ -9206,6 +9867,23 @@ func enableDisableTriggerStatementToJSON(s *ast.EnableDisableTriggerStatement) j return node } +func endConversationStatementToJSON(s *ast.EndConversationStatement) jsonNode { + node := jsonNode{ + "$type": "EndConversationStatement", + "WithCleanup": s.WithCleanup, + } + if s.Conversation != nil { + node["Conversation"] = scalarExpressionToJSON(s.Conversation) + } + if s.ErrorCode != nil { + node["ErrorCode"] = scalarExpressionToJSON(s.ErrorCode) + } + if s.ErrorDescription != nil { + node["ErrorDescription"] = scalarExpressionToJSON(s.ErrorDescription) + } + return node +} + func alterIndexStatementToJSON(s *ast.AlterIndexStatement) jsonNode { node := jsonNode{ "$type": "AlterIndexStatement", @@ -9282,6 +9960,16 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionState": o.OptionState, "OptionKind": o.OptionKind, } + case *ast.CompressionDelayIndexOption: + node := jsonNode{ + "$type": "CompressionDelayIndexOption", + "OptionKind": o.OptionKind, + "TimeUnit": o.TimeUnit, + } + if o.Expression != nil { + node["Expression"] = scalarExpressionToJSON(o.Expression) + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } @@ -9524,6 +10212,43 @@ func dropIndexOptionToJSON(opt ast.DropIndexOption) jsonNode { "CompressionLevel": o.CompressionLevel, "OptionKind": o.OptionKind, } + case *ast.WaitAtLowPriorityOption: + node := jsonNode{ + "$type": "WaitAtLowPriorityOption", + "OptionKind": o.OptionKind, + } + if len(o.Options) > 0 { + options := make([]jsonNode, len(o.Options)) + for i, opt := range o.Options { + options[i] = lowPriorityLockWaitOptionToJSON(opt) + } + node["Options"] = options + } + return node + } + return jsonNode{} +} + +func lowPriorityLockWaitOptionToJSON(opt ast.LowPriorityLockWaitOption) jsonNode { + switch o := opt.(type) { + case *ast.LowPriorityLockWaitMaxDurationOption: + node := jsonNode{ + "$type": "LowPriorityLockWaitMaxDurationOption", + "OptionKind": o.OptionKind, + } + if o.MaxDuration != nil { + node["MaxDuration"] = scalarExpressionToJSON(o.MaxDuration) + } + if o.Unit != "" { + node["Unit"] = o.Unit + } + return node + case *ast.LowPriorityLockWaitAbortAfterWaitOption: + return jsonNode{ + "$type": "LowPriorityLockWaitAbortAfterWaitOption", + "AbortAfterWait": o.AbortAfterWait, + "OptionKind": o.OptionKind, + } } return jsonNode{} } @@ -10294,6 +11019,49 @@ func createExternalTableStatementToJSON(s *ast.CreateExternalTableStatement) jso if s.SchemaObjectName != nil { node["SchemaObjectName"] = schemaObjectNameToJSON(s.SchemaObjectName) } + if len(s.ColumnDefinitions) > 0 { + cols := make([]jsonNode, len(s.ColumnDefinitions)) + for i, col := range s.ColumnDefinitions { + cols[i] = externalTableColumnDefinitionToJSON(col) + } + node["ColumnDefinitions"] = cols + } + if s.DataSource != nil { + node["DataSource"] = identifierToJSON(s.DataSource) + } + if len(s.ExternalTableOptions) > 0 { + opts := make([]jsonNode, len(s.ExternalTableOptions)) + for i, opt := range s.ExternalTableOptions { + opts[i] = externalTableLiteralOrIdentifierOptionToJSON(opt) + } + node["ExternalTableOptions"] = opts + } + return node +} + +func externalTableColumnDefinitionToJSON(col *ast.ExternalTableColumnDefinition) jsonNode { + node := jsonNode{ + "$type": "ExternalTableColumnDefinition", + } + if col.ColumnDefinition != nil { + node["ColumnDefinition"] = columnDefinitionBaseToJSON(col.ColumnDefinition) + } + if col.NullableConstraint != nil { + node["NullableConstraint"] = nullableConstraintToJSON(col.NullableConstraint) + } + return node +} + +func externalTableLiteralOrIdentifierOptionToJSON(opt *ast.ExternalTableLiteralOrIdentifierOption) jsonNode { + node := jsonNode{ + "$type": "ExternalTableLiteralOrIdentifierOption", + } + if opt.Value != nil { + node["Value"] = identifierOrValueExpressionToJSON(opt.Value) + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } return node } @@ -10350,9 +11118,149 @@ func createEventSessionStatementToJSON(s *ast.CreateEventSessionStatement) jsonN if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.SessionScope != "" { + node["SessionScope"] = s.SessionScope + } + if len(s.EventDeclarations) > 0 { + events := make([]jsonNode, len(s.EventDeclarations)) + for i, e := range s.EventDeclarations { + events[i] = eventDeclarationToJSON(e) + } + node["EventDeclarations"] = events + } + if len(s.TargetDeclarations) > 0 { + targets := make([]jsonNode, len(s.TargetDeclarations)) + for i, t := range s.TargetDeclarations { + targets[i] = targetDeclarationToJSON(t) + } + node["TargetDeclarations"] = targets + } + if len(s.SessionOptions) > 0 { + opts := make([]jsonNode, len(s.SessionOptions)) + for i, o := range s.SessionOptions { + opts[i] = sessionOptionToJSON(o) + } + node["SessionOptions"] = opts + } + return node +} + +func eventDeclarationToJSON(e *ast.EventDeclaration) jsonNode { + node := jsonNode{ + "$type": "EventDeclaration", + } + if e.ObjectName != nil { + node["ObjectName"] = eventSessionObjectNameToJSON(e.ObjectName) + } + if len(e.EventDeclarationActionParameters) > 0 { + actions := make([]jsonNode, len(e.EventDeclarationActionParameters)) + for i, a := range e.EventDeclarationActionParameters { + actions[i] = eventSessionObjectNameToJSON(a) + } + node["EventDeclarationActionParameters"] = actions + } + if e.EventDeclarationPredicateParameter != nil { + node["EventDeclarationPredicateParameter"] = booleanExpressionToJSON(e.EventDeclarationPredicateParameter) + } + return node +} + +func targetDeclarationToJSON(t *ast.TargetDeclaration) jsonNode { + node := jsonNode{ + "$type": "TargetDeclaration", + } + if t.ObjectName != nil { + node["ObjectName"] = eventSessionObjectNameToJSON(t.ObjectName) + } + if len(t.TargetDeclarationParameters) > 0 { + params := make([]jsonNode, len(t.TargetDeclarationParameters)) + for i, p := range t.TargetDeclarationParameters { + params[i] = eventDeclarationSetParameterToJSON(p) + } + node["TargetDeclarationParameters"] = params + } + return node +} + +func eventDeclarationSetParameterToJSON(p *ast.EventDeclarationSetParameter) jsonNode { + node := jsonNode{ + "$type": "EventDeclarationSetParameter", + } + if p.EventField != nil { + node["EventField"] = identifierToJSON(p.EventField) + } + if p.EventValue != nil { + node["EventValue"] = scalarExpressionToJSON(p.EventValue) + } return node } +func sessionOptionToJSON(o ast.SessionOption) jsonNode { + switch opt := o.(type) { + case *ast.LiteralSessionOption: + node := jsonNode{ + "$type": "LiteralSessionOption", + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + if opt.Unit != "" { + node["Unit"] = opt.Unit + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.OnOffSessionOption: + node := jsonNode{ + "$type": "OnOffSessionOption", + } + if opt.OptionState != "" { + node["OptionState"] = opt.OptionState + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.EventRetentionSessionOption: + node := jsonNode{ + "$type": "EventRetentionSessionOption", + } + if opt.Value != "" { + node["Value"] = opt.Value + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.MaxDispatchLatencySessionOption: + node := jsonNode{ + "$type": "MaxDispatchLatencySessionOption", + "IsInfinite": opt.IsInfinite, + } + if opt.Value != nil { + node["Value"] = scalarExpressionToJSON(opt.Value) + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + case *ast.MemoryPartitionSessionOption: + node := jsonNode{ + "$type": "MemoryPartitionSessionOption", + } + if opt.Value != "" { + node["Value"] = opt.Value + } + if opt.OptionKind != "" { + node["OptionKind"] = opt.OptionKind + } + return node + default: + return jsonNode{"$type": "UnknownSessionOption"} + } +} + func insertBulkStatementToJSON(s *ast.InsertBulkStatement) jsonNode { node := jsonNode{ "$type": "InsertBulkStatement", @@ -10400,6 +11308,9 @@ func columnDefinitionBaseToJSON(c *ast.ColumnDefinitionBase) jsonNode { if c.DataType != nil { node["DataType"] = dataTypeReferenceToJSON(c.DataType) } + if c.Collation != nil { + node["Collation"] = identifierToJSON(c.Collation) + } return node } @@ -11277,6 +12188,23 @@ func encryptionSourceToJSON(source ast.EncryptionSource) interface{} { switch s := source.(type) { case *ast.ProviderEncryptionSource: return providerEncryptionSourceToJSON(s) + case *ast.AssemblyEncryptionSource: + node := jsonNode{ + "$type": "AssemblyEncryptionSource", + } + if s.Assembly != nil { + node["Assembly"] = identifierToJSON(s.Assembly) + } + return node + case *ast.FileEncryptionSource: + node := jsonNode{ + "$type": "FileEncryptionSource", + "IsExecutable": s.IsExecutable, + } + if s.File != nil { + node["File"] = stringLiteralToJSON(s.File) + } + return node default: return nil } @@ -11393,9 +12321,38 @@ func createCertificateStatementToJSON(s *ast.CreateCertificateStatement) jsonNod node := jsonNode{ "$type": "CreateCertificateStatement", } + if s.CertificateSource != nil { + node["CertificateSource"] = encryptionSourceToJSON(s.CertificateSource) + } + if len(s.CertificateOptions) > 0 { + options := make([]jsonNode, len(s.CertificateOptions)) + for i, opt := range s.CertificateOptions { + options[i] = jsonNode{ + "$type": "CertificateOption", + "Kind": opt.Kind, + "Value": stringLiteralToJSON(opt.Value), + } + } + node["CertificateOptions"] = options + } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.ActiveForBeginDialog != "" { + node["ActiveForBeginDialog"] = s.ActiveForBeginDialog + } + if s.PrivateKeyPath != nil { + node["PrivateKeyPath"] = stringLiteralToJSON(s.PrivateKeyPath) + } + if s.EncryptionPassword != nil { + node["EncryptionPassword"] = stringLiteralToJSON(s.EncryptionPassword) + } + if s.DecryptionPassword != nil { + node["DecryptionPassword"] = stringLiteralToJSON(s.DecryptionPassword) + } return node } diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index 5133ceea..79fb1881 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -1315,6 +1315,75 @@ func (p *Parser) parseDropIndexOptions() []ast.DropIndexOption { CompressionLevel: level, OptionKind: "DataCompression", }) + case "WAIT_AT_LOW_PRIORITY": + p.nextToken() // consume WAIT_AT_LOW_PRIORITY + waitOpt := &ast.WaitAtLowPriorityOption{ + OptionKind: "WaitAtLowPriority", + } + // Parse nested options inside parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for { + optName := strings.ToUpper(p.curTok.Literal) + if optName == "MAX_DURATION" { + p.nextToken() // consume MAX_DURATION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + maxDur := &ast.LowPriorityLockWaitMaxDurationOption{ + OptionKind: "MaxDuration", + } + // Parse integer value + if p.curTok.Type == TokenNumber { + maxDur.MaxDuration = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + // Parse unit: MINUTES or SECONDS + unitUpper := strings.ToUpper(p.curTok.Literal) + if unitUpper == "MINUTES" { + maxDur.Unit = "Minutes" + p.nextToken() + } else if unitUpper == "SECONDS" { + maxDur.Unit = "Seconds" + p.nextToken() + } + waitOpt.Options = append(waitOpt.Options, maxDur) + } else if optName == "ABORT_AFTER_WAIT" { + p.nextToken() // consume ABORT_AFTER_WAIT + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + abortOpt := &ast.LowPriorityLockWaitAbortAfterWaitOption{ + OptionKind: "AbortAfterWait", + } + abortValue := strings.ToUpper(p.curTok.Literal) + switch abortValue { + case "NONE": + abortOpt.AbortAfterWait = "None" + case "SELF": + abortOpt.AbortAfterWait = "Self" + case "BLOCKERS": + abortOpt.AbortAfterWait = "Blockers" + } + p.nextToken() + waitOpt.Options = append(waitOpt.Options, abortOpt) + } else { + break + } + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + options = append(options, waitOpt) default: // Unknown option, skip p.nextToken() @@ -2772,18 +2841,20 @@ func (p *Parser) parseAlterTableAlterIndexStatement(tableName *ast.SchemaObjectN func convertIndexOptionKind(name string) string { optionMap := map[string]string{ - "BUCKET_COUNT": "BucketCount", - "PAD_INDEX": "PadIndex", - "FILLFACTOR": "FillFactor", - "SORT_IN_TEMPDB": "SortInTempDB", - "IGNORE_DUP_KEY": "IgnoreDupKey", - "STATISTICS_NORECOMPUTE": "StatisticsNoRecompute", - "DROP_EXISTING": "DropExisting", - "ONLINE": "Online", - "ALLOW_ROW_LOCKS": "AllowRowLocks", - "ALLOW_PAGE_LOCKS": "AllowPageLocks", - "MAXDOP": "MaxDop", - "DATA_COMPRESSION": "DataCompression", + "BUCKET_COUNT": "BucketCount", + "PAD_INDEX": "PadIndex", + "FILLFACTOR": "FillFactor", + "SORT_IN_TEMPDB": "SortInTempDB", + "IGNORE_DUP_KEY": "IgnoreDupKey", + "STATISTICS_NORECOMPUTE": "StatisticsNoRecompute", + "DROP_EXISTING": "DropExisting", + "ONLINE": "Online", + "ALLOW_ROW_LOCKS": "AllowRowLocks", + "ALLOW_PAGE_LOCKS": "AllowPageLocks", + "MAXDOP": "MaxDop", + "DATA_COMPRESSION": "DataCompression", + "COMPRESS_ALL_ROW_GROUPS": "CompressAllRowGroups", + "COMPRESSION_DELAY": "CompressionDelay", } if mapped, ok := optionMap[name]; ok { return mapped @@ -2915,7 +2986,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* ConstraintIdentifier: constraintName, IsPrimaryKey: true, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH for { upperOpt := strings.ToUpper(p.curTok.Literal) if upperOpt == "CLUSTERED" { @@ -2924,7 +2995,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } else if upperOpt == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if upperOpt == "HASH" { + // HASH without NONCLUSTERED + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } else { break @@ -2967,6 +3048,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } } + // Parse WITH (index_options) + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: convertIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } // Parse NOT ENFORCED if strings.ToUpper(p.curTok.Literal) == "NOT" { p.nextToken() @@ -2987,7 +3096,7 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* ConstraintIdentifier: constraintName, IsPrimaryKey: false, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH for { upperOpt := strings.ToUpper(p.curTok.Literal) if upperOpt == "CLUSTERED" { @@ -2996,7 +3105,17 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } else if upperOpt == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if upperOpt == "HASH" { + // HASH without NONCLUSTERED + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } else { break @@ -3039,6 +3158,34 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() } } + // Parse WITH (index_options) + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: convertIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } // Parse NOT ENFORCED if strings.ToUpper(p.curTok.Literal) == "NOT" { p.nextToken() diff --git a/parser/parse_dml.go b/parser/parse_dml.go index d1a04796..4af7ed06 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -656,11 +656,25 @@ func (p *Parser) parseColumnList() ([]*ast.ColumnReferenceExpression, error) { var cols []*ast.ColumnReferenceExpression for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - col, err := p.parseMultiPartIdentifierAsColumn() - if err != nil { - return nil, err + // Check for pseudo columns + lit := p.curTok.Literal + upperLit := strings.ToUpper(lit) + if upperLit == "$ACTION" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnAction"}) + p.nextToken() + } else if upperLit == "$CUID" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnCuid"}) + p.nextToken() + } else if upperLit == "$ROWGUID" { + cols = append(cols, &ast.ColumnReferenceExpression{ColumnType: "PseudoColumnRowGuid"}) + p.nextToken() + } else { + col, err := p.parseMultiPartIdentifierAsColumn() + if err != nil { + return nil, err + } + cols = append(cols, col) } - cols = append(cols, col) if p.curTok.Type != TokenComma { break @@ -851,20 +865,77 @@ func (p *Parser) parseExecuteSpecification() (*ast.ExecuteSpecification, error) // Parse procedure reference procRef := &ast.ExecutableProcedureReference{} + // Check for OPENDATASOURCE or OPENROWSET + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "OPENDATASOURCE" || upperLit == "OPENROWSET" { + p.nextToken() // consume OPENDATASOURCE/OPENROWSET + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + + // Parse provider name + var providerName *ast.StringLiteral + if p.curTok.Type == TokenString { + providerName = p.parseStringLiteralValue() + p.nextToken() + } + + // Expect comma + if p.curTok.Type == TokenComma { + p.nextToken() + } + + // Parse init string + var initString *ast.StringLiteral + if p.curTok.Type == TokenString { + initString = p.parseStringLiteralValue() + p.nextToken() + } + + // Expect ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + + procRef.AdHocDataSource = &ast.AdHocDataSource{ + ProviderName: providerName, + InitString: initString, + } + + // Expect . and then schema.object.procedure name + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + } + } + } + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { // Procedure variable procRef.ProcedureReference = &ast.ProcedureReferenceName{ ProcedureVariable: &ast.VariableReference{Name: p.curTok.Literal}, } p.nextToken() - } else { + } else if p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon { // Procedure name son, err := p.parseSchemaObjectName() if err != nil { return nil, err } + pr := &ast.ProcedureReference{Name: son} + + // Check for procedure number: ;number + if p.curTok.Type == TokenSemicolon { + p.nextToken() // consume ; + if p.curTok.Type == TokenNumber { + pr.Number = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + } + procRef.ProcedureReference = &ast.ProcedureReferenceName{ - ProcedureReference: &ast.ProcedureReference{Name: son}, + ProcedureReference: pr, } } @@ -1001,11 +1072,83 @@ func (p *Parser) parseExecuteContextForSpec() (*ast.ExecuteContext, error) { func (p *Parser) parseExecuteParameter() (*ast.ExecuteParameter, error) { param := &ast.ExecuteParameter{IsOutput: false} - expr, err := p.parseScalarExpression() - if err != nil { - return nil, err + // Check for DEFAULT keyword + if strings.ToUpper(p.curTok.Literal) == "DEFAULT" { + param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"} + p.nextToken() + return param, nil + } + + // Check for named parameter: @name = value + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + varName := p.curTok.Literal + p.nextToken() + + if p.curTok.Type == TokenEquals { + // Named parameter + p.nextToken() // consume = + param.Variable = &ast.VariableReference{Name: varName} + + // Check for DEFAULT keyword as value + if strings.ToUpper(p.curTok.Literal) == "DEFAULT" { + param.ParameterValue = &ast.DefaultLiteral{LiteralType: "Default", Value: "DEFAULT"} + p.nextToken() + } else { + // Parse the parameter value + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } else { + // Just a variable as value (not a named parameter) + param.ParameterValue = &ast.VariableReference{Name: varName} + } + } else { + // Check for bare identifier as IdentifierLiteral (e.g., EXEC sp_addtype birthday, datetime) + // Only if it's not followed by . or ( which would indicate a column/function reference + if p.curTok.Type == TokenIdent && !strings.HasPrefix(p.curTok.Literal, "@") { + upper := strings.ToUpper(p.curTok.Literal) + // Skip keywords that are expression starters + isKeyword := upper == "NULL" || upper == "DEFAULT" || upper == "NOT" || + upper == "CASE" || upper == "EXISTS" || upper == "CAST" || + upper == "CONVERT" || upper == "COALESCE" || upper == "NULLIF" + if !isKeyword && p.peekTok.Type != TokenDot && p.peekTok.Type != TokenLParen { + // Plain identifier - treat as IdentifierLiteral + quoteType := "NotQuoted" + if strings.HasPrefix(p.curTok.Literal, "[") { + quoteType = "SquareBracket" + } + param.ParameterValue = &ast.IdentifierLiteral{ + LiteralType: "Identifier", + QuoteType: quoteType, + Value: p.curTok.Literal, + } + p.nextToken() + } else { + // Regular value expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } else { + // Regular value expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = expr + } + } + + // Check for OUTPUT modifier + if strings.ToUpper(p.curTok.Literal) == "OUTPUT" || strings.ToUpper(p.curTok.Literal) == "OUT" { + param.IsOutput = true + p.nextToken() } - param.ParameterValue = expr return param, nil } @@ -1355,6 +1498,15 @@ func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) { DeleteSpecification: &ast.DeleteSpecification{}, } + // Parse optional TOP clause + if p.curTok.Type == TokenTop { + topRowFilter, err := p.parseTopRowFilter() + if err != nil { + return nil, err + } + stmt.DeleteSpecification.TopRowFilter = topRowFilter + } + // Skip optional FROM if p.curTok.Type == TokenFrom { p.nextToken() @@ -1367,6 +1519,20 @@ func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) { } stmt.DeleteSpecification.Target = target + // Parse OUTPUT clauses (can have OUTPUT INTO followed by OUTPUT) + for p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + outputClause, outputIntoClause, err := p.parseOutputClause() + if err != nil { + return nil, err + } + if outputIntoClause != nil { + stmt.DeleteSpecification.OutputIntoClause = outputIntoClause + } + if outputClause != nil { + stmt.DeleteSpecification.OutputClause = outputClause + } + } + // Parse optional FROM clause if p.curTok.Type == TokenFrom { fromClause, err := p.parseFromClause() diff --git a/parser/parse_select.go b/parser/parse_select.go index a0c04f6a..43e46b1c 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -186,6 +186,18 @@ func (p *Parser) parseQueryExpressionWithInto() (ast.QueryExpression, *ast.Schem } } + // Parse FOR clause (FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY) + if strings.ToUpper(p.curTok.Literal) == "FOR" { + forClause, err := p.parseForClause() + if err != nil { + return nil, nil, nil, err + } + // Attach to QuerySpecification + if qs, ok := left.(*ast.QuerySpecification); ok { + qs.ForClause = forClause + } + } + return left, into, on, nil } @@ -418,10 +430,42 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } // Not an assignment, treat as regular scalar expression starting with variable - // We need to "un-consume" the variable and let parseScalarExpression handle it - // Create the variable reference and use it as the expression varRef := &ast.VariableReference{Name: varName} - sse := &ast.SelectScalarExpression{Expression: varRef} + + // Check if next token is a binary operator - if so, continue parsing the expression + var expr ast.ScalarExpression = varRef + for p.curTok.Type == TokenPlus || p.curTok.Type == TokenMinus || + p.curTok.Type == TokenStar || p.curTok.Type == TokenSlash || + p.curTok.Type == TokenPercent || p.curTok.Type == TokenDoublePipe { + // We have a variable followed by a binary operator, continue parsing + var opType string + switch p.curTok.Type { + case TokenPlus: + opType = "Add" + case TokenMinus: + opType = "Subtract" + case TokenStar: + opType = "Multiply" + case TokenSlash: + opType = "Divide" + case TokenPercent: + opType = "Modulo" + case TokenDoublePipe: + opType = "Add" // String concatenation + } + p.nextToken() // consume operator + right, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + expr = &ast.BinaryExpression{ + FirstExpression: expr, + SecondExpression: right, + BinaryExpressionType: opType, + } + } + + sse := &ast.SelectScalarExpression{Expression: expr} // Check for column alias if p.curTok.Type == TokenIdent && p.curTok.Literal[0] == '[' { @@ -479,6 +523,19 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } } + // Check for COLLATE clause before creating SelectScalarExpression + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + collation := p.parseIdentifier() + // Attach collation to the expression + switch e := expr.(type) { + case *ast.FunctionCall: + e.Collation = collation + case *ast.ColumnReferenceExpression: + e.Collation = collation + } + } + sse := &ast.SelectScalarExpression{Expression: expr} // Check for column alias: [alias], AS alias, or just alias @@ -499,6 +556,13 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { Value: str.Value, ValueExpression: str, } + } else if p.curTok.Type == TokenNationalString { + // National string literal alias: AS N'alias' + str, _ := p.parseNationalStringFromToken() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: str.Value, + ValueExpression: str, + } } else { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ @@ -509,7 +573,7 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } else if p.curTok.Type == TokenIdent { // Check if this is an alias (not a keyword that starts a new clause) upper := strings.ToUpper(p.curTok.Literal) - if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" { + if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" && upper != "COLLATE" { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ Value: alias.Value, @@ -722,6 +786,17 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) { if upper == "TRY_CONVERT" && p.peekTok.Type == TokenLParen { return p.parseTryConvertCall() } + if upper == "IDENTITY" && p.peekTok.Type == TokenLParen { + return p.parseIdentityFunctionCall() + } + if upper == "IDENTITYCOL" { + p.nextToken() + return &ast.ColumnReferenceExpression{ColumnType: "IdentityCol"}, nil + } + if upper == "ROWGUIDCOL" { + p.nextToken() + return &ast.ColumnReferenceExpression{ColumnType: "RowGuidCol"}, nil + } return p.parseColumnReferenceOrFunctionCall() case TokenNumber: val := p.curTok.Literal @@ -772,6 +847,13 @@ func (p *Parser) parsePrimaryExpression() (ast.ScalarExpression, error) { // Wildcard column reference (e.g., * in count(*)) p.nextToken() return &ast.ColumnReferenceExpression{ColumnType: "Wildcard"}, nil + case TokenDot: + // Multi-part identifier starting with empty parts (e.g., ..t1.c1) + return p.parseColumnReferenceWithLeadingDots() + case TokenMaster, TokenDatabase, TokenKey, TokenTable, TokenIndex, + TokenSchema, TokenUser, TokenView: + // Keywords that can be used as identifiers in column/table references + return p.parseColumnReferenceOrFunctionCall() default: return nil, fmt.Errorf("unexpected token in expression: %s", p.curTok.Literal) } @@ -1046,20 +1128,43 @@ func (p *Parser) parseNationalStringFromToken() (*ast.StringLiteral, error) { }, nil } +func (p *Parser) isIdentifierToken() bool { + switch p.curTok.Type { + case TokenIdent, TokenMaster, TokenDatabase, TokenKey, TokenTable, TokenIndex, + TokenSchema, TokenUser, TokenView, TokenDefault: + return true + default: + return false + } +} + func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, error) { var identifiers []*ast.Identifier + colType := "Regular" for { - if p.curTok.Type != TokenIdent { + if !p.isIdentifierToken() { break } quoteType := "NotQuoted" literal := p.curTok.Literal + upper := strings.ToUpper(literal) + // Handle bracketed identifiers if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { quoteType = "SquareBracket" literal = literal[1 : len(literal)-1] + } else if upper == "IDENTITYCOL" || upper == "ROWGUIDCOL" { + // IDENTITYCOL/ROWGUIDCOL at end of multi-part identifier sets column type + // and is not included in the identifier list + if upper == "IDENTITYCOL" { + colType = "IdentityCol" + } else { + colType = "RowGuidCol" + } + p.nextToken() + break } id := &ast.Identifier{ @@ -1078,6 +1183,12 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err break } p.nextToken() // consume dot + + // Handle consecutive dots (empty parts in multi-part identifier) + for p.curTok.Type == TokenDot { + identifiers = append(identifiers, &ast.Identifier{Value: "", QuoteType: "NotQuoted"}) + p.nextToken() // consume dot + } } // Check for :: (user-defined type method call or property access): a.b::func() or a::prop @@ -1156,12 +1267,21 @@ func (p *Parser) parseColumnReferenceOrFunctionCall() (ast.ScalarExpression, err return p.parseFunctionCallFromIdentifiers(identifiers) } + // If we have identifiers, build a column reference with them + if len(identifiers) > 0 { + return &ast.ColumnReferenceExpression{ + ColumnType: colType, + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil + } + + // No identifiers means just IDENTITYCOL or ROWGUIDCOL (already handled in parsePrimaryExpression) + // but handle the case anyway return &ast.ColumnReferenceExpression{ - ColumnType: "Regular", - MultiPartIdentifier: &ast.MultiPartIdentifier{ - Count: len(identifiers), - Identifiers: identifiers, - }, + ColumnType: colType, }, nil } @@ -1177,6 +1297,71 @@ func (p *Parser) parseColumnReference() (*ast.ColumnReferenceExpression, error) return nil, fmt.Errorf("expected column reference, got function call") } +func (p *Parser) parseColumnReferenceWithLeadingDots() (ast.ScalarExpression, error) { + // Handle multi-part identifiers starting with dots like ..t1.c1 or .db..t1.c1 + var identifiers []*ast.Identifier + + // Add empty identifiers for leading dots + for p.curTok.Type == TokenDot { + identifiers = append(identifiers, &ast.Identifier{Value: "", QuoteType: "NotQuoted"}) + p.nextToken() // consume dot + } + + // Now parse the remaining identifiers + for p.isIdentifierToken() { + quoteType := "NotQuoted" + literal := p.curTok.Literal + // Handle special column types + upper := strings.ToUpper(literal) + if upper == "IDENTITYCOL" || upper == "ROWGUIDCOL" { + // Return with the proper column type + colType := "IdentityCol" + if upper == "ROWGUIDCOL" { + colType = "RowGuidCol" + } + p.nextToken() + return &ast.ColumnReferenceExpression{ + ColumnType: colType, + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil + } + // Handle bracketed identifiers + if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { + quoteType = "SquareBracket" + literal = literal[1 : len(literal)-1] + } + + id := &ast.Identifier{ + Value: literal, + QuoteType: quoteType, + } + identifiers = append(identifiers, id) + p.nextToken() + + if p.curTok.Type != TokenDot { + break + } + // Check for qualified star + if p.peekTok.Type == TokenStar { + break + } + p.nextToken() // consume dot + } + + // Don't consume .* here - let the caller (parseSelectElement) handle qualified stars + + return &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Count: len(identifiers), + Identifiers: identifiers, + }, + }, nil +} + func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) (ast.ScalarExpression, error) { fc := &ast.FunctionCall{ UniqueRowFilter: "NotSpecified", @@ -1211,6 +1396,7 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) } // Parse parameters + funcNameUpper := strings.ToUpper(fc.FunctionName.Value) if p.curTok.Type != TokenRParen { for { param, err := p.parseScalarExpression() @@ -1219,6 +1405,12 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) } fc.Parameters = append(fc.Parameters, param) + // Special handling for TRIM function: FROM keyword acts as separator + if funcNameUpper == "TRIM" && strings.ToUpper(p.curTok.Literal) == "FROM" { + p.nextToken() // consume FROM + continue + } + if p.curTok.Type != TokenComma { break } @@ -1374,14 +1566,51 @@ func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.Scala } p.nextToken() // consume ( - // For now, just skip to closing paren (basic OVER() support) - // TODO: Parse partition by, order by, and window frame + overClause := &ast.OverClause{} + + // Parse PARTITION BY + if strings.ToUpper(p.curTok.Literal) == "PARTITION" { + p.nextToken() // consume PARTITION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + // Parse partition expressions + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + partExpr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + overClause.Partitions = append(overClause.Partitions, partExpr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + + // Parse ORDER BY + if p.curTok.Type == TokenOrder { + orderBy, err := p.parseOrderByClause() + if err != nil { + return nil, err + } + overClause.OrderByClause = orderBy + } + if p.curTok.Type != TokenRParen { return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) } p.nextToken() // consume ) - fc.OverClause = &ast.OverClause{} + fc.OverClause = overClause + } + + // Check for COLLATE clause for function calls + if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + fc.Collation = p.parseIdentifier() + continue } break @@ -1580,7 +1809,7 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { } else if p.curTok.Type == TokenIdent { // Could be an alias without AS, but need to be careful not to consume keywords upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"} p.nextToken() } @@ -1640,7 +1869,7 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a // Could be an alias without AS, but need to be careful not to consume keywords if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { ref.Alias = p.parseIdentifier() } } else { @@ -3055,6 +3284,55 @@ func (p *Parser) parseTryConvertCall() (ast.ScalarExpression, error) { return convert, nil } +// parseIdentityFunctionCall parses an IDENTITY function call: IDENTITY(data_type [, seed, increment]) +func (p *Parser) parseIdentityFunctionCall() (ast.ScalarExpression, error) { + p.nextToken() // consume IDENTITY + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse the data type + dt, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + + identity := &ast.IdentityFunctionCall{ + DataType: dt, + } + + // Check for optional seed and increment + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + seed, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + identity.Seed = seed + + // Expect comma before increment + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , before increment in IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + increment, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + identity.Increment = increment + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in IDENTITY, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + return identity, nil +} + // parsePredictTableReference parses PREDICT(...) in FROM clause // PREDICT(MODEL = expression, DATA = table AS alias, RUNTIME=ident) WITH (columns) AS alias func (p *Parser) parsePredictTableReference() (*ast.PredictTableReference, error) { @@ -3170,3 +3448,168 @@ func (p *Parser) parsePredictTableReference() (*ast.PredictTableReference, error return ref, nil } + +// parseForClause parses FOR BROWSE, FOR XML, FOR UPDATE, FOR READ ONLY clauses. +func (p *Parser) parseForClause() (ast.ForClause, error) { + p.nextToken() // consume FOR + + keyword := strings.ToUpper(p.curTok.Literal) + + switch keyword { + case "BROWSE": + p.nextToken() // consume BROWSE + return &ast.BrowseForClause{}, nil + + case "READ": + p.nextToken() // consume READ + if strings.ToUpper(p.curTok.Literal) == "ONLY" { + p.nextToken() // consume ONLY + } + return &ast.ReadOnlyForClause{}, nil + + case "UPDATE": + p.nextToken() // consume UPDATE + clause := &ast.UpdateForClause{} + + // Check for OF column_list + if strings.ToUpper(p.curTok.Literal) == "OF" { + p.nextToken() // consume OF + + // Parse column list + for { + col, err := p.parseColumnReference() + if err != nil { + return nil, err + } + clause.Columns = append(clause.Columns, col) + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + } + return clause, nil + + case "XML": + p.nextToken() // consume XML + return p.parseXmlForClause() + + default: + return nil, fmt.Errorf("unexpected token after FOR: %s", p.curTok.Literal) + } +} + +// parseXmlForClause parses FOR XML options. +func (p *Parser) parseXmlForClause() (*ast.XmlForClause, error) { + clause := &ast.XmlForClause{} + + // Parse XML options separated by commas + for { + option, err := p.parseXmlForClauseOption() + if err != nil { + return nil, err + } + clause.Options = append(clause.Options, option) + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + + return clause, nil +} + +// parseXmlForClauseOption parses a single XML FOR clause option. +func (p *Parser) parseXmlForClauseOption() (*ast.XmlForClauseOption, error) { + option := &ast.XmlForClauseOption{} + + keyword := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume the option keyword + + switch keyword { + case "AUTO": + option.OptionKind = "Auto" + case "EXPLICIT": + option.OptionKind = "Explicit" + case "RAW": + option.OptionKind = "Raw" + // Check for optional element name: RAW ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "PATH": + option.OptionKind = "Path" + // Check for optional path name: PATH ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "ELEMENTS": + // Check for XSINIL or ABSENT + nextKeyword := strings.ToUpper(p.curTok.Literal) + if nextKeyword == "XSINIL" { + option.OptionKind = "ElementsXsiNil" + p.nextToken() // consume XSINIL + } else if nextKeyword == "ABSENT" { + option.OptionKind = "ElementsAbsent" + p.nextToken() // consume ABSENT + } else { + option.OptionKind = "Elements" + } + case "XMLDATA": + option.OptionKind = "XmlData" + case "XMLSCHEMA": + option.OptionKind = "XmlSchema" + // Check for optional namespace: XMLSCHEMA ('namespace') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "ROOT": + option.OptionKind = "Root" + // Check for optional root name: ROOT ('name') + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if p.curTok.Type == TokenString { + option.Value = p.parseStringLiteralValue() + p.nextToken() // consume string + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + case "TYPE": + option.OptionKind = "Type" + case "BINARY": + // BINARY BASE64 + if strings.ToUpper(p.curTok.Literal) == "BASE64" { + option.OptionKind = "BinaryBase64" + p.nextToken() // consume BASE64 + } + default: + option.OptionKind = keyword + } + + return option, nil +} diff --git a/parser/parse_statements.go b/parser/parse_statements.go index ccee32e0..d674711f 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -238,14 +238,23 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { constraint := &ast.UniqueConstraintDefinition{ IsPrimaryKey: true, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { constraint.Clustered = true constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } // Parse the column list @@ -285,20 +294,57 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { p.nextToken() } } + // Parse WITH (index_options) + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } return constraint, nil } else if upperLit == "UNIQUE" { p.nextToken() // consume UNIQUE constraint := &ast.UniqueConstraintDefinition{ IsPrimaryKey: false, } - // Parse optional CLUSTERED/NONCLUSTERED + // Parse optional CLUSTERED/NONCLUSTERED/HASH if strings.ToUpper(p.curTok.Literal) == "CLUSTERED" { constraint.Clustered = true constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} p.nextToken() } else if strings.ToUpper(p.curTok.Literal) == "NONCLUSTERED" { constraint.Clustered = false - constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + // Check for HASH suffix + if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() + } else { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + } + } else if strings.ToUpper(p.curTok.Literal) == "HASH" { + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} p.nextToken() } // Parse the column list @@ -338,6 +384,34 @@ func (p *Parser) parseTableConstraint() (ast.TableConstraint, error) { p.nextToken() } } + // Parse WITH (index_options) + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + expr, _ := p.parseScalarExpression() + option := &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: expr, + } + constraint.IndexOptions = append(constraint.IndexOptions, option) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } return constraint, nil } else if upperLit == "FOREIGN" { p.nextToken() // consume FOREIGN @@ -597,10 +671,13 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { var quoteType string literal := p.curTok.Literal - // Check if this is a bracketed identifier like [int] + // Check if this is a bracketed or quoted identifier if len(literal) >= 2 && literal[0] == '[' && literal[len(literal)-1] == ']' { typeName = literal[1 : len(literal)-1] quoteType = "SquareBracket" + } else if len(literal) >= 2 && literal[0] == '"' && literal[len(literal)-1] == '"' { + typeName = literal[1 : len(literal)-1] + quoteType = "DoubleQuote" } else { typeName = literal quoteType = "NotQuoted" @@ -620,10 +697,20 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { XmlDataTypeOption: "None", Name: baseName, } - // Check for schema collection: XML(schema_collection) + // Check for schema collection: XML(CONTENT|DOCUMENT schema_collection) if p.curTok.Type == TokenLParen { p.nextToken() // consume ( + // Check for CONTENT or DOCUMENT keyword + upper := strings.ToUpper(p.curTok.Literal) + if upper == "CONTENT" { + xmlRef.XmlDataTypeOption = "Content" + p.nextToken() + } else if upper == "DOCUMENT" { + xmlRef.XmlDataTypeOption = "Document" + p.nextToken() + } + // Parse the schema collection name schemaName, err := p.parseSchemaObjectName() if err != nil { @@ -642,11 +729,79 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { // Check if this is a known SQL data type sqlOption, isKnownType := getSqlDataTypeOption(typeName) + // Check for multi-word types: CHAR VARYING -> VarChar, DOUBLE PRECISION -> Float + if upper := strings.ToUpper(typeName); upper == "CHAR" || upper == "DOUBLE" { + nextUpper := strings.ToUpper(p.curTok.Literal) + if upper == "CHAR" && nextUpper == "VARYING" { + sqlOption = "VarChar" + isKnownType = true + p.nextToken() // consume VARYING + } else if upper == "DOUBLE" && nextUpper == "PRECISION" { + baseName.BaseIdentifier.Value = "FLOAT" // Use FLOAT for output + sqlOption = "Float" + isKnownType = true + p.nextToken() // consume PRECISION + } + } + if !isKnownType { - // Return UserDataTypeReference for unknown types - return &ast.UserDataTypeReference{ + // Check for multi-part type name (e.g., dbo.mytype) + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + // Get the next identifier + nextIdent := p.parseIdentifier() + // Schema.Type structure + baseName.SchemaIdentifier = baseId + baseName.BaseIdentifier = nextIdent + baseName.Count = 2 + baseName.Identifiers = []*ast.Identifier{baseId, nextIdent} + + // Check for third part: database.schema.type + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + thirdIdent := p.parseIdentifier() + // Database.Schema.Type structure + baseName.DatabaseIdentifier = baseId + baseName.SchemaIdentifier = nextIdent + baseName.BaseIdentifier = thirdIdent + baseName.Count = 3 + baseName.Identifiers = []*ast.Identifier{baseId, nextIdent, thirdIdent} + } + } + + userRef := &ast.UserDataTypeReference{ Name: baseName, - }, nil + } + + // Check for parameters: mytype(10) or mytype(10, 20) or mytype(max) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + // Special case: MAX keyword + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "MAX" { + userRef.Parameters = append(userRef.Parameters, &ast.MaxLiteral{ + LiteralType: "Max", + Value: p.curTok.Literal, + }) + p.nextToken() + } else { + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + userRef.Parameters = append(userRef.Parameters, expr) + } + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + return userRef, nil } dt := &ast.SqlDataTypeReference{ @@ -1244,6 +1399,10 @@ func (p *Parser) parseBeginStatement() (ast.Statement, error) { return p.parseBeginTransactionStatementContinued(false) case TokenTry: return p.parseTryCatchStatement() + case TokenDialog: + return p.parseBeginDialogStatement() + case TokenConversation: + return p.parseBeginConversationTimerStatement() case TokenIdent: // Check for DISTRIBUTED if strings.ToUpper(p.curTok.Literal) == "DISTRIBUTED" { @@ -1444,12 +1603,28 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { } // Parse statements until END TRY - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + for p.curTok.Type != TokenEOF { // Skip semicolons if p.curTok.Type == TokenSemicolon { p.nextToken() continue } + // Check for END TRY (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.TryStatements.Statements = append(stmt.TryStatements.Statements, endConvStmt) + } + continue + } + // It's END TRY, break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1478,12 +1653,28 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { stmt.CatchStatements = &ast.StatementList{} // Parse catch statements until END CATCH - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + for p.curTok.Type != TokenEOF { // Skip semicolons if p.curTok.Type == TokenSemicolon { p.nextToken() continue } + // Check for END CATCH (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.CatchStatements.Statements = append(stmt.CatchStatements.Statements, endConvStmt) + } + continue + } + // It's END CATCH, break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1514,8 +1705,24 @@ func (p *Parser) parseBeginEndBlockStatementContinued() (*ast.BeginEndBlockState StatementList: &ast.StatementList{}, } - // Parse statements until END - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + // Parse statements until END (but not END CONVERSATION) + for p.curTok.Type != TokenEOF { + // Check for END (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.StatementList.Statements = append(stmt.StatementList.Statements, endConvStmt) + } + continue + } + // It's END (block terminator), break + break + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1538,28 +1745,211 @@ func (p *Parser) parseBeginEndBlockStatementContinued() (*ast.BeginEndBlockState return stmt, nil } -func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, error) { - // Consume BEGIN - p.nextToken() +func (p *Parser) parseBeginDialogStatement() (*ast.BeginDialogStatement, error) { + p.nextToken() // consume DIALOG - stmt := &ast.BeginEndBlockStatement{ - StatementList: &ast.StatementList{}, + stmt := &ast.BeginDialogStatement{} + + // Check for optional CONVERSATION keyword + if p.curTok.Type == TokenConversation { + stmt.IsConversation = true + p.nextToken() // consume CONVERSATION } - // Parse statements until END - for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { - s, err := p.parseStatement() + // Parse dialog handle (variable reference) + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected variable for dialog handle") + } + + // Parse FROM SERVICE + if p.curTok.Type != TokenFrom { + return nil, fmt.Errorf("expected FROM after dialog handle") + } + p.nextToken() // consume FROM + + if strings.ToUpper(p.curTok.Literal) != "SERVICE" { + return nil, fmt.Errorf("expected SERVICE after FROM") + } + p.nextToken() // consume SERVICE + + // Parse initiator service name (identifier) + id := p.parseIdentifier() + stmt.InitiatorServiceName = &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + } + + // Parse TO SERVICE + if p.curTok.Type != TokenTo { + return nil, fmt.Errorf("expected TO after initiator service name") + } + p.nextToken() // consume TO + + if strings.ToUpper(p.curTok.Literal) != "SERVICE" { + return nil, fmt.Errorf("expected SERVICE after TO") + } + p.nextToken() // consume SERVICE + + // Parse target service name (string literal or variable) + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, err := p.parseStringLiteral() if err != nil { return nil, err } - if s != nil { - stmt.StatementList.Statements = append(stmt.StatementList.Statements, s) + stmt.TargetServiceName = strLit + } else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.TargetServiceName = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected string literal or variable for target service name") + } + + // Check for optional instance spec (after comma) + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, err := p.parseStringLiteral() + if err != nil { + return nil, err + } + stmt.InstanceSpec = strLit + } else if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.InstanceSpec = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() } } - // Consume END - if p.curTok.Type == TokenEnd { + // Parse optional ON CONTRACT + if p.curTok.Type == TokenOn && strings.ToUpper(p.peekTok.Literal) == "CONTRACT" { + p.nextToken() // consume ON + p.nextToken() // consume CONTRACT + id := p.parseIdentifier() + stmt.ContractName = &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + } + } + + // Parse optional WITH options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + for { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + switch optName { + case "RELATED_CONVERSATION": + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.VariableReference{Name: p.curTok.Literal}, + OptionKind: "RelatedConversation", + }) + p.nextToken() + } + case "RELATED_CONVERSATION_GROUP": + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.VariableReference{Name: p.curTok.Literal}, + OptionKind: "RelatedConversationGroup", + }) + p.nextToken() + } + case "ENCRYPTION": + optState := strings.ToUpper(p.curTok.Literal) + if optState == "ON" { + stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{ + OptionState: "On", + OptionKind: "Encryption", + }) + } else if optState == "OFF" { + stmt.Options = append(stmt.Options, &ast.OnOffDialogOption{ + OptionState: "Off", + OptionKind: "Encryption", + }) + } + p.nextToken() + case "LIFETIME": + if p.curTok.Type == TokenNumber { + stmt.Options = append(stmt.Options, &ast.ScalarExpressionDialogOption{ + Value: &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + }, + OptionKind: "Lifetime", + }) + p.nextToken() + } + } + + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume comma + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseBeginConversationTimerStatement() (*ast.BeginConversationTimerStatement, error) { + p.nextToken() // consume CONVERSATION + + // Expect TIMER + if strings.ToUpper(p.curTok.Literal) != "TIMER" { + return nil, fmt.Errorf("expected TIMER after BEGIN CONVERSATION") + } + p.nextToken() // consume TIMER + + stmt := &ast.BeginConversationTimerStatement{} + + // Parse handle in parentheses + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after TIMER") + } + p.nextToken() // consume ( + + if p.curTok.Type == TokenIdent && len(p.curTok.Literal) > 0 && p.curTok.Literal[0] == '@' { + stmt.Handle = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + return nil, fmt.Errorf("expected variable for conversation handle") + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after handle") + } + p.nextToken() // consume ) + + // Parse TIMEOUT = value + if strings.ToUpper(p.curTok.Literal) != "TIMEOUT" { + return nil, fmt.Errorf("expected TIMEOUT") + } + p.nextToken() // consume TIMEOUT + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + if p.curTok.Type == TokenNumber { + stmt.Timeout = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } p.nextToken() + } else { + return nil, fmt.Errorf("expected integer for timeout value") } // Skip optional semicolon @@ -1570,23 +1960,71 @@ func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, err return stmt, nil } -func (p *Parser) parseCreateStatement() (ast.Statement, error) { - // Consume CREATE +func (p *Parser) parseBeginEndBlockStatement() (*ast.BeginEndBlockStatement, error) { + // Consume BEGIN p.nextToken() - switch p.curTok.Type { - case TokenOr: - // Handle CREATE OR ALTER - p.nextToken() // consume OR - if strings.ToUpper(p.curTok.Literal) != "ALTER" { - return nil, fmt.Errorf("expected ALTER after CREATE OR, got %s", p.curTok.Literal) - } - p.nextToken() // consume ALTER - switch p.curTok.Type { - case TokenFunction: - return p.parseCreateOrAlterFunctionStatement() - case TokenProcedure: - return p.parseCreateOrAlterProcedureStatement() + stmt := &ast.BeginEndBlockStatement{ + StatementList: &ast.StatementList{}, + } + + // Parse statements until END (but not END CONVERSATION) + for p.curTok.Type != TokenEOF { + // Check for END (not END CONVERSATION) + if p.curTok.Type == TokenEnd { + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION, parse it + endConvStmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if endConvStmt != nil { + stmt.StatementList.Statements = append(stmt.StatementList.Statements, endConvStmt) + } + continue + } + // It's END (block terminator), break + break + } + s, err := p.parseStatement() + if err != nil { + return nil, err + } + if s != nil { + stmt.StatementList.Statements = append(stmt.StatementList.Statements, s) + } + } + + // Consume END + if p.curTok.Type == TokenEnd { + p.nextToken() + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseCreateStatement() (ast.Statement, error) { + // Consume CREATE + p.nextToken() + + switch p.curTok.Type { + case TokenOr: + // Handle CREATE OR ALTER + p.nextToken() // consume OR + if strings.ToUpper(p.curTok.Literal) != "ALTER" { + return nil, fmt.Errorf("expected ALTER after CREATE OR, got %s", p.curTok.Literal) + } + p.nextToken() // consume ALTER + switch p.curTok.Type { + case TokenFunction: + return p.parseCreateOrAlterFunctionStatement() + case TokenProcedure: + return p.parseCreateOrAlterProcedureStatement() case TokenView: return p.parseCreateOrAlterViewStatement() case TokenTrigger: @@ -1715,6 +2153,8 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { return p.parseCreateSequenceStatement() case "SPATIAL": return p.parseCreateSpatialIndexStatement() + case "MATERIALIZED": + return p.parseCreateMaterializedViewStatement() case "SERVER": // Check if it's SERVER ROLE or SERVER AUDIT p.nextToken() // consume SERVER @@ -2769,8 +3209,21 @@ func (p *Parser) parseStatementList() (*ast.StatementList, error) { continue } - // Check for END (end of BEGIN block or TRY/CATCH) + // Check for END (end of BEGIN block or TRY/CATCH, or END CONVERSATION statement) if p.curTok.Type == TokenEnd { + // Look ahead to check if it's END CONVERSATION (a statement) + if p.peekTok.Type == TokenConversation { + // It's END CONVERSATION statement, parse it + stmt, err := p.parseEndConversationStatement() + if err != nil { + return nil, err + } + if stmt != nil { + sl.Statements = append(sl.Statements, stmt) + } + continue + } + // Otherwise it's the end of a BEGIN block break } @@ -2825,7 +3278,7 @@ func (p *Parser) parseCreateViewStatement() (*ast.CreateViewStatement, error) { p.nextToken() // Parse view options for p.curTok.Type == TokenIdent { - opt := ast.ViewOption{OptionKind: p.curTok.Literal} + opt := &ast.ViewStatementOption{OptionKind: p.curTok.Literal} stmt.ViewOptions = append(stmt.ViewOptions, opt) p.nextToken() if p.curTok.Type == TokenComma { @@ -2853,6 +3306,104 @@ func (p *Parser) parseCreateViewStatement() (*ast.CreateViewStatement, error) { return stmt, nil } +func (p *Parser) parseCreateMaterializedViewStatement() (*ast.CreateViewStatement, error) { + // Consume MATERIALIZED + p.nextToken() + + // Expect VIEW + if p.curTok.Type != TokenView { + return nil, fmt.Errorf("expected VIEW after MATERIALIZED, got %s", p.curTok.Literal) + } + p.nextToken() + + stmt := &ast.CreateViewStatement{ + IsMaterialized: true, + } + + // Parse view name + son, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + stmt.SchemaObjectName = son + + // Parse WITH options for materialized view + if p.curTok.Type == TokenWith || strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if optionName == "DISTRIBUTION" { + // Parse DISTRIBUTION = HASH(col1, col2, ...) + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "HASH" { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + distOpt := &ast.ViewDistributionOption{ + OptionKind: "Distribution", + Value: &ast.ViewHashDistributionPolicy{}, + } + // Parse column list + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col := p.parseIdentifier() + if distOpt.Value.DistributionColumn == nil { + distOpt.Value.DistributionColumn = col + } + distOpt.Value.DistributionColumns = append(distOpt.Value.DistributionColumns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + stmt.ViewOptions = append(stmt.ViewOptions, distOpt) + } + } + } else if optionName == "FOR_APPEND" { + stmt.ViewOptions = append(stmt.ViewOptions, &ast.ViewForAppendOption{ + OptionKind: "ForAppend", + }) + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } else if p.curTok.Type != TokenRParen { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } + + // Expect AS + if p.curTok.Type != TokenAs { + p.skipToEndOfStatement() + return stmt, nil + } + p.nextToken() + + // Parse SELECT statement + selStmt, err := p.parseSelectStatement() + if err != nil { + p.skipToEndOfStatement() + return stmt, nil + } + stmt.SelectStatement = selStmt + + return stmt, nil +} + func (p *Parser) parseCreateSchemaStatement() (*ast.CreateSchemaStatement, error) { // Consume SCHEMA p.nextToken() @@ -5888,7 +6439,6 @@ func (p *Parser) externalFileFormatOptionKind(name string) string { } func (p *Parser) parseCreateExternalTableStatement() (*ast.CreateExternalTableStatement, error) { - // TABLE name - skip rest of statement for now p.nextToken() // consume TABLE name, err := p.parseSchemaObjectName() @@ -5899,16 +6449,133 @@ func (p *Parser) parseCreateExternalTableStatement() (*ast.CreateExternalTableSt SchemaObjectName: name, } - // Skip rest of statement - for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { - p.nextToken() + // Parse column definitions in parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef, err := p.parseExternalTableColumnDefinition() + if err != nil { + return nil, err + } + stmt.ColumnDefinitions = append(stmt.ColumnDefinitions, colDef) + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } } + + // Parse WITH clause for options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + switch optName { + case "DATA_SOURCE": + stmt.DataSource = p.parseIdentifier() + case "LOCATION", "FILE_FORMAT", "TABLE_OPTIONS": + opt := &ast.ExternalTableLiteralOrIdentifierOption{ + Value: &ast.IdentifierOrValueExpression{}, + } + switch optName { + case "LOCATION": + opt.OptionKind = "Location" + case "FILE_FORMAT": + opt.OptionKind = "FileFormat" + case "TABLE_OPTIONS": + opt.OptionKind = "TableOptions" + } + + // Parse the value (can be identifier or string literal) + if p.curTok.Type == TokenString { + strLit := p.parseStringLiteralValue() + p.nextToken() // consume string + opt.Value.Value = strLit.Value + opt.Value.ValueExpression = strLit + } else if p.curTok.Type == TokenNationalString { + strLit, _ := p.parseNationalStringFromToken() + opt.Value.Value = strLit.Value + opt.Value.ValueExpression = strLit + } else if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + ident := p.parseIdentifier() + opt.Value.Value = ident.Value + opt.Value.Identifier = ident + } + stmt.ExternalTableOptions = append(stmt.ExternalTableOptions, opt) + default: + // Skip unknown options + for p.curTok.Type != TokenComma && p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + if p.curTok.Type == TokenSemicolon { p.nextToken() } return stmt, nil } +func (p *Parser) parseExternalTableColumnDefinition() (*ast.ExternalTableColumnDefinition, error) { + colDef := &ast.ExternalTableColumnDefinition{ + ColumnDefinition: &ast.ColumnDefinitionBase{}, + } + + // Parse column name + colDef.ColumnDefinition.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + dt, err := p.parseDataType() + if err != nil { + return nil, err + } + colDef.ColumnDefinition.DataType = dt + + // Parse optional COLLATE + if strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + colDef.ColumnDefinition.Collation = p.parseIdentifier() + } + + // Parse optional NULL/NOT NULL + if strings.ToUpper(p.curTok.Literal) == "NOT" { + p.nextToken() // consume NOT + if strings.ToUpper(p.curTok.Literal) == "NULL" { + p.nextToken() // consume NULL + colDef.NullableConstraint = &ast.NullableConstraintDefinition{ + Nullable: false, + } + } + } else if strings.ToUpper(p.curTok.Literal) == "NULL" { + p.nextToken() // consume NULL + colDef.NullableConstraint = &ast.NullableConstraintDefinition{ + Nullable: true, + } + } + + return colDef, nil +} + func (p *Parser) parseCreateExternalLanguageStatement() (*ast.CreateExternalLanguageStatement, error) { p.nextToken() // consume LANGUAGE stmt := &ast.CreateExternalLanguageStatement{ @@ -6023,17 +6690,57 @@ func (p *Parser) parseCreateEventSessionStatement() (*ast.CreateEventSessionStat Name: p.parseIdentifier(), } - // ON SERVER + // ON SERVER/DATABASE if p.curTok.Type == TokenOn { p.nextToken() - if strings.ToUpper(p.curTok.Literal) == "SERVER" { + scopeUpper := strings.ToUpper(p.curTok.Literal) + if scopeUpper == "SERVER" { + stmt.SessionScope = "Server" + p.nextToken() + } else if scopeUpper == "DATABASE" { + stmt.SessionScope = "Database" p.nextToken() } } - // Skip rest of statement for now - event sessions are complex + // Parse ADD EVENT/TARGET and WITH clauses for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { - p.nextToken() + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "ADD" { + p.nextToken() + addType := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if addType == "EVENT" { + event := p.parseEventDeclaration() + stmt.EventDeclarations = append(stmt.EventDeclarations, event) + } else if addType == "TARGET" { + target := p.parseTargetDeclaration() + stmt.TargetDeclarations = append(stmt.TargetDeclarations, target) + } + } else if upperLit == "WITH" || p.curTok.Type == TokenWith { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + opt := p.parseSessionOption() + if opt != nil { + stmt.SessionOptions = append(stmt.SessionOptions, opt) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + p.nextToken() + } } if p.curTok.Type == TokenSemicolon { p.nextToken() @@ -6041,6 +6748,300 @@ func (p *Parser) parseCreateEventSessionStatement() (*ast.CreateEventSessionStat return stmt, nil } +func (p *Parser) parseEventDeclaration() *ast.EventDeclaration { + event := &ast.EventDeclaration{} + + // Parse package.event_name + event.ObjectName = p.parseEventSessionObjectName() + + // Parse optional ( ACTION(...) WHERE ... ) + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "ACTION" { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + actionName := p.parseEventSessionObjectName() + event.EventDeclarationActionParameters = append(event.EventDeclarationActionParameters, actionName) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else if upperLit == "WHERE" { + p.nextToken() + event.EventDeclarationPredicateParameter = p.parseEventPredicate() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return event +} + +func (p *Parser) parseTargetDeclaration() *ast.TargetDeclaration { + target := &ast.TargetDeclaration{} + + // Parse package.target_name + target.ObjectName = p.parseEventSessionObjectName() + + // Parse optional ( SET ... ) + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if strings.ToUpper(p.curTok.Literal) == "SET" { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + param := &ast.EventDeclarationSetParameter{ + EventField: p.parseIdentifier(), + } + if p.curTok.Type == TokenEquals { + p.nextToken() + param.EventValue, _ = p.parseScalarExpression() + } + target.TargetDeclarationParameters = append(target.TargetDeclarationParameters, param) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return target +} + +func (p *Parser) parseEventSessionObjectName() *ast.EventSessionObjectName { + var identifiers []*ast.Identifier + + for { + if p.curTok.Type != TokenIdent && p.curTok.Type != TokenLBracket { + break + } + identifiers = append(identifiers, p.parseIdentifier()) + if p.curTok.Type != TokenDot { + break + } + p.nextToken() // consume dot + } + + return &ast.EventSessionObjectName{ + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: identifiers, + Count: len(identifiers), + }, + } +} + +func (p *Parser) parseEventPredicate() ast.BooleanExpression { + return p.parseEventPredicateOr() +} + +func (p *Parser) parseEventPredicateOr() ast.BooleanExpression { + left := p.parseEventPredicateAnd() + for strings.ToUpper(p.curTok.Literal) == "OR" { + p.nextToken() + right := p.parseEventPredicateAnd() + left = &ast.BooleanBinaryExpression{ + BinaryExpressionType: "Or", + FirstExpression: left, + SecondExpression: right, + } + } + return left +} + +func (p *Parser) parseEventPredicateAnd() ast.BooleanExpression { + left := p.parseEventPredicatePrimary() + for strings.ToUpper(p.curTok.Literal) == "AND" { + p.nextToken() + right := p.parseEventPredicatePrimary() + left = &ast.BooleanBinaryExpression{ + BinaryExpressionType: "And", + FirstExpression: left, + SecondExpression: right, + } + } + return left +} + +func (p *Parser) parseEventPredicatePrimary() ast.BooleanExpression { + // Handle parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() + expr := p.parseEventPredicateOr() + if p.curTok.Type == TokenRParen { + p.nextToken() + } + return &ast.BooleanParenthesisExpression{Expression: expr} + } + + // Parse [package].[function_or_field](...) or [package].[field] NOT LIKE 'pattern' + name := p.parseEventSessionObjectName() + + // Check for function call + if p.curTok.Type == TokenLParen { + p.nextToken() + // Parse function parameters + var source *ast.SourceDeclaration + var eventValue ast.ScalarExpression + + // First param is usually a source declaration + sourceName := p.parseEventSessionObjectName() + source = &ast.SourceDeclaration{Value: sourceName} + + if p.curTok.Type == TokenComma { + p.nextToken() + eventValue, _ = p.parseScalarExpression() + } + + if p.curTok.Type == TokenRParen { + p.nextToken() + } + + return &ast.EventDeclarationCompareFunctionParameter{ + Name: name, + SourceDeclaration: source, + EventValue: eventValue, + } + } + + // Check for NOT LIKE or LIKE + notLike := false + if strings.ToUpper(p.curTok.Literal) == "NOT" { + notLike = true + p.nextToken() + } + + if strings.ToUpper(p.curTok.Literal) == "LIKE" { + p.nextToken() + pattern, _ := p.parseScalarExpression() + compType := "Like" + if notLike { + compType = "NotLike" + } + return &ast.BooleanComparisonExpression{ + ComparisonType: compType, + FirstExpression: &ast.SourceDeclaration{Value: name}, + SecondExpression: pattern, + } + } + + // Fallback: return source declaration wrapped in something + return &ast.SourceDeclaration{Value: name} +} + +func (p *Parser) parseSessionOption() ast.SessionOption { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + switch optName { + case "MAX_MEMORY", "MAX_EVENT_SIZE": + value, _ := p.parseScalarExpression() + unit := "" + if strings.ToUpper(p.curTok.Literal) == "KB" || strings.ToUpper(p.curTok.Literal) == "MB" { + unit = strings.ToUpper(p.curTok.Literal) + p.nextToken() + } + return &ast.LiteralSessionOption{ + OptionKind: p.sessionOptionKind(optName), + Value: value, + Unit: unit, + } + case "EVENT_RETENTION_MODE": + value := p.curTok.Literal + p.nextToken() + return &ast.EventRetentionSessionOption{ + OptionKind: "EventRetention", + Value: p.eventRetentionValue(value), + } + case "MAX_DISPATCH_LATENCY": + value, _ := p.parseScalarExpression() + // Check for SECONDS + if strings.ToUpper(p.curTok.Literal) == "SECONDS" { + p.nextToken() + } + return &ast.MaxDispatchLatencySessionOption{ + OptionKind: "MaxDispatchLatency", + Value: value, + IsInfinite: false, + } + case "MEMORY_PARTITION_MODE": + value := p.curTok.Literal + p.nextToken() + return &ast.MemoryPartitionSessionOption{ + OptionKind: "MemoryPartition", + Value: p.capitalizeFirst(strings.ToLower(value)), + } + case "TRACK_CAUSALITY", "STARTUP_STATE": + stateUpper := strings.ToUpper(p.curTok.Literal) + p.nextToken() + state := "Off" + if stateUpper == "ON" { + state = "On" + } + return &ast.OnOffSessionOption{ + OptionKind: p.sessionOptionKind(optName), + OptionState: state, + } + default: + // Skip unknown option value + p.nextToken() + return nil + } +} + +func (p *Parser) sessionOptionKind(name string) string { + switch name { + case "MAX_MEMORY": + return "MaxMemory" + case "MAX_EVENT_SIZE": + return "MaxEventSize" + case "TRACK_CAUSALITY": + return "TrackCausality" + case "STARTUP_STATE": + return "StartUpState" + default: + return name + } +} + +func (p *Parser) eventRetentionValue(value string) string { + switch strings.ToUpper(value) { + case "ALLOW_SINGLE_EVENT_LOSS": + return "AllowSingleEventLoss" + case "ALLOW_MULTIPLE_EVENT_LOSS": + return "AllowMultipleEventLoss" + case "NO_EVENT_LOSS": + return "NoEventLoss" + default: + return value + } +} + func (p *Parser) parseCreateEventSessionStatementFromEvent() (*ast.CreateEventSessionStatement, error) { // EVENT has already been consumed, curTok is SESSION p.nextToken() // consume SESSION @@ -6049,16 +7050,61 @@ func (p *Parser) parseCreateEventSessionStatementFromEvent() (*ast.CreateEventSe Name: p.parseIdentifier(), } - // ON SERVER + // ON SERVER/DATABASE if p.curTok.Type == TokenOn { p.nextToken() - if strings.ToUpper(p.curTok.Literal) == "SERVER" { + scopeUpper := strings.ToUpper(p.curTok.Literal) + if scopeUpper == "SERVER" { + stmt.SessionScope = "Server" + p.nextToken() + } else if scopeUpper == "DATABASE" { + stmt.SessionScope = "Database" p.nextToken() } } - // Skip rest of statement - p.skipToEndOfStatement() + // Parse ADD EVENT/TARGET and WITH clauses + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && !p.isStatementTerminator() { + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "ADD" { + p.nextToken() + addType := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + if addType == "EVENT" { + event := p.parseEventDeclaration() + stmt.EventDeclarations = append(stmt.EventDeclarations, event) + } else if addType == "TARGET" { + target := p.parseTargetDeclaration() + stmt.TargetDeclarations = append(stmt.TargetDeclarations, target) + } + } else if upperLit == "WITH" || p.curTok.Type == TokenWith { + p.nextToken() + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + opt := p.parseSessionOption() + if opt != nil { + stmt.SessionOptions = append(stmt.SessionOptions, opt) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + p.nextToken() + } + } + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } return stmt, nil } @@ -7087,7 +8133,14 @@ func (p *Parser) parseCreateIndexStatement() (*ast.CreateIndexStatement, error) // Parse WITH (index options) if p.curTok.Type == TokenWith { p.nextToken() // consume WITH - stmt.IndexOptions = p.parseCreateIndexOptions() + // Check if this is SQL 80 style (no parentheses) or modern style (with parentheses) + if p.curTok.Type == TokenLParen { + stmt.IndexOptions = p.parseCreateIndexOptions() + } else { + // SQL 80 style - no parentheses around options + stmt.Translated80SyntaxTo90 = true + stmt.IndexOptions = p.parseCreateIndexOptions80Style() + } } // Parse ON filegroup/partition_scheme @@ -7201,6 +8254,104 @@ func (p *Parser) parseCreateIndexOptions() []ast.IndexOption { return options } +// parseCreateIndexOptions80Style parses index options without parentheses (SQL 80 style) +// e.g., WITH FILLFACTOR = 23, PAD_INDEX +func (p *Parser) parseCreateIndexOptions80Style() []ast.IndexOption { + var options []ast.IndexOption + + for { + // Check if current token could be an index option + upper := strings.ToUpper(p.curTok.Literal) + if !p.isIndexOption80Style(upper) { + break + } + + optionName := upper + p.nextToken() // consume option name + + var valueStr string + var valueToken Token + + // Check if there's an = value + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + valueToken = p.curTok + valueStr = strings.ToUpper(valueToken.Literal) + p.nextToken() // consume value + } else { + // No value means this is a flag option that is ON + valueStr = "ON" + } + + switch optionName { + case "PAD_INDEX": + options = append(options, &ast.IndexStateOption{ + OptionKind: "PadIndex", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "FILLFACTOR": + options = append(options, &ast.IndexExpressionOption{ + OptionKind: "FillFactor", + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueToken.Literal}, + }) + case "IGNORE_DUP_KEY": + // In SQL 80 style, IGNORE_DUP_KEY uses IndexStateOption + options = append(options, &ast.IndexStateOption{ + OptionKind: "IgnoreDupKey", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "DROP_EXISTING": + options = append(options, &ast.IndexStateOption{ + OptionKind: "DropExisting", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "STATISTICS_NORECOMPUTE": + options = append(options, &ast.IndexStateOption{ + OptionKind: "StatisticsNoRecompute", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + case "SORT_IN_TEMPDB": + options = append(options, &ast.IndexStateOption{ + OptionKind: "SortInTempDB", + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + default: + // Generic handling for other options + if valueStr == "ON" || valueStr == "OFF" { + options = append(options, &ast.IndexStateOption{ + OptionKind: p.getIndexOptionKind(optionName), + OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), + }) + } else if valueToken.Type == TokenNumber || valueToken.Type != 0 { + options = append(options, &ast.IndexExpressionOption{ + OptionKind: p.getIndexOptionKind(optionName), + Expression: &ast.IntegerLiteral{LiteralType: "Integer", Value: valueToken.Literal}, + }) + } + } + + // Check for comma to continue parsing options + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + return options +} + +// isIndexOption80Style checks if a token could be an index option in SQL 80 style +func (p *Parser) isIndexOption80Style(name string) bool { + switch name { + case "PAD_INDEX", "FILLFACTOR", "IGNORE_DUP_KEY", "DROP_EXISTING", + "STATISTICS_NORECOMPUTE", "SORT_IN_TEMPDB": + return true + default: + return false + } +} + func (p *Parser) parseCreateSpatialIndexStatement() (*ast.CreateSpatialIndexStatement, error) { p.nextToken() // consume SPATIAL if p.curTok.Type == TokenIndex { @@ -7827,11 +8978,194 @@ func (p *Parser) parseCreateCertificateStatement() (*ast.CreateCertificateStatem p.nextToken() // consume CERTIFICATE stmt := &ast.CreateCertificateStatement{ - Name: p.parseIdentifier(), + Name: p.parseIdentifier(), + ActiveForBeginDialog: "NotSet", + } + + // Optional AUTHORIZATION + if strings.ToUpper(p.curTok.Literal) == "AUTHORIZATION" { + p.nextToken() + stmt.Owner = p.parseIdentifier() + } + + // Optional ENCRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "ENCRYPTION" { + p.nextToken() // consume ENCRYPTION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.EncryptionPassword = strLit + } + } + } + + // Optional FROM clause + if p.curTok.Type == TokenFrom { + p.nextToken() // consume FROM + sourceType := strings.ToUpper(p.curTok.Literal) + + if sourceType == "ASSEMBLY" { + p.nextToken() // consume ASSEMBLY + stmt.CertificateSource = &ast.AssemblyEncryptionSource{ + Assembly: p.parseIdentifier(), + } + } else if sourceType == "FILE" || sourceType == "EXECUTABLE" { + isExecutable := false + if sourceType == "EXECUTABLE" { + isExecutable = true + p.nextToken() // consume EXECUTABLE + // Next should be FILE + } + if strings.ToUpper(p.curTok.Literal) == "FILE" { + p.nextToken() // consume FILE + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.CertificateSource = &ast.FileEncryptionSource{ + IsExecutable: isExecutable, + File: strLit, + } + } + } + } + } + + // Parse WITH clauses (can appear multiple times for different purposes) + for p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + + // Check if it's PRIVATE KEY or certificate options + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "PRIVATE" { + p.nextToken() // consume PRIVATE + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + switch optName { + case "FILE": + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.PrivateKeyPath = strLit + } + case "DECRYPTION": + // DECRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.DecryptionPassword = strLit + } + } + case "ENCRYPTION": + // ENCRYPTION BY PASSWORD + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + stmt.EncryptionPassword = strLit + } + } + } + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } else { + // Certificate options: SUBJECT, START_DATE, EXPIRY_DATE + for { + optName := strings.ToUpper(p.curTok.Literal) + if optName != "SUBJECT" && optName != "START_DATE" && optName != "EXPIRY_DATE" { + break + } + p.nextToken() // consume option name + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + strLit, _ := p.parseStringLiteral() + kind := "" + switch optName { + case "SUBJECT": + kind = "Subject" + case "START_DATE": + kind = "StartDate" + case "EXPIRY_DATE": + kind = "ExpiryDate" + } + stmt.CertificateOptions = append(stmt.CertificateOptions, &ast.CertificateOption{ + Kind: kind, + Value: strLit, + }) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + } + + // Optional ACTIVE FOR BEGIN_DIALOG + if strings.ToUpper(p.curTok.Literal) == "ACTIVE" { + p.nextToken() // consume ACTIVE + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + } + if strings.ToUpper(p.curTok.Literal) == "BEGIN_DIALOG" { + p.nextToken() // consume BEGIN_DIALOG + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if strings.ToUpper(p.curTok.Literal) == "ON" { + stmt.ActiveForBeginDialog = "On" + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "OFF" { + stmt.ActiveForBeginDialog = "Off" + p.nextToken() + } + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() } - // Skip rest of statement - p.skipToEndOfStatement() return stmt, nil } @@ -8382,7 +9716,7 @@ func (p *Parser) parseCreateFulltextCatalogStatement() (*ast.CreateFullTextCatal } // Parse optional clauses - for p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon && !p.isBatchSeparator() { + for p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon && !p.isBatchSeparator() && !p.isStatementTerminator() { switch strings.ToUpper(p.curTok.Literal) { case "ON": p.nextToken() // consume ON @@ -9287,6 +10621,75 @@ func (p *Parser) parseEnableDisableTriggerStatement(enforcement string) (*ast.En return stmt, nil } +// parseEndConversationStatement parses END CONVERSATION statements +func (p *Parser) parseEndConversationStatement() (*ast.EndConversationStatement, error) { + // Consume END + p.nextToken() + + // Expect CONVERSATION + if strings.ToUpper(p.curTok.Literal) != "CONVERSATION" { + return nil, fmt.Errorf("expected CONVERSATION after END, got %s", p.curTok.Literal) + } + p.nextToken() + + stmt := &ast.EndConversationStatement{} + + // Parse the conversation handle expression + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Conversation = expr + + // Check for WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() + + if strings.ToUpper(p.curTok.Literal) == "CLEANUP" { + stmt.WithCleanup = true + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "ERROR" { + p.nextToken() + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + // Parse error code + errCode, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.ErrorCode = errCode + + // Expect DESCRIPTION + if strings.ToUpper(p.curTok.Literal) == "DESCRIPTION" { + p.nextToken() + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + // Parse error description + errDesc, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.ErrorDescription = errDesc + } + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + // parseCreateWorkloadGroupStatement parses CREATE WORKLOAD GROUP statement. func (p *Parser) parseCreateWorkloadGroupStatement() (*ast.CreateWorkloadGroupStatement, error) { // Consume WORKLOAD diff --git a/parser/parser.go b/parser/parser.go index 3187e3c5..a572618f 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -181,6 +181,12 @@ func (p *Parser) parseStatement() (ast.Statement, error) { return p.parseBackupStatement() case TokenClose: return p.parseCloseStatement() + case TokenEnd: + // Check for END CONVERSATION + if p.peekTok.Type == TokenConversation { + return p.parseEndConversationStatement() + } + return nil, fmt.Errorf("unexpected token: END") case TokenOpen: return p.parseOpenStatement() case TokenDbcc: diff --git a/parser/testdata/AlterIndexStatementTests/metadata.json b/parser/testdata/AlterIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterIndexStatementTests/metadata.json +++ b/parser/testdata/AlterIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlterIndexStatementTests130/metadata.json b/parser/testdata/AlterIndexStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterIndexStatementTests130/metadata.json +++ b/parser/testdata/AlterIndexStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlterProcedureStatementTests/metadata.json b/parser/testdata/AlterProcedureStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterProcedureStatementTests/metadata.json +++ b/parser/testdata/AlterProcedureStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json b/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json +++ b/parser/testdata/Baselines100_ColumnDefinitionTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_InsertStatementTests100/metadata.json b/parser/testdata/Baselines100_InsertStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_InsertStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_InsertStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json b/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_CreateIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines110_SelectWithCollation/metadata.json b/parser/testdata/Baselines110_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines110_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_SelectWithCollation/metadata.json b/parser/testdata/Baselines120_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines120_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json b/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json +++ b/parser/testdata/Baselines120_UniqueConstraintTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json b/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json +++ b/parser/testdata/Baselines130_AlterIndexStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines130_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json b/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json +++ b/parser/testdata/Baselines130_CreateOrAlterStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_SelectWithCollation/metadata.json b/parser/testdata/Baselines130_SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_SelectWithCollation/metadata.json +++ b/parser/testdata/Baselines130_SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_WithinGroupTests130/metadata.json b/parser/testdata/Baselines130_WithinGroupTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_WithinGroupTests130/metadata.json +++ b/parser/testdata/Baselines130_WithinGroupTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines140_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json b/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json +++ b/parser/testdata/Baselines140_DropIndexStatementTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json b/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json +++ b/parser/testdata/Baselines140_MaterializedViewTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json b/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json +++ b/parser/testdata/Baselines140_TrimBuiltInTest140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_WithinGroupTests140/metadata.json b/parser/testdata/Baselines140_WithinGroupTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_WithinGroupTests140/metadata.json +++ b/parser/testdata/Baselines140_WithinGroupTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines150_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json b/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json +++ b/parser/testdata/Baselines150_MaterializedViewTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines160_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json b/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateExternalTableStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json b/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateProcedureStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql b/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql index 179241b1..67cce62b 100644 Binary files a/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql and b/parser/testdata/Baselines160_CreateProcedureStatementTests160/query.sql differ diff --git a/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json b/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json +++ b/parser/testdata/Baselines160_InlineIndexColumnWithINCLUDEtest/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json b/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json +++ b/parser/testdata/Baselines160_MaterializedViewTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/Baselines170_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json b/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json +++ b/parser/testdata/Baselines80_CreateIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json b/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json +++ b/parser/testdata/Baselines90_AlterIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json b/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json +++ b/parser/testdata/Baselines90_BeginConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json b/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json +++ b/parser/testdata/Baselines90_CreateCertificateStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_CreateTableTests90/metadata.json b/parser/testdata/Baselines90_CreateTableTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_CreateTableTests90/metadata.json +++ b/parser/testdata/Baselines90_CreateTableTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json b/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json +++ b/parser/testdata/Baselines90_DeleteStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json b/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json +++ b/parser/testdata/Baselines90_EndConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json b/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json +++ b/parser/testdata/Baselines90_ScalarDataTypeTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json b/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ExecuteStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json b/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ForClauseTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json b/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json +++ b/parser/testdata/BaselinesCommon_SelectExpressionTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/BaselinesFabricDW_CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json b/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json +++ b/parser/testdata/BaselinesFabricDW_CreateExternalTableStatementTestsFabricDW/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BeginConversationStatementTests/metadata.json b/parser/testdata/BeginConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BeginConversationStatementTests/metadata.json +++ b/parser/testdata/BeginConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ColumnDefinitionTests100/metadata.json b/parser/testdata/ColumnDefinitionTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ColumnDefinitionTests100/metadata.json +++ b/parser/testdata/ColumnDefinitionTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateCertificateStatementTests/metadata.json b/parser/testdata/CreateCertificateStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateCertificateStatementTests/metadata.json +++ b/parser/testdata/CreateCertificateStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json b/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json +++ b/parser/testdata/CreateEventSessionNotLikePredicate/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateExternalTableStatementTests160/metadata.json b/parser/testdata/CreateExternalTableStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateExternalTableStatementTests160/metadata.json +++ b/parser/testdata/CreateExternalTableStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json b/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json +++ b/parser/testdata/CreateExternalTableStatementTestsFabricDW/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json b/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json +++ b/parser/testdata/CreateFulltextCatalogStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateFulltextCatalogStatementTests/query.sql b/parser/testdata/CreateFulltextCatalogStatementTests/query.sql index b8047d11..5b1227e7 100644 Binary files a/parser/testdata/CreateFulltextCatalogStatementTests/query.sql and b/parser/testdata/CreateFulltextCatalogStatementTests/query.sql differ diff --git a/parser/testdata/CreateIndexStatementTests/metadata.json b/parser/testdata/CreateIndexStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests/metadata.json +++ b/parser/testdata/CreateIndexStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateIndexStatementTests110/metadata.json b/parser/testdata/CreateIndexStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests110/metadata.json +++ b/parser/testdata/CreateIndexStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateMessageTypeStatementTests/metadata.json b/parser/testdata/CreateMessageTypeStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateMessageTypeStatementTests/metadata.json +++ b/parser/testdata/CreateMessageTypeStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateMessageTypeStatementTests/query.sql b/parser/testdata/CreateMessageTypeStatementTests/query.sql index 36f818ab..38875b95 100644 Binary files a/parser/testdata/CreateMessageTypeStatementTests/query.sql and b/parser/testdata/CreateMessageTypeStatementTests/query.sql differ diff --git a/parser/testdata/CreateOrAlterStatementTests130/metadata.json b/parser/testdata/CreateOrAlterStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateOrAlterStatementTests130/metadata.json +++ b/parser/testdata/CreateOrAlterStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateProcedureStatementTests160/metadata.json b/parser/testdata/CreateProcedureStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateProcedureStatementTests160/metadata.json +++ b/parser/testdata/CreateProcedureStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateProcedureStatementTests90/metadata.json b/parser/testdata/CreateProcedureStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateProcedureStatementTests90/metadata.json +++ b/parser/testdata/CreateProcedureStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DeleteStatementTests90/metadata.json b/parser/testdata/DeleteStatementTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DeleteStatementTests90/metadata.json +++ b/parser/testdata/DeleteStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DropIndexStatementTests140/metadata.json b/parser/testdata/DropIndexStatementTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DropIndexStatementTests140/metadata.json +++ b/parser/testdata/DropIndexStatementTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/EndConversationStatementTests/metadata.json b/parser/testdata/EndConversationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/EndConversationStatementTests/metadata.json +++ b/parser/testdata/EndConversationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ForClauseTests/metadata.json b/parser/testdata/ForClauseTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ForClauseTests/metadata.json +++ b/parser/testdata/ForClauseTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/IdentifierTests/metadata.json b/parser/testdata/IdentifierTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/IdentifierTests/metadata.json +++ b/parser/testdata/IdentifierTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json b/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json +++ b/parser/testdata/InlineIndexColumnWithINCLUDEtest/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/InsertStatementTests100/metadata.json b/parser/testdata/InsertStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/InsertStatementTests100/metadata.json +++ b/parser/testdata/InsertStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests140/metadata.json b/parser/testdata/MaterializedViewTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests140/metadata.json +++ b/parser/testdata/MaterializedViewTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests150/metadata.json b/parser/testdata/MaterializedViewTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests150/metadata.json +++ b/parser/testdata/MaterializedViewTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/MaterializedViewTests160/metadata.json b/parser/testdata/MaterializedViewTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/MaterializedViewTests160/metadata.json +++ b/parser/testdata/MaterializedViewTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ScalarDataTypeTests90/metadata.json b/parser/testdata/ScalarDataTypeTests90/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ScalarDataTypeTests90/metadata.json +++ b/parser/testdata/ScalarDataTypeTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/SelectWithCollation/metadata.json b/parser/testdata/SelectWithCollation/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/SelectWithCollation/metadata.json +++ b/parser/testdata/SelectWithCollation/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/TrimBuiltInTest140/metadata.json b/parser/testdata/TrimBuiltInTest140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/TrimBuiltInTest140/metadata.json +++ b/parser/testdata/TrimBuiltInTest140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/UniqueConstraintTests120/metadata.json b/parser/testdata/UniqueConstraintTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/UniqueConstraintTests120/metadata.json +++ b/parser/testdata/UniqueConstraintTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/WithinGroupTests130/metadata.json b/parser/testdata/WithinGroupTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WithinGroupTests130/metadata.json +++ b/parser/testdata/WithinGroupTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/WithinGroupTests140/metadata.json b/parser/testdata/WithinGroupTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WithinGroupTests140/metadata.json +++ b/parser/testdata/WithinGroupTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{}