diff --git a/ast/alter_database_set_statement.go b/ast/alter_database_set_statement.go index 1a4cc294..99433c54 100644 --- a/ast/alter_database_set_statement.go +++ b/ast/alter_database_set_statement.go @@ -87,7 +87,11 @@ func (l *LiteralDatabaseOption) createDatabaseOption() {} // AlterDatabaseAddFileStatement represents ALTER DATABASE ... ADD FILE statement type AlterDatabaseAddFileStatement struct { - DatabaseName *Identifier + DatabaseName *Identifier + FileDeclarations []*FileDeclaration + FileGroup *Identifier + IsLog bool + UseCurrent bool } func (a *AlterDatabaseAddFileStatement) node() {} diff --git a/ast/alter_function_statement.go b/ast/alter_function_statement.go index 4cb0a1d4..7be82abf 100644 --- a/ast/alter_function_statement.go +++ b/ast/alter_function_statement.go @@ -5,7 +5,7 @@ type AlterFunctionStatement struct { Name *SchemaObjectName Parameters []*ProcedureParameter ReturnType FunctionReturnType - Options []*FunctionOption + Options []FunctionOptionBase StatementList *StatementList } @@ -17,7 +17,7 @@ type CreateFunctionStatement struct { Name *SchemaObjectName Parameters []*ProcedureParameter ReturnType FunctionReturnType - Options []*FunctionOption + Options []FunctionOptionBase StatementList *StatementList } @@ -50,8 +50,37 @@ type SelectFunctionReturnType struct { func (r *SelectFunctionReturnType) functionReturnTypeNode() {} -// FunctionOption represents a function option +// FunctionOptionBase is an interface for function options +type FunctionOptionBase interface { + Node + functionOption() +} + +// FunctionOption represents a function option (like ENCRYPTION, SCHEMABINDING) type FunctionOption struct { - OptionKind string - OptionState string + OptionKind string } + +func (o *FunctionOption) node() {} +func (o *FunctionOption) functionOption() {} + +// InlineFunctionOption represents an INLINE function option +type InlineFunctionOption struct { + OptionKind string // "Inline" + OptionState string // "On", "Off" +} + +func (o *InlineFunctionOption) node() {} +func (o *InlineFunctionOption) functionOption() {} + +// CreateOrAlterFunctionStatement represents a CREATE OR ALTER FUNCTION statement +type CreateOrAlterFunctionStatement struct { + Name *SchemaObjectName + Parameters []*ProcedureParameter + ReturnType FunctionReturnType + Options []FunctionOptionBase + StatementList *StatementList +} + +func (s *CreateOrAlterFunctionStatement) statement() {} +func (s *CreateOrAlterFunctionStatement) node() {} diff --git a/ast/alter_simple_statements.go b/ast/alter_simple_statements.go index e49e4756..cb25967e 100644 --- a/ast/alter_simple_statements.go +++ b/ast/alter_simple_statements.go @@ -2,7 +2,8 @@ package ast // AlterRouteStatement represents an ALTER ROUTE statement. type AlterRouteStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + RouteOptions []*RouteOption `json:"RouteOptions,omitempty"` } func (s *AlterRouteStatement) node() {} @@ -18,15 +19,65 @@ func (s *AlterAssemblyStatement) statement() {} // AlterEndpointStatement represents an ALTER ENDPOINT statement. type AlterEndpointStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + State string `json:"State,omitempty"` // Started, Disabled, NotSpecified + Affinity *EndpointAffinity `json:"Affinity,omitempty"` + Protocol string `json:"Protocol,omitempty"` // None, Tcp, Http + ProtocolOptions []EndpointProtocolOption `json:"ProtocolOptions,omitempty"` + EndpointType string `json:"EndpointType,omitempty"` // NotSpecified, Soap, ServiceBroker, etc. + PayloadOptions []PayloadOption `json:"PayloadOptions,omitempty"` } func (s *AlterEndpointStatement) node() {} func (s *AlterEndpointStatement) statement() {} +// EndpointAffinity represents the affinity setting for an endpoint. +type EndpointAffinity struct { + Kind string `json:"Kind,omitempty"` // None, Admin, Integer + Value *IntegerLiteral `json:"Value,omitempty"` +} + +func (e *EndpointAffinity) node() {} + +// EndpointProtocolOption is an interface for endpoint protocol options. +type EndpointProtocolOption interface { + Node + endpointProtocolOption() +} + +// LiteralEndpointProtocolOption represents a literal endpoint protocol option. +type LiteralEndpointProtocolOption struct { + Value ScalarExpression `json:"Value,omitempty"` + Kind string `json:"Kind,omitempty"` // TcpListenerPort, HttpListenerPort, etc. +} + +func (l *LiteralEndpointProtocolOption) node() {} +func (l *LiteralEndpointProtocolOption) endpointProtocolOption() {} + +// PayloadOption is an interface for endpoint payload options. +type PayloadOption interface { + Node + payloadOption() +} + +// SoapMethod represents a SOAP web method option. +type SoapMethod struct { + Alias *StringLiteral `json:"Alias,omitempty"` + Action string `json:"Action,omitempty"` // Add, Alter, Drop + Name *StringLiteral `json:"Name,omitempty"` + Format string `json:"Format,omitempty"` // NotSpecified, AllResults, RowsetsOnly, None + Schema string `json:"Schema,omitempty"` // NotSpecified, Default, None, Standard + Kind string `json:"Kind,omitempty"` // None, WebMethod +} + +func (s *SoapMethod) node() {} +func (s *SoapMethod) payloadOption() {} + // AlterServiceStatement represents an ALTER SERVICE statement. type AlterServiceStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + QueueName *SchemaObjectName `json:"QueueName,omitempty"` + ServiceContracts []*ServiceContract `json:"ServiceContracts,omitempty"` } func (s *AlterServiceStatement) node() {} diff --git a/ast/alter_trigger_statement.go b/ast/alter_trigger_statement.go index 89fc0297..7f528b69 100644 --- a/ast/alter_trigger_statement.go +++ b/ast/alter_trigger_statement.go @@ -43,10 +43,12 @@ func (o *TriggerOption) triggerOption() {} // ExecuteAsClause represents an EXECUTE AS clause type ExecuteAsClause struct { - ExecuteAsOption string // Caller, Self, Owner, or specific user - Principal ScalarExpression + ExecuteAsOption string // Caller, Self, Owner, String + Literal *StringLiteral // Used when ExecuteAsOption is "String" } +func (e *ExecuteAsClause) node() {} + // ExecuteAsTriggerOption represents an EXECUTE AS trigger option type ExecuteAsTriggerOption struct { OptionKind string // "ExecuteAsClause" diff --git a/ast/alter_user_statement.go b/ast/alter_user_statement.go index 35553ba1..01f5181d 100644 --- a/ast/alter_user_statement.go +++ b/ast/alter_user_statement.go @@ -2,8 +2,8 @@ package ast // AlterUserStatement represents an ALTER USER statement. type AlterUserStatement struct { - Name *Identifier `json:"Name,omitempty"` - Options []UserOption `json:"Options,omitempty"` + Name *Identifier `json:"Name,omitempty"` + UserOptions []UserOption `json:"UserOptions,omitempty"` } func (s *AlterUserStatement) node() {} diff --git a/ast/column_master_key_statement.go b/ast/column_master_key_statement.go new file mode 100644 index 00000000..512f31f2 --- /dev/null +++ b/ast/column_master_key_statement.go @@ -0,0 +1,51 @@ +package ast + +// CreateColumnMasterKeyStatement represents a CREATE COLUMN MASTER KEY statement. +type CreateColumnMasterKeyStatement struct { + Name *Identifier + Parameters []ColumnMasterKeyParameter +} + +func (c *CreateColumnMasterKeyStatement) node() {} +func (c *CreateColumnMasterKeyStatement) statement() {} + +// ColumnMasterKeyParameter is an interface for column master key parameters. +type ColumnMasterKeyParameter interface { + Node + columnMasterKeyParameter() +} + +// ColumnMasterKeyStoreProviderNameParameter represents KEY_STORE_PROVIDER_NAME parameter. +type ColumnMasterKeyStoreProviderNameParameter struct { + Name ScalarExpression + ParameterKind string +} + +func (c *ColumnMasterKeyStoreProviderNameParameter) node() {} +func (c *ColumnMasterKeyStoreProviderNameParameter) columnMasterKeyParameter() {} + +// ColumnMasterKeyPathParameter represents KEY_PATH parameter. +type ColumnMasterKeyPathParameter struct { + Path ScalarExpression + ParameterKind string +} + +func (c *ColumnMasterKeyPathParameter) node() {} +func (c *ColumnMasterKeyPathParameter) columnMasterKeyParameter() {} + +// ColumnMasterKeyEnclaveComputationsParameter represents ENCLAVE_COMPUTATIONS parameter. +type ColumnMasterKeyEnclaveComputationsParameter struct { + Signature ScalarExpression + ParameterKind string +} + +func (c *ColumnMasterKeyEnclaveComputationsParameter) node() {} +func (c *ColumnMasterKeyEnclaveComputationsParameter) columnMasterKeyParameter() {} + +// DropColumnMasterKeyStatement represents a DROP COLUMN MASTER KEY statement. +type DropColumnMasterKeyStatement struct { + Name *Identifier +} + +func (d *DropColumnMasterKeyStatement) node() {} +func (d *DropColumnMasterKeyStatement) statement() {} diff --git a/ast/create_procedure_statement.go b/ast/create_procedure_statement.go index e6c00e5b..ed020c4d 100644 --- a/ast/create_procedure_statement.go +++ b/ast/create_procedure_statement.go @@ -6,6 +6,8 @@ type CreateProcedureStatement struct { Parameters []*ProcedureParameter StatementList *StatementList IsForReplication bool + Options []ProcedureOptionBase + MethodSpecifier *MethodSpecifier } func (c *CreateProcedureStatement) node() {} @@ -22,3 +24,26 @@ type ProcedureParameter struct { } func (p *ProcedureParameter) node() {} + +// ProcedureOptionBase is the interface for procedure options. +type ProcedureOptionBase interface { + Node + procedureOption() +} + +// ProcedureOption represents a simple procedure option like RECOMPILE or ENCRYPTION. +type ProcedureOption struct { + OptionKind string // Recompile, Encryption +} + +func (p *ProcedureOption) node() {} +func (p *ProcedureOption) procedureOption() {} + +// ExecuteAsProcedureOption represents an EXECUTE AS option for a procedure. +type ExecuteAsProcedureOption struct { + ExecuteAs *ExecuteAsClause + OptionKind string // ExecuteAs +} + +func (e *ExecuteAsProcedureOption) node() {} +func (e *ExecuteAsProcedureOption) procedureOption() {} diff --git a/ast/create_simple_statements.go b/ast/create_simple_statements.go index 341c4214..d405b4b7 100644 --- a/ast/create_simple_statements.go +++ b/ast/create_simple_statements.go @@ -2,12 +2,25 @@ package ast // CreateDatabaseStatement represents a CREATE DATABASE statement. type CreateDatabaseStatement struct { - DatabaseName *Identifier `json:"DatabaseName,omitempty"` - Options []CreateDatabaseOption `json:"Options,omitempty"` - AttachMode string `json:"AttachMode,omitempty"` // "None", "Attach", "AttachRebuildLog" - CopyOf *MultiPartIdentifier `json:"CopyOf,omitempty"` // For AS COPY OF syntax + DatabaseName *Identifier `json:"DatabaseName,omitempty"` + Options []CreateDatabaseOption `json:"Options,omitempty"` + AttachMode string `json:"AttachMode,omitempty"` // "None", "Attach", "AttachRebuildLog" + CopyOf *MultiPartIdentifier `json:"CopyOf,omitempty"` // For AS COPY OF syntax + FileGroups []*FileGroupDefinition `json:"FileGroups,omitempty"` + LogOn []*FileDeclaration `json:"LogOn,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` + Containment *ContainmentDatabaseOption `json:"Containment,omitempty"` } +// ContainmentDatabaseOption represents CONTAINMENT = NONE/PARTIAL +type ContainmentDatabaseOption struct { + Value string // "None" or "Partial" + OptionKind string // Always "Containment" +} + +func (c *ContainmentDatabaseOption) node() {} +func (c *ContainmentDatabaseOption) createDatabaseOption() {} + func (s *CreateDatabaseStatement) node() {} func (s *CreateDatabaseStatement) statement() {} @@ -19,9 +32,20 @@ type CreateLoginStatement struct { func (s *CreateLoginStatement) node() {} func (s *CreateLoginStatement) statement() {} +// ServiceContract represents a contract in CREATE/ALTER SERVICE. +type ServiceContract struct { + Name *Identifier `json:"Name,omitempty"` + Action string `json:"Action,omitempty"` // "Add", "Drop", "None" +} + +func (s *ServiceContract) node() {} + // CreateServiceStatement represents a CREATE SERVICE statement. type CreateServiceStatement struct { - Name *Identifier `json:"Name,omitempty"` + Owner *Identifier `json:"Owner,omitempty"` + Name *Identifier `json:"Name,omitempty"` + QueueName *SchemaObjectName `json:"QueueName,omitempty"` + ServiceContracts []*ServiceContract `json:"ServiceContracts,omitempty"` } func (s *CreateServiceStatement) node() {} @@ -61,12 +85,22 @@ func (s *CreateQueueStatement) statement() {} // CreateRouteStatement represents a CREATE ROUTE statement. type CreateRouteStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + Owner *Identifier `json:"Owner,omitempty"` + RouteOptions []*RouteOption `json:"RouteOptions,omitempty"` } func (s *CreateRouteStatement) node() {} func (s *CreateRouteStatement) statement() {} +// RouteOption represents an option in CREATE/ALTER ROUTE statement. +type RouteOption struct { + OptionKind string `json:"OptionKind,omitempty"` + Literal ScalarExpression `json:"Literal,omitempty"` +} + +func (r *RouteOption) node() {} + // CreateEndpointStatement represents a CREATE ENDPOINT statement. type CreateEndpointStatement struct { Name *Identifier `json:"Name,omitempty"` @@ -150,14 +184,36 @@ type CreationDispositionKeyOption struct { func (c *CreationDispositionKeyOption) node() {} func (c *CreationDispositionKeyOption) keyOption() {} +// CryptoMechanism represents an encryption mechanism (CERTIFICATE, KEY, PASSWORD, etc.) +type CryptoMechanism struct { + CryptoMechanismType string `json:"CryptoMechanismType,omitempty"` // "Certificate", "SymmetricKey", "AsymmetricKey", "Password" + Identifier *Identifier `json:"Identifier,omitempty"` + PasswordOrSignature ScalarExpression `json:"PasswordOrSignature,omitempty"` +} + +func (c *CryptoMechanism) node() {} + // CreateSymmetricKeyStatement represents a CREATE SYMMETRIC KEY statement. type CreateSymmetricKeyStatement struct { - Name *Identifier `json:"Name,omitempty"` + KeyOptions []KeyOption `json:"KeyOptions,omitempty"` + Provider *Identifier `json:"Provider,omitempty"` + Name *Identifier `json:"Name,omitempty"` + EncryptingMechanisms []*CryptoMechanism `json:"EncryptingMechanisms,omitempty"` } func (s *CreateSymmetricKeyStatement) node() {} func (s *CreateSymmetricKeyStatement) statement() {} +// DropSymmetricKeyStatement represents a DROP SYMMETRIC KEY statement. +type DropSymmetricKeyStatement struct { + RemoveProviderKey bool `json:"RemoveProviderKey,omitempty"` + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (s *DropSymmetricKeyStatement) node() {} +func (s *DropSymmetricKeyStatement) statement() {} + // CreateMessageTypeStatement represents a CREATE MESSAGE TYPE statement. type CreateMessageTypeStatement struct { Name *Identifier `json:"Name,omitempty"` @@ -171,7 +227,9 @@ func (s *CreateMessageTypeStatement) statement() {} // CreateRemoteServiceBindingStatement represents a CREATE REMOTE SERVICE BINDING statement. type CreateRemoteServiceBindingStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + Service ScalarExpression `json:"Service,omitempty"` + Options []RemoteServiceBindingOption `json:"Options,omitempty"` } func (s *CreateRemoteServiceBindingStatement) node() {} diff --git a/ast/create_spatial_index_statement.go b/ast/create_spatial_index_statement.go index db960306..403a63a9 100644 --- a/ast/create_spatial_index_statement.go +++ b/ast/create_spatial_index_statement.go @@ -74,8 +74,9 @@ type DataCompressionOption struct { PartitionRanges []*CompressionPartitionRange } -func (d *DataCompressionOption) node() {} -func (d *DataCompressionOption) indexOption() {} +func (d *DataCompressionOption) node() {} +func (d *DataCompressionOption) indexOption() {} +func (d *DataCompressionOption) dropIndexOption() {} // IgnoreDupKeyIndexOption represents the IGNORE_DUP_KEY option type IgnoreDupKeyIndexOption struct { diff --git a/ast/create_table_statement.go b/ast/create_table_statement.go index 387a5fca..1ddbbdb5 100644 --- a/ast/create_table_statement.go +++ b/ast/create_table_statement.go @@ -9,8 +9,19 @@ type CreateTableStatement struct { Definition *TableDefinition OnFileGroupOrPartitionScheme *FileGroupOrPartitionScheme TextImageOn *IdentifierOrValueExpression + FileStreamOn *IdentifierOrValueExpression + Options []TableOption } +// TableDataCompressionOption represents a DATA_COMPRESSION option +type TableDataCompressionOption struct { + DataCompressionOption *DataCompressionOption + OptionKind string +} + +func (t *TableDataCompressionOption) node() {} +func (t *TableDataCompressionOption) tableOption() {} + func (s *CreateTableStatement) node() {} func (s *CreateTableStatement) statement() {} @@ -129,6 +140,7 @@ type UniqueConstraintDefinition struct { ConstraintIdentifier *Identifier Clustered bool IsPrimaryKey bool + IsEnforced *bool // nil = not specified (default enforced), true = ENFORCED, false = NOT ENFORCED Columns []*ColumnWithSortOrder IndexType *IndexType } @@ -146,6 +158,7 @@ type ForeignKeyConstraintDefinition struct { DeleteAction string UpdateAction string NotForReplication bool + IsEnforced *bool // nil = not specified (default enforced), true = ENFORCED, false = NOT ENFORCED } func (f *ForeignKeyConstraintDefinition) node() {} diff --git a/ast/create_user_statement.go b/ast/create_user_statement.go index 12006272..c25a5bd8 100644 --- a/ast/create_user_statement.go +++ b/ast/create_user_statement.go @@ -44,3 +44,15 @@ type DefaultSchemaPrincipalOption struct { } func (o *DefaultSchemaPrincipalOption) userOptionNode() {} + +// PasswordAlterPrincipalOption represents a password option for ALTER USER +type PasswordAlterPrincipalOption struct { + Password *StringLiteral + OldPassword *StringLiteral + MustChange bool + Unlock bool + Hashed bool + OptionKind string +} + +func (o *PasswordAlterPrincipalOption) userOptionNode() {} diff --git a/ast/cte.go b/ast/cte.go new file mode 100644 index 00000000..d9f67207 --- /dev/null +++ b/ast/cte.go @@ -0,0 +1,18 @@ +package ast + +// WithCtesAndXmlNamespaces represents the WITH clause containing CTEs and/or XML namespaces. +type WithCtesAndXmlNamespaces struct { + CommonTableExpressions []*CommonTableExpression `json:"CommonTableExpressions,omitempty"` + ChangeTrackingContext ScalarExpression `json:"ChangeTrackingContext,omitempty"` +} + +func (w *WithCtesAndXmlNamespaces) node() {} + +// CommonTableExpression represents a single CTE definition. +type CommonTableExpression struct { + ExpressionName *Identifier `json:"ExpressionName,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` + QueryExpression QueryExpression `json:"QueryExpression,omitempty"` +} + +func (c *CommonTableExpression) node() {} diff --git a/ast/delete_statement.go b/ast/delete_statement.go index 048998be..392e4647 100644 --- a/ast/delete_statement.go +++ b/ast/delete_statement.go @@ -2,8 +2,9 @@ package ast // DeleteStatement represents a DELETE statement. type DeleteStatement struct { - DeleteSpecification *DeleteSpecification `json:"DeleteSpecification,omitempty"` - OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` + DeleteSpecification *DeleteSpecification `json:"DeleteSpecification,omitempty"` + WithCtesAndXmlNamespaces *WithCtesAndXmlNamespaces `json:"WithCtesAndXmlNamespaces,omitempty"` + OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` } func (d *DeleteStatement) node() {} diff --git a/ast/drop_statements.go b/ast/drop_statements.go index 3cfa1753..12c989b8 100644 --- a/ast/drop_statements.go +++ b/ast/drop_statements.go @@ -57,8 +57,8 @@ func (s *DropTriggerStatement) node() {} // DropIndexStatement represents a DROP INDEX statement type DropIndexStatement struct { - IsIfExists bool - Indexes []*DropIndexClause + IsIfExists bool + DropIndexClauses []*DropIndexClause } func (s *DropIndexStatement) statement() {} @@ -66,11 +66,46 @@ func (s *DropIndexStatement) node() {} // DropIndexClause represents a single index to drop type DropIndexClause struct { - Index *SchemaObjectName // For backwards-compatible syntax: table.index - IndexName *Identifier // For new syntax: index ON table - Object *SchemaObjectName // Table name for ON clause syntax + Index *Identifier // Index name for new syntax + Object *SchemaObjectName // Table name for ON clause syntax + Options []DropIndexOption + // Legacy fields for backwards-compatible syntax (table.index) + LegacyIndex *SchemaObjectName +} + +// DropIndexOption is the interface for DROP INDEX options +type DropIndexOption interface { + Node + dropIndexOption() +} + +// OnlineIndexOption represents the ONLINE option +type OnlineIndexOption struct { + OptionState string // On, Off + OptionKind string // Online +} + +func (o *OnlineIndexOption) node() {} +func (o *OnlineIndexOption) dropIndexOption() {} + +// MoveToDropIndexOption represents the MOVE TO option +type MoveToDropIndexOption struct { + MoveTo *FileGroupOrPartitionScheme + OptionKind string // MoveTo +} + +func (o *MoveToDropIndexOption) node() {} +func (o *MoveToDropIndexOption) dropIndexOption() {} + +// FileStreamOnDropIndexOption represents the FILESTREAM_ON option +type FileStreamOnDropIndexOption struct { + FileStreamOn *IdentifierOrValueExpression + OptionKind string // FileStreamOn } +func (o *FileStreamOnDropIndexOption) node() {} +func (o *FileStreamOnDropIndexOption) dropIndexOption() {} + // DropStatisticsStatement represents a DROP STATISTICS statement type DropStatisticsStatement struct { Objects []*SchemaObjectName diff --git a/ast/execute_statement.go b/ast/execute_statement.go index 1ac73277..bcfb8c5e 100644 --- a/ast/execute_statement.go +++ b/ast/execute_statement.go @@ -3,11 +3,67 @@ package ast // ExecuteStatement represents an EXECUTE/EXEC statement. type ExecuteStatement struct { ExecuteSpecification *ExecuteSpecification `json:"ExecuteSpecification,omitempty"` + Options []ExecuteOptionType `json:"Options,omitempty"` } func (e *ExecuteStatement) node() {} func (e *ExecuteStatement) statement() {} +// ExecuteOptionType is an interface for execute options. +type ExecuteOptionType interface { + executeOption() +} + +// ExecuteOption represents a simple execute option like RECOMPILE. +type ExecuteOption struct { + OptionKind string `json:"OptionKind,omitempty"` +} + +func (o *ExecuteOption) executeOption() {} + +// ResultSetsExecuteOption represents the WITH RESULT SETS option. +type ResultSetsExecuteOption struct { + OptionKind string `json:"OptionKind,omitempty"` + ResultSetsOptionKind string `json:"ResultSetsOptionKind,omitempty"` // None, Undefined, ResultSetsDefined + Definitions []ResultSetDefinitionType `json:"Definitions,omitempty"` +} + +func (o *ResultSetsExecuteOption) executeOption() {} + +// ResultSetDefinitionType is an interface for result set definitions. +type ResultSetDefinitionType interface { + resultSetDefinition() +} + +// ResultSetDefinition represents a simple result set type like ForXml. +type ResultSetDefinition struct { + ResultSetType string `json:"ResultSetType,omitempty"` // ForXml, etc. +} + +func (d *ResultSetDefinition) resultSetDefinition() {} + +// InlineResultSetDefinition represents an inline column definition. +type InlineResultSetDefinition struct { + ResultSetType string `json:"ResultSetType,omitempty"` // Inline + ResultColumnDefinitions []*ResultColumnDefinition `json:"ResultColumnDefinitions,omitempty"` +} + +func (d *InlineResultSetDefinition) resultSetDefinition() {} + +// SchemaObjectResultSetDefinition represents AS OBJECT or AS TYPE. +type SchemaObjectResultSetDefinition struct { + ResultSetType string `json:"ResultSetType,omitempty"` // Object, Type + Name *SchemaObjectName `json:"Name,omitempty"` +} + +func (d *SchemaObjectResultSetDefinition) resultSetDefinition() {} + +// ResultColumnDefinition represents a column in a result set. +type ResultColumnDefinition struct { + ColumnDefinition *ColumnDefinitionBase `json:"ColumnDefinition,omitempty"` + Nullable *NullableConstraintDefinition `json:"Nullable,omitempty"` +} + // ExecuteSpecification contains the details of an EXECUTE. type ExecuteSpecification struct { Variable *VariableReference `json:"Variable,omitempty"` diff --git a/ast/file_declaration.go b/ast/file_declaration.go new file mode 100644 index 00000000..9a915fd1 --- /dev/null +++ b/ast/file_declaration.go @@ -0,0 +1,76 @@ +package ast + +// FileGroupDefinition represents a FILEGROUP definition in CREATE DATABASE +type FileGroupDefinition struct { + Name *Identifier + FileDeclarations []*FileDeclaration + IsDefault bool + ContainsFileStream bool + ContainsMemoryOptimizedData bool +} + +func (f *FileGroupDefinition) node() {} + +// FileDeclaration represents a file declaration within a filegroup +type FileDeclaration struct { + Options []FileDeclarationOption + IsPrimary bool +} + +func (f *FileDeclaration) node() {} + +// FileDeclarationOption is an interface for file declaration options +type FileDeclarationOption interface { + Node + fileDeclarationOption() +} + +// NameFileDeclarationOption represents the NAME option for a file +type NameFileDeclarationOption struct { + LogicalFileName *IdentifierOrValueExpression + IsNewName bool + OptionKind string // "Name" or "NewName" +} + +func (n *NameFileDeclarationOption) node() {} +func (n *NameFileDeclarationOption) fileDeclarationOption() {} + +// FileNameFileDeclarationOption represents the FILENAME option for a file +type FileNameFileDeclarationOption struct { + OSFileName *StringLiteral + OptionKind string // "FileName" +} + +func (f *FileNameFileDeclarationOption) node() {} +func (f *FileNameFileDeclarationOption) fileDeclarationOption() {} + +// SizeFileDeclarationOption represents the SIZE option for a file +type SizeFileDeclarationOption struct { + Size ScalarExpression + Units string // "KB", "MB", "GB", "TB", "Unspecified" + OptionKind string // "Size" +} + +func (s *SizeFileDeclarationOption) node() {} +func (s *SizeFileDeclarationOption) fileDeclarationOption() {} + +// MaxSizeFileDeclarationOption represents the MAXSIZE option for a file +type MaxSizeFileDeclarationOption struct { + MaxSize ScalarExpression + Units string // "KB", "MB", "GB", "TB", "Unspecified" + Unlimited bool + OptionKind string // "MaxSize" +} + +func (m *MaxSizeFileDeclarationOption) node() {} +func (m *MaxSizeFileDeclarationOption) fileDeclarationOption() {} + +// FileGrowthFileDeclarationOption represents the FILEGROWTH option for a file +type FileGrowthFileDeclarationOption struct { + GrowthIncrement ScalarExpression + Units string // "KB", "MB", "GB", "TB", "Percent", "Unspecified" + OptionKind string // "FileGrowth" +} + +func (f *FileGrowthFileDeclarationOption) node() {} +func (f *FileGrowthFileDeclarationOption) fileDeclarationOption() {} diff --git a/ast/function_call.go b/ast/function_call.go index 000f78bc..0186624f 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -31,14 +31,23 @@ type OverClause struct { // Add partition by, order by, and window frame as needed } +// WithinGroupClause represents a WITHIN GROUP clause for ordered set aggregate functions. +type WithinGroupClause struct { + OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` + HasGraphPath bool `json:"HasGraphPath,omitempty"` +} + +func (*WithinGroupClause) node() {} + // FunctionCall represents a function call. type FunctionCall struct { - CallTarget CallTarget `json:"CallTarget,omitempty"` - FunctionName *Identifier `json:"FunctionName,omitempty"` - Parameters []ScalarExpression `json:"Parameters,omitempty"` - UniqueRowFilter string `json:"UniqueRowFilter,omitempty"` - OverClause *OverClause `json:"OverClause,omitempty"` - WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"` + CallTarget CallTarget `json:"CallTarget,omitempty"` + FunctionName *Identifier `json:"FunctionName,omitempty"` + Parameters []ScalarExpression `json:"Parameters,omitempty"` + UniqueRowFilter string `json:"UniqueRowFilter,omitempty"` + WithinGroupClause *WithinGroupClause `json:"WithinGroupClause,omitempty"` + OverClause *OverClause `json:"OverClause,omitempty"` + WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"` } func (*FunctionCall) node() {} diff --git a/ast/insert_statement.go b/ast/insert_statement.go index 222e16fb..8c9c55c9 100644 --- a/ast/insert_statement.go +++ b/ast/insert_statement.go @@ -2,8 +2,9 @@ package ast // InsertStatement represents an INSERT statement. type InsertStatement struct { - InsertSpecification *InsertSpecification `json:"InsertSpecification,omitempty"` - OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` + InsertSpecification *InsertSpecification `json:"InsertSpecification,omitempty"` + WithCtesAndXmlNamespaces *WithCtesAndXmlNamespaces `json:"WithCtesAndXmlNamespaces,omitempty"` + OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` } func (i *InsertStatement) node() {} diff --git a/ast/optimizer_hint.go b/ast/optimizer_hint.go index ac5e5768..86468acf 100644 --- a/ast/optimizer_hint.go +++ b/ast/optimizer_hint.go @@ -17,3 +17,12 @@ type TableHintsOptimizerHint struct { func (*TableHintsOptimizerHint) node() {} func (*TableHintsOptimizerHint) optimizerHint() {} + +// UseHintList represents a USE HINT optimizer hint with a list of hint strings. +type UseHintList struct { + HintKind string `json:"HintKind,omitempty"` + Hints []ScalarExpression `json:"Hints,omitempty"` +} + +func (*UseHintList) node() {} +func (*UseHintList) optimizerHint() {} diff --git a/ast/predicate_set_statement.go b/ast/predicate_set_statement.go index 7d66f744..134249dd 100644 --- a/ast/predicate_set_statement.go +++ b/ast/predicate_set_statement.go @@ -1,8 +1,9 @@ package ast // PredicateSetStatement represents a SET statement like SET ANSI_NULLS ON +// Options can contain multiple comma-separated values like "ConcatNullYieldsNull, CursorCloseOnCommit" type PredicateSetStatement struct { - Options SetOptions + Options string IsOn bool } diff --git a/ast/receive_statement.go b/ast/receive_statement.go index 543fde6b..aed2d06e 100644 --- a/ast/receive_statement.go +++ b/ast/receive_statement.go @@ -2,11 +2,11 @@ package ast // ReceiveStatement represents a RECEIVE ... FROM queue statement. type ReceiveStatement struct { - Top ScalarExpression - SelectElements []SelectElement - Queue *SchemaObjectName - Into *VariableTableReference - Where BooleanExpression + Top ScalarExpression + SelectElements []SelectElement + Queue *SchemaObjectName + Into *VariableTableReference + Where ScalarExpression // Just the RHS of the WHERE comparison IsConversationGroupIdWhere bool } diff --git a/ast/set_offsets_statement.go b/ast/set_offsets_statement.go new file mode 100644 index 00000000..07b334df --- /dev/null +++ b/ast/set_offsets_statement.go @@ -0,0 +1,10 @@ +package ast + +// SetOffsetsStatement represents SET OFFSETS statement +type SetOffsetsStatement struct { + Options string + IsOn bool +} + +func (s *SetOffsetsStatement) node() {} +func (s *SetOffsetsStatement) statement() {} diff --git a/ast/set_rowcount_statement.go b/ast/set_rowcount_statement.go new file mode 100644 index 00000000..853a8d6b --- /dev/null +++ b/ast/set_rowcount_statement.go @@ -0,0 +1,9 @@ +package ast + +// SetRowCountStatement represents SET ROWCOUNT statement +type SetRowCountStatement struct { + NumberRows ScalarExpression +} + +func (s *SetRowCountStatement) node() {} +func (s *SetRowCountStatement) statement() {} diff --git a/ast/set_statistics_statement.go b/ast/set_statistics_statement.go index 61eb7a2f..6b78e8c7 100644 --- a/ast/set_statistics_statement.go +++ b/ast/set_statistics_statement.go @@ -1,8 +1,9 @@ package ast // SetStatisticsStatement represents SET STATISTICS IO/PROFILE/TIME/XML statements +// Options can contain multiple comma-separated values like "IO, Profile, Time" type SetStatisticsStatement struct { - Options SetOptions + Options string IsOn bool } diff --git a/ast/trivial_statements.go b/ast/trivial_statements.go index bd2547e9..50078965 100644 --- a/ast/trivial_statements.go +++ b/ast/trivial_statements.go @@ -77,8 +77,7 @@ func (s *OpenMasterKeyStatement) statement() {} // OpenSymmetricKeyStatement represents OPEN SYMMETRIC KEY statement type OpenSymmetricKeyStatement struct { Name *Identifier - DecryptionMechanism string // "Key", "Certificate", "Password", "AsymmetricKey" - DecryptionKey ScalarExpression + DecryptionMechanism *CryptoMechanism } func (s *OpenSymmetricKeyStatement) node() {} diff --git a/ast/update_statement.go b/ast/update_statement.go index 7cbeda30..5b758534 100644 --- a/ast/update_statement.go +++ b/ast/update_statement.go @@ -2,8 +2,9 @@ package ast // UpdateStatement represents an UPDATE statement. type UpdateStatement struct { - UpdateSpecification *UpdateSpecification `json:"UpdateSpecification,omitempty"` - OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` + UpdateSpecification *UpdateSpecification `json:"UpdateSpecification,omitempty"` + WithCtesAndXmlNamespaces *WithCtesAndXmlNamespaces `json:"WithCtesAndXmlNamespaces,omitempty"` + OptimizerHints []OptimizerHintBase `json:"OptimizerHints,omitempty"` } func (u *UpdateStatement) node() {} diff --git a/parser/marshal.go b/parser/marshal.go index a8f651f6..a8b72ec2 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -138,6 +138,10 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterResourceGovernorStatementToJSON(s) case *ast.CreateCryptographicProviderStatement: return createCryptographicProviderStatementToJSON(s) + case *ast.CreateColumnMasterKeyStatement: + return createColumnMasterKeyStatementToJSON(s) + case *ast.DropColumnMasterKeyStatement: + return dropColumnMasterKeyStatementToJSON(s) case *ast.AlterCryptographicProviderStatement: return alterCryptographicProviderStatementToJSON(s) case *ast.DropCryptographicProviderStatement: @@ -214,6 +218,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropAssemblyStatementToJSON(s) case *ast.DropAsymmetricKeyStatement: return dropAsymmetricKeyStatementToJSON(s) + case *ast.DropSymmetricKeyStatement: + return dropSymmetricKeyStatementToJSON(s) case *ast.CreateTableStatement: return createTableStatementToJSON(s) case *ast.GrantStatement: @@ -226,6 +232,10 @@ func statementToJSON(stmt ast.Statement) jsonNode { return predicateSetStatementToJSON(s) case *ast.SetStatisticsStatement: return setStatisticsStatementToJSON(s) + case *ast.SetRowCountStatement: + return setRowCountStatementToJSON(s) + case *ast.SetOffsetsStatement: + return setOffsetsStatementToJSON(s) case *ast.CommitTransactionStatement: return commitTransactionStatementToJSON(s) case *ast.RollbackTransactionStatement: @@ -358,6 +368,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterFunctionStatementToJSON(s) case *ast.CreateFunctionStatement: return createFunctionStatementToJSON(s) + case *ast.CreateOrAlterFunctionStatement: + return createOrAlterFunctionStatementToJSON(s) case *ast.AlterTriggerStatement: return alterTriggerStatementToJSON(s) case *ast.CreateTriggerStatement: @@ -1096,6 +1108,21 @@ func optimizerHintToJSON(h ast.OptimizerHintBase) jsonNode { node["HintKind"] = hint.HintKind } return node + case *ast.UseHintList: + node := jsonNode{ + "$type": "UseHintList", + } + if len(hint.Hints) > 0 { + hints := make([]jsonNode, len(hint.Hints)) + for i, h := range hint.Hints { + hints[i] = scalarExpressionToJSON(h) + } + node["Hints"] = hints + } + if hint.HintKind != "" { + node["HintKind"] = hint.HintKind + } + return node default: return jsonNode{"$type": "UnknownOptimizerHint"} } @@ -1320,6 +1347,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { if e.UniqueRowFilter != "" { node["UniqueRowFilter"] = e.UniqueRowFilter } + if e.WithinGroupClause != nil { + node["WithinGroupClause"] = withinGroupClauseToJSON(e.WithinGroupClause) + } if e.OverClause != nil { node["OverClause"] = jsonNode{ "$type": "OverClause", @@ -1878,7 +1908,7 @@ func booleanExpressionToJSON(expr ast.BooleanExpression) jsonNode { return node case *ast.BooleanInExpression: node := jsonNode{ - "$type": "BooleanInExpression", + "$type": "InPredicate", } if e.Expression != nil { node["Expression"] = scalarExpressionToJSON(e.Expression) @@ -2004,6 +2034,17 @@ func expressionWithSortOrderToJSON(ewso *ast.ExpressionWithSortOrder) jsonNode { return node } +func withinGroupClauseToJSON(wg *ast.WithinGroupClause) jsonNode { + node := jsonNode{ + "$type": "WithinGroupClause", + } + if wg.OrderByClause != nil { + node["OrderByClause"] = orderByClauseToJSON(wg.OrderByClause) + } + node["HasGraphPath"] = wg.HasGraphPath + return node +} + // ======================= New Statement JSON Functions ======================= func tableHintToJSON(h ast.TableHintType) jsonNode { @@ -2043,6 +2084,9 @@ func insertStatementToJSON(s *ast.InsertStatement) jsonNode { if s.InsertSpecification != nil { node["InsertSpecification"] = insertSpecificationToJSON(s.InsertSpecification) } + if s.WithCtesAndXmlNamespaces != nil { + node["WithCtesAndXmlNamespaces"] = withCtesAndXmlNamespacesToJSON(s.WithCtesAndXmlNamespaces) + } if len(s.OptimizerHints) > 0 { hints := make([]jsonNode, len(s.OptimizerHints)) for i, h := range s.OptimizerHints { @@ -2278,6 +2322,9 @@ func updateStatementToJSON(s *ast.UpdateStatement) jsonNode { if s.UpdateSpecification != nil { node["UpdateSpecification"] = updateSpecificationToJSON(s.UpdateSpecification) } + if s.WithCtesAndXmlNamespaces != nil { + node["WithCtesAndXmlNamespaces"] = withCtesAndXmlNamespacesToJSON(s.WithCtesAndXmlNamespaces) + } if len(s.OptimizerHints) > 0 { hints := make([]jsonNode, len(s.OptimizerHints)) for i, h := range s.OptimizerHints { @@ -2342,6 +2389,9 @@ func deleteStatementToJSON(s *ast.DeleteStatement) jsonNode { if s.DeleteSpecification != nil { node["DeleteSpecification"] = deleteSpecificationToJSON(s.DeleteSpecification) } + if s.WithCtesAndXmlNamespaces != nil { + node["WithCtesAndXmlNamespaces"] = withCtesAndXmlNamespacesToJSON(s.WithCtesAndXmlNamespaces) + } if len(s.OptimizerHints) > 0 { hints := make([]jsonNode, len(s.OptimizerHints)) for i, h := range s.OptimizerHints { @@ -2352,6 +2402,43 @@ func deleteStatementToJSON(s *ast.DeleteStatement) jsonNode { return node } +func withCtesAndXmlNamespacesToJSON(w *ast.WithCtesAndXmlNamespaces) jsonNode { + node := jsonNode{ + "$type": "WithCtesAndXmlNamespaces", + } + if len(w.CommonTableExpressions) > 0 { + ctes := make([]jsonNode, len(w.CommonTableExpressions)) + for i, cte := range w.CommonTableExpressions { + ctes[i] = commonTableExpressionToJSON(cte) + } + node["CommonTableExpressions"] = ctes + } + if w.ChangeTrackingContext != nil { + node["ChangeTrackingContext"] = scalarExpressionToJSON(w.ChangeTrackingContext) + } + return node +} + +func commonTableExpressionToJSON(cte *ast.CommonTableExpression) jsonNode { + node := jsonNode{ + "$type": "CommonTableExpression", + } + if cte.ExpressionName != nil { + node["ExpressionName"] = identifierToJSON(cte.ExpressionName) + } + if len(cte.Columns) > 0 { + cols := make([]jsonNode, len(cte.Columns)) + for i, col := range cte.Columns { + cols[i] = identifierToJSON(col) + } + node["Columns"] = cols + } + if cte.QueryExpression != nil { + node["QueryExpression"] = queryExpressionToJSON(cte.QueryExpression) + } + return node +} + func deleteSpecificationToJSON(spec *ast.DeleteSpecification) jsonNode { node := jsonNode{ "$type": "DeleteSpecification", @@ -2664,6 +2751,98 @@ func executeStatementToJSON(s *ast.ExecuteStatement) jsonNode { if s.ExecuteSpecification != nil { node["ExecuteSpecification"] = executeSpecificationToJSON(s.ExecuteSpecification) } + if len(s.Options) > 0 { + opts := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + opts[i] = executeOptionToJSON(opt) + } + node["Options"] = opts + } + return node +} + +func executeOptionToJSON(opt ast.ExecuteOptionType) jsonNode { + switch o := opt.(type) { + case *ast.ExecuteOption: + return jsonNode{ + "$type": "ExecuteOption", + "OptionKind": o.OptionKind, + } + case *ast.ResultSetsExecuteOption: + node := jsonNode{ + "$type": "ResultSetsExecuteOption", + "ResultSetsOptionKind": o.ResultSetsOptionKind, + "OptionKind": o.OptionKind, + } + if len(o.Definitions) > 0 { + defs := make([]jsonNode, len(o.Definitions)) + for i, def := range o.Definitions { + defs[i] = resultSetDefinitionToJSON(def) + } + node["Definitions"] = defs + } + return node + default: + return jsonNode{} + } +} + +func resultSetDefinitionToJSON(def ast.ResultSetDefinitionType) jsonNode { + switch d := def.(type) { + case *ast.ResultSetDefinition: + return jsonNode{ + "$type": "ResultSetDefinition", + "ResultSetType": d.ResultSetType, + } + case *ast.InlineResultSetDefinition: + node := jsonNode{ + "$type": "InlineResultSetDefinition", + "ResultSetType": d.ResultSetType, + } + if len(d.ResultColumnDefinitions) > 0 { + cols := make([]jsonNode, len(d.ResultColumnDefinitions)) + for i, col := range d.ResultColumnDefinitions { + cols[i] = resultColumnDefinitionToJSON(col) + } + node["ResultColumnDefinitions"] = cols + } + return node + case *ast.SchemaObjectResultSetDefinition: + node := jsonNode{ + "$type": "SchemaObjectResultSetDefinition", + "ResultSetType": d.ResultSetType, + } + if d.Name != nil { + node["Name"] = schemaObjectNameToJSON(d.Name) + } + return node + default: + return jsonNode{} + } +} + +func resultColumnDefinitionToJSON(col *ast.ResultColumnDefinition) jsonNode { + node := jsonNode{ + "$type": "ResultColumnDefinition", + } + if col.ColumnDefinition != nil { + colDefNode := jsonNode{ + "$type": "ColumnDefinitionBase", + } + if col.ColumnDefinition.ColumnIdentifier != nil { + colDefNode["ColumnIdentifier"] = identifierToJSON(col.ColumnDefinition.ColumnIdentifier) + } + if col.ColumnDefinition.DataType != nil { + colDefNode["DataType"] = dataTypeReferenceToJSON(col.ColumnDefinition.DataType) + } + node["ColumnDefinition"] = colDefNode + } + if col.Nullable != nil { + node["Nullable"] = jsonNode{ + "$type": "NullableConstraintDefinition", + "Nullable": col.Nullable.Nullable, + } + } return node } @@ -2781,7 +2960,7 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) p.nextToken() } - // Parse optional ON filegroup and TEXTIMAGE_ON filegroup clauses + // Parse optional ON filegroup, TEXTIMAGE_ON, FILESTREAM_ON, and WITH clauses for { upperLit := strings.ToUpper(p.curTok.Literal) if p.curTok.Type == TokenOn { @@ -2796,11 +2975,89 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) } } else if upperLit == "TEXTIMAGE_ON" { p.nextToken() // consume TEXTIMAGE_ON - // Parse filegroup identifier - ident := p.parseIdentifier() - stmt.TextImageOn = &ast.IdentifierOrValueExpression{ - Value: ident.Value, - Identifier: ident, + // Parse filegroup identifier or string literal + if p.curTok.Type == TokenString { + value := p.curTok.Literal + // Strip quotes from string literal + if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' { + value = value[1 : len(value)-1] + } + stmt.TextImageOn = &ast.IdentifierOrValueExpression{ + Value: value, + ValueExpression: &ast.StringLiteral{ + LiteralType: "String", + Value: value, + }, + } + p.nextToken() + } else { + ident := p.parseIdentifier() + stmt.TextImageOn = &ast.IdentifierOrValueExpression{ + Value: ident.Value, + Identifier: ident, + } + } + } else if upperLit == "FILESTREAM_ON" { + p.nextToken() // consume FILESTREAM_ON + // Parse filegroup identifier or string literal + if p.curTok.Type == TokenString { + value := p.curTok.Literal + // Strip quotes from string literal + if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' { + value = value[1 : len(value)-1] + } + stmt.FileStreamOn = &ast.IdentifierOrValueExpression{ + Value: value, + ValueExpression: &ast.StringLiteral{ + LiteralType: "String", + Value: value, + }, + } + p.nextToken() + } else { + ident := p.parseIdentifier() + stmt.FileStreamOn = &ast.IdentifierOrValueExpression{ + Value: ident.Value, + Identifier: ident, + } + } + } else if p.curTok.Type == TokenWith { + // Parse WITH clause with table options + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + // Parse table options + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + if optionName == "DATA_COMPRESSION" { + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + opt, err := p.parseDataCompressionOption() + if err != nil { + break + } + stmt.Options = append(stmt.Options, &ast.TableDataCompressionOption{ + DataCompressionOption: opt, + OptionKind: "DataCompression", + }) + } else { + // Skip unknown option value + if p.curTok.Type == TokenEquals { + p.nextToken() + } + p.nextToken() + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } } } else { break @@ -2815,6 +3072,79 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) return stmt, nil } +func (p *Parser) parseDataCompressionOption() (*ast.DataCompressionOption, error) { + opt := &ast.DataCompressionOption{ + OptionKind: "DataCompression", + } + + // Parse compression level: NONE, ROW, PAGE, COLUMNSTORE, COLUMNSTORE_ARCHIVE + levelStr := strings.ToUpper(p.curTok.Literal) + switch levelStr { + case "NONE": + opt.CompressionLevel = "None" + case "ROW": + opt.CompressionLevel = "Row" + case "PAGE": + opt.CompressionLevel = "Page" + case "COLUMNSTORE": + opt.CompressionLevel = "ColumnStore" + case "COLUMNSTORE_ARCHIVE": + opt.CompressionLevel = "ColumnStoreArchive" + default: + opt.CompressionLevel = levelStr + } + p.nextToken() + + // Parse optional ON PARTITIONS clause + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + if strings.ToUpper(p.curTok.Literal) == "PARTITIONS" { + p.nextToken() // consume PARTITIONS + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + // Parse partition ranges + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + pr := &ast.CompressionPartitionRange{} + + // Parse From + if p.curTok.Type == TokenNumber { + pr.From = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + + // Check for TO + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + if p.curTok.Type == TokenNumber { + pr.To = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + } + + opt.PartitionRanges = append(opt.PartitionRanges, pr) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + } + } + + return opt, nil +} + func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { col := &ast.ColumnDefinition{} @@ -2978,6 +3308,9 @@ func (p *Parser) parseColumnDefinition() (*ast.ColumnDefinition, error) { } // Continue to parse actual constraint in next iteration continue + } else if upperLit == "COLLATE" { + p.nextToken() // consume COLLATE + col.Collation = p.parseIdentifier() } else { break } @@ -3917,6 +4250,63 @@ func createTableStatementToJSON(s *ast.CreateTableStatement) jsonNode { if s.TextImageOn != nil { node["TextImageOn"] = identifierOrValueExpressionToJSON(s.TextImageOn) } + if s.FileStreamOn != nil { + node["FileStreamOn"] = identifierOrValueExpressionToJSON(s.FileStreamOn) + } + if len(s.Options) > 0 { + opts := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + opts[i] = tableOptionToJSON(opt) + } + node["Options"] = opts + } + return node +} + +func tableOptionToJSON(opt ast.TableOption) jsonNode { + switch o := opt.(type) { + case *ast.TableDataCompressionOption: + node := jsonNode{ + "$type": "TableDataCompressionOption", + "OptionKind": o.OptionKind, + } + if o.DataCompressionOption != nil { + node["DataCompressionOption"] = dataCompressionOptionToJSON(o.DataCompressionOption) + } + return node + case *ast.SystemVersioningTableOption: + return systemVersioningTableOptionToJSON(o) + default: + return jsonNode{"$type": "UnknownTableOption"} + } +} + +func dataCompressionOptionToJSON(opt *ast.DataCompressionOption) jsonNode { + node := jsonNode{ + "$type": "DataCompressionOption", + "CompressionLevel": opt.CompressionLevel, + "OptionKind": opt.OptionKind, + } + if len(opt.PartitionRanges) > 0 { + ranges := make([]jsonNode, len(opt.PartitionRanges)) + for i, pr := range opt.PartitionRanges { + ranges[i] = compressionPartitionRangeToJSON(pr) + } + node["PartitionRanges"] = ranges + } + return node +} + +func compressionPartitionRangeToJSON(pr *ast.CompressionPartitionRange) jsonNode { + node := jsonNode{ + "$type": "CompressionPartitionRange", + } + if pr.From != nil { + node["From"] = scalarExpressionToJSON(pr.From) + } + if pr.To != nil { + node["To"] = scalarExpressionToJSON(pr.To) + } return node } @@ -4000,6 +4390,10 @@ func foreignKeyConstraintToJSON(c *ast.ForeignKeyConstraintDefinition) jsonNode updateAction = "NotSpecified" } node["UpdateAction"] = updateAction + // Output IsEnforced if it's explicitly set + if c.IsEnforced != nil { + node["IsEnforced"] = *c.IsEnforced + } return node } @@ -4031,6 +4425,9 @@ func columnDefinitionToJSON(c *ast.ColumnDefinition) jsonNode { if c.DataType != nil { node["DataType"] = dataTypeReferenceToJSON(c.DataType) } + if c.Collation != nil { + node["Collation"] = identifierToJSON(c.Collation) + } return node } @@ -4087,6 +4484,10 @@ func uniqueConstraintToJSON(c *ast.UniqueConstraintDefinition) jsonNode { if c.Clustered || c.IndexType != nil { node["Clustered"] = c.Clustered } + // Output IsEnforced if it's explicitly set + if c.IsEnforced != nil { + node["IsEnforced"] = *c.IsEnforced + } if c.ConstraintIdentifier != nil { node["ConstraintIdentifier"] = identifierToJSON(c.ConstraintIdentifier) } @@ -4281,7 +4682,7 @@ func securityPrincipalToJSON(p *ast.SecurityPrincipal) jsonNode { func predicateSetStatementToJSON(s *ast.PredicateSetStatement) jsonNode { return jsonNode{ "$type": "PredicateSetStatement", - "Options": string(s.Options), + "Options": s.Options, "IsOn": s.IsOn, } } @@ -4289,7 +4690,25 @@ func predicateSetStatementToJSON(s *ast.PredicateSetStatement) jsonNode { func setStatisticsStatementToJSON(s *ast.SetStatisticsStatement) jsonNode { return jsonNode{ "$type": "SetStatisticsStatement", - "Options": string(s.Options), + "Options": s.Options, + "IsOn": s.IsOn, + } +} + +func setRowCountStatementToJSON(s *ast.SetRowCountStatement) jsonNode { + node := jsonNode{ + "$type": "SetRowCountStatement", + } + if s.NumberRows != nil { + node["NumberRows"] = scalarExpressionToJSON(s.NumberRows) + } + return node +} + +func setOffsetsStatementToJSON(s *ast.SetOffsetsStatement) jsonNode { + return jsonNode{ + "$type": "SetOffsetsStatement", + "Options": s.Options, "IsOn": s.IsOn, } } @@ -4400,19 +4819,6 @@ func truncateTableStatementToJSON(s *ast.TruncateTableStatement) jsonNode { return node } -func compressionPartitionRangeToJSON(pr *ast.CompressionPartitionRange) jsonNode { - node := jsonNode{ - "$type": "CompressionPartitionRange", - } - if pr.From != nil { - node["From"] = scalarExpressionToJSON(pr.From) - } - if pr.To != nil { - node["To"] = scalarExpressionToJSON(pr.To) - } - return node -} - func useStatementToJSON(s *ast.UseStatement) jsonNode { node := jsonNode{ "$type": "UseStatement", @@ -4489,11 +4895,8 @@ func openSymmetricKeyStatementToJSON(s *ast.OpenSymmetricKeyStatement) jsonNode if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } - if s.DecryptionMechanism != "" { - node["DecryptionMechanism"] = s.DecryptionMechanism - } - if s.DecryptionKey != nil { - node["DecryptionKey"] = scalarExpressionToJSON(s.DecryptionKey) + if s.DecryptionMechanism != nil { + node["DecryptionMechanism"] = cryptoMechanismToJSON(s.DecryptionMechanism) } return node } @@ -4744,7 +5147,7 @@ func receiveStatementToJSON(s *ast.ReceiveStatement) jsonNode { node["Into"] = variableTableReferenceToJSON(s.Into) } if s.Where != nil { - node["Where"] = booleanExpressionToJSON(s.Where) + node["Where"] = scalarExpressionToJSON(s.Where) } node["IsConversationGroupIdWhere"] = s.IsConversationGroupIdWhere return node @@ -5215,6 +5618,13 @@ func createProcedureStatementToJSON(s *ast.CreateProcedureStatement) jsonNode { 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 { @@ -5222,18 +5632,68 @@ func createProcedureStatementToJSON(s *ast.CreateProcedureStatement) jsonNode { } node["Parameters"] = params } + if s.MethodSpecifier != nil { + node["MethodSpecifier"] = methodSpecifierToJSON(s.MethodSpecifier) + } if s.StatementList != nil { node["StatementList"] = statementListToJSON(s.StatementList) } return node } -func createRoleStatementToJSON(s *ast.CreateRoleStatement) jsonNode { +func procedureOptionToJSON(opt ast.ProcedureOptionBase) jsonNode { + switch o := opt.(type) { + case *ast.ProcedureOption: + return jsonNode{ + "$type": "ProcedureOption", + "OptionKind": o.OptionKind, + } + case *ast.ExecuteAsProcedureOption: + node := jsonNode{ + "$type": "ExecuteAsProcedureOption", + "OptionKind": o.OptionKind, + } + if o.ExecuteAs != nil { + node["ExecuteAs"] = executeAsClauseToJSON(o.ExecuteAs) + } + return node + } + return jsonNode{} +} + +func executeAsClauseToJSON(e *ast.ExecuteAsClause) jsonNode { node := jsonNode{ - "$type": "CreateRoleStatement", + "$type": "ExecuteAsClause", + "ExecuteAsOption": e.ExecuteAsOption, } - if s.Owner != nil { - node["Owner"] = identifierToJSON(s.Owner) + if e.Literal != nil { + node["Literal"] = stringLiteralToJSON(e.Literal) + } + return node +} + +func methodSpecifierToJSON(m *ast.MethodSpecifier) jsonNode { + node := jsonNode{ + "$type": "MethodSpecifier", + } + if m.AssemblyName != nil { + node["AssemblyName"] = identifierToJSON(m.AssemblyName) + } + if m.ClassName != nil { + node["ClassName"] = identifierToJSON(m.ClassName) + } + if m.MethodName != nil { + node["MethodName"] = identifierToJSON(m.MethodName) + } + return node +} + +func createRoleStatementToJSON(s *ast.CreateRoleStatement) jsonNode { + node := jsonNode{ + "$type": "CreateRoleStatement", + } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) @@ -5776,6 +6236,18 @@ func (p *Parser) parseCreateAggregateStatement() (*ast.CreateAggregateStatement, } param.DataType = dataType + // Check for NULL or NOT NULL + if p.curTok.Type == TokenNull { + param.Nullable = &ast.NullableConstraintDefinition{Nullable: true} + p.nextToken() + } else if p.curTok.Type == TokenNot { + p.nextToken() // consume NOT + if p.curTok.Type == TokenNull { + param.Nullable = &ast.NullableConstraintDefinition{Nullable: false} + p.nextToken() + } + } + stmt.Parameters = append(stmt.Parameters, param) if p.curTok.Type == TokenComma { @@ -6183,6 +6655,64 @@ func (p *Parser) parseAlterFunctionStatement() (*ast.AlterFunctionStatement, err DataType: returnDataType, } + // Parse optional WITH clause for function options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + for { + upperOpt := strings.ToUpper(p.curTok.Literal) + switch upperOpt { + case "INLINE": + p.nextToken() // consume INLINE + // Expect = ON|OFF + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + optState := strings.ToUpper(p.curTok.Literal) + state := "On" + if optState == "OFF" { + state = "Off" + } + p.nextToken() // consume ON/OFF + stmt.Options = append(stmt.Options, &ast.InlineFunctionOption{ + OptionKind: "Inline", + OptionState: state, + }) + case "ENCRYPTION", "SCHEMABINDING", "NATIVE_COMPILATION", "CALLED": + optKind := capitalizeFirst(strings.ToLower(p.curTok.Literal)) + p.nextToken() + // Handle CALLED ON NULL INPUT + if optKind == "Called" { + for strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + optKind = "CalledOnNullInput" + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: optKind, + }) + case "RETURNS": + // Handle RETURNS NULL ON NULL INPUT + for strings.ToUpper(p.curTok.Literal) == "RETURNS" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: "ReturnsNullOnNullInput", + }) + default: + // Unknown option - skip it + if p.curTok.Type == TokenIdent { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + } + // Parse AS if p.curTok.Type == TokenAs { p.nextToken() @@ -6638,6 +7168,221 @@ func (p *Parser) parseCreateFunctionStatement() (*ast.CreateFunctionStatement, e DataType: returnDataType, } + // Parse optional WITH clause for function options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + for { + upperOpt := strings.ToUpper(p.curTok.Literal) + switch upperOpt { + case "INLINE": + p.nextToken() // consume INLINE + // Expect = ON|OFF + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + optState := strings.ToUpper(p.curTok.Literal) + state := "On" + if optState == "OFF" { + state = "Off" + } + p.nextToken() // consume ON/OFF + stmt.Options = append(stmt.Options, &ast.InlineFunctionOption{ + OptionKind: "Inline", + OptionState: state, + }) + case "ENCRYPTION", "SCHEMABINDING", "NATIVE_COMPILATION", "CALLED": + optKind := capitalizeFirst(strings.ToLower(p.curTok.Literal)) + p.nextToken() + // Handle CALLED ON NULL INPUT + if optKind == "Called" { + for strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + optKind = "CalledOnNullInput" + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: optKind, + }) + case "RETURNS": + // Handle RETURNS NULL ON NULL INPUT + for strings.ToUpper(p.curTok.Literal) == "RETURNS" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: "ReturnsNullOnNullInput", + }) + default: + // Unknown option - skip it + if p.curTok.Type == TokenIdent { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + } + + // Parse AS + if p.curTok.Type == TokenAs { + p.nextToken() + } + + // Parse statement list + stmtList, err := p.parseFunctionStatementList() + if err != nil { + p.skipToEndOfStatement() + return stmt, nil + } + stmt.StatementList = stmtList + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +// parseCreateOrAlterFunctionStatement parses a CREATE OR ALTER FUNCTION statement +func (p *Parser) parseCreateOrAlterFunctionStatement() (*ast.CreateOrAlterFunctionStatement, error) { + // Consume FUNCTION + p.nextToken() + + stmt := &ast.CreateOrAlterFunctionStatement{} + + // Parse function name + name, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + stmt.Name = name + + // Parse parameters in parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + param := &ast.ProcedureParameter{ + IsVarying: false, + Modifier: "None", + } + + // Parse parameter name + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + param.VariableName = &ast.Identifier{ + Value: p.curTok.Literal, + QuoteType: "NotQuoted", + } + p.nextToken() + } + + // Parse data type if present + if p.curTok.Type != TokenRParen && p.curTok.Type != TokenComma { + dataType, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + param.DataType = dataType + } + + // Check for READONLY modifier + if strings.ToUpper(p.curTok.Literal) == "READONLY" { + param.Modifier = "ReadOnly" + p.nextToken() + } + + stmt.Parameters = append(stmt.Parameters, param) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + // Expect RETURNS - if not present, be lenient and skip + if p.curTok.Type != TokenReturns { + p.skipToEndOfStatement() + return stmt, nil + } + p.nextToken() + + // Parse return type + returnDataType, err := p.parseDataTypeReference() + if err != nil { + p.skipToEndOfStatement() + return stmt, nil + } + stmt.ReturnType = &ast.ScalarFunctionReturnType{ + DataType: returnDataType, + } + + // Parse optional WITH clause for function options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + for { + upperOpt := strings.ToUpper(p.curTok.Literal) + switch upperOpt { + case "INLINE": + p.nextToken() // consume INLINE + // Expect = ON|OFF + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + optState := strings.ToUpper(p.curTok.Literal) + state := "On" + if optState == "OFF" { + state = "Off" + } + p.nextToken() // consume ON/OFF + stmt.Options = append(stmt.Options, &ast.InlineFunctionOption{ + OptionKind: "Inline", + OptionState: state, + }) + case "ENCRYPTION", "SCHEMABINDING", "NATIVE_COMPILATION", "CALLED": + optKind := capitalizeFirst(strings.ToLower(p.curTok.Literal)) + p.nextToken() + // Handle CALLED ON NULL INPUT + if optKind == "Called" { + for strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + optKind = "CalledOnNullInput" + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: optKind, + }) + case "RETURNS": + // Handle RETURNS NULL ON NULL INPUT + for strings.ToUpper(p.curTok.Literal) == "RETURNS" || strings.ToUpper(p.curTok.Literal) == "NULL" || strings.ToUpper(p.curTok.Literal) == "ON" || strings.ToUpper(p.curTok.Literal) == "INPUT" { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.FunctionOption{ + OptionKind: "ReturnsNullOnNullInput", + }) + default: + // Unknown option - skip it + if p.curTok.Type == TokenIdent { + p.nextToken() + } + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + } + // Parse AS if p.curTok.Type == TokenAs { p.nextToken() @@ -6659,6 +7404,24 @@ func (p *Parser) parseCreateFunctionStatement() (*ast.CreateFunctionStatement, e return stmt, nil } +// 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() +} + +// 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() +} + +// 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() +} + // parseCreateTriggerStatement parses a CREATE TRIGGER statement func (p *Parser) parseCreateTriggerStatement() (*ast.CreateTriggerStatement, error) { // Consume TRIGGER @@ -6817,7 +7580,12 @@ func (p *Parser) parseCreateTriggerStatement() (*ast.CreateTriggerStatement, err p.nextToken() } - // Parse statement list + // Skip leading semicolons + for p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + // Parse statement list (all statements until GO/EOF) stmtList := &ast.StatementList{} for p.curTok.Type != TokenEOF { // Check for GO or end of batch @@ -6825,6 +7593,12 @@ func (p *Parser) parseCreateTriggerStatement() (*ast.CreateTriggerStatement, err break } + // Skip semicolons between statements + if p.curTok.Type == TokenSemicolon { + p.nextToken() + continue + } + innerStmt, err := p.parseStatement() if err != nil { return nil, err @@ -6832,9 +7606,6 @@ func (p *Parser) parseCreateTriggerStatement() (*ast.CreateTriggerStatement, err if innerStmt != nil { stmtList.Statements = append(stmtList.Statements, innerStmt) } - - // For simple triggers, stop after parsing one statement - break } stmt.StatementList = stmtList @@ -7100,6 +7871,21 @@ func userOptionToJSON(o ast.UserOption) jsonNode { node["Identifier"] = identifierToJSON(opt.Identifier) } return node + case *ast.PasswordAlterPrincipalOption: + node := jsonNode{ + "$type": "PasswordAlterPrincipalOption", + "OptionKind": opt.OptionKind, + "MustChange": opt.MustChange, + "Unlock": opt.Unlock, + "Hashed": opt.Hashed, + } + if opt.Password != nil { + node["Password"] = stringLiteralToJSON(opt.Password) + } + if opt.OldPassword != nil { + node["OldPassword"] = stringLiteralToJSON(opt.OldPassword) + } + return node default: return jsonNode{"$type": "UnknownUserOption"} } @@ -7332,7 +8118,7 @@ func alterFunctionStatementToJSON(s *ast.AlterFunctionStatement) jsonNode { if len(s.Options) > 0 { opts := make([]jsonNode, len(s.Options)) for i, o := range s.Options { - opts[i] = functionOptionToJSON(o) + opts[i] = functionOptionBaseToJSON(o) } node["Options"] = opts } @@ -7356,6 +8142,25 @@ func functionOptionToJSON(o *ast.FunctionOption) jsonNode { } } +func inlineFunctionOptionToJSON(o *ast.InlineFunctionOption) jsonNode { + return jsonNode{ + "$type": "InlineFunctionOption", + "OptionState": o.OptionState, + "OptionKind": o.OptionKind, + } +} + +func functionOptionBaseToJSON(o ast.FunctionOptionBase) jsonNode { + switch opt := o.(type) { + case *ast.FunctionOption: + return functionOptionToJSON(opt) + case *ast.InlineFunctionOption: + return inlineFunctionOptionToJSON(opt) + default: + return jsonNode{"$type": "UnknownFunctionOption"} + } +} + func createFunctionStatementToJSON(s *ast.CreateFunctionStatement) jsonNode { node := jsonNode{ "$type": "CreateFunctionStatement", @@ -7373,25 +8178,62 @@ func createFunctionStatementToJSON(s *ast.CreateFunctionStatement) jsonNode { if s.ReturnType != nil { node["ReturnType"] = functionReturnTypeToJSON(s.ReturnType) } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = functionOptionBaseToJSON(o) + } + node["Options"] = options + } if s.StatementList != nil { node["StatementList"] = statementListToJSON(s.StatementList) } return node } -func functionReturnTypeToJSON(r ast.FunctionReturnType) jsonNode { - switch rt := r.(type) { - case *ast.ScalarFunctionReturnType: - node := jsonNode{ - "$type": "ScalarFunctionReturnType", - } - if rt.DataType != nil { - node["DataType"] = dataTypeReferenceToJSON(rt.DataType) - } - return node - case *ast.SelectFunctionReturnType: - node := jsonNode{ - "$type": "SelectFunctionReturnType", +func createOrAlterFunctionStatementToJSON(s *ast.CreateOrAlterFunctionStatement) jsonNode { + node := jsonNode{ + "$type": "CreateOrAlterFunctionStatement", + } + if s.Name != nil { + node["Name"] = schemaObjectNameToJSON(s.Name) + } + 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.ReturnType != nil { + node["ReturnType"] = functionReturnTypeToJSON(s.ReturnType) + } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = functionOptionBaseToJSON(o) + } + node["Options"] = options + } + if s.StatementList != nil { + node["StatementList"] = statementListToJSON(s.StatementList) + } + return node +} + +func functionReturnTypeToJSON(r ast.FunctionReturnType) jsonNode { + switch rt := r.(type) { + case *ast.ScalarFunctionReturnType: + node := jsonNode{ + "$type": "ScalarFunctionReturnType", + } + if rt.DataType != nil { + node["DataType"] = dataTypeReferenceToJSON(rt.DataType) + } + return node + case *ast.SelectFunctionReturnType: + node := jsonNode{ + "$type": "SelectFunctionReturnType", } if rt.SelectStatement != nil { node["SelectStatement"] = selectStatementToJSON(rt.SelectStatement) @@ -7611,12 +8453,13 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { func convertUserOptionKind(name string) string { // Convert option names to the expected format optionMap := map[string]string{ - "OBJECT_ID": "Object_ID", - "DEFAULT_SCHEMA": "Default_Schema", - "SID": "Sid", - "PASSWORD": "Password", - "NAME": "Name", - "LOGIN": "Login", + "OBJECT_ID": "Object_ID", + "DEFAULT_SCHEMA": "DefaultSchema", + "DEFAULT_LANGUAGE": "DefaultLanguage", + "SID": "Sid", + "PASSWORD": "Password", + "NAME": "Name", + "LOGIN": "Login", } upper := strings.ToUpper(name) if mapped, ok := optionMap[upper]; ok { @@ -7724,9 +8567,9 @@ func dropIndexStatementToJSON(s *ast.DropIndexStatement) jsonNode { "$type": "DropIndexStatement", "IsIfExists": s.IsIfExists, } - if len(s.Indexes) > 0 { - clauses := make([]jsonNode, len(s.Indexes)) - for i, clause := range s.Indexes { + if len(s.DropIndexClauses) > 0 { + clauses := make([]jsonNode, len(s.DropIndexClauses)) + for i, clause := range s.DropIndexClauses { clauses[i] = dropIndexClauseToJSON(clause) } node["DropIndexClauses"] = clauses @@ -7740,26 +8583,114 @@ func dropIndexClauseToJSON(c *ast.DropIndexClause) jsonNode { node := jsonNode{ "$type": "DropIndexClause", } - if c.IndexName != nil { - node["Index"] = identifierToJSON(c.IndexName) + if c.Index != nil { + node["Index"] = identifierToJSON(c.Index) } node["Object"] = schemaObjectNameToJSON(c.Object) + if len(c.Options) > 0 { + options := make([]jsonNode, len(c.Options)) + for i, opt := range c.Options { + options[i] = dropIndexOptionToJSON(opt) + } + node["Options"] = options + } return node } - // Otherwise use DropIndexClauseBase for backwards-compatible syntax + // Otherwise use BackwardsCompatibleDropIndexClause for backwards-compatible syntax node := jsonNode{ - "$type": "DropIndexClauseBase", + "$type": "BackwardsCompatibleDropIndexClause", } - if c.Index != nil { - node["Index"] = schemaObjectNameToJSON(c.Index) - } else if c.IndexName != nil { + if c.LegacyIndex != nil { + node["Index"] = childObjectNameToJSON(c.LegacyIndex) + } else if c.Index != nil { // Just index name without object - use identifier - node["Index"] = identifierToJSON(c.IndexName) + node["Index"] = identifierToJSON(c.Index) + } + return node +} + +// childObjectNameToJSON converts a SchemaObjectName to a ChildObjectName JSON format +// where the BaseIdentifier is the parent and the last identifier becomes ChildIdentifier +func childObjectNameToJSON(s *ast.SchemaObjectName) jsonNode { + node := jsonNode{ + "$type": "ChildObjectName", + "Count": s.Count, + } + + // For ChildObjectName: BaseIdentifier is the parent, ChildIdentifier is the actual child + if s.Count >= 2 { + // For 2-part: BaseIdentifier.ChildIdentifier + if s.SchemaIdentifier != nil { + node["BaseIdentifier"] = identifierToJSON(s.SchemaIdentifier) + } + if s.BaseIdentifier != nil { + node["ChildIdentifier"] = identifierToJSON(s.BaseIdentifier) + } + } + + // For 3+ parts: add DatabaseIdentifier/SchemaIdentifier + if s.Count >= 3 { + if s.DatabaseIdentifier != nil { + node["SchemaIdentifier"] = identifierToJSON(s.DatabaseIdentifier) + } + } + + if s.Count >= 4 { + if s.ServerIdentifier != nil { + node["DatabaseIdentifier"] = identifierToJSON(s.ServerIdentifier) + } + } + + // Add identifiers array + if len(s.Identifiers) > 0 { + idents := make([]jsonNode, len(s.Identifiers)) + for i, id := range s.Identifiers { + idents[i] = jsonNode{"$ref": "Identifier"} + _ = id + } + node["Identifiers"] = idents } + return node } +func dropIndexOptionToJSON(opt ast.DropIndexOption) jsonNode { + switch o := opt.(type) { + case *ast.OnlineIndexOption: + return jsonNode{ + "$type": "OnlineIndexOption", + "OptionState": o.OptionState, + "OptionKind": o.OptionKind, + } + case *ast.MoveToDropIndexOption: + node := jsonNode{ + "$type": "MoveToDropIndexOption", + "OptionKind": o.OptionKind, + } + if o.MoveTo != nil { + node["MoveTo"] = fileGroupOrPartitionSchemeToJSON(o.MoveTo) + } + return node + case *ast.FileStreamOnDropIndexOption: + node := jsonNode{ + "$type": "FileStreamOnDropIndexOption", + "OptionKind": o.OptionKind, + } + if o.FileStreamOn != nil { + node["FileStreamOn"] = identifierOrValueExpressionToJSON(o.FileStreamOn) + } + return node + case *ast.DataCompressionOption: + return jsonNode{ + "$type": "DataCompressionOption", + "CompressionLevel": o.CompressionLevel, + "OptionKind": o.OptionKind, + } + } + return jsonNode{} +} + func dropStatisticsStatementToJSON(s *ast.DropStatisticsStatement) jsonNode { node := jsonNode{ "$type": "DropStatisticsStatement", @@ -7767,7 +8698,7 @@ func dropStatisticsStatementToJSON(s *ast.DropStatisticsStatement) jsonNode { if len(s.Objects) > 0 { objects := make([]jsonNode, len(s.Objects)) for i, obj := range s.Objects { - objects[i] = schemaObjectNameToJSON(obj) + objects[i] = childObjectNameToJSON(obj) } node["Objects"] = objects } @@ -8299,6 +9230,18 @@ func dropAsymmetricKeyStatementToJSON(s *ast.DropAsymmetricKeyStatement) jsonNod return node } +func dropSymmetricKeyStatementToJSON(s *ast.DropSymmetricKeyStatement) jsonNode { + node := jsonNode{ + "$type": "DropSymmetricKeyStatement", + "RemoveProviderKey": s.RemoveProviderKey, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + node["IsIfExists"] = s.IsIfExists + return node +} + func alterTableTriggerModificationStatementToJSON(s *ast.AlterTableTriggerModificationStatement) jsonNode { node := jsonNode{ "$type": "AlterTableTriggerModificationStatement", @@ -8394,15 +9337,6 @@ func alterTableSetStatementToJSON(s *ast.AlterTableSetStatement) jsonNode { return node } -func tableOptionToJSON(opt ast.TableOption) jsonNode { - switch o := opt.(type) { - case *ast.SystemVersioningTableOption: - return systemVersioningTableOptionToJSON(o) - default: - return jsonNode{"$type": "UnknownTableOption"} - } -} - func systemVersioningTableOptionToJSON(o *ast.SystemVersioningTableOption) jsonNode { node := jsonNode{ "$type": "SystemVersioningTableOption", @@ -8694,6 +9628,13 @@ func alterUserStatementToJSON(s *ast.AlterUserStatement) jsonNode { if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if len(s.UserOptions) > 0 { + options := make([]jsonNode, len(s.UserOptions)) + for i, o := range s.UserOptions { + options[i] = userOptionToJSON(o) + } + node["UserOptions"] = options + } return node } @@ -8704,6 +9645,13 @@ func alterRouteStatementToJSON(s *ast.AlterRouteStatement) jsonNode { if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if len(s.RouteOptions) > 0 { + opts := make([]jsonNode, len(s.RouteOptions)) + for i, opt := range s.RouteOptions { + opts[i] = routeOptionToJSON(opt) + } + node["RouteOptions"] = opts + } return node } @@ -8724,9 +9672,96 @@ func alterEndpointStatementToJSON(s *ast.AlterEndpointStatement) jsonNode { if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.State != "" { + node["State"] = s.State + } + if s.Affinity != nil { + node["Affinity"] = endpointAffinityToJSON(s.Affinity) + } + if s.Protocol != "" { + node["Protocol"] = s.Protocol + } + if len(s.ProtocolOptions) > 0 { + opts := make([]jsonNode, len(s.ProtocolOptions)) + for i, opt := range s.ProtocolOptions { + opts[i] = endpointProtocolOptionToJSON(opt) + } + node["ProtocolOptions"] = opts + } + if s.EndpointType != "" { + node["EndpointType"] = s.EndpointType + } + if len(s.PayloadOptions) > 0 { + opts := make([]jsonNode, len(s.PayloadOptions)) + for i, opt := range s.PayloadOptions { + opts[i] = payloadOptionToJSON(opt) + } + node["PayloadOptions"] = opts + } + return node +} + +func endpointAffinityToJSON(a *ast.EndpointAffinity) jsonNode { + node := jsonNode{ + "$type": "EndpointAffinity", + } + if a.Kind != "" { + node["Kind"] = a.Kind + } + if a.Value != nil { + node["Value"] = scalarExpressionToJSON(a.Value) + } return node } +func endpointProtocolOptionToJSON(opt ast.EndpointProtocolOption) jsonNode { + switch o := opt.(type) { + case *ast.LiteralEndpointProtocolOption: + node := jsonNode{ + "$type": "LiteralEndpointProtocolOption", + } + if o.Value != nil { + node["Value"] = scalarExpressionToJSON(o.Value) + } + if o.Kind != "" { + node["Kind"] = o.Kind + } + return node + default: + return jsonNode{"$type": "UnknownProtocolOption"} + } +} + +func payloadOptionToJSON(opt ast.PayloadOption) jsonNode { + switch o := opt.(type) { + case *ast.SoapMethod: + node := jsonNode{ + "$type": "SoapMethod", + } + if o.Alias != nil { + node["Alias"] = stringLiteralToJSON(o.Alias) + } + if o.Action != "" { + node["Action"] = o.Action + } + if o.Name != nil { + node["Name"] = stringLiteralToJSON(o.Name) + } + if o.Format != "" { + node["Format"] = o.Format + } + if o.Schema != "" { + node["Schema"] = o.Schema + } + if o.Kind != "" { + node["Kind"] = o.Kind + } + return node + default: + return jsonNode{"$type": "UnknownPayloadOption"} + } +} + func alterServiceStatementToJSON(s *ast.AlterServiceStatement) jsonNode { node := jsonNode{ "$type": "AlterServiceStatement", @@ -8734,6 +9769,16 @@ func alterServiceStatementToJSON(s *ast.AlterServiceStatement) jsonNode { if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.QueueName != nil { + node["QueueName"] = schemaObjectNameToJSON(s.QueueName) + } + if len(s.ServiceContracts) > 0 { + contracts := make([]jsonNode, len(s.ServiceContracts)) + for i, c := range s.ServiceContracts { + contracts[i] = serviceContractToJSON(c) + } + node["ServiceContracts"] = contracts + } return node } @@ -8978,6 +10023,9 @@ func createDatabaseStatementToJSON(s *ast.CreateDatabaseStatement) jsonNode { if s.DatabaseName != nil { node["DatabaseName"] = identifierToJSON(s.DatabaseName) } + if s.Containment != nil { + node["Containment"] = containmentDatabaseOptionToJSON(s.Containment) + } if len(s.Options) > 0 { opts := make([]jsonNode, len(s.Options)) for i, opt := range s.Options { @@ -8985,16 +10033,133 @@ func createDatabaseStatementToJSON(s *ast.CreateDatabaseStatement) jsonNode { } node["Options"] = opts } - // AttachMode is output when there are Options or CopyOf - if (len(s.Options) > 0 || s.CopyOf != nil) && s.AttachMode != "" { + if len(s.FileGroups) > 0 { + fgs := make([]jsonNode, len(s.FileGroups)) + for i, fg := range s.FileGroups { + fgs[i] = fileGroupDefinitionToJSON(fg) + } + node["FileGroups"] = fgs + } + if len(s.LogOn) > 0 { + logs := make([]jsonNode, len(s.LogOn)) + for i, fd := range s.LogOn { + logs[i] = fileDeclarationToJSON(fd) + } + node["LogOn"] = logs + } + // AttachMode is output when there are FileGroups, Options, Collation, CopyOf, or Containment + if len(s.FileGroups) > 0 || len(s.Options) > 0 || s.Collation != nil || s.CopyOf != nil || s.Containment != nil { node["AttachMode"] = s.AttachMode } if s.CopyOf != nil { node["CopyOf"] = multiPartIdentifierToJSON(s.CopyOf) } + if s.Collation != nil { + node["Collation"] = identifierToJSON(s.Collation) + } + return node +} + +func containmentDatabaseOptionToJSON(c *ast.ContainmentDatabaseOption) jsonNode { + return jsonNode{ + "$type": "ContainmentDatabaseOption", + "Value": c.Value, + "OptionKind": c.OptionKind, + } +} + +func fileGroupDefinitionToJSON(fg *ast.FileGroupDefinition) jsonNode { + node := jsonNode{ + "$type": "FileGroupDefinition", + } + if fg.Name != nil { + node["Name"] = identifierToJSON(fg.Name) + } + if len(fg.FileDeclarations) > 0 { + decls := make([]jsonNode, len(fg.FileDeclarations)) + for i, fd := range fg.FileDeclarations { + decls[i] = fileDeclarationToJSON(fd) + } + node["FileDeclarations"] = decls + } + node["IsDefault"] = fg.IsDefault + node["ContainsFileStream"] = fg.ContainsFileStream + node["ContainsMemoryOptimizedData"] = fg.ContainsMemoryOptimizedData + return node +} + +func fileDeclarationToJSON(fd *ast.FileDeclaration) jsonNode { + node := jsonNode{ + "$type": "FileDeclaration", + } + if len(fd.Options) > 0 { + opts := make([]jsonNode, len(fd.Options)) + for i, opt := range fd.Options { + opts[i] = fileDeclarationOptionToJSON(opt) + } + node["Options"] = opts + } + node["IsPrimary"] = fd.IsPrimary return node } +func fileDeclarationOptionToJSON(opt ast.FileDeclarationOption) jsonNode { + switch o := opt.(type) { + case *ast.NameFileDeclarationOption: + node := jsonNode{ + "$type": "NameFileDeclarationOption", + "IsNewName": o.IsNewName, + "OptionKind": o.OptionKind, + } + if o.LogicalFileName != nil { + node["LogicalFileName"] = identifierOrValueExpressionToJSON(o.LogicalFileName) + } + return node + case *ast.FileNameFileDeclarationOption: + node := jsonNode{ + "$type": "FileNameFileDeclarationOption", + "OptionKind": o.OptionKind, + } + if o.OSFileName != nil { + node["OSFileName"] = stringLiteralToJSON(o.OSFileName) + } + return node + case *ast.SizeFileDeclarationOption: + node := jsonNode{ + "$type": "SizeFileDeclarationOption", + "Units": o.Units, + "OptionKind": o.OptionKind, + } + if o.Size != nil { + node["Size"] = scalarExpressionToJSON(o.Size) + } + return node + case *ast.MaxSizeFileDeclarationOption: + node := jsonNode{ + "$type": "MaxSizeFileDeclarationOption", + "Units": o.Units, + "Unlimited": o.Unlimited, + "OptionKind": o.OptionKind, + } + if o.MaxSize != nil { + node["MaxSize"] = scalarExpressionToJSON(o.MaxSize) + } + return node + case *ast.FileGrowthFileDeclarationOption: + node := jsonNode{ + "$type": "FileGrowthFileDeclarationOption", + "Units": o.Units, + "OptionKind": o.OptionKind, + } + if o.GrowthIncrement != nil { + node["GrowthIncrement"] = scalarExpressionToJSON(o.GrowthIncrement) + } + return node + default: + return jsonNode{"$type": "FileDeclarationOption"} + } +} + func createDatabaseOptionToJSON(opt ast.CreateDatabaseOption) jsonNode { switch o := opt.(type) { case *ast.OnOffDatabaseOption: @@ -9142,9 +10307,40 @@ func createSymmetricKeyStatementToJSON(s *ast.CreateSymmetricKeyStatement) jsonN node := jsonNode{ "$type": "CreateSymmetricKeyStatement", } + if len(s.KeyOptions) > 0 { + opts := make([]interface{}, len(s.KeyOptions)) + for i, opt := range s.KeyOptions { + opts[i] = keyOptionToJSON(opt) + } + node["KeyOptions"] = opts + } + if s.Provider != nil { + node["Provider"] = identifierToJSON(s.Provider) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if len(s.EncryptingMechanisms) > 0 { + mechs := make([]jsonNode, len(s.EncryptingMechanisms)) + for i, mech := range s.EncryptingMechanisms { + mechs[i] = cryptoMechanismToJSON(mech) + } + node["EncryptingMechanisms"] = mechs + } + return node +} + +func cryptoMechanismToJSON(mech *ast.CryptoMechanism) jsonNode { + node := jsonNode{ + "$type": "CryptoMechanism", + "CryptoMechanismType": mech.CryptoMechanismType, + } + if mech.Identifier != nil { + node["Identifier"] = identifierToJSON(mech.Identifier) + } + if mech.PasswordOrSignature != nil { + node["PasswordOrSignature"] = scalarExpressionToJSON(mech.PasswordOrSignature) + } return node } @@ -9181,9 +10377,33 @@ func createServiceStatementToJSON(s *ast.CreateServiceStatement) jsonNode { node := jsonNode{ "$type": "CreateServiceStatement", } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if s.QueueName != nil { + node["QueueName"] = schemaObjectNameToJSON(s.QueueName) + } + if len(s.ServiceContracts) > 0 { + contracts := make([]jsonNode, len(s.ServiceContracts)) + for i, c := range s.ServiceContracts { + contracts[i] = serviceContractToJSON(c) + } + node["ServiceContracts"] = contracts + } + return node +} + +func serviceContractToJSON(c *ast.ServiceContract) jsonNode { + node := jsonNode{ + "$type": "ServiceContract", + } + if c.Name != nil { + node["Name"] = identifierToJSON(c.Name) + } + node["Action"] = c.Action return node } @@ -9228,9 +10448,30 @@ func createRouteStatementToJSON(s *ast.CreateRouteStatement) jsonNode { node := jsonNode{ "$type": "CreateRouteStatement", } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if len(s.RouteOptions) > 0 { + opts := make([]jsonNode, len(s.RouteOptions)) + for i, opt := range s.RouteOptions { + opts[i] = routeOptionToJSON(opt) + } + node["RouteOptions"] = opts + } + return node +} + +func routeOptionToJSON(opt *ast.RouteOption) jsonNode { + node := jsonNode{ + "$type": "RouteOption", + "OptionKind": opt.OptionKind, + } + if opt.Literal != nil { + node["Literal"] = scalarExpressionToJSON(opt.Literal) + } return node } @@ -9306,9 +10547,19 @@ func createRemoteServiceBindingStatementToJSON(s *ast.CreateRemoteServiceBinding node := jsonNode{ "$type": "CreateRemoteServiceBindingStatement", } + if s.Service != nil { + node["Service"] = scalarExpressionToJSON(s.Service) + } if s.Name != nil { node["Name"] = identifierToJSON(s.Name) } + if len(s.Options) > 0 { + options := make([]jsonNode, len(s.Options)) + for i, o := range s.Options { + options[i] = remoteServiceBindingOptionToJSON(o) + } + node["Options"] = options + } return node } @@ -9503,9 +10754,34 @@ func alterDatabaseAddFileStatementToJSON(s *ast.AlterDatabaseAddFileStatement) j node := jsonNode{ "$type": "AlterDatabaseAddFileStatement", } + // Check if we have any complete file declarations (with options) + hasCompleteDeclarations := false + for _, fd := range s.FileDeclarations { + if len(fd.Options) > 0 { + hasCompleteDeclarations = true + break + } + } + if hasCompleteDeclarations { + decls := make([]jsonNode, len(s.FileDeclarations)) + for i, fd := range s.FileDeclarations { + decls[i] = fileDeclarationToJSON(fd) + } + node["FileDeclarations"] = decls + } + if s.FileGroup != nil { + node["FileGroup"] = identifierToJSON(s.FileGroup) + } + // Only include IsLog/UseCurrent if we have complete declarations + if hasCompleteDeclarations { + node["IsLog"] = s.IsLog + } if s.DatabaseName != nil { node["DatabaseName"] = identifierToJSON(s.DatabaseName) } + if hasCompleteDeclarations { + node["UseCurrent"] = s.UseCurrent + } return node } @@ -9646,6 +10922,67 @@ func createCryptographicProviderStatementToJSON(s *ast.CreateCryptographicProvid return node } +func createColumnMasterKeyStatementToJSON(s *ast.CreateColumnMasterKeyStatement) jsonNode { + node := jsonNode{ + "$type": "CreateColumnMasterKeyStatement", + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + if len(s.Parameters) > 0 { + params := make([]jsonNode, len(s.Parameters)) + for i, p := range s.Parameters { + params[i] = columnMasterKeyParameterToJSON(p) + } + node["Parameters"] = params + } + return node +} + +func columnMasterKeyParameterToJSON(p ast.ColumnMasterKeyParameter) jsonNode { + switch param := p.(type) { + case *ast.ColumnMasterKeyStoreProviderNameParameter: + node := jsonNode{ + "$type": "ColumnMasterKeyStoreProviderNameParameter", + } + if param.Name != nil { + node["Name"] = scalarExpressionToJSON(param.Name) + } + node["ParameterKind"] = param.ParameterKind + return node + case *ast.ColumnMasterKeyPathParameter: + node := jsonNode{ + "$type": "ColumnMasterKeyPathParameter", + } + if param.Path != nil { + node["Path"] = scalarExpressionToJSON(param.Path) + } + node["ParameterKind"] = param.ParameterKind + return node + case *ast.ColumnMasterKeyEnclaveComputationsParameter: + node := jsonNode{ + "$type": "ColumnMasterKeyEnclaveComputationsParameter", + } + if param.Signature != nil { + node["Signature"] = scalarExpressionToJSON(param.Signature) + } + node["ParameterKind"] = param.ParameterKind + return node + default: + return jsonNode{"$type": "UnknownColumnMasterKeyParameter"} + } +} + +func dropColumnMasterKeyStatementToJSON(s *ast.DropColumnMasterKeyStatement) jsonNode { + node := jsonNode{ + "$type": "DropColumnMasterKeyStatement", + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + func alterCryptographicProviderStatementToJSON(s *ast.AlterCryptographicProviderStatement) jsonNode { node := jsonNode{ "$type": "AlterCryptographicProviderStatement", diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index bf19646e..84e4af01 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -112,6 +112,8 @@ func (p *Parser) parseDropStatement() (ast.Statement, error) { return p.parseDropCryptographicProviderStatement() case "ASYMMETRIC": return p.parseDropAsymmetricKeyStatement() + case "SYMMETRIC": + return p.parseDropSymmetricKeyStatement() } return nil, fmt.Errorf("unexpected token after DROP: %s", p.curTok.Literal) @@ -698,6 +700,50 @@ func (p *Parser) parseDropAsymmetricKeyStatement() (*ast.DropAsymmetricKeyStatem return stmt, nil } +func (p *Parser) parseDropSymmetricKeyStatement() (*ast.DropSymmetricKeyStatement, error) { + // Consume SYMMETRIC + p.nextToken() + + // Expect KEY + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() + } + + stmt := &ast.DropSymmetricKeyStatement{} + + // Check for IF EXISTS + if p.curTok.Type == TokenIf { + p.nextToken() + if strings.ToUpper(p.curTok.Literal) != "EXISTS" { + return nil, fmt.Errorf("expected EXISTS after IF, got %s", p.curTok.Literal) + } + p.nextToken() + stmt.IsIfExists = true + } + + // Parse key name + stmt.Name = p.parseIdentifier() + + // Check for REMOVE PROVIDER KEY + if strings.ToUpper(p.curTok.Literal) == "REMOVE" { + p.nextToken() // consume REMOVE + if strings.ToUpper(p.curTok.Literal) == "PROVIDER" { + p.nextToken() // consume PROVIDER + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + stmt.RemoveProviderKey = true + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseDropDatabaseStatement() (ast.Statement, error) { // Consume DATABASE p.nextToken() @@ -1036,7 +1082,9 @@ func (p *Parser) parseDropTriggerStatement() (*ast.DropTriggerStatement, error) // Consume TRIGGER p.nextToken() - stmt := &ast.DropTriggerStatement{} + stmt := &ast.DropTriggerStatement{ + TriggerScope: "Normal", // Default to Normal for regular DROP TRIGGER + } // Check for IF EXISTS if strings.ToUpper(p.curTok.Literal) == "IF" { @@ -1117,22 +1165,36 @@ func (p *Parser) parseDropIndexStatement() (*ast.DropIndexStatement, error) { if err != nil { return nil, err } - clause.IndexName = indexName + clause.Index = indexName clause.Object = tableName } else if p.curTok.Type == TokenDot { // Old backwards-compatible syntax: table.index p.nextToken() // consume dot childName := p.parseIdentifier() - clause.Index = &ast.SchemaObjectName{ + clause.LegacyIndex = &ast.SchemaObjectName{ SchemaIdentifier: indexName, BaseIdentifier: childName, + Count: 2, + Identifiers: []*ast.Identifier{indexName, childName}, } } else { // Just index name without ON or dot - clause.IndexName = indexName + clause.Index = indexName } - stmt.Indexes = append(stmt.Indexes, clause) + // Parse WITH options if present + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + clause.Options = p.parseDropIndexOptions() + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + + stmt.DropIndexClauses = append(stmt.DropIndexClauses, clause) if p.curTok.Type != TokenComma { break @@ -1148,6 +1210,105 @@ func (p *Parser) parseDropIndexStatement() (*ast.DropIndexStatement, error) { return stmt, nil } +func (p *Parser) parseDropIndexOptions() []ast.DropIndexOption { + var options []ast.DropIndexOption + + for { + upperLit := strings.ToUpper(p.curTok.Literal) + switch upperLit { + case "ONLINE": + p.nextToken() // consume ONLINE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + optState := "Off" + if strings.ToUpper(p.curTok.Literal) == "ON" { + optState = "On" + } + p.nextToken() + options = append(options, &ast.OnlineIndexOption{ + OptionState: optState, + OptionKind: "Online", + }) + case "MOVE": + p.nextToken() // consume MOVE + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + } + moveTo := &ast.FileGroupOrPartitionScheme{} + // Parse filegroup name + fgName := p.parseIdentifier() + moveTo.Name = &ast.IdentifierOrValueExpression{ + Value: fgName.Value, + Identifier: fgName, + } + // Check for partition columns + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + var cols []*ast.Identifier + for { + cols = append(cols, p.parseIdentifier()) + if p.curTok.Type != TokenComma { + break + } + p.nextToken() + } + moveTo.PartitionSchemeColumns = cols + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + options = append(options, &ast.MoveToDropIndexOption{ + MoveTo: moveTo, + OptionKind: "MoveTo", + }) + case "FILESTREAM_ON": + p.nextToken() // consume FILESTREAM_ON + ident := p.parseIdentifier() + options = append(options, &ast.FileStreamOnDropIndexOption{ + FileStreamOn: &ast.IdentifierOrValueExpression{ + Value: ident.Value, + Identifier: ident, + }, + OptionKind: "FileStreamOn", + }) + case "DATA_COMPRESSION": + p.nextToken() // consume DATA_COMPRESSION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + level := "None" + upperLevel := strings.ToUpper(p.curTok.Literal) + switch upperLevel { + case "ROW": + level = "Row" + case "PAGE": + level = "Page" + case "NONE": + level = "None" + } + p.nextToken() + options = append(options, &ast.DataCompressionOption{ + CompressionLevel: level, + OptionKind: "DataCompression", + }) + default: + // Unknown option, skip + p.nextToken() + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else if p.curTok.Type == TokenRParen { + break + } else if p.curTok.Type == TokenEOF || p.curTok.Type == TokenSemicolon { + break + } + } + + return options +} + func (p *Parser) parseDropStatisticsStatement() (*ast.DropStatisticsStatement, error) { // Consume STATISTICS p.nextToken() @@ -1580,6 +1741,21 @@ func (p *Parser) parseAlterDatabaseAddStatement(dbName *ast.Identifier) (ast.Sta p.nextToken() // consume FILE stmt := &ast.AlterDatabaseAddFileStatement{ DatabaseName: dbName, + IsLog: false, + } + // Parse file declarations + decls, err := p.parseFileDeclarationList(false) + if err != nil { + return nil, err + } + stmt.FileDeclarations = decls + // Parse TO FILEGROUP + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { + p.nextToken() // consume FILEGROUP + } + stmt.FileGroup = p.parseIdentifier() } p.skipToEndOfStatement() return stmt, nil @@ -1590,7 +1766,14 @@ func (p *Parser) parseAlterDatabaseAddStatement(dbName *ast.Identifier) (ast.Sta } stmt := &ast.AlterDatabaseAddFileStatement{ DatabaseName: dbName, + IsLog: true, } + // Parse file declarations + decls, err := p.parseFileDeclarationList(false) + if err != nil { + return nil, err + } + stmt.FileDeclarations = decls p.skipToEndOfStatement() return stmt, nil case "FILEGROUP": @@ -2556,9 +2739,229 @@ func (p *Parser) parseAlterTableAddStatement(tableName *ast.SchemaObjectName) (* p.nextToken() // consume CONSTRAINT // Parse constraint name constraintName := p.parseIdentifier() - _ = constraintName // We'll use this later when we implement full constraint support - // Skip to end of statement (lenient parsing for incomplete constraints) - p.skipToEndOfStatement() + + // Check what type of constraint follows + upperLit := strings.ToUpper(p.curTok.Literal) + + switch upperLit { + case "PRIMARY": + p.nextToken() // consume PRIMARY + if p.curTok.Type == TokenKey { + p.nextToken() // consume KEY + } + constraint := &ast.UniqueConstraintDefinition{ + ConstraintIdentifier: constraintName, + IsPrimaryKey: true, + } + // Parse optional CLUSTERED/NONCLUSTERED + for { + upperOpt := strings.ToUpper(p.curTok.Literal) + if upperOpt == "CLUSTERED" { + constraint.Clustered = true + constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} + p.nextToken() + } else if upperOpt == "NONCLUSTERED" { + constraint.Clustered = false + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + } else { + break + } + } + // Parse column list - only add constraint if we have a column list + hasColumnsPK := false + if p.curTok.Type == TokenLParen { + hasColumnsPK = true + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + } + colName := p.parseIdentifier() + colRef.MultiPartIdentifier = &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{colName}, + Count: 1, + } + sortOrder := ast.SortOrderNotSpecified + upperSort := strings.ToUpper(p.curTok.Literal) + if upperSort == "ASC" { + sortOrder = ast.SortOrderAscending + p.nextToken() + } else if upperSort == "DESC" { + sortOrder = ast.SortOrderDescending + p.nextToken() + } + constraint.Columns = append(constraint.Columns, &ast.ColumnWithSortOrder{ + Column: colRef, + SortOrder: sortOrder, + }) + 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() + if strings.ToUpper(p.curTok.Literal) == "ENFORCED" { + p.nextToken() + f := false + constraint.IsEnforced = &f + } + } + // Only add constraint if we successfully parsed a column list + if hasColumnsPK { + stmt.Definition.TableConstraints = append(stmt.Definition.TableConstraints, constraint) + } + + case "UNIQUE": + p.nextToken() // consume UNIQUE + constraint := &ast.UniqueConstraintDefinition{ + ConstraintIdentifier: constraintName, + IsPrimaryKey: false, + } + // Parse optional CLUSTERED/NONCLUSTERED + for { + upperOpt := strings.ToUpper(p.curTok.Literal) + if upperOpt == "CLUSTERED" { + constraint.Clustered = true + constraint.IndexType = &ast.IndexType{IndexTypeKind: "Clustered"} + p.nextToken() + } else if upperOpt == "NONCLUSTERED" { + constraint.Clustered = false + constraint.IndexType = &ast.IndexType{IndexTypeKind: "NonClustered"} + p.nextToken() + } else { + break + } + } + // Parse column list - only add constraint if we have a column list + hasColumnsUQ := false + if p.curTok.Type == TokenLParen { + hasColumnsUQ = true + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colRef := &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + } + colName := p.parseIdentifier() + colRef.MultiPartIdentifier = &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{colName}, + Count: 1, + } + sortOrder := ast.SortOrderNotSpecified + upperSort := strings.ToUpper(p.curTok.Literal) + if upperSort == "ASC" { + sortOrder = ast.SortOrderAscending + p.nextToken() + } else if upperSort == "DESC" { + sortOrder = ast.SortOrderDescending + p.nextToken() + } + constraint.Columns = append(constraint.Columns, &ast.ColumnWithSortOrder{ + Column: colRef, + SortOrder: sortOrder, + }) + 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() + if strings.ToUpper(p.curTok.Literal) == "ENFORCED" { + p.nextToken() + f := false + constraint.IsEnforced = &f + } + } + // Only add constraint if we successfully parsed a column list + if hasColumnsUQ { + stmt.Definition.TableConstraints = append(stmt.Definition.TableConstraints, constraint) + } + + case "FOREIGN": + p.nextToken() // consume FOREIGN + if p.curTok.Type == TokenKey { + p.nextToken() // consume KEY + } + constraint := &ast.ForeignKeyConstraintDefinition{ + ConstraintIdentifier: constraintName, + } + // Parse column list - track if we have a complete constraint + hasColumnsFK := false + hasReferences := false + if p.curTok.Type == TokenLParen { + hasColumnsFK = true + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ident := p.parseIdentifier() + constraint.Columns = append(constraint.Columns, ident) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + // Parse REFERENCES + if strings.ToUpper(p.curTok.Literal) == "REFERENCES" { + hasReferences = true + p.nextToken() + refName, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + constraint.ReferenceTableName = refName + // Parse referenced column list + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ident := p.parseIdentifier() + constraint.ReferencedColumns = append(constraint.ReferencedColumns, ident) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + // Parse NOT ENFORCED + if strings.ToUpper(p.curTok.Literal) == "NOT" { + p.nextToken() + if strings.ToUpper(p.curTok.Literal) == "ENFORCED" { + p.nextToken() + f := false + constraint.IsEnforced = &f + } + } + // Only add constraint if we have columns and references + if hasColumnsFK && hasReferences { + stmt.Definition.TableConstraints = append(stmt.Definition.TableConstraints, constraint) + } + + default: + // Unknown constraint type - skip to end of statement + p.skipToEndOfStatement() + } return stmt, nil } @@ -3708,8 +4111,71 @@ func (p *Parser) parseAlterUserStatement() (*ast.AlterUserStatement, error) { // Parse user name stmt.Name = p.parseIdentifier() - // Skip rest of statement - p.skipToEndOfStatement() + // Parse WITH options + if p.curTok.Type == TokenWith { + p.nextToken() + + for { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + // Handle PASSWORD specially for ALTER USER (can have OLD_PASSWORD) + if optionName == "PASSWORD" { + if p.curTok.Type == TokenEquals { + p.nextToken() + } + passwordOpt := &ast.PasswordAlterPrincipalOption{ + OptionKind: "Password", + } + if p.curTok.Type == TokenString { + passwordOpt.Password = p.parseStringLiteralValue() + p.nextToken() + } + // Check for OLD_PASSWORD + if strings.ToUpper(p.curTok.Literal) == "OLD_PASSWORD" { + p.nextToken() // consume OLD_PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString { + passwordOpt.OldPassword = p.parseStringLiteralValue() + p.nextToken() + } + } + stmt.UserOptions = append(stmt.UserOptions, passwordOpt) + } else { + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + value, err := p.parseScalarExpression() + if err != nil { + break + } + + // Check if value is a simple identifier + var opt ast.UserOption + if colRef, ok := value.(*ast.ColumnReferenceExpression); ok && colRef.MultiPartIdentifier != nil && len(colRef.MultiPartIdentifier.Identifiers) == 1 { + opt = &ast.IdentifierPrincipalOption{ + OptionKind: convertUserOptionKind(optionName), + Identifier: colRef.MultiPartIdentifier.Identifiers[0], + } + } else { + opt = &ast.LiteralPrincipalOption{ + OptionKind: convertUserOptionKind(optionName), + Value: value, + } + } + stmt.UserOptions = append(stmt.UserOptions, opt) + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } return stmt, nil } @@ -3723,8 +4189,16 @@ func (p *Parser) parseAlterRouteStatement() (*ast.AlterRouteStatement, error) { // Parse route name stmt.Name = p.parseIdentifier() - // Skip rest of statement - p.skipToEndOfStatement() + // Parse WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + stmt.RouteOptions = p.parseRouteOptions() + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } return stmt, nil } @@ -3749,12 +4223,258 @@ func (p *Parser) parseAlterEndpointStatement() (*ast.AlterEndpointStatement, err p.nextToken() stmt := &ast.AlterEndpointStatement{} + hasOptions := false // Parse endpoint name stmt.Name = p.parseIdentifier() - // Skip rest of statement - p.skipToEndOfStatement() + // Parse endpoint options (STATE, AFFINITY) + for p.curTok.Type != TokenEOF && p.curTok.Type != TokenSemicolon { + upper := strings.ToUpper(p.curTok.Literal) + + switch upper { + case "STATE": + hasOptions = true + p.nextToken() // consume STATE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + stateUpper := strings.ToUpper(p.curTok.Literal) + switch stateUpper { + case "STARTED": + stmt.State = "Started" + case "STOPPED": + stmt.State = "Stopped" + case "DISABLED": + stmt.State = "Disabled" + } + p.nextToken() + + case "AFFINITY": + hasOptions = true + p.nextToken() // consume AFFINITY + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + affinity := &ast.EndpointAffinity{} + affinityUpper := strings.ToUpper(p.curTok.Literal) + switch affinityUpper { + case "NONE": + affinity.Kind = "None" + p.nextToken() + case "ADMIN": + affinity.Kind = "Admin" + p.nextToken() + default: + // Integer affinity + affinity.Kind = "Integer" + if p.curTok.Type == TokenNumber { + affinity.Value = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + } + stmt.Affinity = affinity + + case "AS": + hasOptions = true + p.nextToken() // consume AS + // Protocol type (TCP, HTTP) + protocolUpper := strings.ToUpper(p.curTok.Literal) + switch protocolUpper { + case "TCP": + stmt.Protocol = "Tcp" + case "HTTP": + stmt.Protocol = "Http" + } + p.nextToken() + // Parse protocol options (listener_port = value) + 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() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + opt := &ast.LiteralEndpointProtocolOption{} + switch optName { + case "LISTENER_PORT": + opt.Kind = "TcpListenerPort" + case "LISTENER_IP": + opt.Kind = "TcpListenerIP" + default: + opt.Kind = optName + } + if p.curTok.Type == TokenNumber { + opt.Value = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } else if p.curTok.Type == TokenString { + opt.Value = &ast.StringLiteral{ + LiteralType: "String", + Value: p.curTok.Literal, + } + p.nextToken() + } + stmt.ProtocolOptions = append(stmt.ProtocolOptions, opt) + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + case "FOR": + hasOptions = true + p.nextToken() // consume FOR + // Endpoint type (SOAP, SERVICE_BROKER, etc.) + endpointTypeUpper := strings.ToUpper(p.curTok.Literal) + switch endpointTypeUpper { + case "SOAP": + stmt.EndpointType = "Soap" + case "SERVICE_BROKER": + stmt.EndpointType = "ServiceBroker" + case "DATABASE_MIRRORING": + stmt.EndpointType = "DatabaseMirroring" + case "TSQL": + stmt.EndpointType = "Tsql" + default: + stmt.EndpointType = endpointTypeUpper + } + p.nextToken() + // Parse payload options + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + actionUpper := strings.ToUpper(p.curTok.Literal) + if actionUpper == "ADD" || actionUpper == "ALTER" || actionUpper == "DROP" { + p.nextToken() // consume ADD/ALTER/DROP + // Parse WEBMETHOD + if strings.ToUpper(p.curTok.Literal) == "WEBMETHOD" { + p.nextToken() // consume WEBMETHOD + method := &ast.SoapMethod{ + Format: "NotSpecified", + Schema: "NotSpecified", + } + switch actionUpper { + case "ADD": + method.Action = "Add" + method.Kind = "WebMethod" + case "ALTER": + method.Action = "Alter" + method.Kind = "WebMethod" + case "DROP": + method.Action = "Drop" + method.Kind = "None" + } + // Parse alias (string literal) + if p.curTok.Type == TokenString { + method.Alias = p.parseStringLiteralValue() + p.nextToken() + } + // Parse method options + 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() + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + if optName == "NAME" && p.curTok.Type == TokenString { + method.Name = p.parseStringLiteralValue() + p.nextToken() + } else if optName == "FORMAT" { + formatUpper := strings.ToUpper(p.curTok.Literal) + switch formatUpper { + case "ALL_RESULTS": + method.Format = "AllResults" + case "ROWSETS_ONLY": + method.Format = "RowsetsOnly" + case "NONE": + method.Format = "None" + default: + method.Format = formatUpper + } + p.nextToken() + } else if optName == "SCHEMA" { + schemaUpper := strings.ToUpper(p.curTok.Literal) + switch schemaUpper { + case "DEFAULT": + method.Schema = "Default" + case "NONE": + method.Schema = "None" + case "STANDARD": + method.Schema = "Standard" + default: + method.Schema = schemaUpper + } + p.nextToken() + } else { + p.nextToken() + } + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + stmt.PayloadOptions = append(stmt.PayloadOptions, method) + } + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else if p.curTok.Type != TokenRParen { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + case ",": + p.nextToken() + + default: + // Unknown token, break out + if hasOptions { + // Set defaults for unspecified fields when options were parsed + if stmt.State == "" { + stmt.State = "NotSpecified" + } + if stmt.Protocol == "" { + stmt.Protocol = "None" + } + if stmt.EndpointType == "" { + stmt.EndpointType = "NotSpecified" + } + } + return stmt, nil + } + } + + // Set defaults for unspecified fields when options were parsed + if hasOptions { + if stmt.State == "" { + stmt.State = "NotSpecified" + } + if stmt.Protocol == "" { + stmt.Protocol = "None" + } + if stmt.EndpointType == "" { + stmt.EndpointType = "NotSpecified" + } + } return stmt, nil } @@ -3790,6 +4510,49 @@ func (p *Parser) parseAlterServiceStatement() (ast.Statement, error) { // Parse service name stmt.Name = p.parseIdentifier() + // Check for ON QUEUE clause + if p.curTok.Type == TokenOn && strings.ToUpper(p.peekTok.Literal) == "QUEUE" { + p.nextToken() // consume ON + p.nextToken() // consume QUEUE + queueName, _ := p.parseSchemaObjectName() + stmt.QueueName = queueName + } + + // Check for contract modifications (ADD CONTRACT, DROP CONTRACT) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + var contracts []*ast.ServiceContract + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + action := "None" + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "ADD" { + action = "Add" + p.nextToken() // consume ADD + if strings.ToUpper(p.curTok.Literal) == "CONTRACT" { + p.nextToken() // consume CONTRACT + } + } else if upperLit == "DROP" { + action = "Drop" + p.nextToken() // consume DROP + if strings.ToUpper(p.curTok.Literal) == "CONTRACT" { + p.nextToken() // consume CONTRACT + } + } + contract := &ast.ServiceContract{ + Name: p.parseIdentifier(), + Action: action, + } + contracts = append(contracts, contract) + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + stmt.ServiceContracts = contracts + } + // Skip rest of statement p.skipToEndOfStatement() diff --git a/parser/parse_dml.go b/parser/parse_dml.go index fa3a0328..c0cc7b1b 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -8,6 +8,111 @@ import ( "github.com/sqlc-dev/teesql/ast" ) +func (p *Parser) parseWithStatement() (ast.Statement, error) { + // Consume WITH + p.nextToken() + + withClause := &ast.WithCtesAndXmlNamespaces{} + + // Parse CHANGE_TRACKING_CONTEXT or CTEs + for { + if strings.ToUpper(p.curTok.Literal) == "CHANGE_TRACKING_CONTEXT" { + p.nextToken() // consume CHANGE_TRACKING_CONTEXT + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + expr, _ := p.parseScalarExpression() + withClause.ChangeTrackingContext = expr + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } else if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + // Parse CTE: name (columns) AS (query) + cte := &ast.CommonTableExpression{ + ExpressionName: p.parseIdentifier(), + } + + // Parse optional column list + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + cte.Columns = append(cte.Columns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + // Expect AS + if p.curTok.Type == TokenAs { + p.nextToken() // consume AS + } + + // Parse query in parentheses + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + queryExpr, err := p.parseQueryExpression() + if err != nil { + return nil, err + } + cte.QueryExpression = queryExpr + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + withClause.CommonTableExpressions = append(withClause.CommonTableExpressions, cte) + } else { + break + } + + // Check for comma (more CTEs) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + // Now dispatch to the appropriate statement parser + switch p.curTok.Type { + case TokenInsert: + stmt, err := p.parseInsertStatement() + if err != nil { + return nil, err + } + if ins, ok := stmt.(*ast.InsertStatement); ok { + ins.WithCtesAndXmlNamespaces = withClause + } + return stmt, nil + case TokenUpdate: + stmt, err := p.parseUpdateOrUpdateStatisticsStatement() + if err != nil { + return nil, err + } + if upd, ok := stmt.(*ast.UpdateStatement); ok { + upd.WithCtesAndXmlNamespaces = withClause + } + return stmt, nil + case TokenDelete: + stmt, err := p.parseDeleteStatement() + if err != nil { + return nil, err + } + stmt.WithCtesAndXmlNamespaces = withClause + return stmt, nil + case TokenSelect: + // For SELECT, we need to handle it differently + // Skip for now - return the select without CTE + return p.parseSelectStatement() + } + + return nil, fmt.Errorf("expected INSERT, UPDATE, DELETE, or SELECT after WITH clause, got %s", p.curTok.Literal) +} + func (p *Parser) parseInsertStatement() (ast.Statement, error) { // Consume INSERT p.nextToken() @@ -446,6 +551,10 @@ func (p *Parser) parseTableHints() ([]ast.TableHintType, error) { if p.curTok.Type == TokenComma { p.nextToken() } else if p.curTok.Type != TokenRParen { + // Check if the next token is a valid table hint (space-separated hints) + if p.curTok.Type == TokenIdent && isTableHintKeyword(strings.ToUpper(p.curTok.Literal)) { + continue // Continue parsing space-separated hints + } break } } @@ -457,6 +566,19 @@ func (p *Parser) parseTableHints() ([]ast.TableHintType, error) { return hints, nil } +// isTableHintKeyword checks if a string is a valid table hint keyword +func isTableHintKeyword(name string) bool { + switch name { + case "HOLDLOCK", "NOLOCK", "PAGLOCK", "READCOMMITTED", "READPAST", + "READUNCOMMITTED", "REPEATABLEREAD", "ROWLOCK", "SERIALIZABLE", + "SNAPSHOT", "TABLOCK", "TABLOCKX", "UPDLOCK", "XLOCK", "NOWAIT", + "INDEX", "FORCESEEK", "FORCESCAN", "KEEPIDENTITY", "KEEPDEFAULTS", + "IGNORE_CONSTRAINTS", "IGNORE_TRIGGERS", "NOEXPAND", "SPATIAL_WINDOW_MAX_CELLS": + return true + } + return false +} + func convertTableHintKind(hint string) string { hintMap := map[string]string{ "HOLDLOCK": "HoldLock", @@ -714,23 +836,25 @@ func (p *Parser) parseExecutableStringList() (*ast.ExecutableStringList, error) strList := &ast.ExecutableStringList{} - // Parse the first string expression (may be concatenated with +) - for { - if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + // Parse the string expressions (may be strings, variables, or concatenations with +) + // Strings are added to Strings, other parameters (after comma) go to Parameters + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + isVariable := p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString || isVariable { expr, err := p.parseScalarExpression() if err != nil { return nil, err } - // parseScalarExpression handles the + concatenation, so we get a BinaryExpression - // But we need to flatten it to individual StringLiterals for the Strings array + // Flatten concatenated expressions to individual parts for the Strings array p.flattenStringExpression(expr, &strList.Strings) } else { break } - // Check for comma (parameters follow) or closing paren + // Check for comma or closing paren if p.curTok.Type == TokenComma { p.nextToken() + // After comma, we switch to parsing parameters break } if p.curTok.Type == TokenRParen { @@ -738,7 +862,7 @@ func (p *Parser) parseExecutableStringList() (*ast.ExecutableStringList, error) } } - // Parse parameters (after the first comma) + // Parse parameters (after the first comma following strings) for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { param, err := p.parseExecuteParameter() if err != nil { diff --git a/parser/parse_select.go b/parser/parse_select.go index 7ab0107d..dfa33359 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -431,10 +431,20 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } } else if p.curTok.Type == TokenAs { p.nextToken() - alias := p.parseIdentifier() - sse.ColumnName = &ast.IdentifierOrValueExpression{ - Value: alias.Value, - Identifier: alias, + if p.curTok.Type == TokenString { + // String literal alias: AS 'alias' + str := p.parseStringLiteralValue() + p.nextToken() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: str.Value, + ValueExpression: str, + } + } else { + alias := p.parseIdentifier() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: alias.Value, + Identifier: alias, + } } } else if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) @@ -467,10 +477,20 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } } else if p.curTok.Type == TokenAs { p.nextToken() // consume AS - alias := p.parseIdentifier() - sse.ColumnName = &ast.IdentifierOrValueExpression{ - Value: alias.Value, - Identifier: alias, + if p.curTok.Type == TokenString { + // String literal alias: AS 'alias' + str := p.parseStringLiteralValue() + p.nextToken() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: str.Value, + ValueExpression: str, + } + } else { + alias := p.parseIdentifier() + sse.ColumnName = &ast.IdentifierOrValueExpression{ + Value: alias.Value, + Identifier: alias, + } } } else if p.curTok.Type == TokenIdent { // Check if this is an alias (not a keyword that starts a new clause) @@ -936,6 +956,31 @@ func (p *Parser) parseStringLiteral() (*ast.StringLiteral, error) { }, nil } +// parseStringLiteralValue creates a StringLiteral from the current token without consuming it +func (p *Parser) parseStringLiteralValue() *ast.StringLiteral { + raw := p.curTok.Literal + + // Remove surrounding quotes and handle escaped quotes + if len(raw) >= 2 && raw[0] == '\'' && raw[len(raw)-1] == '\'' { + inner := raw[1 : len(raw)-1] + // Replace escaped quotes + value := strings.ReplaceAll(inner, "''", "'") + return &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: value, + } + } + + return &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: raw, + } +} + func (p *Parser) parseNationalStringLiteral() (*ast.StringLiteral, error) { raw := p.curTok.Literal p.nextToken() @@ -1244,6 +1289,40 @@ func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.Scala continue } + // Check for WITHIN GROUP clause for function calls (e.g., PERCENTILE_CONT) + if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "WITHIN" { + p.nextToken() // consume WITHIN + if strings.ToUpper(p.curTok.Literal) == "GROUP" { + p.nextToken() // consume GROUP + } + + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after WITHIN GROUP, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse ORDER BY clause + withinGroup := &ast.WithinGroupClause{ + HasGraphPath: false, + } + + if p.curTok.Type == TokenOrder { + orderBy, err := p.parseOrderByClause() + if err != nil { + return nil, err + } + withinGroup.OrderByClause = orderBy + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in WITHIN GROUP clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + fc.WithinGroupClause = withinGroup + continue // continue to check for more clauses like OVER + } + // Check for OVER clause for function calls if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "OVER" { p.nextToken() // consume OVER @@ -1438,10 +1517,14 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { } } - // Parse optional table hints WITH (hint, hint, ...) + // Parse optional table hints WITH (hint, hint, ...) or old-style (hint, hint, ...) if p.curTok.Type == TokenWith { p.nextToken() // consume WITH - if p.curTok.Type == TokenLParen { + } + if p.curTok.Type == TokenLParen { + // Check if this looks like hints (first token is a hint keyword) + // Save position to peek + if p.peekIsTableHint() { p.nextToken() // consume ( for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { hint, err := p.parseTableHint() @@ -1454,6 +1537,10 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { if p.curTok.Type == TokenComma { p.nextToken() } else if p.curTok.Type != TokenRParen { + // Check if the next token is a valid table hint (space-separated hints) + if p.isTableHintToken() { + continue // Continue parsing space-separated hints + } break } } @@ -1564,6 +1651,46 @@ func getTableHintKind(name string) string { } } +// isTableHintToken checks if the current token is a valid table hint keyword +func (p *Parser) isTableHintToken() bool { + // Check for keyword tokens that are table hints + if p.curTok.Type == TokenHoldlock || p.curTok.Type == TokenNowait { + return true + } + // Check for identifiers that are table hints + if p.curTok.Type == TokenIdent { + switch strings.ToUpper(p.curTok.Literal) { + case "HOLDLOCK", "NOLOCK", "PAGLOCK", "READCOMMITTED", "READPAST", + "READUNCOMMITTED", "REPEATABLEREAD", "ROWLOCK", "SERIALIZABLE", + "SNAPSHOT", "TABLOCK", "TABLOCKX", "UPDLOCK", "XLOCK", "NOWAIT", + "INDEX", "FORCESEEK", "FORCESCAN", "KEEPIDENTITY", "KEEPDEFAULTS", + "IGNORE_CONSTRAINTS", "IGNORE_TRIGGERS", "NOEXPAND", "SPATIAL_WINDOW_MAX_CELLS": + return true + } + } + return false +} + +// peekIsTableHint checks if the peek token (next token after current) is a valid table hint keyword +func (p *Parser) peekIsTableHint() bool { + // Check for keyword tokens that are table hints + if p.peekTok.Type == TokenHoldlock || p.peekTok.Type == TokenNowait || p.peekTok.Type == TokenIndex { + return true + } + // Check for identifiers that are table hints + if p.peekTok.Type == TokenIdent { + switch strings.ToUpper(p.peekTok.Literal) { + case "HOLDLOCK", "NOLOCK", "PAGLOCK", "READCOMMITTED", "READPAST", + "READUNCOMMITTED", "REPEATABLEREAD", "ROWLOCK", "SERIALIZABLE", + "SNAPSHOT", "TABLOCK", "TABLOCKX", "UPDLOCK", "XLOCK", "NOWAIT", + "INDEX", "FORCESEEK", "FORCESCAN", "KEEPIDENTITY", "KEEPDEFAULTS", + "IGNORE_CONSTRAINTS", "IGNORE_TRIGGERS", "NOEXPAND", "SPATIAL_WINDOW_MAX_CELLS": + return true + } + } + return false +} + func (p *Parser) parseSchemaObjectName() (*ast.SchemaObjectName, error) { var identifiers []*ast.Identifier @@ -1688,6 +1815,10 @@ func (p *Parser) parseOptimizerHint() (ast.OptimizerHintBase, error) { } return &ast.LiteralOptimizerHint{HintKind: "UsePlan", Value: value}, nil } + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "HINT" { + p.nextToken() // consume HINT + return p.parseUseHintList() + } return &ast.OptimizerHint{HintKind: "Use"}, nil } @@ -1827,6 +1958,10 @@ func (p *Parser) parseOptimizerHint() (ast.OptimizerHintBase, error) { } return &ast.OptimizerHint{HintKind: "Fast"}, nil + case "NO_PERFORMANCE_SPOOL": + p.nextToken() // consume NO_PERFORMANCE_SPOOL + return &ast.OptimizerHint{HintKind: "NoPerformanceSpool"}, nil + default: // Handle generic hints hintKind := convertHintKind(p.curTok.Literal) @@ -1858,6 +1993,44 @@ func (p *Parser) parseOptimizerHint() (ast.OptimizerHintBase, error) { } } +func (p *Parser) parseUseHintList() (ast.OptimizerHintBase, error) { + hint := &ast.UseHintList{ + HintKind: "Unspecified", + } + + // Expect ( + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after USE HINT, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse hint string literals + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if p.curTok.Type == TokenComma { + p.nextToken() + continue + } + + if p.curTok.Type == TokenString { + str := p.parseStringLiteralValue() + p.nextToken() + hint.Hints = append(hint.Hints, str) + } else if p.curTok.Type == TokenNationalString { + str, _ := p.parseNationalStringFromToken() + hint.Hints = append(hint.Hints, str) + } else { + break + } + } + + // Expect ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + + return hint, nil +} + func (p *Parser) parseTableHintsOptimizerHint() (ast.OptimizerHintBase, error) { hint := &ast.TableHintsOptimizerHint{ HintKind: "TableHints", @@ -2197,8 +2370,33 @@ func (p *Parser) parseBooleanAndExpression() (ast.BooleanExpression, error) { } func (p *Parser) parseBooleanPrimaryExpression() (ast.BooleanExpression, error) { - // Check for parenthesized boolean expression + // Check for parenthesized expression - could be boolean or scalar subquery if p.curTok.Type == TokenLParen { + // Peek ahead to see if it's a subquery (SELECT) + if p.peekTok.Type == TokenSelect { + // Parse as scalar subquery that will be used in a comparison + p.nextToken() // consume ( + qe, err := p.parseQueryExpression() + if err != nil { + return nil, err + } + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + subquery := &ast.ScalarSubquery{QueryExpression: qe} + + // Now check for comparison operators + if p.isComparisonOperator() { + return p.parseComparisonAfterLeft(subquery) + } + // If no comparison, this might be used in other contexts + // For now, treat it as an error if used standalone + return nil, fmt.Errorf("scalar subquery must be followed by a comparison operator") + } + + // Parse as parenthesized boolean expression p.nextToken() // consume ( // Parse inner boolean expression @@ -2394,6 +2592,51 @@ func (p *Parser) parseBooleanPrimaryExpression() (ast.BooleanExpression, error) }, nil } +// isComparisonOperator checks if the current token is a comparison operator +func (p *Parser) isComparisonOperator() bool { + switch p.curTok.Type { + case TokenEquals, TokenNotEqual, TokenLessThan, TokenGreaterThan, + TokenLessOrEqual, TokenGreaterOrEqual: + return true + default: + return false + } +} + +// parseComparisonAfterLeft parses a comparison expression after the left operand is already parsed +func (p *Parser) parseComparisonAfterLeft(left ast.ScalarExpression) (ast.BooleanExpression, error) { + var compType string + switch p.curTok.Type { + case TokenEquals: + compType = "Equals" + case TokenNotEqual: + compType = "NotEqualToBrackets" + case TokenLessThan: + compType = "LessThan" + case TokenGreaterThan: + compType = "GreaterThan" + case TokenLessOrEqual: + compType = "LessThanOrEqualTo" + case TokenGreaterOrEqual: + compType = "GreaterThanOrEqualTo" + default: + return nil, fmt.Errorf("expected comparison operator, got %s", p.curTok.Literal) + } + p.nextToken() + + // Parse right scalar expression + right, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + + return &ast.BooleanComparisonExpression{ + ComparisonType: compType, + FirstExpression: left, + SecondExpression: right, + }, nil +} + // identifiersToSchemaObjectName converts a slice of identifiers to a SchemaObjectName. // For 1 identifier: BaseIdentifier // For 2 identifiers: SchemaIdentifier.BaseIdentifier diff --git a/parser/parse_statements.go b/parser/parse_statements.go index a320f765..7c735eb4 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -553,25 +553,29 @@ func (p *Parser) parseDataTypeReference() (ast.DataTypeReference, error) { Identifiers: []*ast.Identifier{baseId}, } - // Check for XML with schema collection: XML(schema_collection) - if strings.ToUpper(typeName) == "XML" && p.curTok.Type == TokenLParen { - p.nextToken() // consume ( - - // Parse the schema collection name - schemaName, err := p.parseSchemaObjectName() - if err != nil { - return nil, err + // Check for XML type - returns XmlDataTypeReference + if strings.ToUpper(typeName) == "XML" { + xmlRef := &ast.XmlDataTypeReference{ + XmlDataTypeOption: "None", + Name: baseName, } + // Check for schema collection: XML(schema_collection) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( - if p.curTok.Type == TokenRParen { - p.nextToken() + // Parse the schema collection name + schemaName, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + xmlRef.XmlSchemaCollection = schemaName + + if p.curTok.Type == TokenRParen { + p.nextToken() + } } - return &ast.XmlDataTypeReference{ - XmlDataTypeOption: "None", - XmlSchemaCollection: schemaName, - Name: baseName, - }, nil + return xmlRef, nil } // Check if this is a known SQL data type @@ -685,113 +689,42 @@ func (p *Parser) parseSetVariableStatement() (ast.Statement, error) { // Consume SET p.nextToken() - // Check for predicate SET options like SET ANSI_NULLS ON/OFF + // Check for special SET statements if p.curTok.Type == TokenIdent { optionName := strings.ToUpper(p.curTok.Literal) - var setOpt ast.SetOptions - switch optionName { - case "ANSI_DEFAULTS": - setOpt = ast.SetOptionsAnsiDefaults - case "ANSI_NULLS": - setOpt = ast.SetOptionsAnsiNulls - case "ANSI_NULL_DFLT_OFF": - setOpt = ast.SetOptionsAnsiNullDfltOff - case "ANSI_NULL_DFLT_ON": - setOpt = ast.SetOptionsAnsiNullDfltOn - case "ANSI_PADDING": - setOpt = ast.SetOptionsAnsiPadding - case "ANSI_WARNINGS": - setOpt = ast.SetOptionsAnsiWarnings - case "ARITHABORT": - setOpt = ast.SetOptionsArithAbort - case "ARITHIGNORE": - setOpt = ast.SetOptionsArithIgnore - case "CONCAT_NULL_YIELDS_NULL": - setOpt = ast.SetOptionsConcatNullYieldsNull - case "CURSOR_CLOSE_ON_COMMIT": - setOpt = ast.SetOptionsCursorCloseOnCommit - case "FMTONLY": - setOpt = ast.SetOptionsFmtOnly - case "FORCEPLAN": - setOpt = ast.SetOptionsForceplan - case "IMPLICIT_TRANSACTIONS": - setOpt = ast.SetOptionsImplicitTransactions - case "NOCOUNT": - setOpt = ast.SetOptionsNoCount - case "NOEXEC": - setOpt = ast.SetOptionsNoExec - case "NO_BROWSETABLE": - setOpt = ast.SetOptionsNoBrowsetable - case "NUMERIC_ROUNDABORT": - setOpt = ast.SetOptionsNumericRoundAbort - case "PARSEONLY": - setOpt = ast.SetOptionsParseOnly - case "QUOTED_IDENTIFIER": - setOpt = ast.SetOptionsQuotedIdentifier - case "REMOTE_PROC_TRANSACTIONS": - setOpt = ast.SetOptionsRemoteProcTransactions - case "SHOWPLAN_ALL": - setOpt = ast.SetOptionsShowplanAll - case "SHOWPLAN_TEXT": - setOpt = ast.SetOptionsShowplanText - case "SHOWPLAN_XML": - setOpt = ast.SetOptionsShowplanXml - case "STATISTICS": - // Handle SET STATISTICS IO/PROFILE/TIME/XML - returns SetStatisticsStatement - p.nextToken() // consume STATISTICS - var statOpt ast.SetOptions - if p.curTok.Type == TokenTime { - statOpt = ast.SetOptionsTime - } else if p.curTok.Type == TokenIdent { - switch strings.ToUpper(p.curTok.Literal) { - case "IO": - statOpt = ast.SetOptionsIO - case "PROFILE": - statOpt = ast.SetOptionsProfile - case "XML": - statOpt = ast.SetOptionsStatisticsXml - } - } - if statOpt != "" { - p.nextToken() // consume the statistic option - isOn := false - if p.curTok.Type == TokenOn || (p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "ON") { - isOn = true - p.nextToken() - } else if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OFF" { - isOn = false - p.nextToken() - } - if p.curTok.Type == TokenSemicolon { - p.nextToken() - } - return &ast.SetStatisticsStatement{ - Options: statOpt, - IsOn: isOn, - }, nil - } - case "XACT_ABORT": - setOpt = ast.SetOptionsXactAbort - } - if setOpt != "" { - p.nextToken() // consume option name - isOn := false - // ON is tokenized as TokenOn, not TokenIdent - if p.curTok.Type == TokenOn || (p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "ON") { - isOn = true + + // Handle SET ROWCOUNT + if optionName == "ROWCOUNT" { + p.nextToken() // consume ROWCOUNT + var numRows ast.ScalarExpression + if strings.HasPrefix(p.curTok.Literal, "@") { + numRows = &ast.VariableReference{Name: p.curTok.Literal} p.nextToken() - } else if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OFF" { - isOn = false + } else { + numRows = &ast.IntegerLiteral{LiteralType: "Integer", Value: p.curTok.Literal} p.nextToken() } - // Skip optional semicolon if p.curTok.Type == TokenSemicolon { p.nextToken() } - return &ast.PredicateSetStatement{ - Options: setOpt, - IsOn: isOn, - }, nil + return &ast.SetRowCountStatement{NumberRows: numRows}, nil + } + + // Handle SET STATISTICS + if optionName == "STATISTICS" { + return p.parseSetStatisticsStatement() + } + + // Handle SET OFFSETS + if optionName == "OFFSETS" { + return p.parseSetOffsetsStatement() + } + + // Handle predicate SET options like SET ANSI_NULLS ON/OFF + // These can have multiple options with commas + setOpt := p.mapPredicateSetOption(optionName) + if setOpt != "" { + return p.parsePredicateSetStatement(setOpt) } } @@ -899,6 +832,292 @@ func (p *Parser) parseSetVariableStatement() (ast.Statement, error) { return stmt, nil } +// mapPredicateSetOption maps option names to their SetOptions values +func (p *Parser) mapPredicateSetOption(name string) string { + switch name { + case "ANSI_DEFAULTS": + return "AnsiDefaults" + case "ANSI_NULLS": + return "AnsiNulls" + case "ANSI_NULL_DFLT_OFF": + return "AnsiNullDfltOff" + case "ANSI_NULL_DFLT_ON": + return "AnsiNullDfltOn" + case "ANSI_PADDING": + return "AnsiPadding" + case "ANSI_WARNINGS": + return "AnsiWarnings" + case "ARITHABORT": + return "ArithAbort" + case "ARITHIGNORE": + return "ArithIgnore" + case "CONCAT_NULL_YIELDS_NULL": + return "ConcatNullYieldsNull" + case "CURSOR_CLOSE_ON_COMMIT": + return "CursorCloseOnCommit" + case "FMTONLY": + return "FmtOnly" + case "FORCEPLAN": + return "ForcePlan" + case "IMPLICIT_TRANSACTIONS": + return "ImplicitTransactions" + case "NOCOUNT": + return "NoCount" + case "NOEXEC": + return "NoExec" + case "NO_BROWSETABLE": + return "NoBrowsetable" + case "NUMERIC_ROUNDABORT": + return "NumericRoundAbort" + case "PARSEONLY": + return "ParseOnly" + case "QUOTED_IDENTIFIER": + return "QuotedIdentifier" + case "REMOTE_PROC_TRANSACTIONS": + return "RemoteProcTransactions" + case "SHOWPLAN_ALL": + return "ShowPlanAll" + case "SHOWPLAN_TEXT": + return "ShowPlanText" + case "SHOWPLAN_XML": + return "ShowPlanXml" + case "XACT_ABORT": + return "XactAbort" + default: + return "" + } +} + +// predicateSetOptionOrder defines the sort order for predicate SET options +var predicateSetOptionOrder = map[string]int{ + "AnsiNulls": 1, + "AnsiNullDfltOff": 2, + "AnsiNullDfltOn": 3, + "AnsiPadding": 4, + "AnsiWarnings": 5, + "ConcatNullYieldsNull": 6, + "CursorCloseOnCommit": 7, + "ImplicitTransactions": 8, + "QuotedIdentifier": 9, + "ArithAbort": 10, + "ArithIgnore": 11, + "FmtOnly": 12, + "NoCount": 13, + "NoExec": 14, + "NumericRoundAbort": 15, + "ParseOnly": 16, + "AnsiDefaults": 17, + "ForcePlan": 18, + "ShowPlanAll": 19, + "ShowPlanText": 20, + "ShowPlanXml": 21, + "NoBrowsetable": 22, + "RemoteProcTransactions": 23, + "XactAbort": 24, +} + +// parsePredicateSetStatement parses SET option1, option2, ... ON/OFF +func (p *Parser) parsePredicateSetStatement(firstOpt string) (*ast.PredicateSetStatement, error) { + options := []string{firstOpt} + p.nextToken() // consume first option + + // Check for more options with commas + for p.curTok.Type == TokenComma { + p.nextToken() // consume comma + if p.curTok.Type == TokenIdent { + nextOpt := p.mapPredicateSetOption(strings.ToUpper(p.curTok.Literal)) + if nextOpt != "" { + options = append(options, nextOpt) + p.nextToken() + } else { + break + } + } + } + + // Sort options according to ScriptDom order + sort.Slice(options, func(i, j int) bool { + return predicateSetOptionOrder[options[i]] < predicateSetOptionOrder[options[j]] + }) + + // Parse ON/OFF + isOn := false + if p.curTok.Type == TokenOn || (p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "ON") { + isOn = true + p.nextToken() + } else if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OFF" { + isOn = false + p.nextToken() + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return &ast.PredicateSetStatement{ + Options: strings.Join(options, ", "), + IsOn: isOn, + }, nil +} + +// parseSetStatisticsStatement parses SET STATISTICS opt1, opt2, ... ON/OFF +func (p *Parser) parseSetStatisticsStatement() (*ast.SetStatisticsStatement, error) { + p.nextToken() // consume STATISTICS + + // Map statistics options + mapStatOpt := func(name string) string { + switch name { + case "IO": + return "IO" + case "PROFILE": + return "Profile" + case "TIME": + return "Time" + case "XML": + return "Xml" + default: + return "" + } + } + + // Statistics option order for sorting + statisticsOptionOrder := map[string]int{ + "IO": 1, + "Profile": 2, + "Time": 3, + "Xml": 4, + } + + var options []string + for { + var optName string + if p.curTok.Type == TokenTime { + optName = "Time" + } else if p.curTok.Type == TokenIdent { + optName = mapStatOpt(strings.ToUpper(p.curTok.Literal)) + } + if optName == "" { + break + } + options = append(options, optName) + p.nextToken() + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + // Sort options according to ScriptDom order + sort.Slice(options, func(i, j int) bool { + return statisticsOptionOrder[options[i]] < statisticsOptionOrder[options[j]] + }) + + // Parse ON/OFF + isOn := false + if p.curTok.Type == TokenOn || (p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "ON") { + isOn = true + p.nextToken() + } else if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OFF" { + isOn = false + p.nextToken() + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return &ast.SetStatisticsStatement{ + Options: strings.Join(options, ", "), + IsOn: isOn, + }, nil +} + +// setOffsetsOptionOrder defines the sort order for SET OFFSETS options +var setOffsetsOptionOrder = map[string]int{ + "Select": 1, + "From": 2, + "Order": 3, + "Compute": 4, + "Table": 5, + "Procedure": 6, + "Execute": 7, + "Statement": 8, + "Param": 9, +} + +// parseSetOffsetsStatement parses SET OFFSETS opt1, opt2, ... ON/OFF +func (p *Parser) parseSetOffsetsStatement() (*ast.SetOffsetsStatement, error) { + p.nextToken() // consume OFFSETS + + // Map offset options - these can be either tokens or identifiers + mapOffsetOpt := func() string { + switch p.curTok.Type { + case TokenSelect: + return "Select" + case TokenFrom: + return "From" + case TokenOrder: + return "Order" + case TokenTable: + return "Table" + case TokenProcedure: + return "Procedure" + case TokenExecute: + return "Execute" + case TokenIdent: + switch strings.ToUpper(p.curTok.Literal) { + case "COMPUTE": + return "Compute" + case "STATEMENT": + return "Statement" + case "PARAM": + return "Param" + } + } + return "" + } + + var options []string + for { + optName := mapOffsetOpt() + if optName == "" { + break + } + options = append(options, optName) + p.nextToken() + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + // Sort options according to ScriptDom order + sort.Slice(options, func(i, j int) bool { + return setOffsetsOptionOrder[options[i]] < setOffsetsOptionOrder[options[j]] + }) + + // Parse ON/OFF + isOn := false + if p.curTok.Type == TokenOn || (p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "ON") { + isOn = true + p.nextToken() + } else if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OFF" { + isOn = false + p.nextToken() + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return &ast.SetOffsetsStatement{ + Options: strings.Join(options, ", "), + IsOn: isOn, + }, nil +} + func (p *Parser) parseIfStatement() (*ast.IfStatement, error) { // Consume IF p.nextToken() @@ -1165,6 +1384,11 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { // Parse statements until END TRY for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + // Skip semicolons + if p.curTok.Type == TokenSemicolon { + p.nextToken() + continue + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1194,6 +1418,11 @@ func (p *Parser) parseTryCatchStatement() (*ast.TryCatchStatement, error) { // Parse catch statements until END CATCH for p.curTok.Type != TokenEnd && p.curTok.Type != TokenEOF { + // Skip semicolons + if p.curTok.Type == TokenSemicolon { + p.nextToken() + continue + } s, err := p.parseStatement() if err != nil { return nil, err @@ -1285,6 +1514,27 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { 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: + return p.parseCreateOrAlterTriggerStatement() + default: + // Lenient: skip unknown CREATE OR ALTER statements + p.skipToEndOfStatement() + return &ast.CreateProcedureStatement{}, nil + } case TokenTable: return p.parseCreateTableStatement() case TokenView: @@ -1387,6 +1637,8 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { return p.parseCreateIndexStatement() case "PRIMARY": return p.parseCreateXmlIndexStatement() + case "COLUMN": + return p.parseCreateColumnMasterKeyStatement() case "CRYPTOGRAPHIC": return p.parseCreateCryptographicProviderStatement() case "FEDERATION": @@ -1526,6 +1778,117 @@ func (p *Parser) parseDropCryptographicProviderStatement() (*ast.DropCryptograph return stmt, nil } +func (p *Parser) parseCreateColumnMasterKeyStatement() (*ast.CreateColumnMasterKeyStatement, error) { + // CREATE COLUMN MASTER KEY name WITH (options) + // Already consumed CREATE COLUMN, now need to consume MASTER KEY + p.nextToken() // consume COLUMN + + if strings.ToUpper(p.curTok.Literal) != "MASTER" { + return nil, fmt.Errorf("expected MASTER after COLUMN, got %s", p.curTok.Literal) + } + p.nextToken() // consume MASTER + + if strings.ToUpper(p.curTok.Literal) != "KEY" { + return nil, fmt.Errorf("expected KEY after MASTER, got %s", p.curTok.Literal) + } + p.nextToken() // consume KEY + + stmt := &ast.CreateColumnMasterKeyStatement{} + + // Parse key name + stmt.Name = p.parseIdentifier() + + // Parse WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after WITH, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse parameters + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + paramName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume parameter name + + switch paramName { + case "KEY_STORE_PROVIDER_NAME": + if p.curTok.Type == TokenEquals { + p.nextToken() + } + value, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Parameters = append(stmt.Parameters, &ast.ColumnMasterKeyStoreProviderNameParameter{ + Name: value, + ParameterKind: "KeyStoreProviderName", + }) + case "KEY_PATH": + if p.curTok.Type == TokenEquals { + p.nextToken() + } + value, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Parameters = append(stmt.Parameters, &ast.ColumnMasterKeyPathParameter{ + Path: value, + ParameterKind: "KeyPath", + }) + case "ENCLAVE_COMPUTATIONS": + // ENCLAVE_COMPUTATIONS ( SIGNATURE = value ) + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after ENCLAVE_COMPUTATIONS, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse SIGNATURE = value + if strings.ToUpper(p.curTok.Literal) == "SIGNATURE" { + p.nextToken() // consume SIGNATURE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + value, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Parameters = append(stmt.Parameters, &ast.ColumnMasterKeyEnclaveComputationsParameter{ + Signature: value, + ParameterKind: "Signature", + }) + } + + // Consume closing ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + default: + // Skip unknown parameter + p.nextToken() + } + + // Skip comma if present + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + + // Consume closing ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + // Skip any remaining tokens + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseCreateRoleStatement() (*ast.CreateRoleStatement, error) { // Consume ROLE p.nextToken() @@ -2138,20 +2501,64 @@ func (p *Parser) parseCreateProcedureStatement() (*ast.CreateProcedureStatement, stmt.Parameters = params } - // Skip WITH options (like RECOMPILE, ENCRYPTION, etc.) + // Parse WITH options (like RECOMPILE, ENCRYPTION, EXECUTE AS, etc.) if p.curTok.Type == TokenWith { p.nextToken() for { if strings.ToUpper(p.curTok.Literal) == "FOR" || p.curTok.Type == TokenAs || p.curTok.Type == TokenEOF { break } - if strings.ToUpper(p.curTok.Literal) == "REPLICATION" { + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "RECOMPILE" { + stmt.Options = append(stmt.Options, &ast.ProcedureOption{OptionKind: "Recompile"}) + p.nextToken() + } else if upperLit == "ENCRYPTION" { + stmt.Options = append(stmt.Options, &ast.ProcedureOption{OptionKind: "Encryption"}) + p.nextToken() + } else if upperLit == "EXECUTE" { + p.nextToken() // consume EXECUTE + if p.curTok.Type == TokenAs { + p.nextToken() // consume AS + } + executeAsOpt := &ast.ExecuteAsProcedureOption{ + OptionKind: "ExecuteAs", + ExecuteAs: &ast.ExecuteAsClause{}, + } + upperOption := strings.ToUpper(p.curTok.Literal) + if upperOption == "CALLER" { + executeAsOpt.ExecuteAs.ExecuteAsOption = "Caller" + p.nextToken() + } else if upperOption == "SELF" { + executeAsOpt.ExecuteAs.ExecuteAsOption = "Self" + p.nextToken() + } else if upperOption == "OWNER" { + executeAsOpt.ExecuteAs.ExecuteAsOption = "Owner" + p.nextToken() + } else if p.curTok.Type == TokenString { + executeAsOpt.ExecuteAs.ExecuteAsOption = "String" + value := p.curTok.Literal + // Strip quotes + if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' { + value = value[1 : len(value)-1] + } + executeAsOpt.ExecuteAs.Literal = &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: value, + } + p.nextToken() + } + stmt.Options = append(stmt.Options, executeAsOpt) + } else if upperLit == "REPLICATION" { stmt.IsForReplication = true + p.nextToken() + } else { + p.nextToken() } - p.nextToken() if p.curTok.Type == TokenComma { p.nextToken() - } else { + } else if p.curTok.Type == TokenAs || strings.ToUpper(p.curTok.Literal) == "FOR" || p.curTok.Type == TokenEOF { break } } @@ -2162,6 +2569,26 @@ func (p *Parser) parseCreateProcedureStatement() (*ast.CreateProcedureStatement, p.nextToken() } + // Check for EXTERNAL NAME (CLR procedure) + if strings.ToUpper(p.curTok.Literal) == "EXTERNAL" { + p.nextToken() // consume EXTERNAL + if strings.ToUpper(p.curTok.Literal) == "NAME" { + p.nextToken() // consume NAME + } + // Parse assembly.class.method + stmt.MethodSpecifier = &ast.MethodSpecifier{} + stmt.MethodSpecifier.AssemblyName = p.parseIdentifier() + if p.curTok.Type == TokenDot { + p.nextToken() + stmt.MethodSpecifier.ClassName = p.parseIdentifier() + } + if p.curTok.Type == TokenDot { + p.nextToken() + stmt.MethodSpecifier.MethodName = p.parseIdentifier() + } + return stmt, nil + } + // Parse statement list stmts, err := p.parseStatementList() if err != nil { @@ -2612,17 +3039,165 @@ func (p *Parser) parseExecuteStatement() (ast.Statement, error) { return nil, err } - // Skip optional semicolon - if p.curTok.Type == TokenSemicolon { - p.nextToken() - } + stmt := &ast.ExecuteStatement{ExecuteSpecification: execSpec} - return &ast.ExecuteStatement{ExecuteSpecification: execSpec}, nil -} + // Parse WITH options (RESULT SETS, RECOMPILE) + for p.curTok.Type == TokenWith { + p.nextToken() // consume WITH -func (p *Parser) parseExecuteAsStatement() (*ast.ExecuteAsStatement, error) { - // We're positioned after EXECUTE, at AS - p.nextToken() // consume AS + for { + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "RESULT" { + p.nextToken() // consume RESULT + if strings.ToUpper(p.curTok.Literal) == "SETS" { + p.nextToken() // consume SETS + } + + opt := &ast.ResultSetsExecuteOption{ + OptionKind: "ResultSets", + } + + // Check for NONE, UNDEFINED, or definitions + upperLit = strings.ToUpper(p.curTok.Literal) + if upperLit == "NONE" { + opt.ResultSetsOptionKind = "None" + p.nextToken() + } else if upperLit == "UNDEFINED" { + opt.ResultSetsOptionKind = "Undefined" + p.nextToken() + } else if p.curTok.Type == TokenLParen { + opt.ResultSetsOptionKind = "ResultSetsDefined" + p.nextToken() // consume ( + opt.Definitions = p.parseResultSetDefinitions() + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + stmt.Options = append(stmt.Options, opt) + } else if upperLit == "RECOMPILE" { + p.nextToken() // consume RECOMPILE + stmt.Options = append(stmt.Options, &ast.ExecuteOption{ + OptionKind: "Recompile", + }) + } else { + break + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseResultSetDefinitions() []ast.ResultSetDefinitionType { + var definitions []ast.ResultSetDefinitionType + + for { + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "AS" { + p.nextToken() // consume AS + upperLit = strings.ToUpper(p.curTok.Literal) + + if upperLit == "OBJECT" { + p.nextToken() // consume OBJECT + name, _ := p.parseSchemaObjectName() + def := &ast.SchemaObjectResultSetDefinition{ + ResultSetType: "Object", + Name: name, + } + definitions = append(definitions, def) + } else if upperLit == "FOR" { + p.nextToken() // consume FOR + if strings.ToUpper(p.curTok.Literal) == "XML" { + p.nextToken() // consume XML + } + def := &ast.ResultSetDefinition{ + ResultSetType: "ForXml", + } + definitions = append(definitions, def) + } else if upperLit == "TYPE" { + p.nextToken() // consume TYPE + name, _ := p.parseSchemaObjectName() + def := &ast.SchemaObjectResultSetDefinition{ + ResultSetType: "Type", + Name: name, + } + definitions = append(definitions, def) + } + } else if p.curTok.Type == TokenLParen { + // Inline column definitions: (col1 int, col2 varchar(50), ...) + p.nextToken() // consume ( + def := &ast.InlineResultSetDefinition{ + ResultSetType: "Inline", + } + + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef := &ast.ResultColumnDefinition{ + ColumnDefinition: &ast.ColumnDefinitionBase{}, + } + + // Parse column name + colDef.ColumnDefinition.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + colDef.ColumnDefinition.DataType, _ = p.parseDataType() + + // Check for 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.Nullable = &ast.NullableConstraintDefinition{Nullable: false} + } + } else if strings.ToUpper(p.curTok.Literal) == "NULL" { + p.nextToken() // consume NULL + colDef.Nullable = &ast.NullableConstraintDefinition{Nullable: true} + } + + def.ResultColumnDefinitions = append(def.ResultColumnDefinitions, colDef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + definitions = append(definitions, def) + } else { + break + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + return definitions +} + +func (p *Parser) parseExecuteAsStatement() (*ast.ExecuteAsStatement, error) { + // We're positioned after EXECUTE, at AS + p.nextToken() // consume AS stmt := &ast.ExecuteAsStatement{} @@ -4239,16 +4814,25 @@ func (p *Parser) parseReceiveStatement() (*ast.ReceiveStatement, error) { p.nextToken() } - // Optional WHERE clause + // Optional WHERE clause - RECEIVE uses simplified WHERE: CONVERSATION_GROUP_ID = value or CONVERSATION_HANDLE = value if p.curTok.Type == TokenWhere { p.nextToken() // consume WHERE // Check for conversation_group_id - if strings.ToLower(p.curTok.Literal) == "conversation_group_id" { + if strings.ToUpper(p.curTok.Literal) == "CONVERSATION_GROUP_ID" { stmt.IsConversationGroupIdWhere = true + p.nextToken() // consume CONVERSATION_GROUP_ID + } else if strings.ToUpper(p.curTok.Literal) == "CONVERSATION_HANDLE" { + p.nextToken() // consume CONVERSATION_HANDLE + } + + // Skip equals sign + if p.curTok.Type == TokenEquals { + p.nextToken() } - where, err := p.parseBooleanExpression() + // Parse the value (usually a variable reference) + where, err := p.parseScalarExpression() if err != nil { return nil, err } @@ -4768,6 +5352,86 @@ func (p *Parser) parseOpenStatement() (ast.Statement, error) { } p.nextToken() // consume KEY stmt := &ast.OpenSymmetricKeyStatement{Name: p.parseIdentifier()} + + // Parse DECRYPTION BY + if p.curTok.Type == TokenDecryption { + p.nextToken() // consume DECRYPTION + if p.curTok.Type == TokenBy { + p.nextToken() // consume BY + } + mechanism := &ast.CryptoMechanism{} + upperLit := strings.ToUpper(p.curTok.Literal) + + switch upperLit { + case "CERTIFICATE": + p.nextToken() // consume CERTIFICATE + mechanism.CryptoMechanismType = "Certificate" + mechanism.Identifier = p.parseIdentifier() + // Check for optional WITH PASSWORD + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenPassword { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + if p.curTok.Type == TokenNationalString { + str, _ := p.parseNationalStringFromToken() + mechanism.PasswordOrSignature = str + } else if p.curTok.Type == TokenString { + mechanism.PasswordOrSignature = p.parseStringLiteralValue() + p.nextToken() + } + } + } + case "ASYMMETRIC": + p.nextToken() // consume ASYMMETRIC + if p.curTok.Type == TokenKey { + p.nextToken() // consume KEY + } + mechanism.CryptoMechanismType = "AsymmetricKey" + mechanism.Identifier = p.parseIdentifier() + // Check for optional WITH PASSWORD + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenPassword { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + if p.curTok.Type == TokenNationalString { + str, _ := p.parseNationalStringFromToken() + mechanism.PasswordOrSignature = str + } else if p.curTok.Type == TokenString { + mechanism.PasswordOrSignature = p.parseStringLiteralValue() + p.nextToken() + } + } + } + case "SYMMETRIC": + p.nextToken() // consume SYMMETRIC + if p.curTok.Type == TokenKey { + p.nextToken() // consume KEY + } + mechanism.CryptoMechanismType = "SymmetricKey" + mechanism.Identifier = p.parseIdentifier() + case "PASSWORD": + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + mechanism.CryptoMechanismType = "Password" + if p.curTok.Type == TokenNationalString { + str, _ := p.parseNationalStringFromToken() + mechanism.PasswordOrSignature = str + } else if p.curTok.Type == TokenString { + mechanism.PasswordOrSignature = p.parseStringLiteralValue() + p.nextToken() + } + } + stmt.DecryptionMechanism = mechanism + } + if p.curTok.Type == TokenSemicolon { p.nextToken() } @@ -5570,6 +6234,24 @@ func (p *Parser) parseCreateDatabaseStatement() (ast.Statement, error) { } } + // Check for CONTAINMENT = NONE/PARTIAL + if strings.ToUpper(p.curTok.Literal) == "CONTAINMENT" { + p.nextToken() // consume CONTAINMENT + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + val := strings.ToUpper(p.curTok.Literal) + containmentVal := "None" + if val == "PARTIAL" { + containmentVal = "Partial" + } + stmt.Containment = &ast.ContainmentDatabaseOption{ + Value: containmentVal, + OptionKind: "Containment", + } + p.nextToken() + } + // Check for AS COPY OF syntax if p.curTok.Type == TokenAs { p.nextToken() // consume AS @@ -5594,6 +6276,48 @@ func (p *Parser) parseCreateDatabaseStatement() (ast.Statement, error) { } } + // Check for ON clause (file groups) + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + fileGroups, err := p.parseFileGroups() + if err != nil { + return nil, err + } + stmt.FileGroups = fileGroups + } + + // Check for LOG ON clause + if strings.ToUpper(p.curTok.Literal) == "LOG" { + p.nextToken() // consume LOG + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + logDecls, err := p.parseFileDeclarationList(false) + if err != nil { + return nil, err + } + stmt.LogOn = logDecls + } + } + + // Check for COLLATE clause + if strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + stmt.Collation = p.parseIdentifier() + } + + // Check for FOR ATTACH clause + if strings.ToUpper(p.curTok.Literal) == "FOR" { + p.nextToken() // consume FOR + switch strings.ToUpper(p.curTok.Literal) { + case "ATTACH": + stmt.AttachMode = "Attach" + p.nextToken() + case "ATTACH_REBUILD_LOG": + stmt.AttachMode = "AttachRebuildLog" + p.nextToken() + } + } + // Check for WITH clause if p.curTok.Type == TokenWith { p.nextToken() // consume WITH @@ -5641,6 +6365,45 @@ func (p *Parser) parseCreateDatabaseOptions() ([]ast.CreateDatabaseOption, error } options = append(options, opt) + case "TRANSFORM_NOISE_WORDS": + p.nextToken() // consume TRANSFORM_NOISE_WORDS + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + state := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume ON/OFF + opt := &ast.OnOffDatabaseOption{ + OptionKind: "TransformNoiseWords", + OptionState: capitalizeFirst(state), + } + options = append(options, opt) + + case "DB_CHAINING": + p.nextToken() // consume DB_CHAINING + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = (optional) + } + state := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume ON/OFF + opt := &ast.OnOffDatabaseOption{ + OptionKind: "DBChaining", + OptionState: capitalizeFirst(state), + } + options = append(options, opt) + + case "NESTED_TRIGGERS": + p.nextToken() // consume NESTED_TRIGGERS + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + state := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume ON/OFF + opt := &ast.OnOffDatabaseOption{ + OptionKind: "NestedTriggers", + OptionState: capitalizeFirst(state), + } + options = append(options, opt) + default: // Unknown option, return what we have return options, nil @@ -5730,65 +6493,368 @@ func (p *Parser) parseAzureDatabaseOptions() ([]ast.CreateDatabaseOption, error) return options, nil } -func (p *Parser) parseCreateLoginStatement() (*ast.CreateLoginStatement, error) { - p.nextToken() // consume LOGIN +// parseFileGroups parses the file group definitions in CREATE DATABASE ON clause +func (p *Parser) parseFileGroups() ([]*ast.FileGroupDefinition, error) { + var fileGroups []*ast.FileGroupDefinition - stmt := &ast.CreateLoginStatement{ - Name: p.parseIdentifier(), - } + for { + fg := &ast.FileGroupDefinition{} + isPrimary := false - // Skip rest of statement - p.skipToEndOfStatement() - return stmt, nil -} + // Check for PRIMARY keyword or FILEGROUP keyword + switch strings.ToUpper(p.curTok.Literal) { + case "PRIMARY": + isPrimary = true + p.nextToken() // consume PRIMARY + case "FILEGROUP": + p.nextToken() // consume FILEGROUP + fg.Name = p.parseIdentifier() + // Check for CONTAINS FILESTREAM or CONTAINS MEMORY_OPTIMIZED_DATA + if strings.ToUpper(p.curTok.Literal) == "CONTAINS" { + p.nextToken() // consume CONTAINS + switch strings.ToUpper(p.curTok.Literal) { + case "FILESTREAM": + fg.ContainsFileStream = true + p.nextToken() + case "MEMORY_OPTIMIZED_DATA": + fg.ContainsMemoryOptimizedData = true + p.nextToken() + } + } + // Check for DEFAULT keyword + if p.curTok.Type == TokenDefault { + fg.IsDefault = true + p.nextToken() + } + } -func (p *Parser) parseCreateIndexStatement() (*ast.CreateIndexStatement, error) { - // May already be past INDEX keyword if called from UNIQUE case - if p.curTok.Type == TokenIndex { - p.nextToken() // consume INDEX - } else if strings.ToUpper(p.curTok.Literal) == "UNIQUE" { - p.nextToken() // consume UNIQUE - if p.curTok.Type == TokenIndex { - p.nextToken() // consume INDEX + // Parse file declarations for this group + decls, err := p.parseFileDeclarationList(isPrimary) + if err != nil { + return nil, err } - } + fg.FileDeclarations = decls + fileGroups = append(fileGroups, fg) - stmt := &ast.CreateIndexStatement{ - Name: p.parseIdentifier(), + // Check if there's a comma followed by another FILEGROUP + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + // Check if next is FILEGROUP - if so, continue the loop + if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { + continue + } + // Otherwise it might be another file in primary, so we need to handle that + // by going back and adding it to the first filegroup + // Actually, this case means there are more files after the comma + // Check if it's a new filegroup or more declarations for the first group + } + + // Check for FILEGROUP keyword for next group + if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { + continue + } + + break } - // Skip rest of statement - p.skipToEndOfStatement() - return stmt, nil + return fileGroups, nil } -func (p *Parser) parseCreateSpatialIndexStatement() (*ast.CreateSpatialIndexStatement, error) { - p.nextToken() // consume SPATIAL - if p.curTok.Type == TokenIndex { - p.nextToken() // consume INDEX - } +// parseFileDeclarationList parses a comma-separated list of file declarations +func (p *Parser) parseFileDeclarationList(firstIsPrimary bool) ([]*ast.FileDeclaration, error) { + var decls []*ast.FileDeclaration - stmt := &ast.CreateSpatialIndexStatement{ - Name: p.parseIdentifier(), - SpatialIndexingScheme: "None", - } + isFirst := true + for { + // Expect opening paren for file declaration + if p.curTok.Type != TokenLParen { + break + } + p.nextToken() // consume ( - // Parse ON table_name(column_name) - if p.curTok.Type == TokenOn { - p.nextToken() // consume ON - stmt.Object, _ = p.parseSchemaObjectName() + decl := &ast.FileDeclaration{} + if isFirst && firstIsPrimary { + decl.IsPrimary = true + } + isFirst = false - // Parse (column_name) - if p.curTok.Type == TokenLParen { - p.nextToken() // consume ( - stmt.SpatialColumnName = p.parseIdentifier() - if p.curTok.Type == TokenRParen { - p.nextToken() // consume ) - } + // Parse file options + opts, err := p.parseFileDeclarationOptions() + if err != nil { + return nil, err } - } + decl.Options = opts - // Parse USING clause for spatial indexing scheme + // Expect closing paren + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + decls = append(decls, decl) + + // Check for comma + if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + // If next token is FILEGROUP or LOG, break out + upper := strings.ToUpper(p.curTok.Literal) + if upper == "FILEGROUP" || upper == "LOG" { + break + } + // Otherwise, continue parsing more declarations + continue + } + + break + } + + return decls, nil +} + +// parseFileDeclarationOptions parses the options inside a file declaration +func (p *Parser) parseFileDeclarationOptions() ([]ast.FileDeclarationOption, error) { + var opts []ast.FileDeclarationOption + + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if p.curTok.Type == TokenComma { + p.nextToken() + continue + } + + optName := strings.ToUpper(p.curTok.Literal) + + switch optName { + case "NAME": + p.nextToken() // consume NAME + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + var opt *ast.NameFileDeclarationOption + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + // Parse as string literal + strLit, _ := p.parseStringLiteral() + opt = &ast.NameFileDeclarationOption{ + LogicalFileName: &ast.IdentifierOrValueExpression{ + Value: strLit.Value, + ValueExpression: strLit, + }, + IsNewName: false, + OptionKind: "Name", + } + } else { + // Parse as identifier + id := p.parseIdentifier() + opt = &ast.NameFileDeclarationOption{ + LogicalFileName: &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + }, + IsNewName: false, + OptionKind: "Name", + } + } + opts = append(opts, opt) + + case "NEWNAME": + p.nextToken() // consume NEWNAME + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + id := p.parseIdentifier() + opt := &ast.NameFileDeclarationOption{ + LogicalFileName: &ast.IdentifierOrValueExpression{ + Value: id.Value, + Identifier: id, + }, + IsNewName: true, + OptionKind: "NewName", + } + opts = append(opts, opt) + + case "FILENAME": + p.nextToken() // consume FILENAME + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + strLit, _ := p.parseStringLiteral() + opt := &ast.FileNameFileDeclarationOption{ + OSFileName: strLit, + OptionKind: "FileName", + } + opts = append(opts, opt) + + case "SIZE": + p.nextToken() // consume SIZE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + size, units := p.parseSizeValue() + opt := &ast.SizeFileDeclarationOption{ + Size: size, + Units: units, + OptionKind: "Size", + } + opts = append(opts, opt) + + case "MAXSIZE": + p.nextToken() // consume MAXSIZE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + // Check for UNLIMITED + if strings.ToUpper(p.curTok.Literal) == "UNLIMITED" { + p.nextToken() + opt := &ast.MaxSizeFileDeclarationOption{ + Units: "Unspecified", + Unlimited: true, + OptionKind: "MaxSize", + } + opts = append(opts, opt) + } else { + size, units := p.parseSizeValue() + opt := &ast.MaxSizeFileDeclarationOption{ + MaxSize: size, + Units: units, + Unlimited: false, + OptionKind: "MaxSize", + } + opts = append(opts, opt) + } + + case "FILEGROWTH": + p.nextToken() // consume FILEGROWTH + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + size, units := p.parseSizeValue() + opt := &ast.FileGrowthFileDeclarationOption{ + GrowthIncrement: size, + Units: units, + OptionKind: "FileGrowth", + } + opts = append(opts, opt) + + default: + // Unknown option, break + return opts, nil + } + } + + return opts, nil +} + +// parseSizeValue parses a size value with optional unit (e.g., "10", "5 MB", "15%") +func (p *Parser) parseSizeValue() (ast.ScalarExpression, string) { + value := p.curTok.Literal + p.nextToken() // consume value + + // Check if unit is attached to value (e.g., "5MB", "15%") + upperVal := strings.ToUpper(value) + if strings.HasSuffix(upperVal, "%") { + numVal := strings.TrimSuffix(value, "%") + return &ast.IntegerLiteral{LiteralType: "Integer", Value: numVal}, "Percent" + } + if strings.HasSuffix(upperVal, "KB") { + numVal := strings.TrimSuffix(upperVal, "KB") + return &ast.IntegerLiteral{LiteralType: "Integer", Value: numVal}, "KB" + } + if strings.HasSuffix(upperVal, "MB") { + numVal := strings.TrimSuffix(upperVal, "MB") + return &ast.IntegerLiteral{LiteralType: "Integer", Value: numVal}, "MB" + } + if strings.HasSuffix(upperVal, "GB") { + numVal := strings.TrimSuffix(upperVal, "GB") + return &ast.IntegerLiteral{LiteralType: "Integer", Value: numVal}, "GB" + } + if strings.HasSuffix(upperVal, "TB") { + numVal := strings.TrimSuffix(upperVal, "TB") + return &ast.IntegerLiteral{LiteralType: "Integer", Value: numVal}, "TB" + } + + // Check for separate unit token + units := "Unspecified" + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenModulo { + unitStr := strings.ToUpper(p.curTok.Literal) + switch unitStr { + case "KB": + units = "KB" + p.nextToken() + case "MB": + units = "MB" + p.nextToken() + case "GB": + units = "GB" + p.nextToken() + case "TB": + units = "TB" + p.nextToken() + case "%": + units = "Percent" + p.nextToken() + } + } + + return &ast.IntegerLiteral{LiteralType: "Integer", Value: value}, units +} + +func (p *Parser) parseCreateLoginStatement() (*ast.CreateLoginStatement, error) { + p.nextToken() // consume LOGIN + + stmt := &ast.CreateLoginStatement{ + Name: p.parseIdentifier(), + } + + // Skip rest of statement + p.skipToEndOfStatement() + return stmt, nil +} + +func (p *Parser) parseCreateIndexStatement() (*ast.CreateIndexStatement, error) { + // May already be past INDEX keyword if called from UNIQUE case + if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + } else if strings.ToUpper(p.curTok.Literal) == "UNIQUE" { + p.nextToken() // consume UNIQUE + if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + } + } + + stmt := &ast.CreateIndexStatement{ + Name: p.parseIdentifier(), + } + + // Skip rest of statement + p.skipToEndOfStatement() + return stmt, nil +} + +func (p *Parser) parseCreateSpatialIndexStatement() (*ast.CreateSpatialIndexStatement, error) { + p.nextToken() // consume SPATIAL + if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + } + + stmt := &ast.CreateSpatialIndexStatement{ + Name: p.parseIdentifier(), + SpatialIndexingScheme: "None", + } + + // Parse ON table_name(column_name) + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + stmt.Object, _ = p.parseSchemaObjectName() + + // Parse (column_name) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + stmt.SpatialColumnName = p.parseIdentifier() + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + + // Parse USING clause for spatial indexing scheme if strings.ToUpper(p.curTok.Literal) == "USING" { p.nextToken() // consume USING scheme := strings.ToUpper(p.curTok.Literal) @@ -6173,11 +7239,149 @@ func (p *Parser) parseCreateSymmetricKeyStatement() (*ast.CreateSymmetricKeyStat Name: p.parseIdentifier(), } + // Check for FROM PROVIDER clause + if p.curTok.Type == TokenFrom && strings.ToUpper(p.peekTok.Literal) == "PROVIDER" { + p.nextToken() // consume FROM + p.nextToken() // consume PROVIDER + stmt.Provider = p.parseIdentifier() + } + + // Check for WITH clause (key options) + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + keyOpts, err := p.parseSymmetricKeyOptions() + if err != nil { + return nil, err + } + stmt.KeyOptions = keyOpts + } + + // Check for ENCRYPTION BY clause + if strings.ToUpper(p.curTok.Literal) == "ENCRYPTION" { + p.nextToken() // consume ENCRYPTION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + mechanisms, err := p.parseCryptoMechanisms() + if err != nil { + return nil, err + } + stmt.EncryptingMechanisms = mechanisms + } + // Skip rest of statement p.skipToEndOfStatement() return stmt, nil } +func (p *Parser) parseSymmetricKeyOptions() ([]ast.KeyOption, error) { + var options []ast.KeyOption + + for { + optName := strings.ToUpper(p.curTok.Literal) + switch optName { + case "PROVIDER_KEY_NAME": + p.nextToken() // consume PROVIDER_KEY_NAME + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + keyName, _ := p.parseScalarExpression() + opt := &ast.ProviderKeyNameKeyOption{ + KeyName: keyName, + OptionKind: "ProviderKeyName", + } + options = append(options, opt) + + case "ALGORITHM": + p.nextToken() // consume ALGORITHM + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + algo := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume algorithm name + opt := &ast.AlgorithmKeyOption{ + Algorithm: algo, + OptionKind: "Algorithm", + } + options = append(options, opt) + + case "CREATION_DISPOSITION": + p.nextToken() // consume CREATION_DISPOSITION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + disposition := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume CREATE_NEW or OPEN_EXISTING + opt := &ast.CreationDispositionKeyOption{ + IsCreateNew: disposition == "CREATE_NEW", + OptionKind: "CreationDisposition", + } + options = append(options, opt) + + default: + return options, nil + } + + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + } else { + break + } + } + + return options, nil +} + +func (p *Parser) parseCryptoMechanisms() ([]*ast.CryptoMechanism, error) { + var mechanisms []*ast.CryptoMechanism + + for { + mechanism := &ast.CryptoMechanism{} + upperLit := strings.ToUpper(p.curTok.Literal) + + switch upperLit { + case "CERTIFICATE": + p.nextToken() // consume CERTIFICATE + mechanism.CryptoMechanismType = "Certificate" + mechanism.Identifier = p.parseIdentifier() + case "SYMMETRIC": + p.nextToken() // consume SYMMETRIC + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + mechanism.CryptoMechanismType = "SymmetricKey" + mechanism.Identifier = p.parseIdentifier() + case "ASYMMETRIC": + p.nextToken() // consume ASYMMETRIC + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + mechanism.CryptoMechanismType = "AsymmetricKey" + mechanism.Identifier = p.parseIdentifier() + case "PASSWORD": + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + mechanism.CryptoMechanismType = "Password" + // Password should be a string literal + mechanism.PasswordOrSignature, _ = p.parseScalarExpression() + default: + return mechanisms, nil + } + + mechanisms = append(mechanisms, mechanism) + + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + } else { + break + } + } + + return mechanisms, nil +} + func (p *Parser) parseCreateCertificateStatement() (*ast.CreateCertificateStatement, error) { p.nextToken() // consume CERTIFICATE @@ -6256,6 +7460,40 @@ func (p *Parser) parseCreateServiceStatement() (*ast.CreateServiceStatement, err Name: p.parseIdentifier(), } + // Check for AUTHORIZATION clause + if strings.ToUpper(p.curTok.Literal) == "AUTHORIZATION" { + p.nextToken() // consume AUTHORIZATION + stmt.Owner = p.parseIdentifier() + } + + // Check for ON QUEUE clause + if p.curTok.Type == TokenOn && strings.ToUpper(p.peekTok.Literal) == "QUEUE" { + p.nextToken() // consume ON + p.nextToken() // consume QUEUE + queueName, _ := p.parseSchemaObjectName() + stmt.QueueName = queueName + } + + // Check for contract list (c1, c2, ...) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + var contracts []*ast.ServiceContract + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + contract := &ast.ServiceContract{ + Name: p.parseIdentifier(), + Action: "None", + } + contracts = append(contracts, contract) + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + stmt.ServiceContracts = contracts + } + // Skip rest of statement p.skipToEndOfStatement() return stmt, nil @@ -6398,11 +7636,99 @@ func (p *Parser) parseCreateRouteStatement() (*ast.CreateRouteStatement, error) Name: p.parseIdentifier(), } - // Skip rest of statement - p.skipToEndOfStatement() + // Parse optional AUTHORIZATION clause + if strings.ToUpper(p.curTok.Literal) == "AUTHORIZATION" { + p.nextToken() // consume AUTHORIZATION + stmt.Owner = p.parseIdentifier() + } + + // Parse WITH clause + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + stmt.RouteOptions = p.parseRouteOptions() + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + return stmt, nil } +func (p *Parser) parseRouteOptions() []*ast.RouteOption { + var options []*ast.RouteOption + + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { + optionName := strings.ToUpper(p.curTok.Literal) + p.nextToken() // consume option name + + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + var optionKind string + switch optionName { + case "BROKER_INSTANCE": + optionKind = "BrokerInstance" + case "SERVICE_NAME": + optionKind = "ServiceName" + case "LIFETIME": + optionKind = "Lifetime" + case "ADDRESS": + optionKind = "Address" + case "MIRROR_ADDRESS": + optionKind = "MirrorAddress" + default: + // Unknown option, skip + if p.curTok.Type == TokenComma { + p.nextToken() + } + continue + } + + // Parse literal value + var literal ast.ScalarExpression + if p.curTok.Type == TokenString { + value := p.curTok.Literal + // Strip quotes + if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' { + value = value[1 : len(value)-1] + } + literal = &ast.StringLiteral{ + LiteralType: "String", + Value: value, + } + p.nextToken() + } else if p.curTok.Type == TokenNumber { + literal = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } else { + // Unknown value, try to skip + p.nextToken() + } + + if literal != nil { + options = append(options, &ast.RouteOption{ + OptionKind: optionKind, + Literal: literal, + }) + } + + // Skip comma if present + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + return options +} + func (p *Parser) parseCreateEndpointStatement() (*ast.CreateEndpointStatement, error) { p.nextToken() // consume ENDPOINT @@ -6648,11 +7974,76 @@ func (p *Parser) parseCreateRemoteServiceBindingStatement() (*ast.CreateRemoteSe Name: p.parseIdentifier(), } - // Skip rest of statement + // Parse TO SERVICE 'service_name' + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + if strings.ToUpper(p.curTok.Literal) == "SERVICE" { + p.nextToken() // consume SERVICE + } + // Parse service name string + stmt.Service = p.parseStringLiteralValue() + p.nextToken() // consume string + } + + // Parse WITH options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + stmt.Options = p.parseRemoteServiceBindingOptions() + } + + // Skip any remaining parts p.skipToEndOfStatement() return stmt, nil } +func (p *Parser) parseRemoteServiceBindingOptions() []ast.RemoteServiceBindingOption { + var options []ast.RemoteServiceBindingOption + + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { + // Check for GO batch separator + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "GO" { + break + } + upper := strings.ToUpper(p.curTok.Literal) + + if upper == "USER" { + p.nextToken() // consume USER + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + opt := &ast.UserRemoteServiceBindingOption{ + OptionKind: "User", + User: p.parseIdentifier(), + } + options = append(options, opt) + } else if upper == "ANONYMOUS" { + p.nextToken() // consume ANONYMOUS + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + optState := "Off" + if strings.ToUpper(p.curTok.Literal) == "ON" { + optState = "On" + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "OFF" { + optState = "Off" + p.nextToken() + } + opt := &ast.OnOffRemoteServiceBindingOption{ + OptionKind: "Anonymous", + OptionState: optState, + } + options = append(options, opt) + } else if p.curTok.Type == TokenComma { + p.nextToken() // consume comma + } else { + break + } + } + + return options +} + func (p *Parser) parseCreateStatisticsStatement() (*ast.CreateStatisticsStatement, error) { p.nextToken() // consume STATISTICS @@ -6692,11 +8083,21 @@ func (p *Parser) parseCreateStatisticsStatement() (*ast.CreateStatisticsStatemen } } + // Parse optional WHERE clause (filter predicate) + if p.curTok.Type == TokenWhere { + p.nextToken() // consume WHERE + pred, err := p.parseBooleanExpression() + if err != nil { + return nil, err + } + stmt.FilterPredicate = pred + } + // Parse optional WITH clause (reuse UPDATE STATISTICS options logic) if p.curTok.Type == TokenWith { p.nextToken() // consume WITH - for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && p.curTok.Type != TokenWhere { + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { optionName := strings.ToUpper(p.curTok.Literal) p.nextToken() // consume option name @@ -6721,6 +8122,18 @@ func (p *Parser) parseCreateStatisticsStatement() (*ast.CreateStatisticsStatemen OptionKind: "Sample" + strings.Title(strings.ToLower(mode)), Literal: value, }) + case "STATS_STREAM": + if p.curTok.Type == TokenEquals { + p.nextToken() + } + value, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.StatisticsOptions = append(stmt.StatisticsOptions, &ast.LiteralStatisticsOption{ + OptionKind: "StatsStream", + Literal: value, + }) case "INCREMENTAL": if p.curTok.Type == TokenEquals { p.nextToken() @@ -6748,9 +8161,9 @@ func (p *Parser) parseCreateStatisticsStatement() (*ast.CreateStatisticsStatemen } } - // Skip optional WHERE clause and rest of statement - if p.curTok.Type == TokenWhere || p.curTok.Type == TokenSemicolon { - p.skipToEndOfStatement() + // Skip any remaining tokens + if p.curTok.Type == TokenSemicolon { + p.nextToken() } return stmt, nil diff --git a/parser/parser.go b/parser/parser.go index b625e5dc..346953fc 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -87,6 +87,8 @@ func (p *Parser) parseBatch() (*ast.Batch, error) { func (p *Parser) parseStatement() (ast.Statement, error) { switch p.curTok.Type { + case TokenWith: + return p.parseWithStatement() case TokenSelect, TokenLParen: return p.parseSelectStatement() case TokenInsert: diff --git a/parser/testdata/AlterCreateDatabaseFilePath150/metadata.json b/parser/testdata/AlterCreateDatabaseFilePath150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterCreateDatabaseFilePath150/metadata.json +++ b/parser/testdata/AlterCreateDatabaseFilePath150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlterCreateDatabaseStatementTests120/metadata.json b/parser/testdata/AlterCreateDatabaseStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterCreateDatabaseStatementTests120/metadata.json +++ b/parser/testdata/AlterCreateDatabaseStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlterCreateServiceStatementTests/metadata.json b/parser/testdata/AlterCreateServiceStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterCreateServiceStatementTests/metadata.json +++ b/parser/testdata/AlterCreateServiceStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlterEndpointStatementTests/metadata.json b/parser/testdata/AlterEndpointStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlterEndpointStatementTests/metadata.json +++ b/parser/testdata/AlterEndpointStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/AlwaysEncryptedTests150/metadata.json b/parser/testdata/AlwaysEncryptedTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/AlwaysEncryptedTests150/metadata.json +++ b/parser/testdata/AlwaysEncryptedTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_CTEStatementTests100/metadata.json b/parser/testdata/Baselines100_CTEStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_CTEStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_CTEStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_CreateStatisticsStatementTests100/metadata.json b/parser/testdata/Baselines100_CreateStatisticsStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_CreateStatisticsStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_CreateStatisticsStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_CreateTableTests100/metadata.json b/parser/testdata/Baselines100_CreateTableTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_CreateTableTests100/metadata.json +++ b/parser/testdata/Baselines100_CreateTableTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_DropIndexStatementTests100/metadata.json b/parser/testdata/Baselines100_DropIndexStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_DropIndexStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_DropIndexStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_SymmetricKeyStatementTests100/metadata.json b/parser/testdata/Baselines100_SymmetricKeyStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_SymmetricKeyStatementTests100/metadata.json +++ b/parser/testdata/Baselines100_SymmetricKeyStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines110_CreateAlterUserStatementTests110/metadata.json b/parser/testdata/Baselines110_CreateAlterUserStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_CreateAlterUserStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_CreateAlterUserStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines110_ExecuteStatementTests110/metadata.json b/parser/testdata/Baselines110_ExecuteStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_ExecuteStatementTests110/metadata.json +++ b/parser/testdata/Baselines110_ExecuteStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_AlterCreateDatabaseStatementTests120/metadata.json b/parser/testdata/Baselines120_AlterCreateDatabaseStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_AlterCreateDatabaseStatementTests120/metadata.json +++ b/parser/testdata/Baselines120_AlterCreateDatabaseStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_CreateAggregateStatementTests120/metadata.json b/parser/testdata/Baselines120_CreateAggregateStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_CreateAggregateStatementTests120/metadata.json +++ b/parser/testdata/Baselines120_CreateAggregateStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_CreateDatabaseTests140/metadata.json b/parser/testdata/Baselines140_CreateDatabaseTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_CreateDatabaseTests140/metadata.json +++ b/parser/testdata/Baselines140_CreateDatabaseTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines140_OptimizerHintsTests140/metadata.json b/parser/testdata/Baselines140_OptimizerHintsTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines140_OptimizerHintsTests140/metadata.json +++ b/parser/testdata/Baselines140_OptimizerHintsTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_AlterCreateDatabaseFilePath150/metadata.json b/parser/testdata/Baselines150_AlterCreateDatabaseFilePath150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_AlterCreateDatabaseFilePath150/metadata.json +++ b/parser/testdata/Baselines150_AlterCreateDatabaseFilePath150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_AlwaysEncryptedTests150/metadata.json b/parser/testdata/Baselines150_AlwaysEncryptedTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_AlwaysEncryptedTests150/metadata.json +++ b/parser/testdata/Baselines150_AlwaysEncryptedTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_ScalarUDFInlineTests150/metadata.json b/parser/testdata/Baselines150_ScalarUDFInlineTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_ScalarUDFInlineTests150/metadata.json +++ b/parser/testdata/Baselines150_ScalarUDFInlineTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_NotEnforcedConstraintTests160/metadata.json b/parser/testdata/Baselines160_NotEnforcedConstraintTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_NotEnforcedConstraintTests160/metadata.json +++ b/parser/testdata/Baselines160_NotEnforcedConstraintTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_WithinGroupTests160/metadata.json b/parser/testdata/Baselines160_WithinGroupTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_WithinGroupTests160/metadata.json +++ b/parser/testdata/Baselines160_WithinGroupTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_AlterCreateServiceStatementTests/metadata.json b/parser/testdata/Baselines90_AlterCreateServiceStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_AlterCreateServiceStatementTests/metadata.json +++ b/parser/testdata/Baselines90_AlterCreateServiceStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_AlterEndpointStatementTests/metadata.json b/parser/testdata/Baselines90_AlterEndpointStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_AlterEndpointStatementTests/metadata.json +++ b/parser/testdata/Baselines90_AlterEndpointStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_CreateProcedureStatementTests90/metadata.json b/parser/testdata/Baselines90_CreateProcedureStatementTests90/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/Baselines90_CreateProcedureStatementTests90/metadata.json +++ b/parser/testdata/Baselines90_CreateProcedureStatementTests90/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/Baselines90_CreateRemoteServiceBindingStatementTests/metadata.json b/parser/testdata/Baselines90_CreateRemoteServiceBindingStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_CreateRemoteServiceBindingStatementTests/metadata.json +++ b/parser/testdata/Baselines90_CreateRemoteServiceBindingStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_OpenSymmetricKeyStatementTests/metadata.json b/parser/testdata/Baselines90_OpenSymmetricKeyStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_OpenSymmetricKeyStatementTests/metadata.json +++ b/parser/testdata/Baselines90_OpenSymmetricKeyStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_ReceiveStatementTests/metadata.json b/parser/testdata/Baselines90_ReceiveStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_ReceiveStatementTests/metadata.json +++ b/parser/testdata/Baselines90_ReceiveStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_RouteStatementTests/metadata.json b/parser/testdata/Baselines90_RouteStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_RouteStatementTests/metadata.json +++ b/parser/testdata/Baselines90_RouteStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_CreateTriggerStatementTests/metadata.json b/parser/testdata/BaselinesCommon_CreateTriggerStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_CreateTriggerStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_CreateTriggerStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_DropStatementsTests/metadata.json b/parser/testdata/BaselinesCommon_DropStatementsTests/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/BaselinesCommon_DropStatementsTests/metadata.json +++ b/parser/testdata/BaselinesCommon_DropStatementsTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/BaselinesCommon_ReturnStatementTests/metadata.json b/parser/testdata/BaselinesCommon_ReturnStatementTests/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/BaselinesCommon_ReturnStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ReturnStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/BaselinesCommon_ScalarDataTypeTests/metadata.json b/parser/testdata/BaselinesCommon_ScalarDataTypeTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_ScalarDataTypeTests/metadata.json +++ b/parser/testdata/BaselinesCommon_ScalarDataTypeTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_SetOffsetsAndOnOffSetTests/metadata.json b/parser/testdata/BaselinesCommon_SetOffsetsAndOnOffSetTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_SetOffsetsAndOnOffSetTests/metadata.json +++ b/parser/testdata/BaselinesCommon_SetOffsetsAndOnOffSetTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_TSqlParserTestScript1/metadata.json b/parser/testdata/BaselinesCommon_TSqlParserTestScript1/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_TSqlParserTestScript1/metadata.json +++ b/parser/testdata/BaselinesCommon_TSqlParserTestScript1/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CTEStatementTests100/metadata.json b/parser/testdata/CTEStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CTEStatementTests100/metadata.json +++ b/parser/testdata/CTEStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateAggregateStatementTests120/metadata.json b/parser/testdata/CreateAggregateStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateAggregateStatementTests120/metadata.json +++ b/parser/testdata/CreateAggregateStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateAlterUserStatementTests110/metadata.json b/parser/testdata/CreateAlterUserStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateAlterUserStatementTests110/metadata.json +++ b/parser/testdata/CreateAlterUserStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateDatabaseTests140/metadata.json b/parser/testdata/CreateDatabaseTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateDatabaseTests140/metadata.json +++ b/parser/testdata/CreateDatabaseTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateRemoteServiceBindingStatementTests/metadata.json b/parser/testdata/CreateRemoteServiceBindingStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateRemoteServiceBindingStatementTests/metadata.json +++ b/parser/testdata/CreateRemoteServiceBindingStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateStatisticsStatementTests100/metadata.json b/parser/testdata/CreateStatisticsStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateStatisticsStatementTests100/metadata.json +++ b/parser/testdata/CreateStatisticsStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateTableTests100/metadata.json b/parser/testdata/CreateTableTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateTableTests100/metadata.json +++ b/parser/testdata/CreateTableTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateTriggerStatementTests/metadata.json b/parser/testdata/CreateTriggerStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateTriggerStatementTests/metadata.json +++ b/parser/testdata/CreateTriggerStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DropIndexStatementTests100/metadata.json b/parser/testdata/DropIndexStatementTests100/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/DropIndexStatementTests100/metadata.json +++ b/parser/testdata/DropIndexStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/DropStatementsTests/metadata.json b/parser/testdata/DropStatementsTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DropStatementsTests/metadata.json +++ b/parser/testdata/DropStatementsTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ExecuteStatementTests110/metadata.json b/parser/testdata/ExecuteStatementTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ExecuteStatementTests110/metadata.json +++ b/parser/testdata/ExecuteStatementTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/FromClauseTests120/metadata.json b/parser/testdata/FromClauseTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/FromClauseTests120/metadata.json +++ b/parser/testdata/FromClauseTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/NotEnforcedConstraintTests160/metadata.json b/parser/testdata/NotEnforcedConstraintTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/NotEnforcedConstraintTests160/metadata.json +++ b/parser/testdata/NotEnforcedConstraintTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/OpenSymmetricKeyStatementTests/metadata.json b/parser/testdata/OpenSymmetricKeyStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/OpenSymmetricKeyStatementTests/metadata.json +++ b/parser/testdata/OpenSymmetricKeyStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/OptimizerHintsTests140/metadata.json b/parser/testdata/OptimizerHintsTests140/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/OptimizerHintsTests140/metadata.json +++ b/parser/testdata/OptimizerHintsTests140/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ReceiveStatementTests/metadata.json b/parser/testdata/ReceiveStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ReceiveStatementTests/metadata.json +++ b/parser/testdata/ReceiveStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ReturnStatementTests/metadata.json b/parser/testdata/ReturnStatementTests/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/ReturnStatementTests/metadata.json +++ b/parser/testdata/ReturnStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/RouteStatementTests/metadata.json b/parser/testdata/RouteStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/RouteStatementTests/metadata.json +++ b/parser/testdata/RouteStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/ScalarUDFInlineTests150/metadata.json b/parser/testdata/ScalarUDFInlineTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/ScalarUDFInlineTests150/metadata.json +++ b/parser/testdata/ScalarUDFInlineTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/SemicolonsBeforeStatementTests2/metadata.json b/parser/testdata/SemicolonsBeforeStatementTests2/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/SemicolonsBeforeStatementTests2/metadata.json +++ b/parser/testdata/SemicolonsBeforeStatementTests2/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/SetOffsetsAndOnOffSetTests/metadata.json b/parser/testdata/SetOffsetsAndOnOffSetTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/SetOffsetsAndOnOffSetTests/metadata.json +++ b/parser/testdata/SetOffsetsAndOnOffSetTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/SymmetricKeyStatementTests100/metadata.json b/parser/testdata/SymmetricKeyStatementTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/SymmetricKeyStatementTests100/metadata.json +++ b/parser/testdata/SymmetricKeyStatementTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/TSqlParserTestScript1/metadata.json b/parser/testdata/TSqlParserTestScript1/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/TSqlParserTestScript1/metadata.json +++ b/parser/testdata/TSqlParserTestScript1/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/WithinGroupTests160/metadata.json b/parser/testdata/WithinGroupTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WithinGroupTests160/metadata.json +++ b/parser/testdata/WithinGroupTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{}