diff --git a/ast/alter_table_alter_index_statement.go b/ast/alter_table_alter_index_statement.go index e69de983..86cf90ca 100644 --- a/ast/alter_table_alter_index_statement.go +++ b/ast/alter_table_alter_index_statement.go @@ -32,8 +32,9 @@ type IndexExpressionOption struct { Expression ScalarExpression } -func (i *IndexExpressionOption) indexOption() {} -func (i *IndexExpressionOption) node() {} +func (i *IndexExpressionOption) indexOption() {} +func (i *IndexExpressionOption) dropIndexOption() {} +func (i *IndexExpressionOption) node() {} // CompressionDelayIndexOption represents a COMPRESSION_DELAY option type CompressionDelayIndexOption struct { diff --git a/ast/bulk_insert_statement.go b/ast/bulk_insert_statement.go index a0803e2f..bcd38749 100644 --- a/ast/bulk_insert_statement.go +++ b/ast/bulk_insert_statement.go @@ -66,11 +66,12 @@ func (o *OrderBulkInsertOption) bulkInsertOption() {} // BulkOpenRowset represents an OPENROWSET (BULK ...) table reference. type BulkOpenRowset struct { - DataFiles []ScalarExpression `json:"DataFiles,omitempty"` - Options []BulkInsertOption `json:"Options,omitempty"` - Columns []*Identifier `json:"Columns,omitempty"` - Alias *Identifier `json:"Alias,omitempty"` - ForPath bool `json:"ForPath"` + DataFiles []ScalarExpression `json:"DataFiles,omitempty"` + Options []BulkInsertOption `json:"Options,omitempty"` + WithColumns []*OpenRowsetColumnDefinition `json:"WithColumns,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + ForPath bool `json:"ForPath"` } func (b *BulkOpenRowset) node() {} diff --git a/ast/changetable_reference.go b/ast/changetable_reference.go new file mode 100644 index 00000000..056f79b8 --- /dev/null +++ b/ast/changetable_reference.go @@ -0,0 +1,28 @@ +package ast + +// ChangeTableChangesTableReference represents CHANGETABLE(CHANGES ...) table reference +type ChangeTableChangesTableReference struct { + Target *SchemaObjectName `json:"Target,omitempty"` + SinceVersion ScalarExpression `json:"SinceVersion,omitempty"` + ForceSeek bool `json:"ForceSeek"` + Columns []*Identifier `json:"Columns,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + ForPath bool `json:"ForPath"` +} + +func (c *ChangeTableChangesTableReference) node() {} +func (c *ChangeTableChangesTableReference) tableReference() {} + +// ChangeTableVersionTableReference represents CHANGETABLE(VERSION ...) table reference +type ChangeTableVersionTableReference struct { + Target *SchemaObjectName `json:"Target,omitempty"` + PrimaryKeyColumns []*Identifier `json:"PrimaryKeyColumns,omitempty"` + PrimaryKeyValues []ScalarExpression `json:"PrimaryKeyValues,omitempty"` + ForceSeek bool `json:"ForceSeek"` + Columns []*Identifier `json:"Columns,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + ForPath bool `json:"ForPath"` +} + +func (c *ChangeTableVersionTableReference) node() {} +func (c *ChangeTableVersionTableReference) tableReference() {} diff --git a/ast/create_simple_statements.go b/ast/create_simple_statements.go index eaa30c7c..d9f9bd2d 100644 --- a/ast/create_simple_statements.go +++ b/ast/create_simple_statements.go @@ -34,6 +34,33 @@ type CreateLoginStatement struct { func (s *CreateLoginStatement) node() {} func (s *CreateLoginStatement) statement() {} +// AlterLoginEnableDisableStatement represents ALTER LOGIN name ENABLE/DISABLE +type AlterLoginEnableDisableStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsEnable bool `json:"IsEnable"` +} + +func (s *AlterLoginEnableDisableStatement) node() {} +func (s *AlterLoginEnableDisableStatement) statement() {} + +// AlterLoginOptionsStatement represents ALTER LOGIN name WITH options +type AlterLoginOptionsStatement struct { + Name *Identifier `json:"Name,omitempty"` + Options []PrincipalOption `json:"Options,omitempty"` +} + +func (s *AlterLoginOptionsStatement) node() {} +func (s *AlterLoginOptionsStatement) statement() {} + +// DropLoginStatement represents DROP LOGIN name +type DropLoginStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (s *DropLoginStatement) node() {} +func (s *DropLoginStatement) statement() {} + // CreateLoginSource is an interface for login sources type CreateLoginSource interface { createLoginSource() @@ -46,6 +73,39 @@ type ExternalCreateLoginSource struct { func (s *ExternalCreateLoginSource) createLoginSource() {} +// PasswordCreateLoginSource represents WITH PASSWORD = '...' source +type PasswordCreateLoginSource struct { + Password ScalarExpression `json:"Password,omitempty"` + Hashed bool `json:"Hashed"` + MustChange bool `json:"MustChange"` + Options []PrincipalOption `json:"Options,omitempty"` +} + +func (s *PasswordCreateLoginSource) createLoginSource() {} + +// WindowsCreateLoginSource represents FROM WINDOWS source +type WindowsCreateLoginSource struct { + Options []PrincipalOption `json:"Options,omitempty"` +} + +func (s *WindowsCreateLoginSource) createLoginSource() {} + +// CertificateCreateLoginSource represents FROM CERTIFICATE source +type CertificateCreateLoginSource struct { + Certificate *Identifier `json:"Certificate,omitempty"` + Credential *Identifier `json:"Credential,omitempty"` +} + +func (s *CertificateCreateLoginSource) createLoginSource() {} + +// AsymmetricKeyCreateLoginSource represents FROM ASYMMETRIC KEY source +type AsymmetricKeyCreateLoginSource struct { + Key *Identifier `json:"Key,omitempty"` + Credential *Identifier `json:"Credential,omitempty"` +} + +func (s *AsymmetricKeyCreateLoginSource) createLoginSource() {} + // PrincipalOption is an interface for principal options (SID, TYPE, etc.) type PrincipalOption interface { principalOptionNode() diff --git a/ast/create_user_statement.go b/ast/create_user_statement.go index 59084406..bbf9a9bf 100644 --- a/ast/create_user_statement.go +++ b/ast/create_user_statement.go @@ -39,6 +39,23 @@ type IdentifierPrincipalOption struct { func (o *IdentifierPrincipalOption) userOptionNode() {} func (o *IdentifierPrincipalOption) principalOptionNode() {} +// OnOffPrincipalOption represents an ON/OFF principal option +type OnOffPrincipalOption struct { + OptionKind string + OptionState string // "On" or "Off" +} + +func (o *OnOffPrincipalOption) userOptionNode() {} +func (o *OnOffPrincipalOption) principalOptionNode() {} + +// PrincipalOptionSimple represents a simple principal option with just an option kind +type PrincipalOptionSimple struct { + OptionKind string +} + +func (o *PrincipalOptionSimple) userOptionNode() {} +func (o *PrincipalOptionSimple) principalOptionNode() {} + // DefaultSchemaPrincipalOption represents a default schema option type DefaultSchemaPrincipalOption struct { OptionKind string @@ -47,9 +64,9 @@ type DefaultSchemaPrincipalOption struct { func (o *DefaultSchemaPrincipalOption) userOptionNode() {} -// PasswordAlterPrincipalOption represents a password option for ALTER USER +// PasswordAlterPrincipalOption represents a password option for ALTER USER/LOGIN type PasswordAlterPrincipalOption struct { - Password *StringLiteral + Password ScalarExpression OldPassword *StringLiteral MustChange bool Unlock bool @@ -57,4 +74,5 @@ type PasswordAlterPrincipalOption struct { OptionKind string } -func (o *PasswordAlterPrincipalOption) userOptionNode() {} +func (o *PasswordAlterPrincipalOption) userOptionNode() {} +func (o *PasswordAlterPrincipalOption) principalOptionNode() {} diff --git a/ast/data_modification_table_reference.go b/ast/data_modification_table_reference.go new file mode 100644 index 00000000..f87bbadb --- /dev/null +++ b/ast/data_modification_table_reference.go @@ -0,0 +1,19 @@ +package ast + +// DataModificationTableReference represents a DML statement used as a table source in FROM clause +// This allows using INSERT/UPDATE/DELETE/MERGE with OUTPUT clause as table sources +type DataModificationTableReference struct { + DataModificationSpecification DataModificationSpecification + Alias *Identifier + Columns []*Identifier + ForPath bool +} + +func (d *DataModificationTableReference) node() {} +func (d *DataModificationTableReference) tableReference() {} + +// DataModificationSpecification is the interface for DML specifications +type DataModificationSpecification interface { + Node + dataModificationSpecification() +} diff --git a/ast/delete_statement.go b/ast/delete_statement.go index d1a2b665..be6f172d 100644 --- a/ast/delete_statement.go +++ b/ast/delete_statement.go @@ -19,3 +19,6 @@ type DeleteSpecification struct { OutputClause *OutputClause `json:"OutputClause,omitempty"` OutputIntoClause *OutputIntoClause `json:"OutputIntoClause,omitempty"` } + +func (d *DeleteSpecification) node() {} +func (d *DeleteSpecification) dataModificationSpecification() {} diff --git a/ast/drop_service_broker_statements.go b/ast/drop_service_broker_statements.go new file mode 100644 index 00000000..ad156815 --- /dev/null +++ b/ast/drop_service_broker_statements.go @@ -0,0 +1,122 @@ +package ast + +// DropPartitionFunctionStatement represents a DROP PARTITION FUNCTION statement +type DropPartitionFunctionStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropPartitionFunctionStatement) statement() {} +func (*DropPartitionFunctionStatement) node() {} + +// DropPartitionSchemeStatement represents a DROP PARTITION SCHEME statement +type DropPartitionSchemeStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropPartitionSchemeStatement) statement() {} +func (*DropPartitionSchemeStatement) node() {} + +// DropApplicationRoleStatement represents a DROP APPLICATION ROLE statement +type DropApplicationRoleStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropApplicationRoleStatement) statement() {} +func (*DropApplicationRoleStatement) node() {} + +// DropCertificateStatement represents a DROP CERTIFICATE statement +type DropCertificateStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropCertificateStatement) statement() {} +func (*DropCertificateStatement) node() {} + +// DropMasterKeyStatement represents a DROP MASTER KEY statement +type DropMasterKeyStatement struct{} + +func (*DropMasterKeyStatement) statement() {} +func (*DropMasterKeyStatement) node() {} + +// DropXmlSchemaCollectionStatement represents a DROP XML SCHEMA COLLECTION statement +type DropXmlSchemaCollectionStatement struct { + Name *SchemaObjectName `json:"Name,omitempty"` +} + +func (*DropXmlSchemaCollectionStatement) statement() {} +func (*DropXmlSchemaCollectionStatement) node() {} + +// DropContractStatement represents a DROP CONTRACT statement +type DropContractStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropContractStatement) statement() {} +func (*DropContractStatement) node() {} + +// DropEndpointStatement represents a DROP ENDPOINT statement +type DropEndpointStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropEndpointStatement) statement() {} +func (*DropEndpointStatement) node() {} + +// DropMessageTypeStatement represents a DROP MESSAGE TYPE statement +type DropMessageTypeStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropMessageTypeStatement) statement() {} +func (*DropMessageTypeStatement) node() {} + +// DropQueueStatement represents a DROP QUEUE statement +type DropQueueStatement struct { + Name *SchemaObjectName `json:"Name,omitempty"` +} + +func (*DropQueueStatement) statement() {} +func (*DropQueueStatement) node() {} + +// DropRemoteServiceBindingStatement represents a DROP REMOTE SERVICE BINDING statement +type DropRemoteServiceBindingStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropRemoteServiceBindingStatement) statement() {} +func (*DropRemoteServiceBindingStatement) node() {} + +// DropRouteStatement represents a DROP ROUTE statement +type DropRouteStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropRouteStatement) statement() {} +func (*DropRouteStatement) node() {} + +// DropServiceStatement represents a DROP SERVICE statement +type DropServiceStatement struct { + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` +} + +func (*DropServiceStatement) statement() {} +func (*DropServiceStatement) node() {} + +// DropEventNotificationStatement represents a DROP EVENT NOTIFICATION statement +type DropEventNotificationStatement struct { + Notifications []*Identifier `json:"Notifications,omitempty"` + Scope *EventNotificationObjectScope `json:"Scope,omitempty"` +} + +func (*DropEventNotificationStatement) statement() {} +func (*DropEventNotificationStatement) node() {} diff --git a/ast/drop_statements.go b/ast/drop_statements.go index e8da6ad6..8c48c864 100644 --- a/ast/drop_statements.go +++ b/ast/drop_statements.go @@ -81,14 +81,22 @@ type DropIndexOption interface { // OnlineIndexOption represents the ONLINE option type OnlineIndexOption struct { - OptionState string // On, Off - OptionKind string // Online + LowPriorityLockWaitOption *OnlineIndexLowPriorityLockWaitOption // For ONLINE = ON (WAIT_AT_LOW_PRIORITY (...)) + OptionState string // On, Off + OptionKind string // Online } func (o *OnlineIndexOption) node() {} func (o *OnlineIndexOption) dropIndexOption() {} func (o *OnlineIndexOption) indexOption() {} +// OnlineIndexLowPriorityLockWaitOption represents WAIT_AT_LOW_PRIORITY options for ONLINE = ON +type OnlineIndexLowPriorityLockWaitOption struct { + Options []LowPriorityLockWaitOption +} + +func (o *OnlineIndexLowPriorityLockWaitOption) node() {} + // MoveToDropIndexOption represents the MOVE TO option type MoveToDropIndexOption struct { MoveTo *FileGroupOrPartitionScheme diff --git a/ast/fulltext_stoplist_statement.go b/ast/fulltext_stoplist_statement.go index 1a77dd7d..cb58ac06 100644 --- a/ast/fulltext_stoplist_statement.go +++ b/ast/fulltext_stoplist_statement.go @@ -42,7 +42,8 @@ func (s *DropFullTextStopListStatement) statement() {} // DropFullTextCatalogStatement represents DROP FULLTEXT CATALOG statement type DropFullTextCatalogStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` + IsIfExists bool `json:"IsIfExists"` } func (s *DropFullTextCatalogStatement) node() {} diff --git a/ast/function_call.go b/ast/function_call.go index ff3cd9fc..c5bbca47 100644 --- a/ast/function_call.go +++ b/ast/function_call.go @@ -28,10 +28,29 @@ func (*UserDefinedTypeCallTarget) callTarget() {} // OverClause represents an OVER clause for window functions. type OverClause struct { - Partitions []ScalarExpression `json:"Partitions,omitempty"` - OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` + WindowName *Identifier `json:"WindowName,omitempty"` + Partitions []ScalarExpression `json:"Partitions,omitempty"` + OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` + WindowFrameClause *WindowFrameClause `json:"WindowFrameClause,omitempty"` } +// WindowFrameClause represents ROWS/RANGE frame specification in OVER clause +type WindowFrameClause struct { + WindowFrameType string // "Rows", "Range" + Top *WindowDelimiter // Top boundary + Bottom *WindowDelimiter // Bottom boundary (for BETWEEN) +} + +func (w *WindowFrameClause) node() {} + +// WindowDelimiter represents window frame boundary +type WindowDelimiter struct { + WindowDelimiterType string // "CurrentRow", "UnboundedPreceding", "UnboundedFollowing", "ValuePreceding", "ValueFollowing" + OffsetValue ScalarExpression // For ValuePreceding/ValueFollowing +} + +func (w *WindowDelimiter) node() {} + // WithinGroupClause represents a WITHIN GROUP clause for ordered set aggregate functions. type WithinGroupClause struct { OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` @@ -50,6 +69,7 @@ type FunctionCall struct { OverClause *OverClause `json:"OverClause,omitempty"` IgnoreRespectNulls []*Identifier `json:"IgnoreRespectNulls,omitempty"` WithArrayWrapper bool `json:"WithArrayWrapper,omitempty"` + TrimOptions *Identifier `json:"TrimOptions,omitempty"` // For TRIM(LEADING/TRAILING/BOTH chars FROM string) Collation *Identifier `json:"Collation,omitempty"` } diff --git a/ast/global_function_table_reference.go b/ast/global_function_table_reference.go new file mode 100644 index 00000000..7ea30858 --- /dev/null +++ b/ast/global_function_table_reference.go @@ -0,0 +1,13 @@ +package ast + +// GlobalFunctionTableReference represents a built-in function used as a table source (e.g., STRING_SPLIT, OPENJSON) +type GlobalFunctionTableReference struct { + Name *Identifier `json:"Name,omitempty"` + Parameters []ScalarExpression `json:"Parameters,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` // For column list in AS alias(c1, c2, ...) + ForPath bool `json:"ForPath"` +} + +func (g *GlobalFunctionTableReference) node() {} +func (g *GlobalFunctionTableReference) tableReference() {} diff --git a/ast/inline_derived_table.go b/ast/inline_derived_table.go new file mode 100644 index 00000000..0a1a6250 --- /dev/null +++ b/ast/inline_derived_table.go @@ -0,0 +1,13 @@ +package ast + +// InlineDerivedTable represents a VALUES clause used as a table reference +// Example: (VALUES ('a'), ('b')) AS x(col) +type InlineDerivedTable struct { + RowValues []*RowValue `json:"RowValues,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` + ForPath bool `json:"ForPath"` +} + +func (t *InlineDerivedTable) node() {} +func (t *InlineDerivedTable) tableReference() {} diff --git a/ast/insert_statement.go b/ast/insert_statement.go index 8c9c55c9..4a6c3215 100644 --- a/ast/insert_statement.go +++ b/ast/insert_statement.go @@ -21,6 +21,9 @@ type InsertSpecification struct { OutputIntoClause *OutputIntoClause `json:"OutputIntoClause,omitempty"` } +func (i *InsertSpecification) node() {} +func (i *InsertSpecification) dataModificationSpecification() {} + // OutputClause represents an OUTPUT clause. type OutputClause struct { SelectColumns []SelectElement `json:"SelectColumns,omitempty"` diff --git a/ast/merge_statement.go b/ast/merge_statement.go index 4abb8c52..5873d991 100644 --- a/ast/merge_statement.go +++ b/ast/merge_statement.go @@ -18,7 +18,8 @@ type MergeSpecification struct { OutputClause *OutputClause } -func (s *MergeSpecification) node() {} +func (s *MergeSpecification) node() {} +func (s *MergeSpecification) dataModificationSpecification() {} // MergeActionClause represents a WHEN clause in a MERGE statement type MergeActionClause struct { diff --git a/ast/openrowset.go b/ast/openrowset.go new file mode 100644 index 00000000..23434300 --- /dev/null +++ b/ast/openrowset.go @@ -0,0 +1,46 @@ +package ast + +// OpenRowsetCosmos represents an OPENROWSET with PROVIDER = ..., CONNECTION = ..., OBJECT = ... syntax. +type OpenRowsetCosmos struct { + Options []OpenRowsetCosmosOption `json:"Options,omitempty"` + WithColumns []*OpenRowsetColumnDefinition `json:"WithColumns,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + ForPath bool `json:"ForPath"` +} + +func (o *OpenRowsetCosmos) node() {} +func (o *OpenRowsetCosmos) tableReference() {} + +// OpenRowsetCosmosOption is the interface for OpenRowset Cosmos options. +type OpenRowsetCosmosOption interface { + openRowsetCosmosOption() +} + +// LiteralOpenRowsetCosmosOption represents an option with a literal value. +type LiteralOpenRowsetCosmosOption struct { + Value ScalarExpression `json:"Value,omitempty"` + OptionKind string `json:"OptionKind,omitempty"` +} + +func (l *LiteralOpenRowsetCosmosOption) openRowsetCosmosOption() {} + +// OpenRowsetTableReference represents a traditional OPENROWSET('provider', 'connstr', object) syntax. +type OpenRowsetTableReference struct { + ProviderName ScalarExpression `json:"ProviderName,omitempty"` + ProviderString ScalarExpression `json:"ProviderString,omitempty"` + Object *SchemaObjectName `json:"Object,omitempty"` + WithColumns []*OpenRowsetColumnDefinition `json:"WithColumns,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + ForPath bool `json:"ForPath"` +} + +func (o *OpenRowsetTableReference) node() {} +func (o *OpenRowsetTableReference) tableReference() {} + +// OpenRowsetColumnDefinition represents a column definition in WITH clause. +type OpenRowsetColumnDefinition struct { + ColumnOrdinal ScalarExpression `json:"ColumnOrdinal,omitempty"` + ColumnIdentifier *Identifier `json:"ColumnIdentifier,omitempty"` + DataType DataTypeReference `json:"DataType,omitempty"` + Collation *Identifier `json:"Collation,omitempty"` +} diff --git a/ast/pivoted_table_reference.go b/ast/pivoted_table_reference.go new file mode 100644 index 00000000..03eb723e --- /dev/null +++ b/ast/pivoted_table_reference.go @@ -0,0 +1,29 @@ +package ast + +// PivotedTableReference represents a table with PIVOT +type PivotedTableReference struct { + TableReference TableReference + InColumns []*Identifier + PivotColumn *ColumnReferenceExpression + ValueColumns []*ColumnReferenceExpression + AggregateFunctionIdentifier *MultiPartIdentifier + Alias *Identifier + ForPath bool +} + +func (p *PivotedTableReference) node() {} +func (p *PivotedTableReference) tableReference() {} + +// UnpivotedTableReference represents a table with UNPIVOT +type UnpivotedTableReference struct { + TableReference TableReference + InColumns []*ColumnReferenceExpression + PivotColumn *Identifier + PivotValue *Identifier + NullHandling string // "None", "ExcludeNulls", "IncludeNulls" + Alias *Identifier + ForPath bool +} + +func (u *UnpivotedTableReference) node() {} +func (u *UnpivotedTableReference) tableReference() {} diff --git a/ast/query_specification.go b/ast/query_specification.go index 531e18c2..80eeafdf 100644 --- a/ast/query_specification.go +++ b/ast/query_specification.go @@ -9,6 +9,7 @@ type QuerySpecification struct { WhereClause *WhereClause `json:"WhereClause,omitempty"` GroupByClause *GroupByClause `json:"GroupByClause,omitempty"` HavingClause *HavingClause `json:"HavingClause,omitempty"` + WindowClause *WindowClause `json:"WindowClause,omitempty"` OrderByClause *OrderByClause `json:"OrderByClause,omitempty"` OffsetClause *OffsetClause `json:"OffsetClause,omitempty"` ForClause ForClause `json:"ForClause,omitempty"` diff --git a/ast/resource_pool_statement.go b/ast/resource_pool_statement.go index 4abd54bd..38cd0be5 100644 --- a/ast/resource_pool_statement.go +++ b/ast/resource_pool_statement.go @@ -47,9 +47,18 @@ type LiteralRange struct { To ScalarExpression `json:"To,omitempty"` } +// CreateExternalResourcePoolStatement represents a CREATE EXTERNAL RESOURCE POOL statement +type CreateExternalResourcePoolStatement struct { + Name *Identifier `json:"Name,omitempty"` + ExternalResourcePoolParameters []*ExternalResourcePoolParameter `json:"ExternalResourcePoolParameters,omitempty"` +} + +func (*CreateExternalResourcePoolStatement) node() {} +func (*CreateExternalResourcePoolStatement) statement() {} + // AlterExternalResourcePoolStatement represents an ALTER EXTERNAL RESOURCE POOL statement type AlterExternalResourcePoolStatement struct { - Name *Identifier `json:"Name,omitempty"` + Name *Identifier `json:"Name,omitempty"` ExternalResourcePoolParameters []*ExternalResourcePoolParameter `json:"ExternalResourcePoolParameters,omitempty"` } diff --git a/ast/restore_statement.go b/ast/restore_statement.go index 6e5e1795..fc844261 100644 --- a/ast/restore_statement.go +++ b/ast/restore_statement.go @@ -49,9 +49,9 @@ func (o *GeneralSetCommandRestoreOption) restoreOptionNode() {} // MoveRestoreOption represents a MOVE restore option type MoveRestoreOption struct { - OptionKind string - LogicalFileName *IdentifierOrValueExpression - OSFileName *IdentifierOrValueExpression + OptionKind string + LogicalFileName ScalarExpression + OSFileName ScalarExpression } func (o *MoveRestoreOption) restoreOptionNode() {} diff --git a/ast/schema_object_function_table_reference.go b/ast/schema_object_function_table_reference.go index 3a35fc31..59f0e0e2 100644 --- a/ast/schema_object_function_table_reference.go +++ b/ast/schema_object_function_table_reference.go @@ -4,6 +4,8 @@ package ast type SchemaObjectFunctionTableReference struct { SchemaObject *SchemaObjectName `json:"SchemaObject,omitempty"` Parameters []ScalarExpression `json:"Parameters,omitempty"` + Alias *Identifier `json:"Alias,omitempty"` + Columns []*Identifier `json:"Columns,omitempty"` // For column list in AS alias(c1, c2, ...) ForPath bool `json:"ForPath"` } diff --git a/ast/server_audit_statement.go b/ast/server_audit_statement.go index 175c6d49..44363235 100644 --- a/ast/server_audit_statement.go +++ b/ast/server_audit_statement.go @@ -205,3 +205,30 @@ type AuditActionGroupReference struct { func (r *AuditActionGroupReference) node() {} func (r *AuditActionGroupReference) auditSpecificationDetail() {} + +// AuditActionSpecification represents an action specification in audit parts +// Example: (select, INSERT, update ON t1 BY dbo) +type AuditActionSpecification struct { + Actions []*DatabaseAuditAction + Principals []*SecurityPrincipal + TargetObject *SecurityTargetObject +} + +func (a *AuditActionSpecification) node() {} +func (a *AuditActionSpecification) auditSpecificationDetail() {} + +// DatabaseAuditAction represents a database audit action +type DatabaseAuditAction struct { + ActionKind string // Select, Insert, Update, Delete, Execute, Receive, References +} + +func (a *DatabaseAuditAction) node() {} + +// DropDatabaseAuditSpecificationStatement represents DROP DATABASE AUDIT SPECIFICATION +type DropDatabaseAuditSpecificationStatement struct { + Name *Identifier + IsIfExists bool +} + +func (s *DropDatabaseAuditSpecificationStatement) statement() {} +func (s *DropDatabaseAuditSpecificationStatement) node() {} diff --git a/ast/table_distribution_option.go b/ast/table_distribution_option.go new file mode 100644 index 00000000..17b47ee8 --- /dev/null +++ b/ast/table_distribution_option.go @@ -0,0 +1,18 @@ +package ast + +// TableDistributionOption represents DISTRIBUTION option for tables +type TableDistributionOption struct { + Value *TableHashDistributionPolicy + OptionKind string // "Distribution" +} + +func (t *TableDistributionOption) node() {} +func (t *TableDistributionOption) tableOption() {} + +// TableHashDistributionPolicy represents HASH distribution for tables +type TableHashDistributionPolicy struct { + DistributionColumn *Identifier + DistributionColumns []*Identifier +} + +func (t *TableHashDistributionPolicy) node() {} diff --git a/ast/table_hint.go b/ast/table_hint.go index 181ec97f..fcf44ffa 100644 --- a/ast/table_hint.go +++ b/ast/table_hint.go @@ -27,3 +27,12 @@ type LiteralTableHint struct { } func (*LiteralTableHint) tableHint() {} + +// ForceSeekTableHint represents FORCESEEK table hint with optional index and column list. +type ForceSeekTableHint struct { + HintKind string `json:"HintKind,omitempty"` + IndexValue *IdentifierOrValueExpression `json:"IndexValue,omitempty"` + ColumnValues []*ColumnReferenceExpression `json:"ColumnValues,omitempty"` +} + +func (*ForceSeekTableHint) tableHint() {} diff --git a/ast/table_index_option.go b/ast/table_index_option.go new file mode 100644 index 00000000..81104d51 --- /dev/null +++ b/ast/table_index_option.go @@ -0,0 +1,31 @@ +package ast + +// TableIndexOption represents a table index option in CREATE TABLE WITH +type TableIndexOption struct { + Value TableIndexType + OptionKind string // "LockEscalation" (incorrect but matches expected output) +} + +func (t *TableIndexOption) node() {} +func (t *TableIndexOption) tableOption() {} + +// TableIndexType is an interface for different table index types +type TableIndexType interface { + Node + tableIndexType() +} + +// TableClusteredIndexType represents a clustered index type +type TableClusteredIndexType struct { + Columns []*ColumnWithSortOrder + ColumnStore bool +} + +func (t *TableClusteredIndexType) node() {} +func (t *TableClusteredIndexType) tableIndexType() {} + +// TableNonClusteredIndexType represents HEAP (non-clustered) +type TableNonClusteredIndexType struct{} + +func (t *TableNonClusteredIndexType) node() {} +func (t *TableNonClusteredIndexType) tableIndexType() {} diff --git a/ast/update_statement.go b/ast/update_statement.go index 0935bbc6..27649699 100644 --- a/ast/update_statement.go +++ b/ast/update_statement.go @@ -21,6 +21,9 @@ type UpdateSpecification struct { OutputIntoClause *OutputIntoClause `json:"OutputIntoClause,omitempty"` } +func (u *UpdateSpecification) node() {} +func (u *UpdateSpecification) dataModificationSpecification() {} + // SetClause is an interface for SET clauses. type SetClause interface { setClause() diff --git a/ast/window_clause.go b/ast/window_clause.go new file mode 100644 index 00000000..ba059700 --- /dev/null +++ b/ast/window_clause.go @@ -0,0 +1,18 @@ +package ast + +// WindowClause represents a WINDOW clause in SELECT statement +type WindowClause struct { + WindowDefinition []*WindowDefinition +} + +func (w *WindowClause) node() {} + +// WindowDefinition represents a single window definition (WindowName AS (...)) +type WindowDefinition struct { + WindowName *Identifier // The name of this window + RefWindowName *Identifier // Reference to another window name (optional) + Partitions []ScalarExpression // PARTITION BY expressions + OrderByClause *OrderByClause // ORDER BY clause +} + +func (w *WindowDefinition) node() {} diff --git a/ast/xml_compression_option.go b/ast/xml_compression_option.go new file mode 100644 index 00000000..391ba8eb --- /dev/null +++ b/ast/xml_compression_option.go @@ -0,0 +1,21 @@ +package ast + +// XmlCompressionOption represents an XML compression option +type XmlCompressionOption struct { + IsCompressed string // "On", "Off" + PartitionRanges []*CompressionPartitionRange + OptionKind string // "XmlCompression" +} + +func (x *XmlCompressionOption) node() {} +func (x *XmlCompressionOption) tableOption() {} +func (x *XmlCompressionOption) indexOption() {} + +// TableXmlCompressionOption represents a table-level XML compression option +type TableXmlCompressionOption struct { + XmlCompressionOption *XmlCompressionOption + OptionKind string // "XmlCompression" +} + +func (t *TableXmlCompressionOption) node() {} +func (t *TableXmlCompressionOption) tableOption() {} diff --git a/parser/marshal.go b/parser/marshal.go index 33e96dcf..4726c3ea 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -160,6 +160,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropResourcePoolStatementToJSON(s) case *ast.AlterExternalResourcePoolStatement: return alterExternalResourcePoolStatementToJSON(s) + case *ast.CreateExternalResourcePoolStatement: + return createExternalResourcePoolStatementToJSON(s) case *ast.CreateCryptographicProviderStatement: return createCryptographicProviderStatementToJSON(s) case *ast.CreateColumnMasterKeyStatement: @@ -198,6 +200,8 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropServerRoleStatementToJSON(s) case *ast.DropServerAuditStatement: return dropServerAuditStatementToJSON(s) + case *ast.DropDatabaseAuditSpecificationStatement: + return dropDatabaseAuditSpecificationStatementToJSON(s) case *ast.DropAvailabilityGroupStatement: return dropAvailabilityGroupStatementToJSON(s) case *ast.DropFederationStatement: @@ -444,6 +448,12 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropDatabaseEncryptionKeyStatementToJSON(s) case *ast.CreateLoginStatement: return createLoginStatementToJSON(s) + case *ast.AlterLoginEnableDisableStatement: + return alterLoginEnableDisableStatementToJSON(s) + case *ast.AlterLoginOptionsStatement: + return alterLoginOptionsStatementToJSON(s) + case *ast.DropLoginStatement: + return dropLoginStatementToJSON(s) case *ast.CreateIndexStatement: return createIndexStatementToJSON(s) case *ast.CreateSpatialIndexStatement: @@ -514,6 +524,34 @@ func statementToJSON(stmt ast.Statement) jsonNode { return dropRuleStatementToJSON(s) case *ast.DropSchemaStatement: return dropSchemaStatementToJSON(s) + case *ast.DropPartitionFunctionStatement: + return dropPartitionFunctionStatementToJSON(s) + case *ast.DropPartitionSchemeStatement: + return dropPartitionSchemeStatementToJSON(s) + case *ast.DropApplicationRoleStatement: + return dropApplicationRoleStatementToJSON(s) + case *ast.DropCertificateStatement: + return dropCertificateStatementToJSON(s) + case *ast.DropMasterKeyStatement: + return dropMasterKeyStatementToJSON(s) + case *ast.DropXmlSchemaCollectionStatement: + return dropXmlSchemaCollectionStatementToJSON(s) + case *ast.DropContractStatement: + return dropContractStatementToJSON(s) + case *ast.DropEndpointStatement: + return dropEndpointStatementToJSON(s) + case *ast.DropMessageTypeStatement: + return dropMessageTypeStatementToJSON(s) + case *ast.DropQueueStatement: + return dropQueueStatementToJSON(s) + case *ast.DropRemoteServiceBindingStatement: + return dropRemoteServiceBindingStatementToJSON(s) + case *ast.DropRouteStatement: + return dropRouteStatementToJSON(s) + case *ast.DropServiceStatement: + return dropServiceStatementToJSON(s) + case *ast.DropEventNotificationStatement: + return dropEventNotificationStatementToJSON(s) case *ast.AlterTableTriggerModificationStatement: return alterTableTriggerModificationStatementToJSON(s) case *ast.AlterTableFileTableNamespaceStatement: @@ -568,6 +606,10 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterFullTextStopListStatementToJSON(s) case *ast.DropFullTextStopListStatement: return dropFullTextStopListStatementToJSON(s) + case *ast.DropFullTextCatalogStatement: + return dropFullTextCatalogStatementToJSON(s) + case *ast.DropFulltextIndexStatement: + return dropFulltextIndexStatementToJSON(s) case *ast.AlterSymmetricKeyStatement: return alterSymmetricKeyStatementToJSON(s) case *ast.AlterServiceMasterKeyStatement: @@ -811,6 +853,20 @@ func lowPriorityLockWaitOptionToJSON(o ast.LowPriorityLockWaitOption) jsonNode { } } +func onlineIndexLowPriorityLockWaitOptionToJSON(o *ast.OnlineIndexLowPriorityLockWaitOption) jsonNode { + node := jsonNode{ + "$type": "OnlineIndexLowPriorityLockWaitOption", + } + if len(o.Options) > 0 { + options := make([]jsonNode, len(o.Options)) + for i, opt := range o.Options { + options[i] = lowPriorityLockWaitOptionToJSON(opt) + } + node["Options"] = options + } + return node +} + func fileGroupOrPartitionSchemeToJSON(fg *ast.FileGroupOrPartitionScheme) jsonNode { node := jsonNode{ "$type": "FileGroupOrPartitionScheme", @@ -1584,6 +1640,9 @@ func querySpecificationToJSON(q *ast.QuerySpecification) jsonNode { if q.HavingClause != nil { node["HavingClause"] = havingClauseToJSON(q.HavingClause) } + if q.WindowClause != nil { + node["WindowClause"] = windowClauseToJSON(q.WindowClause) + } if q.OrderByClause != nil { node["OrderByClause"] = orderByClauseToJSON(q.OrderByClause) } @@ -1830,6 +1889,9 @@ func scalarExpressionToJSON(expr ast.ScalarExpression) jsonNode { node["IgnoreRespectNulls"] = idents } node["WithArrayWrapper"] = e.WithArrayWrapper + if e.TrimOptions != nil { + node["TrimOptions"] = identifierToJSON(e.TrimOptions) + } if e.Collation != nil { node["Collation"] = identifierToJSON(e.Collation) } @@ -2348,6 +2410,151 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode { } node["Parameters"] = params } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + node["ForPath"] = r.ForPath + return node + case *ast.GlobalFunctionTableReference: + node := jsonNode{ + "$type": "GlobalFunctionTableReference", + } + if r.Name != nil { + node["Name"] = identifierToJSON(r.Name) + } + if len(r.Parameters) > 0 { + params := make([]jsonNode, len(r.Parameters)) + for i, p := range r.Parameters { + params[i] = scalarExpressionToJSON(p) + } + node["Parameters"] = params + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + node["ForPath"] = r.ForPath + return node + case *ast.InlineDerivedTable: + node := jsonNode{ + "$type": "InlineDerivedTable", + } + if len(r.RowValues) > 0 { + rows := make([]jsonNode, len(r.RowValues)) + for i, row := range r.RowValues { + rowNode := jsonNode{ + "$type": "RowValue", + } + if len(row.ColumnValues) > 0 { + vals := make([]jsonNode, len(row.ColumnValues)) + for j, v := range row.ColumnValues { + vals[j] = scalarExpressionToJSON(v) + } + rowNode["ColumnValues"] = vals + } + rows[i] = rowNode + } + node["RowValues"] = rows + } + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node + case *ast.DataModificationTableReference: + node := jsonNode{ + "$type": "DataModificationTableReference", + } + if r.DataModificationSpecification != nil { + node["DataModificationSpecification"] = dataModificationSpecificationToJSON(r.DataModificationSpecification) + } + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node + case *ast.ChangeTableChangesTableReference: + node := jsonNode{ + "$type": "ChangeTableChangesTableReference", + } + if r.Target != nil { + node["Target"] = schemaObjectNameToJSON(r.Target) + } + if r.SinceVersion != nil { + node["SinceVersion"] = scalarExpressionToJSON(r.SinceVersion) + } + node["ForceSeek"] = r.ForceSeek + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node + case *ast.ChangeTableVersionTableReference: + node := jsonNode{ + "$type": "ChangeTableVersionTableReference", + } + if r.Target != nil { + node["Target"] = schemaObjectNameToJSON(r.Target) + } + if len(r.PrimaryKeyColumns) > 0 { + cols := make([]jsonNode, len(r.PrimaryKeyColumns)) + for i, c := range r.PrimaryKeyColumns { + cols[i] = identifierToJSON(c) + } + node["PrimaryKeyColumns"] = cols + } + if len(r.PrimaryKeyValues) > 0 { + vals := make([]jsonNode, len(r.PrimaryKeyValues)) + for i, v := range r.PrimaryKeyValues { + vals[i] = scalarExpressionToJSON(v) + } + node["PrimaryKeyValues"] = vals + } + node["ForceSeek"] = r.ForceSeek + if len(r.Columns) > 0 { + cols := make([]jsonNode, len(r.Columns)) + for i, c := range r.Columns { + cols[i] = identifierToJSON(c) + } + node["Columns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } node["ForPath"] = r.ForPath return node case *ast.InternalOpenRowset: @@ -2384,6 +2591,13 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode { } node["Options"] = opts } + if len(r.WithColumns) > 0 { + cols := make([]jsonNode, len(r.WithColumns)) + for i, c := range r.WithColumns { + cols[i] = openRowsetColumnDefinitionToJSON(c) + } + node["WithColumns"] = cols + } if len(r.Columns) > 0 { cols := make([]jsonNode, len(r.Columns)) for i, c := range r.Columns { @@ -2396,6 +2610,54 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode { } node["ForPath"] = r.ForPath return node + case *ast.OpenRowsetCosmos: + node := jsonNode{ + "$type": "OpenRowsetCosmos", + } + if len(r.Options) > 0 { + opts := make([]jsonNode, len(r.Options)) + for i, o := range r.Options { + opts[i] = openRowsetCosmosOptionToJSON(o) + } + node["Options"] = opts + } + if len(r.WithColumns) > 0 { + cols := make([]jsonNode, len(r.WithColumns)) + for i, c := range r.WithColumns { + cols[i] = openRowsetColumnDefinitionToJSON(c) + } + node["WithColumns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node + case *ast.OpenRowsetTableReference: + node := jsonNode{ + "$type": "OpenRowsetTableReference", + } + if r.ProviderName != nil { + node["ProviderName"] = scalarExpressionToJSON(r.ProviderName) + } + if r.ProviderString != nil { + node["ProviderString"] = scalarExpressionToJSON(r.ProviderString) + } + if r.Object != nil { + node["Object"] = schemaObjectNameToJSON(r.Object) + } + if len(r.WithColumns) > 0 { + cols := make([]jsonNode, len(r.WithColumns)) + for i, c := range r.WithColumns { + cols[i] = openRowsetColumnDefinitionToJSON(c) + } + node["WithColumns"] = cols + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node case *ast.PredictTableReference: node := jsonNode{ "$type": "PredictTableReference", @@ -2429,6 +2691,66 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode { node["Join"] = tableReferenceToJSON(r.Join) } return node + case *ast.PivotedTableReference: + node := jsonNode{ + "$type": "PivotedTableReference", + } + if r.TableReference != nil { + node["TableReference"] = tableReferenceToJSON(r.TableReference) + } + if len(r.InColumns) > 0 { + cols := make([]jsonNode, len(r.InColumns)) + for i, col := range r.InColumns { + cols[i] = identifierToJSON(col) + } + node["InColumns"] = cols + } + if r.PivotColumn != nil { + node["PivotColumn"] = columnReferenceExpressionToJSON(r.PivotColumn) + } + if len(r.ValueColumns) > 0 { + cols := make([]jsonNode, len(r.ValueColumns)) + for i, col := range r.ValueColumns { + cols[i] = columnReferenceExpressionToJSON(col) + } + node["ValueColumns"] = cols + } + if r.AggregateFunctionIdentifier != nil { + node["AggregateFunctionIdentifier"] = multiPartIdentifierToJSON(r.AggregateFunctionIdentifier) + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node + case *ast.UnpivotedTableReference: + node := jsonNode{ + "$type": "UnpivotedTableReference", + } + if r.TableReference != nil { + node["TableReference"] = tableReferenceToJSON(r.TableReference) + } + if len(r.InColumns) > 0 { + cols := make([]jsonNode, len(r.InColumns)) + for i, col := range r.InColumns { + cols[i] = columnReferenceExpressionToJSON(col) + } + node["InColumns"] = cols + } + if r.PivotColumn != nil { + node["PivotColumn"] = identifierToJSON(r.PivotColumn) + } + if r.PivotValue != nil { + node["PivotValue"] = identifierToJSON(r.PivotValue) + } + if r.NullHandling != "" && r.NullHandling != "None" { + node["NullHandling"] = r.NullHandling + } + if r.Alias != nil { + node["Alias"] = identifierToJSON(r.Alias) + } + node["ForPath"] = r.ForPath + return node case *ast.QueryDerivedTable: node := jsonNode{ "$type": "QueryDerivedTable", @@ -2880,6 +3202,43 @@ func havingClauseToJSON(hc *ast.HavingClause) jsonNode { return node } +func windowClauseToJSON(wc *ast.WindowClause) jsonNode { + node := jsonNode{ + "$type": "WindowClause", + } + if len(wc.WindowDefinition) > 0 { + defs := make([]jsonNode, len(wc.WindowDefinition)) + for i, def := range wc.WindowDefinition { + defs[i] = windowDefinitionToJSON(def) + } + node["WindowDefinition"] = defs + } + return node +} + +func windowDefinitionToJSON(wd *ast.WindowDefinition) jsonNode { + node := jsonNode{ + "$type": "WindowDefinition", + } + if wd.WindowName != nil { + node["WindowName"] = identifierToJSON(wd.WindowName) + } + if wd.RefWindowName != nil { + node["RefWindowName"] = identifierToJSON(wd.RefWindowName) + } + if len(wd.Partitions) > 0 { + partitions := make([]jsonNode, len(wd.Partitions)) + for i, p := range wd.Partitions { + partitions[i] = scalarExpressionToJSON(p) + } + node["Partitions"] = partitions + } + if wd.OrderByClause != nil { + node["OrderByClause"] = orderByClauseToJSON(wd.OrderByClause) + } + return node +} + func orderByClauseToJSON(obc *ast.OrderByClause) jsonNode { node := jsonNode{ "$type": "OrderByClause", @@ -2922,6 +3281,9 @@ func overClauseToJSON(oc *ast.OverClause) jsonNode { node := jsonNode{ "$type": "OverClause", } + if oc.WindowName != nil { + node["WindowName"] = identifierToJSON(oc.WindowName) + } if len(oc.Partitions) > 0 { partitions := make([]jsonNode, len(oc.Partitions)) for i, p := range oc.Partitions { @@ -2932,6 +3294,34 @@ func overClauseToJSON(oc *ast.OverClause) jsonNode { if oc.OrderByClause != nil { node["OrderByClause"] = orderByClauseToJSON(oc.OrderByClause) } + if oc.WindowFrameClause != nil { + node["WindowFrameClause"] = windowFrameClauseToJSON(oc.WindowFrameClause) + } + return node +} + +func windowFrameClauseToJSON(wfc *ast.WindowFrameClause) jsonNode { + node := jsonNode{ + "$type": "WindowFrameClause", + "WindowFrameType": wfc.WindowFrameType, + } + if wfc.Top != nil { + node["Top"] = windowDelimiterToJSON(wfc.Top) + } + if wfc.Bottom != nil { + node["Bottom"] = windowDelimiterToJSON(wfc.Bottom) + } + return node +} + +func windowDelimiterToJSON(wd *ast.WindowDelimiter) jsonNode { + node := jsonNode{ + "$type": "WindowDelimiter", + "WindowDelimiterType": wd.WindowDelimiterType, + } + if wd.OffsetValue != nil { + node["OffsetValue"] = scalarExpressionToJSON(wd.OffsetValue) + } return node } @@ -2973,6 +3363,24 @@ func tableHintToJSON(h ast.TableHintType) jsonNode { node["HintKind"] = th.HintKind } return node + case *ast.ForceSeekTableHint: + node := jsonNode{ + "$type": "ForceSeekTableHint", + } + if th.IndexValue != nil { + node["IndexValue"] = identifierOrValueExpressionToJSON(th.IndexValue) + } + if len(th.ColumnValues) > 0 { + cols := make([]jsonNode, len(th.ColumnValues)) + for i, c := range th.ColumnValues { + cols[i] = columnReferenceExpressionToJSON(c) + } + node["ColumnValues"] = cols + } + if th.HintKind != "" { + node["HintKind"] = th.HintKind + } + return node default: return jsonNode{"$type": "TableHint"} } @@ -2998,6 +3406,21 @@ func insertStatementToJSON(s *ast.InsertStatement) jsonNode { return node } +func dataModificationSpecificationToJSON(spec ast.DataModificationSpecification) jsonNode { + switch s := spec.(type) { + case *ast.InsertSpecification: + return insertSpecificationToJSON(s) + case *ast.UpdateSpecification: + return updateSpecificationToJSON(s) + case *ast.DeleteSpecification: + return deleteSpecificationToJSON(s) + case *ast.MergeSpecification: + return mergeSpecificationToJSON(s) + default: + return jsonNode{"$type": "UnknownDataModificationSpecification"} + } +} + func insertSpecificationToJSON(spec *ast.InsertSpecification) jsonNode { node := jsonNode{ "$type": "InsertSpecification", @@ -4283,6 +4706,18 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) DataCompressionOption: opt, OptionKind: "DataCompression", }) + } else if optionName == "XML_COMPRESSION" { + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + opt, err := p.parseXmlCompressionOption() + if err != nil { + break + } + stmt.Options = append(stmt.Options, &ast.TableXmlCompressionOption{ + XmlCompressionOption: opt, + OptionKind: "XmlCompression", + }) } else if optionName == "MEMORY_OPTIMIZED" { if p.curTok.Type == TokenEquals { p.nextToken() // consume = @@ -4317,6 +4752,95 @@ func (p *Parser) parseCreateTableStatement() (*ast.CreateTableStatement, error) return nil, err } stmt.Options = append(stmt.Options, opt) + } else if optionName == "CLUSTERED" { + // Could be CLUSTERED INDEX or CLUSTERED COLUMNSTORE INDEX + if strings.ToUpper(p.curTok.Literal) == "COLUMNSTORE" { + p.nextToken() // consume COLUMNSTORE + if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + } + stmt.Options = append(stmt.Options, &ast.TableIndexOption{ + Value: &ast.TableClusteredIndexType{ + ColumnStore: true, + }, + OptionKind: "LockEscalation", + }) + } else if p.curTok.Type == TokenIndex { + p.nextToken() // consume INDEX + // Parse column list + indexType := &ast.TableClusteredIndexType{ + ColumnStore: false, + } + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col := &ast.ColumnWithSortOrder{ + SortOrder: ast.SortOrderNotSpecified, + Column: &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{p.parseIdentifier()}, + Count: 1, + }, + }, + } + indexType.Columns = append(indexType.Columns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + stmt.Options = append(stmt.Options, &ast.TableIndexOption{ + Value: indexType, + OptionKind: "LockEscalation", + }) + } + } else if optionName == "HEAP" { + stmt.Options = append(stmt.Options, &ast.TableIndexOption{ + Value: &ast.TableNonClusteredIndexType{}, + OptionKind: "LockEscalation", + }) + } else if optionName == "DISTRIBUTION" { + // Parse DISTRIBUTION = HASH(col1, col2, ...) or ROUND_ROBIN or REPLICATE + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + distTypeUpper := strings.ToUpper(p.curTok.Literal) + if distTypeUpper == "HASH" { + p.nextToken() // consume HASH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + distOpt := &ast.TableDistributionOption{ + OptionKind: "Distribution", + Value: &ast.TableHashDistributionPolicy{}, + } + // Parse column list + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col := p.parseIdentifier() + if distOpt.Value.DistributionColumn == nil { + distOpt.Value.DistributionColumn = col + } + distOpt.Value.DistributionColumns = append(distOpt.Value.DistributionColumns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + stmt.Options = append(stmt.Options, distOpt) + } + } else { + // ROUND_ROBIN or REPLICATE - skip for now + p.nextToken() + } } else { // Skip unknown option value if p.curTok.Type == TokenEquals { @@ -4779,11 +5303,88 @@ func (p *Parser) parseMergeStatement() (*ast.MergeStatement, error) { return stmt, nil } -// parseMergeSourceTableReference parses the source table reference in a MERGE statement -func (p *Parser) parseMergeSourceTableReference() (ast.TableReference, error) { - // Check for parenthesized expression - if p.curTok.Type == TokenLParen { - // Check if this is a derived table (subquery) or a join +// parseMergeSpecification parses a MERGE specification (used in DataModificationTableReference) +func (p *Parser) parseMergeSpecification() (*ast.MergeSpecification, error) { + // Consume MERGE + p.nextToken() + + spec := &ast.MergeSpecification{} + + // Optional INTO keyword + if strings.ToUpper(p.curTok.Literal) == "INTO" { + p.nextToken() + } + + // Parse target table + target, err := p.parseSingleTableReference() + if err != nil { + return nil, err + } + // If target has an alias, move it to TableAlias (ScriptDOM convention) + if ntr, ok := target.(*ast.NamedTableReference); ok && ntr.Alias != nil { + spec.TableAlias = ntr.Alias + ntr.Alias = nil + } + spec.Target = target + + // Expect USING + if strings.ToUpper(p.curTok.Literal) == "USING" { + p.nextToken() + } + + // Parse source table reference (may be parenthesized join or subquery) + sourceRef, err := p.parseMergeSourceTableReference() + if err != nil { + return nil, err + } + spec.TableReference = sourceRef + + // Expect ON + if p.curTok.Type == TokenOn { + p.nextToken() + } + + // Parse ON condition - check for MATCH predicate + if strings.ToUpper(p.curTok.Literal) == "MATCH" { + matchPred, err := p.parseGraphMatchPredicate() + if err != nil { + return nil, err + } + spec.SearchCondition = matchPred + } else { + cond, err := p.parseBooleanExpression() + if err != nil { + return nil, err + } + spec.SearchCondition = cond + } + + // Parse WHEN clauses + for strings.ToUpper(p.curTok.Literal) == "WHEN" { + clause, err := p.parseMergeActionClause() + if err != nil { + return nil, err + } + spec.ActionClauses = append(spec.ActionClauses, clause) + } + + // Parse optional OUTPUT clause + if strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + output, _, err := p.parseOutputClause() + if err != nil { + return nil, err + } + spec.OutputClause = output + } + + return spec, nil +} + +// parseMergeSourceTableReference parses the source table reference in a MERGE statement +func (p *Parser) parseMergeSourceTableReference() (ast.TableReference, error) { + // Check for parenthesized expression + if p.curTok.Type == TokenLParen { + // Check if this is a derived table (subquery) or a join if p.peekTok.Type == TokenSelect { // This is a derived table like (SELECT ...) AS alias return p.parseDerivedTableReference() @@ -5156,6 +5757,70 @@ func (p *Parser) parseDataCompressionOption() (*ast.DataCompressionOption, error return opt, nil } +func (p *Parser) parseXmlCompressionOption() (*ast.XmlCompressionOption, error) { + opt := &ast.XmlCompressionOption{ + OptionKind: "XmlCompression", + } + + // Parse ON or OFF + levelStr := strings.ToUpper(p.curTok.Literal) + if levelStr == "ON" { + opt.IsCompressed = "On" + } else { + opt.IsCompressed = "Off" + } + 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{} @@ -7112,6 +7777,33 @@ func tableOptionToJSON(opt ast.TableOption) jsonNode { node["DataCompressionOption"] = dataCompressionOptionToJSON(o.DataCompressionOption) } return node + case *ast.TableXmlCompressionOption: + node := jsonNode{ + "$type": "TableXmlCompressionOption", + "OptionKind": o.OptionKind, + } + if o.XmlCompressionOption != nil { + node["XmlCompressionOption"] = xmlCompressionOptionToJSON(o.XmlCompressionOption) + } + return node + case *ast.TableIndexOption: + node := jsonNode{ + "$type": "TableIndexOption", + "OptionKind": o.OptionKind, + } + if o.Value != nil { + node["Value"] = tableIndexTypeToJSON(o.Value) + } + return node + case *ast.TableDistributionOption: + node := jsonNode{ + "$type": "TableDistributionOption", + "OptionKind": o.OptionKind, + } + if o.Value != nil { + node["Value"] = tableHashDistributionPolicyToJSON(o.Value) + } + return node case *ast.SystemVersioningTableOption: return systemVersioningTableOptionToJSON(o) case *ast.MemoryOptimizedTableOption: @@ -7213,6 +7905,68 @@ func dataCompressionOptionToJSON(opt *ast.DataCompressionOption) jsonNode { return node } +func xmlCompressionOptionToJSON(opt *ast.XmlCompressionOption) jsonNode { + node := jsonNode{ + "$type": "XmlCompressionOption", + "IsCompressed": opt.IsCompressed, + "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 tableHashDistributionPolicyToJSON(policy *ast.TableHashDistributionPolicy) jsonNode { + node := jsonNode{ + "$type": "TableHashDistributionPolicy", + } + if policy.DistributionColumn != nil { + node["DistributionColumn"] = identifierToJSON(policy.DistributionColumn) + } + if len(policy.DistributionColumns) > 0 { + cols := make([]jsonNode, len(policy.DistributionColumns)) + for i, c := range policy.DistributionColumns { + // First column is same as DistributionColumn, use $ref + if i == 0 && policy.DistributionColumn != nil { + cols[i] = jsonNode{"$ref": "Identifier"} + } else { + cols[i] = identifierToJSON(c) + } + } + node["DistributionColumns"] = cols + } + return node +} + +func tableIndexTypeToJSON(t ast.TableIndexType) jsonNode { + switch v := t.(type) { + case *ast.TableClusteredIndexType: + node := jsonNode{ + "$type": "TableClusteredIndexType", + "ColumnStore": v.ColumnStore, + } + if len(v.Columns) > 0 { + cols := make([]jsonNode, len(v.Columns)) + for i, c := range v.Columns { + cols[i] = columnWithSortOrderToJSON(c) + } + node["Columns"] = cols + } + return node + case *ast.TableNonClusteredIndexType: + return jsonNode{ + "$type": "TableNonClusteredIndexType", + } + default: + return jsonNode{"$type": "UnknownTableIndexType"} + } +} + func compressionPartitionRangeToJSON(pr *ast.CompressionPartitionRange) jsonNode { node := jsonNode{ "$type": "CompressionPartitionRange", @@ -8630,6 +9384,38 @@ func auditSpecificationDetailToJSON(d ast.AuditSpecificationDetail) jsonNode { "$type": "AuditActionGroupReference", "Group": detail.Group, } + case *ast.AuditActionSpecification: + node := jsonNode{ + "$type": "AuditActionSpecification", + } + if len(detail.Actions) > 0 { + actions := make([]jsonNode, len(detail.Actions)) + for i, a := range detail.Actions { + actions[i] = jsonNode{ + "$type": "DatabaseAuditAction", + "ActionKind": a.ActionKind, + } + } + node["Actions"] = actions + } + if len(detail.Principals) > 0 { + principals := make([]jsonNode, len(detail.Principals)) + for i, p := range detail.Principals { + principalNode := jsonNode{ + "$type": "SecurityPrincipal", + "PrincipalType": p.PrincipalType, + } + if p.Identifier != nil { + principalNode["Identifier"] = identifierToJSON(p.Identifier) + } + principals[i] = principalNode + } + node["Principals"] = principals + } + if detail.TargetObject != nil { + node["TargetObject"] = securityTargetObjectToJSON(detail.TargetObject) + } + return node default: return jsonNode{} } @@ -8646,6 +9432,17 @@ func dropServerAuditStatementToJSON(s *ast.DropServerAuditStatement) jsonNode { return node } +func dropDatabaseAuditSpecificationStatementToJSON(s *ast.DropDatabaseAuditSpecificationStatement) jsonNode { + node := jsonNode{ + "$type": "DropDatabaseAuditSpecificationStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + func auditTargetToJSON(t *ast.AuditTarget) jsonNode { node := jsonNode{ "$type": "AuditTarget", @@ -9126,6 +9923,7 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) { } stmt := &ast.RestoreStatement{} + hasDatabaseName := true // Parse restore kind (DATABASE, LOG, etc.) switch strings.ToUpper(p.curTok.Literal) { @@ -9135,74 +9933,95 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) { case "LOG": stmt.Kind = "TransactionLog" p.nextToken() + case "FILELISTONLY": + stmt.Kind = "FileListOnly" + p.nextToken() + hasDatabaseName = false + case "VERIFYONLY": + stmt.Kind = "VerifyOnly" + p.nextToken() + hasDatabaseName = false + case "LABELONLY": + stmt.Kind = "LabelOnly" + p.nextToken() + hasDatabaseName = false + case "REWINDONLY": + stmt.Kind = "RewindOnly" + p.nextToken() + hasDatabaseName = false + case "HEADERONLY": + stmt.Kind = "HeaderOnly" + p.nextToken() + hasDatabaseName = false default: stmt.Kind = "Database" } - // Parse database name - dbName := &ast.IdentifierOrValueExpression{} - if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { - // Variable reference - varRef := &ast.VariableReference{Name: p.curTok.Literal} - p.nextToken() - dbName.Value = varRef.Name - dbName.ValueExpression = varRef - } else { - ident := p.parseIdentifier() - dbName.Value = ident.Value - dbName.Identifier = ident - } - stmt.DatabaseName = dbName - - // Parse optional FILE = or FILEGROUP = before FROM - for strings.ToUpper(p.curTok.Literal) == "FILE" || strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { - itemKind := "Files" - if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { - itemKind = "FileGroups" - } - p.nextToken() // consume FILE/FILEGROUP - if p.curTok.Type != TokenEquals { - return nil, fmt.Errorf("expected = after FILE/FILEGROUP, got %s", p.curTok.Literal) - } - p.nextToken() // consume = - - fileInfo := &ast.BackupRestoreFileInfo{ItemKind: itemKind} - // Parse the file name - var item ast.ScalarExpression - if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { - // Strip surrounding quotes - val := p.curTok.Literal - if len(val) >= 2 && ((val[0] == '\'' && val[len(val)-1] == '\'') || (val[0] == '"' && val[len(val)-1] == '"')) { - val = val[1 : len(val)-1] - } - item = &ast.StringLiteral{ - LiteralType: "String", - Value: val, - IsNational: p.curTok.Type == TokenNationalString, - IsLargeObject: false, - } - p.nextToken() - } else if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { - item = &ast.VariableReference{Name: p.curTok.Literal} + // Parse database name (only for DATABASE and LOG kinds) + if hasDatabaseName { + dbName := &ast.IdentifierOrValueExpression{} + if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + // Variable reference + varRef := &ast.VariableReference{Name: p.curTok.Literal} p.nextToken() + dbName.Value = varRef.Name + dbName.ValueExpression = varRef } else { ident := p.parseIdentifier() - item = &ast.ColumnReferenceExpression{ - ColumnType: "Regular", - MultiPartIdentifier: &ast.MultiPartIdentifier{ - Identifiers: []*ast.Identifier{ident}, - Count: 1, - }, - } + dbName.Value = ident.Value + dbName.Identifier = ident } - fileInfo.Items = append(fileInfo.Items, item) - stmt.Files = append(stmt.Files, fileInfo) + stmt.DatabaseName = dbName - if p.curTok.Type == TokenComma { - p.nextToken() + // Parse optional FILE = or FILEGROUP = before FROM + for strings.ToUpper(p.curTok.Literal) == "FILE" || strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { + itemKind := "Files" + if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" { + itemKind = "FileGroups" + } + p.nextToken() // consume FILE/FILEGROUP + if p.curTok.Type != TokenEquals { + return nil, fmt.Errorf("expected = after FILE/FILEGROUP, got %s", p.curTok.Literal) + } + p.nextToken() // consume = + + fileInfo := &ast.BackupRestoreFileInfo{ItemKind: itemKind} + // Parse the file name + var item ast.ScalarExpression + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + // Strip surrounding quotes + val := p.curTok.Literal + if len(val) >= 2 && ((val[0] == '\'' && val[len(val)-1] == '\'') || (val[0] == '"' && val[len(val)-1] == '"')) { + val = val[1 : len(val)-1] + } + item = &ast.StringLiteral{ + LiteralType: "String", + Value: val, + IsNational: p.curTok.Type == TokenNationalString, + IsLargeObject: false, + } + p.nextToken() + } else if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { + item = &ast.VariableReference{Name: p.curTok.Literal} + p.nextToken() + } else { + ident := p.parseIdentifier() + item = &ast.ColumnReferenceExpression{ + ColumnType: "Regular", + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{ident}, + Count: 1, + }, + } + } + fileInfo.Items = append(fileInfo.Items, item) + stmt.Files = append(stmt.Files, fileInfo) + + if p.curTok.Type == TokenComma { + p.nextToken() + } } } - // Check for optional FROM clause if strings.ToUpper(p.curTok.Literal) != "FROM" { // No FROM clause - check for WITH clause @@ -9223,8 +10042,15 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) { // Check for device type switch strings.ToUpper(p.curTok.Literal) { - case "DISK": - device.DeviceType = "Disk" + case "TAPE": + device.DeviceType = "Tape" + p.nextToken() + if p.curTok.Type != TokenEquals { + return nil, fmt.Errorf("expected = after TAPE, got %s", p.curTok.Literal) + } + p.nextToken() + case "DISK": + device.DeviceType = "Disk" p.nextToken() if p.curTok.Type != TokenEquals { return nil, fmt.Errorf("expected = after DISK, got %s", p.curTok.Literal) @@ -9240,8 +10066,8 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) { } // Parse device name - if device.DeviceType == "Disk" || device.DeviceType == "URL" { - // For DISK and URL, use PhysicalDevice with the string literal directly + if device.DeviceType == "Disk" || device.DeviceType == "URL" || device.DeviceType == "Tape" { + // For DISK, URL, and TAPE, use PhysicalDevice with the string literal directly if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { // Strip the surrounding quotes from the literal val := p.curTok.Literal @@ -9353,6 +10179,28 @@ parseWithClause: } stmt.Options = append(stmt.Options, fsOpt) + case "MOVE": + // MOVE 'logical_file_name' TO 'os_file_name' + opt := &ast.MoveRestoreOption{OptionKind: "Move"} + // Parse logical file name + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + opt.LogicalFileName = expr + // Expect TO + if strings.ToUpper(p.curTok.Literal) != "TO" { + return nil, fmt.Errorf("expected TO after logical file name, got %s", p.curTok.Literal) + } + p.nextToken() + // Parse OS file name + osExpr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + opt.OSFileName = osExpr + stmt.Options = append(stmt.Options, opt) + case "STOPATMARK", "STOPBEFOREMARK": opt := &ast.StopRestoreOption{ OptionKind: "StopAt", @@ -9394,21 +10242,73 @@ parseWithClause: } stmt.Options = append(stmt.Options, opt) - case "KEEP_TEMPORAL_RETENTION", "NOREWIND", "NOUNLOAD", "STATS", + case "FILE", "MEDIANAME", "MEDIAPASSWORD", "PASSWORD", "STOPAT": + // Options that take a scalar expression value + optKind := optionName + switch optionName { + case "MEDIANAME": + optKind = "MediaName" + case "MEDIAPASSWORD": + optKind = "MediaPassword" + case "PASSWORD": + optKind = "Password" + case "STOPAT": + optKind = "StopAt" + case "FILE": + optKind = "File" + } + opt := &ast.ScalarExpressionRestoreOption{OptionKind: optKind} + if p.curTok.Type == TokenEquals { + p.nextToken() + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + opt.Value = expr + } + stmt.Options = append(stmt.Options, opt) + + case "STATS": + // STATS can optionally have a value: STATS or STATS = 10 + if p.curTok.Type == TokenEquals { + p.nextToken() + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + stmt.Options = append(stmt.Options, &ast.ScalarExpressionRestoreOption{ + OptionKind: "Stats", + Value: expr, + }) + } else { + stmt.Options = append(stmt.Options, &ast.SimpleRestoreOption{OptionKind: "Stats"}) + } + + case "ENABLE_BROKER", "ERROR_BROKER_CONVERSATIONS", "NEW_BROKER", + "KEEP_REPLICATION", "RESTRICTED_USER", + "KEEP_TEMPORAL_RETENTION", "NOREWIND", "NOUNLOAD", "RECOVERY", "NORECOVERY", "REPLACE", "RESTART", "REWIND", "UNLOAD", "CHECKSUM", "NO_CHECKSUM", "STOP_ON_ERROR", "CONTINUE_AFTER_ERROR": // Map option names to proper casing optKind := optionName switch optionName { + case "ENABLE_BROKER": + optKind = "EnableBroker" + case "ERROR_BROKER_CONVERSATIONS": + optKind = "ErrorBrokerConversations" + case "NEW_BROKER": + optKind = "NewBroker" + case "KEEP_REPLICATION": + optKind = "KeepReplication" + case "RESTRICTED_USER": + optKind = "RestrictedUser" case "KEEP_TEMPORAL_RETENTION": optKind = "KeepTemporalRetention" case "NOREWIND": optKind = "NoRewind" case "NOUNLOAD": optKind = "NoUnload" - case "STATS": - optKind = "Stats" case "RECOVERY": optKind = "Recovery" case "NORECOVERY": @@ -10047,6 +10947,31 @@ func (p *Parser) parseCreateColumnStoreIndexStatement() (*ast.CreateColumnStoreI stmt.IndexOptions = append(stmt.IndexOptions, orderOpt) } + case "DATA_COMPRESSION": + p.nextToken() // consume DATA_COMPRESSION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + level := strings.ToUpper(p.curTok.Literal) + compressionLevel := "None" + switch level { + case "COLUMNSTORE": + compressionLevel = "ColumnStore" + case "COLUMNSTORE_ARCHIVE": + compressionLevel = "ColumnStoreArchive" + case "PAGE": + compressionLevel = "Page" + case "ROW": + compressionLevel = "Row" + case "NONE": + compressionLevel = "None" + } + p.nextToken() // consume compression level + stmt.IndexOptions = append(stmt.IndexOptions, &ast.DataCompressionOption{ + CompressionLevel: compressionLevel, + OptionKind: "DataCompression", + }) + default: // Skip unknown options p.nextToken() @@ -10129,6 +11054,11 @@ func (p *Parser) parseAlterFunctionStatement() (*ast.AlterFunctionStatement, err p.nextToken() } + // Skip optional AS keyword (e.g., @param AS int) + if p.curTok.Type == TokenAs { + p.nextToken() + } + // Parse data type if present if p.curTok.Type != TokenRParen && p.curTok.Type != TokenComma && p.curTok.Type != TokenEquals { dataType, err := p.parseDataType() @@ -10836,6 +11766,11 @@ func (p *Parser) parseCreateFunctionStatement() (*ast.CreateFunctionStatement, e p.nextToken() } + // Skip optional AS keyword (e.g., @param AS int) + if p.curTok.Type == TokenAs { + p.nextToken() + } + // Parse data type if present if p.curTok.Type != TokenRParen && p.curTok.Type != TokenComma { dataType, err := p.parseDataTypeReference() @@ -11296,6 +12231,11 @@ func (p *Parser) parseCreateOrAlterFunctionStatement() (*ast.CreateOrAlterFuncti p.nextToken() } + // Skip optional AS keyword (e.g., @param AS int) + if p.curTok.Type == TokenAs { + p.nextToken() + } + // Parse data type if present if p.curTok.Type != TokenRParen && p.curTok.Type != TokenComma { dataType, err := p.parseDataTypeReference() @@ -12110,10 +13050,10 @@ func restoreOptionToJSON(o ast.RestoreOption) jsonNode { "OptionKind": opt.OptionKind, } if opt.LogicalFileName != nil { - node["LogicalFileName"] = identifierOrValueExpressionToJSON(opt.LogicalFileName) + node["LogicalFileName"] = scalarExpressionToJSON(opt.LogicalFileName) } if opt.OSFileName != nil { - node["OSFileName"] = identifierOrValueExpressionToJSON(opt.OSFileName) + node["OSFileName"] = scalarExpressionToJSON(opt.OSFileName) } return node default: @@ -12192,7 +13132,7 @@ func userOptionToJSON(o ast.UserOption) jsonNode { "Hashed": opt.Hashed, } if opt.Password != nil { - node["Password"] = stringLiteralToJSON(opt.Password) + node["Password"] = scalarExpressionToJSON(opt.Password) } if opt.OldPassword != nil { node["OldPassword"] = stringLiteralToJSON(opt.OldPassword) @@ -12322,6 +13262,20 @@ func columnStoreIndexOptionToJSON(opt ast.IndexOption) jsonNode { node["Expression"] = scalarExpressionToJSON(o.Expression) } return node + case *ast.DataCompressionOption: + node := jsonNode{ + "$type": "DataCompressionOption", + "CompressionLevel": o.CompressionLevel, + "OptionKind": o.OptionKind, + } + if len(o.PartitionRanges) > 0 { + ranges := make([]jsonNode, len(o.PartitionRanges)) + for i, r := range o.PartitionRanges { + ranges[i] = compressionPartitionRangeToJSON(r) + } + node["PartitionRanges"] = ranges + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } @@ -12948,11 +13902,15 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { "OptionKind": o.OptionKind, } case *ast.OnlineIndexOption: - return jsonNode{ + node := jsonNode{ "$type": "OnlineIndexOption", "OptionState": o.OptionState, "OptionKind": o.OptionKind, } + if o.LowPriorityLockWaitOption != nil { + node["LowPriorityLockWaitOption"] = onlineIndexLowPriorityLockWaitOptionToJSON(o.LowPriorityLockWaitOption) + } + return node case *ast.CompressionDelayIndexOption: node := jsonNode{ "$type": "CompressionDelayIndexOption", @@ -12975,6 +13933,20 @@ func indexOptionToJSON(opt ast.IndexOption) jsonNode { node["Unit"] = o.Unit } return node + case *ast.XmlCompressionOption: + node := jsonNode{ + "$type": "XmlCompressionOption", + "IsCompressed": o.IsCompressed, + "OptionKind": o.OptionKind, + } + if len(o.PartitionRanges) > 0 { + ranges := make([]jsonNode, len(o.PartitionRanges)) + for i, r := range o.PartitionRanges { + ranges[i] = compressionPartitionRangeToJSON(r) + } + node["PartitionRanges"] = ranges + } + return node default: return jsonNode{"$type": "UnknownIndexOption"} } @@ -13188,11 +14160,15 @@ func childObjectNameToJSON(s *ast.SchemaObjectName) jsonNode { func dropIndexOptionToJSON(opt ast.DropIndexOption) jsonNode { switch o := opt.(type) { case *ast.OnlineIndexOption: - return jsonNode{ + node := jsonNode{ "$type": "OnlineIndexOption", "OptionState": o.OptionState, "OptionKind": o.OptionKind, } + if o.LowPriorityLockWaitOption != nil { + node["LowPriorityLockWaitOption"] = onlineIndexLowPriorityLockWaitOptionToJSON(o.LowPriorityLockWaitOption) + } + return node case *ast.MoveToDropIndexOption: node := jsonNode{ "$type": "MoveToDropIndexOption", @@ -13230,6 +14206,15 @@ func dropIndexOptionToJSON(opt ast.DropIndexOption) jsonNode { node["Options"] = options } return node + case *ast.IndexExpressionOption: + node := jsonNode{ + "$type": "IndexExpressionOption", + "OptionKind": o.OptionKind, + } + if o.Expression != nil { + node["Expression"] = scalarExpressionToJSON(o.Expression) + } + return node } return jsonNode{} } @@ -13295,6 +14280,166 @@ func dropSchemaStatementToJSON(s *ast.DropSchemaStatement) jsonNode { return node } +func dropPartitionFunctionStatementToJSON(s *ast.DropPartitionFunctionStatement) jsonNode { + node := jsonNode{ + "$type": "DropPartitionFunctionStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropPartitionSchemeStatementToJSON(s *ast.DropPartitionSchemeStatement) jsonNode { + node := jsonNode{ + "$type": "DropPartitionSchemeStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropApplicationRoleStatementToJSON(s *ast.DropApplicationRoleStatement) jsonNode { + node := jsonNode{ + "$type": "DropApplicationRoleStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropCertificateStatementToJSON(s *ast.DropCertificateStatement) jsonNode { + node := jsonNode{ + "$type": "DropCertificateStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropMasterKeyStatementToJSON(s *ast.DropMasterKeyStatement) jsonNode { + return jsonNode{ + "$type": "DropMasterKeyStatement", + } +} + +func dropXmlSchemaCollectionStatementToJSON(s *ast.DropXmlSchemaCollectionStatement) jsonNode { + node := jsonNode{ + "$type": "DropXmlSchemaCollectionStatement", + } + if s.Name != nil { + node["Name"] = schemaObjectNameToJSON(s.Name) + } + return node +} + +func dropContractStatementToJSON(s *ast.DropContractStatement) jsonNode { + node := jsonNode{ + "$type": "DropContractStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropEndpointStatementToJSON(s *ast.DropEndpointStatement) jsonNode { + node := jsonNode{ + "$type": "DropEndpointStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropMessageTypeStatementToJSON(s *ast.DropMessageTypeStatement) jsonNode { + node := jsonNode{ + "$type": "DropMessageTypeStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropQueueStatementToJSON(s *ast.DropQueueStatement) jsonNode { + node := jsonNode{ + "$type": "DropQueueStatement", + } + if s.Name != nil { + node["Name"] = schemaObjectNameToJSON(s.Name) + } + return node +} + +func dropRemoteServiceBindingStatementToJSON(s *ast.DropRemoteServiceBindingStatement) jsonNode { + node := jsonNode{ + "$type": "DropRemoteServiceBindingStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropRouteStatementToJSON(s *ast.DropRouteStatement) jsonNode { + node := jsonNode{ + "$type": "DropRouteStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropServiceStatementToJSON(s *ast.DropServiceStatement) jsonNode { + node := jsonNode{ + "$type": "DropServiceStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropEventNotificationStatementToJSON(s *ast.DropEventNotificationStatement) jsonNode { + node := jsonNode{ + "$type": "DropEventNotificationStatement", + } + if len(s.Notifications) > 0 { + notifications := make([]jsonNode, len(s.Notifications)) + for i, n := range s.Notifications { + notifications[i] = identifierToJSON(n) + } + node["Notifications"] = notifications + } + if s.Scope != nil { + scope := jsonNode{ + "$type": "EventNotificationObjectScope", + "Target": s.Scope.Target, + } + if s.Scope.QueueName != nil { + scope["QueueName"] = schemaObjectNameToJSON(s.Scope.QueueName) + } + node["Scope"] = scope + } + return node +} + func dropSecurityPolicyStatementToJSON(s *ast.DropSecurityPolicyStatement) jsonNode { node := jsonNode{ "$type": "DropSecurityPolicyStatement", @@ -14981,6 +16126,27 @@ func dropFullTextStopListStatementToJSON(s *ast.DropFullTextStopListStatement) j return node } +func dropFullTextCatalogStatementToJSON(s *ast.DropFullTextCatalogStatement) jsonNode { + node := jsonNode{ + "$type": "DropFullTextCatalogStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func dropFulltextIndexStatementToJSON(s *ast.DropFulltextIndexStatement) jsonNode { + node := jsonNode{ + "$type": "DropFullTextIndexStatement", + } + if s.OnName != nil { + node["OnName"] = schemaObjectNameToJSON(s.OnName) + } + return node +} + func alterFulltextIndexStatementToJSON(s *ast.AlterFulltextIndexStatement) jsonNode { node := jsonNode{ "$type": "AlterFullTextIndexStatement", @@ -15377,6 +16543,57 @@ func createLoginSourceToJSON(s ast.CreateLoginSource) jsonNode { node["Options"] = opts } return node + case *ast.PasswordCreateLoginSource: + node := jsonNode{ + "$type": "PasswordCreateLoginSource", + "Hashed": src.Hashed, + "MustChange": src.MustChange, + } + if src.Password != nil { + node["Password"] = scalarExpressionToJSON(src.Password) + } + if len(src.Options) > 0 { + opts := make([]jsonNode, len(src.Options)) + for i, opt := range src.Options { + opts[i] = principalOptionToJSON(opt) + } + node["Options"] = opts + } + return node + case *ast.WindowsCreateLoginSource: + node := jsonNode{ + "$type": "WindowsCreateLoginSource", + } + if len(src.Options) > 0 { + opts := make([]jsonNode, len(src.Options)) + for i, opt := range src.Options { + opts[i] = principalOptionToJSON(opt) + } + node["Options"] = opts + } + return node + case *ast.CertificateCreateLoginSource: + node := jsonNode{ + "$type": "CertificateCreateLoginSource", + } + if src.Certificate != nil { + node["Certificate"] = identifierToJSON(src.Certificate) + } + if src.Credential != nil { + node["Credential"] = identifierToJSON(src.Credential) + } + return node + case *ast.AsymmetricKeyCreateLoginSource: + node := jsonNode{ + "$type": "AsymmetricKeyCreateLoginSource", + } + if src.Key != nil { + node["Key"] = identifierToJSON(src.Key) + } + if src.Credential != nil { + node["Credential"] = identifierToJSON(src.Credential) + } + return node default: return jsonNode{} } @@ -15402,11 +16619,76 @@ func principalOptionToJSON(o ast.PrincipalOption) jsonNode { node["Identifier"] = identifierToJSON(opt.Identifier) } return node + case *ast.OnOffPrincipalOption: + return jsonNode{ + "$type": "OnOffPrincipalOption", + "OptionKind": opt.OptionKind, + "OptionState": opt.OptionState, + } + case *ast.PrincipalOptionSimple: + return jsonNode{ + "$type": "PrincipalOption", + "OptionKind": opt.OptionKind, + } + 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"] = scalarExpressionToJSON(opt.Password) + } + if opt.OldPassword != nil { + node["OldPassword"] = stringLiteralToJSON(opt.OldPassword) + } + return node default: return jsonNode{} } } +func alterLoginEnableDisableStatementToJSON(s *ast.AlterLoginEnableDisableStatement) jsonNode { + node := jsonNode{ + "$type": "AlterLoginEnableDisableStatement", + "IsEnable": s.IsEnable, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func alterLoginOptionsStatementToJSON(s *ast.AlterLoginOptionsStatement) jsonNode { + node := jsonNode{ + "$type": "AlterLoginOptionsStatement", + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + if len(s.Options) > 0 { + opts := make([]jsonNode, len(s.Options)) + for i, opt := range s.Options { + opts[i] = principalOptionToJSON(opt) + } + node["Options"] = opts + } + return node +} + +func dropLoginStatementToJSON(s *ast.DropLoginStatement) jsonNode { + node := jsonNode{ + "$type": "DropLoginStatement", + "IsIfExists": s.IsIfExists, + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + func createIndexStatementToJSON(s *ast.CreateIndexStatement) jsonNode { node := jsonNode{ "$type": "CreateIndexStatement", @@ -16388,6 +17670,23 @@ func alterExternalResourcePoolStatementToJSON(s *ast.AlterExternalResourcePoolSt return node } +func createExternalResourcePoolStatementToJSON(s *ast.CreateExternalResourcePoolStatement) jsonNode { + node := jsonNode{ + "$type": "CreateExternalResourcePoolStatement", + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + if len(s.ExternalResourcePoolParameters) > 0 { + params := make([]jsonNode, len(s.ExternalResourcePoolParameters)) + for i, param := range s.ExternalResourcePoolParameters { + params[i] = externalResourcePoolParameterToJSON(param) + } + node["ExternalResourcePoolParameters"] = params + } + return node +} + func externalResourcePoolParameterToJSON(p *ast.ExternalResourcePoolParameter) jsonNode { node := jsonNode{ "$type": "ExternalResourcePoolParameter", @@ -17092,3 +18391,38 @@ func sensitivityClassificationOptionToJSON(opt *ast.SensitivityClassificationOpt } return node } + +func openRowsetCosmosOptionToJSON(opt ast.OpenRowsetCosmosOption) jsonNode { + switch o := opt.(type) { + case *ast.LiteralOpenRowsetCosmosOption: + node := jsonNode{ + "$type": "LiteralOpenRowsetCosmosOption", + "OptionKind": o.OptionKind, + } + if o.Value != nil { + node["Value"] = scalarExpressionToJSON(o.Value) + } + return node + default: + return jsonNode{"$type": "UnknownOpenRowsetCosmosOption"} + } +} + +func openRowsetColumnDefinitionToJSON(col *ast.OpenRowsetColumnDefinition) jsonNode { + node := jsonNode{ + "$type": "OpenRowsetColumnDefinition", + } + if col.ColumnOrdinal != nil { + node["ColumnOrdinal"] = scalarExpressionToJSON(col.ColumnOrdinal) + } + if col.ColumnIdentifier != nil { + node["ColumnIdentifier"] = identifierToJSON(col.ColumnIdentifier) + } + if col.DataType != nil { + node["DataType"] = dataTypeReferenceToJSON(col.DataType) + } + if col.Collation != nil { + node["Collation"] = identifierToJSON(col.Collation) + } + return node +} diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index e0da5aac..0e6b6084 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -130,6 +130,41 @@ func (p *Parser) parseDropStatement() (ast.Statement, error) { return p.parseDropBrokerPriorityStatement() case "RESOURCE": return p.parseDropResourcePoolStatement() + case "LOGIN": + return p.parseDropLoginStatement() + case "PARTITION": + return p.parseDropPartitionStatement() + case "APPLICATION": + return p.parseDropApplicationRoleStatement() + case "CERTIFICATE": + return p.parseDropCertificateStatement() + case "CREDENTIAL": + return p.parseDropCredentialStatement(false) + case "MASTER": + return p.parseDropMasterKeyStatement() + case "XML": + return p.parseDropXmlSchemaCollectionStatement() + case "CONTRACT": + return p.parseDropContractStatement() + case "ENDPOINT": + return p.parseDropEndpointStatement() + case "MESSAGE": + return p.parseDropMessageTypeStatement() + case "QUEUE": + return p.parseDropQueueStatement() + case "REMOTE": + return p.parseDropRemoteServiceBindingStatement() + case "ROUTE": + return p.parseDropRouteStatement() + case "SERVICE": + return p.parseDropServiceStatement() + case "EVENT": + return p.parseDropEventNotificationStatement() + } + + // Handle LOGIN token explicitly + if p.curTok.Type == TokenLogin { + return p.parseDropLoginStatement() } return nil, fmt.Errorf("unexpected token after DROP: %s", p.curTok.Literal) @@ -659,6 +694,22 @@ func (p *Parser) parseDropSynonymStatement() (*ast.DropSynonymStatement, error) return stmt, nil } +func (p *Parser) parseDropLoginStatement() (*ast.DropLoginStatement, error) { + // Consume LOGIN + p.nextToken() + + stmt := &ast.DropLoginStatement{ + Name: p.parseIdentifier(), + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseDropUserStatement() (*ast.DropUserStatement, error) { // Consume USER p.nextToken() @@ -743,6 +794,18 @@ func (p *Parser) parseDropAssemblyStatement() (*ast.DropAssemblyStatement, error p.nextToken() // consume comma } + // Check for WITH NO DEPENDENTS + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if strings.ToUpper(p.curTok.Literal) == "NO" { + p.nextToken() // consume NO + if strings.ToUpper(p.curTok.Literal) == "DEPENDENTS" { + p.nextToken() // consume DEPENDENTS + stmt.WithNoDependents = true + } + } + } + // Skip optional semicolon if p.curTok.Type == TokenSemicolon { p.nextToken() @@ -843,6 +906,25 @@ func (p *Parser) parseDropDatabaseStatement() (ast.Statement, error) { // Consume DATABASE p.nextToken() + // Check for DATABASE AUDIT SPECIFICATION + if strings.ToUpper(p.curTok.Literal) == "AUDIT" { + p.nextToken() // consume AUDIT + if strings.ToUpper(p.curTok.Literal) == "SPECIFICATION" { + p.nextToken() // consume SPECIFICATION + } + stmt := &ast.DropDatabaseAuditSpecificationStatement{} + // Check for IF EXISTS + if strings.ToUpper(p.curTok.Literal) == "IF" { + p.nextToken() + if strings.ToUpper(p.curTok.Literal) == "EXISTS" { + p.nextToken() + stmt.IsIfExists = true + } + } + stmt.Name = p.parseIdentifier() + return stmt, nil + } + // Check for DATABASE ENCRYPTION KEY if strings.ToUpper(p.curTok.Literal) == "ENCRYPTION" { p.nextToken() // consume ENCRYPTION @@ -1352,10 +1434,75 @@ func (p *Parser) parseDropIndexOptions() []ast.DropIndexOption { optState = "On" } p.nextToken() - options = append(options, &ast.OnlineIndexOption{ + onlineOpt := &ast.OnlineIndexOption{ OptionState: optState, OptionKind: "Online", - }) + } + // Check for optional (WAIT_AT_LOW_PRIORITY (...)) + if optState == "On" && p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if strings.ToUpper(p.curTok.Literal) == "WAIT_AT_LOW_PRIORITY" { + p.nextToken() // consume WAIT_AT_LOW_PRIORITY + lowPriorityOpt := &ast.OnlineIndexLowPriorityLockWaitOption{} + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optName := strings.ToUpper(p.curTok.Literal) + if optName == "MAX_DURATION" { + p.nextToken() // consume MAX_DURATION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + durVal, _ := p.parsePrimaryExpression() + unit := "Minutes" + if strings.ToUpper(p.curTok.Literal) == "MINUTES" { + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "SECONDS" { + unit = "Seconds" + p.nextToken() + } + lowPriorityOpt.Options = append(lowPriorityOpt.Options, &ast.LowPriorityLockWaitMaxDurationOption{ + MaxDuration: durVal, + Unit: unit, + OptionKind: "MaxDuration", + }) + } else if optName == "ABORT_AFTER_WAIT" { + p.nextToken() // consume ABORT_AFTER_WAIT + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + abortType := "None" + switch strings.ToUpper(p.curTok.Literal) { + case "NONE": + abortType = "None" + case "SELF": + abortType = "Self" + case "BLOCKERS": + abortType = "Blockers" + } + p.nextToken() + lowPriorityOpt.Options = append(lowPriorityOpt.Options, &ast.LowPriorityLockWaitAbortAfterWaitOption{ + AbortAfterWait: abortType, + OptionKind: "AbortAfterWait", + }) + } else { + break + } + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + onlineOpt.LowPriorityLockWaitOption = lowPriorityOpt + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + options = append(options, onlineOpt) case "MOVE": p.nextToken() // consume MOVE if strings.ToUpper(p.curTok.Literal) == "TO" { @@ -1418,6 +1565,23 @@ func (p *Parser) parseDropIndexOptions() []ast.DropIndexOption { CompressionLevel: level, OptionKind: "DataCompression", }) + case "MAXDOP": + p.nextToken() // consume MAXDOP + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + var expr ast.ScalarExpression + if p.curTok.Type == TokenNumber { + expr = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } + options = append(options, &ast.IndexExpressionOption{ + Expression: expr, + OptionKind: "MaxDop", + }) case "WAIT_AT_LOW_PRIORITY": p.nextToken() // consume WAIT_AT_LOW_PRIORITY waitOpt := &ast.WaitAtLowPriorityOption{ @@ -1612,7 +1776,9 @@ func (p *Parser) parseDropSchemaStatement() (*ast.DropSchemaStatement, error) { // Consume SCHEMA p.nextToken() - stmt := &ast.DropSchemaStatement{} + stmt := &ast.DropSchemaStatement{ + DropBehavior: "None", + } // Check for IF EXISTS if strings.ToUpper(p.curTok.Literal) == "IF" { @@ -1631,6 +1797,16 @@ func (p *Parser) parseDropSchemaStatement() (*ast.DropSchemaStatement, error) { } stmt.Schema = schema + // Check for CASCADE or RESTRICT + switch strings.ToUpper(p.curTok.Literal) { + case "CASCADE": + stmt.DropBehavior = "Cascade" + p.nextToken() + case "RESTRICT": + stmt.DropBehavior = "Restrict" + p.nextToken() + } + // Skip optional semicolon if p.curTok.Type == TokenSemicolon { p.nextToken() @@ -1639,6 +1815,287 @@ func (p *Parser) parseDropSchemaStatement() (*ast.DropSchemaStatement, error) { return stmt, nil } +func (p *Parser) parseDropPartitionStatement() (ast.Statement, error) { + // Consume PARTITION + p.nextToken() + + keyword := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + switch keyword { + case "FUNCTION": + stmt := &ast.DropPartitionFunctionStatement{ + Name: p.parseIdentifier(), + } + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + return stmt, nil + case "SCHEME": + stmt := &ast.DropPartitionSchemeStatement{ + Name: p.parseIdentifier(), + } + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + return stmt, nil + } + + return nil, fmt.Errorf("expected FUNCTION or SCHEME after PARTITION, got %s", keyword) +} + +func (p *Parser) parseDropApplicationRoleStatement() (*ast.DropApplicationRoleStatement, error) { + // Consume APPLICATION + p.nextToken() + // Consume ROLE + if strings.ToUpper(p.curTok.Literal) == "ROLE" { + p.nextToken() + } + + stmt := &ast.DropApplicationRoleStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropCertificateStatement() (*ast.DropCertificateStatement, error) { + // Consume CERTIFICATE + p.nextToken() + + stmt := &ast.DropCertificateStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropMasterKeyStatement() (*ast.DropMasterKeyStatement, error) { + // Consume MASTER + p.nextToken() + // Consume KEY + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() + } + + stmt := &ast.DropMasterKeyStatement{} + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropXmlSchemaCollectionStatement() (*ast.DropXmlSchemaCollectionStatement, error) { + // Consume XML + p.nextToken() + // Consume SCHEMA + if strings.ToUpper(p.curTok.Literal) == "SCHEMA" { + p.nextToken() + } + // Consume COLLECTION + if strings.ToUpper(p.curTok.Literal) == "COLLECTION" { + p.nextToken() + } + + name, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + + stmt := &ast.DropXmlSchemaCollectionStatement{ + Name: name, + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropContractStatement() (*ast.DropContractStatement, error) { + // Consume CONTRACT + p.nextToken() + + stmt := &ast.DropContractStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropEndpointStatement() (*ast.DropEndpointStatement, error) { + // Consume ENDPOINT + p.nextToken() + + stmt := &ast.DropEndpointStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropMessageTypeStatement() (*ast.DropMessageTypeStatement, error) { + // Consume MESSAGE + p.nextToken() + // Consume TYPE + if strings.ToUpper(p.curTok.Literal) == "TYPE" { + p.nextToken() + } + + stmt := &ast.DropMessageTypeStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropQueueStatement() (*ast.DropQueueStatement, error) { + // Consume QUEUE + p.nextToken() + + name, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + + stmt := &ast.DropQueueStatement{ + Name: name, + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropRemoteServiceBindingStatement() (*ast.DropRemoteServiceBindingStatement, error) { + // Consume REMOTE + p.nextToken() + // Consume SERVICE + if strings.ToUpper(p.curTok.Literal) == "SERVICE" { + p.nextToken() + } + // Consume BINDING + if strings.ToUpper(p.curTok.Literal) == "BINDING" { + p.nextToken() + } + + stmt := &ast.DropRemoteServiceBindingStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropRouteStatement() (*ast.DropRouteStatement, error) { + // Consume ROUTE + p.nextToken() + + stmt := &ast.DropRouteStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropServiceStatement() (*ast.DropServiceStatement, error) { + // Consume SERVICE + p.nextToken() + + stmt := &ast.DropServiceStatement{ + Name: p.parseIdentifier(), + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + +func (p *Parser) parseDropEventNotificationStatement() (*ast.DropEventNotificationStatement, error) { + // Consume EVENT + p.nextToken() + // Consume NOTIFICATION + if strings.ToUpper(p.curTok.Literal) == "NOTIFICATION" { + p.nextToken() + } + + stmt := &ast.DropEventNotificationStatement{} + + // Parse notification names (comma-separated) + for { + stmt.Notifications = append(stmt.Notifications, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + continue + } + break + } + + // Parse ON clause + if p.curTok.Type == TokenOn { + p.nextToken() + } + + scope := &ast.EventNotificationObjectScope{} + switch strings.ToUpper(p.curTok.Literal) { + case "SERVER": + scope.Target = "Server" + p.nextToken() + case "DATABASE": + scope.Target = "Database" + p.nextToken() + case "QUEUE": + scope.Target = "Queue" + p.nextToken() + queueName, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + scope.QueueName = queueName + } + stmt.Scope = scope + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseAlterStatement() (ast.Statement, error) { // Consume ALTER p.nextToken() @@ -5898,39 +6355,223 @@ func (p *Parser) parseAlterSchemaStatement() (*ast.AlterSchemaStatement, error) return stmt, nil } -func (p *Parser) parseAlterLoginStatement() (*ast.AlterLoginAddDropCredentialStatement, error) { +func (p *Parser) parseAlterLoginStatement() (ast.Statement, error) { // Consume LOGIN p.nextToken() - stmt := &ast.AlterLoginAddDropCredentialStatement{} - // Parse login name - stmt.Name = p.parseIdentifier() + name := p.parseIdentifier() - // Check for ADD or DROP - if not present, skip to end - if p.curTok.Type == TokenAdd { - stmt.IsAdd = true - p.nextToken() // consume ADD - } else if p.curTok.Type == TokenDrop { - stmt.IsAdd = false - p.nextToken() // consume DROP - } else { - // Handle incomplete statement - p.skipToEndOfStatement() + // Check for ENABLE/DISABLE + upper := strings.ToUpper(p.curTok.Literal) + if upper == "ENABLE" { + p.nextToken() + return &ast.AlterLoginEnableDisableStatement{ + Name: name, + IsEnable: true, + }, nil + } else if upper == "DISABLE" { + p.nextToken() + return &ast.AlterLoginEnableDisableStatement{ + Name: name, + IsEnable: false, + }, nil + } + + // Check for ADD or DROP CREDENTIAL + if p.curTok.Type == TokenAdd || p.curTok.Type == TokenDrop { + stmt := &ast.AlterLoginAddDropCredentialStatement{ + Name: name, + IsAdd: p.curTok.Type == TokenAdd, + } + p.nextToken() // consume ADD/DROP + + // Expect CREDENTIAL + if p.curTok.Type == TokenCredential { + p.nextToken() + stmt.CredentialName = p.parseIdentifier() + } + + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } return stmt, nil } - // Expect CREDENTIAL - if p.curTok.Type != TokenCredential { + // Handle WITH options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + + // Check if we have valid options to parse + upper := strings.ToUpper(p.curTok.Literal) + if upper == "PASSWORD" || upper == "NO" || upper == "NAME" || upper == "DEFAULT_DATABASE" || + upper == "DEFAULT_LANGUAGE" || upper == "CHECK_POLICY" || upper == "CHECK_EXPIRATION" || upper == "CREDENTIAL" { + return p.parseAlterLoginOptions(name) + } + // For incomplete statements like "alter login l1 with", fall back to old behavior p.skipToEndOfStatement() - return stmt, nil + return &ast.AlterLoginAddDropCredentialStatement{Name: name, IsAdd: false}, nil } - p.nextToken() - // Parse credential name - stmt.CredentialName = p.parseIdentifier() + // Skip to end if we don't recognize the syntax + p.skipToEndOfStatement() + return &ast.AlterLoginAddDropCredentialStatement{Name: name, IsAdd: false}, nil +} + +func (p *Parser) parseAlterLoginOptions(name *ast.Identifier) (*ast.AlterLoginOptionsStatement, error) { + stmt := &ast.AlterLoginOptionsStatement{ + Name: name, + } + + for { + optName := strings.ToUpper(p.curTok.Literal) + + if optName == "PASSWORD" { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + opt := &ast.PasswordAlterPrincipalOption{ + OptionKind: "Password", + } + + // Parse password value + if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString { + val := p.curTok.Literal + isNational := p.curTok.Type == TokenNationalString + // Strip N prefix for national strings + if isNational && len(val) > 0 && (val[0] == 'N' || val[0] == 'n') { + val = val[1:] + } + // Strip surrounding quotes + if len(val) >= 2 && val[0] == '\'' && val[len(val)-1] == '\'' { + val = val[1 : len(val)-1] + } + opt.Password = &ast.StringLiteral{ + LiteralType: "String", + Value: val, + IsNational: isNational, + IsLargeObject: false, + } + p.nextToken() + } else if p.curTok.Type == TokenBinary { + opt.Password = &ast.BinaryLiteral{ + LiteralType: "Binary", + IsLargeObject: false, + Value: p.curTok.Literal, + } + p.nextToken() + } + + // Parse optional flags + for { + upper := strings.ToUpper(p.curTok.Literal) + if upper == "HASHED" { + opt.Hashed = true + p.nextToken() + } else if upper == "MUST_CHANGE" { + opt.MustChange = true + p.nextToken() + } else if upper == "UNLOCK" { + opt.Unlock = true + p.nextToken() + } else if upper == "OLD_PASSWORD" { + p.nextToken() // consume OLD_PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() + } + if p.curTok.Type == TokenString { + opt.OldPassword = p.parseStringLiteralValue() + p.nextToken() + } + } else { + break + } + } + + stmt.Options = append(stmt.Options, opt) + } else if optName == "NO" && strings.ToUpper(p.peekTok.Literal) == "CREDENTIAL" { + p.nextToken() // consume NO + p.nextToken() // consume CREDENTIAL + stmt.Options = append(stmt.Options, &ast.PrincipalOptionSimple{ + OptionKind: "NoCredential", + }) + } else if optName == "NAME" { + p.nextToken() // consume NAME + if p.curTok.Type == TokenEquals { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.IdentifierPrincipalOption{ + OptionKind: "Name", + Identifier: p.parseIdentifier(), + }) + } else if optName == "DEFAULT_DATABASE" { + p.nextToken() // consume DEFAULT_DATABASE + if p.curTok.Type == TokenEquals { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.IdentifierPrincipalOption{ + OptionKind: "DefaultDatabase", + Identifier: p.parseIdentifier(), + }) + } else if optName == "DEFAULT_LANGUAGE" { + p.nextToken() // consume DEFAULT_LANGUAGE + if p.curTok.Type == TokenEquals { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.IdentifierPrincipalOption{ + OptionKind: "DefaultLanguage", + Identifier: p.parseIdentifier(), + }) + } else if optName == "CHECK_POLICY" { + p.nextToken() // consume CHECK_POLICY + if p.curTok.Type == TokenEquals { + p.nextToken() + } + optState := "On" + if strings.ToUpper(p.curTok.Literal) == "OFF" { + optState = "Off" + } + p.nextToken() + stmt.Options = append(stmt.Options, &ast.OnOffPrincipalOption{ + OptionKind: "CheckPolicy", + OptionState: optState, + }) + } else if optName == "CHECK_EXPIRATION" { + p.nextToken() // consume CHECK_EXPIRATION + if p.curTok.Type == TokenEquals { + p.nextToken() + } + optState := "On" + if strings.ToUpper(p.curTok.Literal) == "OFF" { + optState = "Off" + } + p.nextToken() + stmt.Options = append(stmt.Options, &ast.OnOffPrincipalOption{ + OptionKind: "CheckExpiration", + OptionState: optState, + }) + } else if optName == "CREDENTIAL" { + p.nextToken() // consume CREDENTIAL + if p.curTok.Type == TokenEquals { + p.nextToken() + } + stmt.Options = append(stmt.Options, &ast.IdentifierPrincipalOption{ + OptionKind: "Credential", + Identifier: p.parseIdentifier(), + }) + } else { + break + } + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } - // Skip optional semicolon if p.curTok.Type == TokenSemicolon { p.nextToken() } diff --git a/parser/parse_dml.go b/parser/parse_dml.go index d56ddb65..2315a379 100644 --- a/parser/parse_dml.go +++ b/parser/parse_dml.go @@ -236,6 +236,73 @@ func (p *Parser) parseInsertStatement() (ast.Statement, error) { return stmt, nil } +// parseInsertSpecification parses an INSERT specification (used in DataModificationTableReference) +func (p *Parser) parseInsertSpecification() (*ast.InsertSpecification, error) { + // Consume INSERT + p.nextToken() + + spec := &ast.InsertSpecification{ + InsertOption: "None", + } + + // Check for TOP clause + if p.curTok.Type == TokenTop { + top, err := p.parseTopRowFilter() + if err != nil { + return nil, err + } + spec.TopRowFilter = top + } + + // Check for INTO or OVER + if p.curTok.Type == TokenInto { + spec.InsertOption = "Into" + p.nextToken() + } else if p.curTok.Type == TokenOver { + spec.InsertOption = "Over" + p.nextToken() + } + + // Parse target + target, err := p.parseInsertTarget() + if err != nil { + return nil, err + } + spec.Target = target + + // Parse optional column list + if p.curTok.Type == TokenLParen { + cols, err := p.parseColumnList() + if err != nil { + return nil, err + } + spec.Columns = cols + } + + // Parse OUTPUT clauses (can have OUTPUT INTO followed by OUTPUT) + for p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + outputClause, outputIntoClause, err := p.parseOutputClause() + if err != nil { + return nil, err + } + if outputIntoClause != nil { + spec.OutputIntoClause = outputIntoClause + } + if outputClause != nil { + spec.OutputClause = outputClause + } + } + + // Parse insert source + source, err := p.parseInsertSource() + if err != nil { + return nil, err + } + spec.InsertSource = source + + return spec, nil +} + func (p *Parser) parseDMLTarget() (ast.TableReference, error) { // Check for variable if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") { @@ -405,6 +472,16 @@ func (p *Parser) parseOpenRowset() (ast.TableReference, error) { return p.parseBulkOpenRowset() } + // Check for Cosmos form: OPENROWSET(PROVIDER = '...', CONNECTION = '...', ...) + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "PROVIDER" && p.peekTok.Type == TokenEquals { + return p.parseOpenRowsetCosmos() + } + + // Check for traditional form: OPENROWSET('provider', 'connstr', tablename) + if p.curTok.Type == TokenString { + return p.parseOpenRowsetTableReference() + } + // Parse identifier if p.curTok.Type != TokenIdent { return nil, fmt.Errorf("expected identifier in OPENROWSET, got %s", p.curTok.Literal) @@ -434,6 +511,192 @@ func (p *Parser) parseOpenRowset() (ast.TableReference, error) { }, nil } +func (p *Parser) parseOpenRowsetCosmos() (*ast.OpenRowsetCosmos, error) { + result := &ast.OpenRowsetCosmos{ + ForPath: false, + } + + // Parse options: PROVIDER = 'value', CONNECTION = 'value', etc. + // Note: Some option names like CREDENTIAL are keywords, so check for those too + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + // Check if this is a valid option name (identifier or keyword like CREDENTIAL) + optionName := strings.ToUpper(p.curTok.Literal) + isValidOption := p.curTok.Type == TokenIdent || p.curTok.Type == TokenCredential || + optionName == "PROVIDER" || optionName == "CONNECTION" || optionName == "OBJECT" || + optionName == "SERVER_CREDENTIAL" + if !isValidOption { + break + } + + p.nextToken() // consume option name + + if p.curTok.Type != TokenEquals { + return nil, fmt.Errorf("expected = after %s, got %s", optionName, p.curTok.Literal) + } + p.nextToken() // consume = + + // Parse option value + value, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + + // Map option names to expected OptionKind values + optionKind := optionName + switch optionName { + case "PROVIDER": + optionKind = "Provider" + case "CONNECTION": + optionKind = "Connection" + case "OBJECT": + optionKind = "Object" + case "CREDENTIAL": + optionKind = "Credential" + case "SERVER_CREDENTIAL": + optionKind = "Server_Credential" + } + + opt := &ast.LiteralOpenRowsetCosmosOption{ + Value: value, + OptionKind: optionKind, + } + result.Options = append(result.Options, opt) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in OPENROWSET, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse optional WITH (columns) + if strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef := &ast.OpenRowsetColumnDefinition{} + colDef.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + dataType, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + colDef.DataType = dataType + + result.WithColumns = append(result.WithColumns, colDef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + + // Parse optional alias + if p.curTok.Type == TokenAs { + p.nextToken() // consume AS + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + result.Alias = p.parseIdentifier() + } + } + + return result, nil +} + +func (p *Parser) parseOpenRowsetTableReference() (*ast.OpenRowsetTableReference, error) { + result := &ast.OpenRowsetTableReference{ + ForPath: false, + } + + // Parse provider name (string literal) + providerName, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + result.ProviderName = providerName + + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , after provider name, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + // Parse provider string (string literal) + providerString, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + result.ProviderString = providerString + + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , after provider string, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + // Parse object (schema object name or expression) + obj, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + result.Object = obj + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in OPENROWSET, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse optional WITH (columns) + if strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef := &ast.OpenRowsetColumnDefinition{} + colDef.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + dataType, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + colDef.DataType = dataType + + result.WithColumns = append(result.WithColumns, colDef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + + // Parse optional alias + if p.curTok.Type == TokenAs { + p.nextToken() // consume AS + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + result.Alias = p.parseIdentifier() + } + } + + return result, nil +} + func (p *Parser) parseBulkOpenRowset() (*ast.BulkOpenRowset, error) { // We're positioned on BULK, consume it p.nextToken() @@ -491,6 +754,60 @@ func (p *Parser) parseBulkOpenRowset() (*ast.BulkOpenRowset, error) { } p.nextToken() + // Parse optional WITH (column_definitions) - for schema specification + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + colDef := &ast.OpenRowsetColumnDefinition{} + colDef.ColumnIdentifier = p.parseIdentifier() + + // Parse data type + dataType, err := p.parseDataTypeReference() + if err != nil { + return nil, err + } + colDef.DataType = dataType + + // Parse optional COLLATE + if strings.ToUpper(p.curTok.Literal) == "COLLATE" { + p.nextToken() // consume COLLATE + colDef.Collation = p.parseIdentifier() + } + + // Parse optional column ordinal (integer) or JSON path (string) + if p.curTok.Type == TokenNumber { + colDef.ColumnOrdinal = &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + } + p.nextToken() + } else if p.curTok.Type == TokenString { + // JSON path specification like '$.stateName' or 'strict $.population' + colDef.ColumnOrdinal = &ast.StringLiteral{ + LiteralType: "String", + IsNational: false, + IsLargeObject: false, + Value: strings.Trim(p.curTok.Literal, "'"), + } + p.nextToken() + } + + result.WithColumns = append(result.WithColumns, colDef) + + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + // Parse optional alias if p.curTok.Type == TokenAs { p.nextToken() @@ -1320,6 +1637,77 @@ func (p *Parser) parseUpdateStatement() (*ast.UpdateStatement, error) { return stmt, nil } +// parseUpdateSpecification parses an UPDATE specification (used in DataModificationTableReference) +func (p *Parser) parseUpdateSpecification() (*ast.UpdateSpecification, error) { + // Consume UPDATE + p.nextToken() + + spec := &ast.UpdateSpecification{} + + // Check for TOP clause + if p.curTok.Type == TokenTop { + top, err := p.parseTopRowFilter() + if err != nil { + return nil, err + } + spec.TopRowFilter = top + } + + // Parse target + target, err := p.parseDMLTarget() + if err != nil { + return nil, err + } + spec.Target = target + + // Expect SET + if p.curTok.Type != TokenSet { + return nil, fmt.Errorf("expected SET, got %s", p.curTok.Literal) + } + p.nextToken() + + // Parse SET clauses + setClauses, err := p.parseSetClauses() + if err != nil { + return nil, err + } + spec.SetClauses = setClauses + + // Parse OUTPUT clauses (can have OUTPUT INTO followed by OUTPUT) + for p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + outputClause, outputIntoClause, err := p.parseOutputClause() + if err != nil { + return nil, err + } + if outputIntoClause != nil { + spec.OutputIntoClause = outputIntoClause + } + if outputClause != nil { + spec.OutputClause = outputClause + } + } + + // Parse optional FROM clause + if p.curTok.Type == TokenFrom { + fromClause, err := p.parseFromClause() + if err != nil { + return nil, err + } + spec.FromClause = fromClause + } + + // Parse optional WHERE clause + if p.curTok.Type == TokenWhere { + whereClause, err := p.parseWhereClause() + if err != nil { + return nil, err + } + spec.WhereClause = whereClause + } + + return spec, nil +} + func (p *Parser) parseSetClauses() ([]ast.SetClause, error) { var clauses []ast.SetClause @@ -1650,6 +2038,69 @@ func (p *Parser) parseDeleteStatement() (*ast.DeleteStatement, error) { return stmt, nil } +// parseDeleteSpecification parses a DELETE specification (used in DataModificationTableReference) +func (p *Parser) parseDeleteSpecification() (*ast.DeleteSpecification, error) { + // Consume DELETE + p.nextToken() + + spec := &ast.DeleteSpecification{} + + // Parse optional TOP clause + if p.curTok.Type == TokenTop { + topRowFilter, err := p.parseTopRowFilter() + if err != nil { + return nil, err + } + spec.TopRowFilter = topRowFilter + } + + // Skip optional FROM + if p.curTok.Type == TokenFrom { + p.nextToken() + } + + // Parse target + target, err := p.parseDMLTarget() + if err != nil { + return nil, err + } + spec.Target = target + + // Parse OUTPUT clauses (can have OUTPUT INTO followed by OUTPUT) + for p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "OUTPUT" { + outputClause, outputIntoClause, err := p.parseOutputClause() + if err != nil { + return nil, err + } + if outputIntoClause != nil { + spec.OutputIntoClause = outputIntoClause + } + if outputClause != nil { + spec.OutputClause = outputClause + } + } + + // Parse optional FROM clause + if p.curTok.Type == TokenFrom { + fromClause, err := p.parseFromClause() + if err != nil { + return nil, err + } + spec.FromClause = fromClause + } + + // Parse optional WHERE clause + if p.curTok.Type == TokenWhere { + whereClause, err := p.parseDeleteWhereClause() + if err != nil { + return nil, err + } + spec.WhereClause = whereClause + } + + return spec, nil +} + func (p *Parser) parseDeleteWhereClause() (*ast.WhereClause, error) { // Consume WHERE p.nextToken() diff --git a/parser/parse_select.go b/parser/parse_select.go index a9c8d4bc..1a764faf 100644 --- a/parser/parse_select.go +++ b/parser/parse_select.go @@ -308,6 +308,15 @@ func (p *Parser) parseQuerySpecificationWithInto() (*ast.QuerySpecification, *as qs.HavingClause = havingClause } + // Parse optional WINDOW clause + if strings.ToUpper(p.curTok.Literal) == "WINDOW" { + windowClause, err := p.parseWindowClause() + if err != nil { + return nil, nil, nil, err + } + qs.WindowClause = windowClause + } + // Note: ORDER BY is parsed at the top level in parseQueryExpressionWithInto // to correctly handle UNION/EXCEPT/INTERSECT cases @@ -530,7 +539,7 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } } else if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" { + if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ Value: alias.Value, @@ -670,7 +679,7 @@ func (p *Parser) parseSelectElement() (ast.SelectElement, error) { } else if p.curTok.Type == TokenIdent { // Check if this is an alias (not a keyword that starts a new clause) upper := strings.ToUpper(p.curTok.Literal) - if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" && upper != "COLLATE" { + if upper != "FROM" && upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "INTO" && upper != "UNION" && upper != "EXCEPT" && upper != "INTERSECT" && upper != "GO" && upper != "COLLATE" { alias := p.parseIdentifier() sse.ColumnName = &ast.IdentifierOrValueExpression{ Value: alias.Value, @@ -890,49 +899,10 @@ func (p *Parser) parsePostfixExpression() (ast.ScalarExpression, error) { // Check for OVER clause if strings.ToUpper(p.curTok.Literal) == "OVER" { - p.nextToken() // consume OVER - if p.curTok.Type != TokenLParen { - return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal) - } - p.nextToken() // consume ( - - overClause := &ast.OverClause{} - - // Parse PARTITION BY - if strings.ToUpper(p.curTok.Literal) == "PARTITION" { - p.nextToken() // consume PARTITION - if strings.ToUpper(p.curTok.Literal) == "BY" { - p.nextToken() // consume BY - } - // Parse partition expressions - for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - partExpr, err := p.parseScalarExpression() - if err != nil { - return nil, err - } - overClause.Partitions = append(overClause.Partitions, partExpr) - if p.curTok.Type == TokenComma { - p.nextToken() - } else { - break - } - } - } - - // Parse ORDER BY - if p.curTok.Type == TokenOrder { - orderBy, err := p.parseOrderByClause() - if err != nil { - return nil, err - } - overClause.OrderByClause = orderBy - } - - if p.curTok.Type != TokenRParen { - return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) + overClause, err := p.parseOverClause() + if err != nil { + return nil, err } - p.nextToken() // consume ) - fc.OverClause = overClause } @@ -1035,48 +1005,10 @@ func (p *Parser) handlePostfixOperations(expr ast.ScalarExpression) (ast.ScalarE // Check for OVER clause if strings.ToUpper(p.curTok.Literal) == "OVER" { - p.nextToken() // consume OVER - if p.curTok.Type != TokenLParen { - return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal) - } - p.nextToken() // consume ( - - overClause := &ast.OverClause{} - - // Parse PARTITION BY - if strings.ToUpper(p.curTok.Literal) == "PARTITION" { - p.nextToken() // consume PARTITION - if strings.ToUpper(p.curTok.Literal) == "BY" { - p.nextToken() // consume BY - } - for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - partExpr, err := p.parseScalarExpression() - if err != nil { - return nil, err - } - overClause.Partitions = append(overClause.Partitions, partExpr) - if p.curTok.Type == TokenComma { - p.nextToken() - } else { - break - } - } - } - - // Parse ORDER BY - if p.curTok.Type == TokenOrder { - orderBy, err := p.parseOrderByClause() - if err != nil { - return nil, err - } - overClause.OrderByClause = orderBy - } - - if p.curTok.Type != TokenRParen { - return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) + overClause, err := p.parseOverClause() + if err != nil { + return nil, err } - p.nextToken() // consume ) - fc.OverClause = overClause } @@ -1379,51 +1311,11 @@ func (p *Parser) parseNextValueForExpression() (*ast.NextValueForExpression, err expr.SequenceName = seqName // Check for optional OVER clause - if p.curTok.Type == TokenOver { - p.nextToken() // consume OVER - - if p.curTok.Type != TokenLParen { - return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal) - } - p.nextToken() // consume ( - - overClause := &ast.OverClause{} - - // Parse PARTITION BY - if strings.ToUpper(p.curTok.Literal) == "PARTITION" { - p.nextToken() // consume PARTITION - if strings.ToUpper(p.curTok.Literal) == "BY" { - p.nextToken() // consume BY - } - // Parse partition expressions - for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - partExpr, err := p.parseScalarExpression() - if err != nil { - return nil, err - } - overClause.Partitions = append(overClause.Partitions, partExpr) - if p.curTok.Type == TokenComma { - p.nextToken() - } else { - break - } - } - } - - // Parse ORDER BY - if p.curTok.Type == TokenOrder { - orderBy, err := p.parseOrderByClause() - if err != nil { - return nil, err - } - overClause.OrderByClause = orderBy - } - - if p.curTok.Type != TokenRParen { - return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) + if p.curTok.Type == TokenOver || strings.ToUpper(p.curTok.Literal) == "OVER" { + overClause, err := p.parseOverClause() + if err != nil { + return nil, err } - p.nextToken() // consume ) - expr.OverClause = overClause } @@ -2044,6 +1936,17 @@ func (p *Parser) parseFunctionCallFromIdentifiers(identifiers []*ast.Identifier) // Parse parameters funcNameUpper := strings.ToUpper(fc.FunctionName.Value) + + // Special handling for TRIM function with LEADING/TRAILING/BOTH options + if funcNameUpper == "TRIM" && p.curTok.Type != TokenRParen { + // Check for LEADING, TRAILING, or BOTH keyword + trimOpt := strings.ToUpper(p.curTok.Literal) + if trimOpt == "LEADING" || trimOpt == "TRAILING" || trimOpt == "BOTH" { + fc.TrimOptions = &ast.Identifier{Value: trimOpt, QuoteType: "NotQuoted"} + p.nextToken() + } + } + if p.curTok.Type != TokenRParen { for { param, err := p.parseScalarExpression() @@ -2206,50 +2109,10 @@ func (p *Parser) parsePostExpressionAccess(expr ast.ScalarExpression) (ast.Scala // Check for OVER clause for function calls if fc, ok := expr.(*ast.FunctionCall); ok && strings.ToUpper(p.curTok.Literal) == "OVER" { - p.nextToken() // consume OVER - - if p.curTok.Type != TokenLParen { - return nil, fmt.Errorf("expected ( after OVER, got %s", p.curTok.Literal) - } - p.nextToken() // consume ( - - overClause := &ast.OverClause{} - - // Parse PARTITION BY - if strings.ToUpper(p.curTok.Literal) == "PARTITION" { - p.nextToken() // consume PARTITION - if strings.ToUpper(p.curTok.Literal) == "BY" { - p.nextToken() // consume BY - } - // Parse partition expressions - for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { - partExpr, err := p.parseScalarExpression() - if err != nil { - return nil, err - } - overClause.Partitions = append(overClause.Partitions, partExpr) - if p.curTok.Type == TokenComma { - p.nextToken() - } else { - break - } - } - } - - // Parse ORDER BY - if p.curTok.Type == TokenOrder { - orderBy, err := p.parseOrderByClause() - if err != nil { - return nil, err - } - overClause.OrderByClause = orderBy - } - - if p.curTok.Type != TokenRParen { - return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) + overClause, err := p.parseOverClause() + if err != nil { + return nil, err } - p.nextToken() // consume ) - fc.OverClause = overClause } @@ -2301,6 +2164,21 @@ func (p *Parser) parseTableReference() (ast.TableReference, error) { } var left ast.TableReference = baseRef + // Check for PIVOT or UNPIVOT + if strings.ToUpper(p.curTok.Literal) == "PIVOT" { + pivoted, err := p.parsePivotedTableReference(left) + if err != nil { + return nil, err + } + left = pivoted + } else if strings.ToUpper(p.curTok.Literal) == "UNPIVOT" { + unpivoted, err := p.parseUnpivotedTableReference(left) + if err != nil { + return nil, err + } + left = unpivoted + } + // Check for JOINs for { // Check for CROSS JOIN or CROSS APPLY @@ -2436,6 +2314,11 @@ func (p *Parser) parseSingleTableReference() (ast.TableReference, error) { return p.parsePredictTableReference() } + // Check for CHANGETABLE + if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "CHANGETABLE" { + return p.parseChangeTableReference() + } + // Check for full-text table functions (CONTAINSTABLE, FREETEXTTABLE) if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) @@ -2471,9 +2354,57 @@ func (p *Parser) parseSingleTableReference() (ast.TableReference, error) { if err != nil { return nil, err } + + // Parse optional alias (AS alias or just alias) and optional column list + var alias *ast.Identifier + var columns []*ast.Identifier + if p.curTok.Type == TokenAs { + p.nextToken() + alias = p.parseIdentifier() + } else if p.curTok.Type == TokenIdent { + upper := strings.ToUpper(p.curTok.Literal) + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && + upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && + upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && + upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { + alias = p.parseIdentifier() + } + } + // Check for column list: alias(c1, c2, ...) + if alias != nil && p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + columns = append(columns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + // Use GlobalFunctionTableReference for specific built-in global functions + if son.Count == 1 && son.BaseIdentifier != nil { + upper := strings.ToUpper(son.BaseIdentifier.Value) + if upper == "STRING_SPLIT" || upper == "GENERATE_SERIES" { + return &ast.GlobalFunctionTableReference{ + Name: son.BaseIdentifier, + Parameters: params, + Alias: alias, + Columns: columns, + ForPath: false, + }, nil + } + } + ref := &ast.SchemaObjectFunctionTableReference{ SchemaObject: son, Parameters: params, + Alias: alias, + Columns: columns, ForPath: false, } return ref, nil @@ -2484,9 +2415,30 @@ func (p *Parser) parseSingleTableReference() (ast.TableReference, error) { } // parseDerivedTableReference parses a derived table (parenthesized query) like (SELECT ...) AS alias -func (p *Parser) parseDerivedTableReference() (*ast.QueryDerivedTable, error) { +// or an inline derived table (VALUES clause) like (VALUES (...), (...)) AS alias(cols) +// or a data modification table reference (DML with OUTPUT) like (INSERT ... OUTPUT ...) AS alias +func (p *Parser) parseDerivedTableReference() (ast.TableReference, error) { p.nextToken() // consume ( + // Check for VALUES clause (inline derived table) + if strings.ToUpper(p.curTok.Literal) == "VALUES" { + return p.parseInlineDerivedTable() + } + + // Check for DML statements (INSERT, UPDATE, DELETE, MERGE) as table sources + if p.curTok.Type == TokenInsert { + return p.parseDataModificationTableReference("INSERT") + } + if p.curTok.Type == TokenUpdate { + return p.parseDataModificationTableReference("UPDATE") + } + if p.curTok.Type == TokenDelete { + return p.parseDataModificationTableReference("DELETE") + } + if strings.ToUpper(p.curTok.Literal) == "MERGE" { + return p.parseDataModificationTableReference("MERGE") + } + // Parse the query expression qe, err := p.parseQueryExpression() if err != nil { @@ -2512,7 +2464,7 @@ func (p *Parser) parseDerivedTableReference() (*ast.QueryDerivedTable, error) { // Could be an alias without AS, but need to be careful not to consume keywords if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" && upper != "PIVOT" && upper != "UNPIVOT" { ref.Alias = p.parseIdentifier() } } else { @@ -2523,6 +2475,149 @@ func (p *Parser) parseDerivedTableReference() (*ast.QueryDerivedTable, error) { return ref, nil } +// parseDataModificationTableReference parses a DML statement used as a table source +// This is called after ( is consumed and the DML keyword is the current token +func (p *Parser) parseDataModificationTableReference(dmlType string) (*ast.DataModificationTableReference, error) { + ref := &ast.DataModificationTableReference{ + ForPath: false, + } + + var err error + switch dmlType { + case "INSERT": + spec, parseErr := p.parseInsertSpecification() + if parseErr != nil { + return nil, parseErr + } + ref.DataModificationSpecification = spec + case "UPDATE": + spec, parseErr := p.parseUpdateSpecification() + if parseErr != nil { + return nil, parseErr + } + ref.DataModificationSpecification = spec + case "DELETE": + spec, parseErr := p.parseDeleteSpecification() + if parseErr != nil { + return nil, parseErr + } + ref.DataModificationSpecification = spec + case "MERGE": + spec, parseErr := p.parseMergeSpecification() + if parseErr != nil { + return nil, parseErr + } + ref.DataModificationSpecification = spec + default: + return nil, fmt.Errorf("unknown DML type: %s", dmlType) + } + if err != nil { + return nil, err + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after data modification statement, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse required alias (AS alias) + if p.curTok.Type == TokenAs { + p.nextToken() + ref.Alias = p.parseIdentifier() + } else if p.curTok.Type == TokenIdent { + upper := strings.ToUpper(p.curTok.Literal) + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && + upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && + upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && + upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { + ref.Alias = p.parseIdentifier() + } + } + + return ref, nil +} + +// parseInlineDerivedTable parses a VALUES clause used as a table source +// Called after ( is consumed and VALUES is the current token +func (p *Parser) parseInlineDerivedTable() (*ast.InlineDerivedTable, error) { + p.nextToken() // consume VALUES + + ref := &ast.InlineDerivedTable{ + ForPath: false, + } + + // Parse row values: (val1, val2), (val3, val4), ... + for { + if p.curTok.Type != TokenLParen { + break + } + p.nextToken() // consume ( + + row := &ast.RowValue{} + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + expr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + row.ColumnValues = append(row.ColumnValues, expr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + ref.RowValues = append(ref.RowValues, row) + + if p.curTok.Type == TokenComma { + p.nextToken() // consume , between rows + } else { + break + } + } + + // Expect ) to close the VALUES clause + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after VALUES clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse optional alias: AS alias or just alias + if p.curTok.Type == TokenAs { + p.nextToken() + ref.Alias = p.parseIdentifier() + } else if p.curTok.Type == TokenIdent { + upper := strings.ToUpper(p.curTok.Literal) + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && + upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && + upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && + upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { + ref.Alias = p.parseIdentifier() + } + } + + // Parse optional column list: alias(col1, col2, ...) + if ref.Alias != nil && p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ref.Columns = append(ref.Columns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + return ref, nil +} + func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { ref := &ast.NamedTableReference{ ForPath: false, @@ -2535,6 +2630,36 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { } ref.SchemaObject = son + // T-SQL supports two syntaxes for table hints: + // 1. Old-style: table_name (nolock) AS alias - hints before alias, no WITH + // 2. New-style: table_name AS alias WITH (hints) - alias before hints, WITH required + + // Check for old-style hints (without WITH keyword): table (nolock) as alias + if p.curTok.Type == TokenLParen && p.peekIsTableHint() { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + hint, err := p.parseTableHint() + if err != nil { + return nil, err + } + if hint != nil { + ref.TableHints = append(ref.TableHints, hint) + } + 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 + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + // Parse optional alias (AS alias or just alias) if p.curTok.Type == TokenAs { p.nextToken() @@ -2546,20 +2671,16 @@ func (p *Parser) parseNamedTableReference() (*ast.NamedTableReference, error) { } else if p.curTok.Type == TokenIdent { // Could be an alias without AS, but need to be careful not to consume keywords upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" && upper != "PIVOT" && upper != "UNPIVOT" { ref.Alias = &ast.Identifier{Value: p.curTok.Literal, QuoteType: "NotQuoted"} p.nextToken() } } - // Parse optional table hints WITH (hint, hint, ...) or old-style (hint, hint, ...) + // Check for new-style hints (with WITH keyword): alias WITH (hints) if p.curTok.Type == TokenWith && p.peekTok.Type == TokenLParen { p.nextToken() // consume WITH - } - if p.curTok.Type == TokenLParen { - // Check if this looks like hints (first token is a hint keyword) - // Save position to peek - if p.peekIsTableHint() { + if p.curTok.Type == TokenLParen && p.peekIsTableHint() { p.nextToken() // consume ( for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { hint, err := p.parseTableHint() @@ -2572,9 +2693,8 @@ 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 + continue } break } @@ -2595,8 +2715,37 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a ForPath: false, } - // Parse optional alias (AS alias or just alias) - if p.curTok.Type == TokenAs { + // T-SQL supports two syntaxes for table hints: + // 1. Old-style: table_name (nolock) AS alias - hints before alias, no WITH + // 2. New-style: table_name AS alias WITH (hints) - alias before hints, WITH required + + // Check for old-style hints (without WITH keyword): table (nolock) as alias + if p.curTok.Type == TokenLParen && p.peekIsTableHint() { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + hint, err := p.parseTableHint() + if err != nil { + return nil, err + } + if hint != nil { + ref.TableHints = append(ref.TableHints, hint) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else if p.curTok.Type != TokenRParen { + if p.isTableHintToken() { + continue + } + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + // Parse optional alias (AS alias or just alias) + if p.curTok.Type == TokenAs { p.nextToken() if p.curTok.Type != TokenIdent && p.curTok.Type != TokenLBracket { return nil, fmt.Errorf("expected identifier after AS, got %s", p.curTok.Literal) @@ -2606,7 +2755,7 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a // Could be an alias without AS, but need to be careful not to consume keywords if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" && upper != "USING" && upper != "WHEN" && upper != "OUTPUT" && upper != "PIVOT" && upper != "UNPIVOT" { ref.Alias = p.parseIdentifier() } } else { @@ -2614,13 +2763,10 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a } } - // Parse optional table hints WITH (hint, hint, ...) or old-style (hint, hint, ...) + // Check for new-style hints (with WITH keyword): alias WITH (hints) if p.curTok.Type == TokenWith && p.peekTok.Type == TokenLParen { p.nextToken() // consume WITH - } - if p.curTok.Type == TokenLParen { - // Check if this looks like hints (first token is a hint keyword) - if p.peekIsTableHint() { + if p.curTok.Type == TokenLParen && p.peekIsTableHint() { p.nextToken() // consume ( for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { hint, err := p.parseTableHint() @@ -2803,7 +2949,7 @@ func (p *Parser) parseFullTextTableReference(funcType string) (*ast.FullTextTabl ref.Alias = p.parseIdentifier() } else if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { ref.Alias = p.parseIdentifier() } } @@ -2937,7 +3083,7 @@ func (p *Parser) parseSemanticTableReference(funcType string) (*ast.SemanticTabl ref.Alias = p.parseIdentifier() } else if p.curTok.Type == TokenIdent { upper := strings.ToUpper(p.curTok.Literal) - if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { + if upper != "WHERE" && upper != "GROUP" && upper != "HAVING" && upper != "WINDOW" && upper != "ORDER" && upper != "OPTION" && upper != "GO" && upper != "WITH" && upper != "ON" && upper != "JOIN" && upper != "INNER" && upper != "LEFT" && upper != "RIGHT" && upper != "FULL" && upper != "CROSS" && upper != "OUTER" && upper != "FOR" { ref.Alias = p.parseIdentifier() } } @@ -3028,6 +3174,61 @@ func (p *Parser) parseTableHint() (ast.TableHintType, error) { return hint, nil } + // FORCESEEK hint with optional index and column list + if hintName == "FORCESEEK" { + hint := &ast.ForceSeekTableHint{ + HintKind: "ForceSeek", + } + // Check for optional parenthesis with index and columns + if p.curTok.Type != TokenLParen { + return hint, nil + } + p.nextToken() // consume ( + // Parse index value (identifier or number) + if p.curTok.Type == TokenNumber { + hint.IndexValue = &ast.IdentifierOrValueExpression{ + Value: p.curTok.Literal, + ValueExpression: &ast.IntegerLiteral{ + LiteralType: "Integer", + Value: p.curTok.Literal, + }, + } + p.nextToken() + } else if p.curTok.Type == TokenIdent { + hint.IndexValue = &ast.IdentifierOrValueExpression{ + Value: p.curTok.Literal, + Identifier: &ast.Identifier{ + Value: p.curTok.Literal, + QuoteType: "NotQuoted", + }, + } + p.nextToken() + } + // Parse optional column list + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col, _ := p.parseColumnReference() + if col != nil { + hint.ColumnValues = append(hint.ColumnValues, col) + } + if p.curTok.Type == TokenComma { + p.nextToken() + } else if p.curTok.Type != TokenRParen { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + // Consume outer ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + return hint, nil + } + // Map hint names to HintKind hintKind := getTableHintKind(hintName) if hintKind == "" { @@ -3072,6 +3273,10 @@ func getTableHintKind(name string) string { return "XLock" case "NOWAIT": return "NoWait" + case "FORCESEEK": + return "ForceSeek" + case "FORCESCAN": + return "ForceScan" default: return "" } @@ -5660,3 +5865,650 @@ func (p *Parser) parseParseCall(isTry bool) (ast.ScalarExpression, error) { Culture: culture, }, nil } + +// parseChangeTableReference parses CHANGETABLE(CHANGES ...) or CHANGETABLE(VERSION ...) +func (p *Parser) parseChangeTableReference() (ast.TableReference, error) { + p.nextToken() // consume CHANGETABLE + + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after CHANGETABLE, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + upper := strings.ToUpper(p.curTok.Literal) + if upper == "CHANGES" { + return p.parseChangeTableChangesReference() + } else if upper == "VERSION" { + return p.parseChangeTableVersionReference() + } + + return nil, fmt.Errorf("expected CHANGES or VERSION after CHANGETABLE(, got %s", p.curTok.Literal) +} + +// parseChangeTableChangesReference parses CHANGETABLE(CHANGES table, version [, FORCESEEK]) +func (p *Parser) parseChangeTableChangesReference() (*ast.ChangeTableChangesTableReference, error) { + p.nextToken() // consume CHANGES + + ref := &ast.ChangeTableChangesTableReference{ + ForPath: false, + } + + // Parse target table + son, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + ref.Target = son + + // Expect comma + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , after table name in CHANGETABLE, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + // Parse since version + version, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + ref.SinceVersion = version + + // Check for optional FORCESEEK + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + if strings.ToUpper(p.curTok.Literal) == "FORCESEEK" { + ref.ForceSeek = true + p.nextToken() + } + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after CHANGETABLE arguments, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse AS alias + if p.curTok.Type != TokenAs { + return nil, fmt.Errorf("expected AS after CHANGETABLE(...), got %s", p.curTok.Literal) + } + p.nextToken() // consume AS + ref.Alias = p.parseIdentifier() + + // Check for column list: alias(c1, c2, ...) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ref.Columns = append(ref.Columns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return ref, nil +} + +// parseChangeTableVersionReference parses CHANGETABLE(VERSION table, (cols), (vals) [, FORCESEEK]) +func (p *Parser) parseChangeTableVersionReference() (*ast.ChangeTableVersionTableReference, error) { + p.nextToken() // consume VERSION + + ref := &ast.ChangeTableVersionTableReference{ + ForPath: false, + } + + // Parse target table + son, err := p.parseSchemaObjectName() + if err != nil { + return nil, err + } + ref.Target = son + + // Expect comma + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , after table name in CHANGETABLE VERSION, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + // Parse primary key columns: (c1, c2, ...) + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( for primary key columns, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ref.PrimaryKeyColumns = append(ref.PrimaryKeyColumns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + // Expect comma + if p.curTok.Type != TokenComma { + return nil, fmt.Errorf("expected , after primary key columns, got %s", p.curTok.Literal) + } + p.nextToken() // consume , + + // Parse primary key values: (v1, v2, ...) + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( for primary key values, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + val, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + ref.PrimaryKeyValues = append(ref.PrimaryKeyValues, val) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + + // Check for optional FORCESEEK + if p.curTok.Type == TokenComma { + p.nextToken() // consume , + if strings.ToUpper(p.curTok.Literal) == "FORCESEEK" { + ref.ForceSeek = true + p.nextToken() + } + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after CHANGETABLE VERSION arguments, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse AS alias + if p.curTok.Type != TokenAs { + return nil, fmt.Errorf("expected AS after CHANGETABLE(...), got %s", p.curTok.Literal) + } + p.nextToken() // consume AS + ref.Alias = p.parseIdentifier() + + // Check for column list: alias(c1, c2, ...) + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + ref.Columns = append(ref.Columns, p.parseIdentifier()) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + return ref, nil +} + +// parseOverClause parses an OVER clause after a function call +// Handles both: OVER Win1 and OVER (PARTITION BY c1 ORDER BY c2 ROWS ...) +func (p *Parser) parseOverClause() (*ast.OverClause, error) { + // Current token should be OVER, consume it + p.nextToken() // consume OVER + + overClause := &ast.OverClause{} + + // Check if it's just a window name (no parentheses) + if p.curTok.Type != TokenLParen { + // It's OVER WindowName + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + overClause.WindowName = p.parseIdentifier() + return overClause, nil + } + return nil, fmt.Errorf("expected ( or window name after OVER, got %s", p.curTok.Literal) + } + + p.nextToken() // consume ( + + // Check if it starts with a window name reference + // OVER (Win1 ORDER BY ...) or OVER (Win1 PARTITION BY ... ) + // This is tricky because we need to distinguish between Win1 (window name) and c1 (column name in PARTITION BY) + if p.curTok.Type == TokenIdent && p.peekTok.Type != TokenComma && p.peekTok.Type != TokenRParen { + upperPeek := strings.ToUpper(p.peekTok.Literal) + if upperPeek != "BY" && upperPeek != "," { + // Could be a window name reference if followed by ORDER, PARTITION, ROWS, RANGE, or ) + if upperPeek == "ORDER" || upperPeek == "PARTITION" || upperPeek == "ROWS" || upperPeek == "RANGE" || p.peekTok.Type == TokenRParen { + overClause.WindowName = p.parseIdentifier() + } + } + } + + // Parse PARTITION BY + if strings.ToUpper(p.curTok.Literal) == "PARTITION" { + p.nextToken() // consume PARTITION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + // Parse partition expressions + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if strings.ToUpper(p.curTok.Literal) == "ORDER" || strings.ToUpper(p.curTok.Literal) == "ROWS" || strings.ToUpper(p.curTok.Literal) == "RANGE" { + break + } + partExpr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + overClause.Partitions = append(overClause.Partitions, partExpr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + + // Parse ORDER BY + if p.curTok.Type == TokenOrder { + orderBy, err := p.parseOrderByClause() + if err != nil { + return nil, err + } + overClause.OrderByClause = orderBy + } + + // Parse window frame (ROWS/RANGE) + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "ROWS" || upperLit == "RANGE" { + frameClause, err := p.parseWindowFrameClause() + if err != nil { + return nil, err + } + overClause.WindowFrameClause = frameClause + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in OVER clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + return overClause, nil +} + +// parseWindowFrameClause parses ROWS/RANGE ... BETWEEN ... AND ... +func (p *Parser) parseWindowFrameClause() (*ast.WindowFrameClause, error) { + frame := &ast.WindowFrameClause{} + + // Parse ROWS or RANGE + upperLit := strings.ToUpper(p.curTok.Literal) + if upperLit == "ROWS" { + frame.WindowFrameType = "Rows" + } else if upperLit == "RANGE" { + frame.WindowFrameType = "Range" + } else { + return nil, fmt.Errorf("expected ROWS or RANGE, got %s", p.curTok.Literal) + } + p.nextToken() + + // Parse BETWEEN or single boundary + if strings.ToUpper(p.curTok.Literal) == "BETWEEN" { + p.nextToken() // consume BETWEEN + top, err := p.parseWindowDelimiter() + if err != nil { + return nil, err + } + frame.Top = top + + if strings.ToUpper(p.curTok.Literal) != "AND" { + return nil, fmt.Errorf("expected AND in ROWS BETWEEN, got %s", p.curTok.Literal) + } + p.nextToken() // consume AND + + bottom, err := p.parseWindowDelimiter() + if err != nil { + return nil, err + } + frame.Bottom = bottom + } else { + // Single boundary (e.g., ROWS UNBOUNDED PRECEDING) + top, err := p.parseWindowDelimiter() + if err != nil { + return nil, err + } + frame.Top = top + } + + return frame, nil +} + +// parseWindowDelimiter parses UNBOUNDED PRECEDING/FOLLOWING, CURRENT ROW, n PRECEDING/FOLLOWING +func (p *Parser) parseWindowDelimiter() (*ast.WindowDelimiter, error) { + delim := &ast.WindowDelimiter{} + + upperLit := strings.ToUpper(p.curTok.Literal) + + if upperLit == "CURRENT" { + p.nextToken() // consume CURRENT + if strings.ToUpper(p.curTok.Literal) != "ROW" { + return nil, fmt.Errorf("expected ROW after CURRENT, got %s", p.curTok.Literal) + } + p.nextToken() // consume ROW + delim.WindowDelimiterType = "CurrentRow" + } else if upperLit == "UNBOUNDED" { + p.nextToken() // consume UNBOUNDED + upperDir := strings.ToUpper(p.curTok.Literal) + if upperDir == "PRECEDING" { + delim.WindowDelimiterType = "UnboundedPreceding" + } else if upperDir == "FOLLOWING" { + delim.WindowDelimiterType = "UnboundedFollowing" + } else { + return nil, fmt.Errorf("expected PRECEDING or FOLLOWING after UNBOUNDED, got %s", p.curTok.Literal) + } + p.nextToken() + } else { + // n PRECEDING or n FOLLOWING + offset, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + delim.OffsetValue = offset + + upperDir := strings.ToUpper(p.curTok.Literal) + if upperDir == "PRECEDING" { + delim.WindowDelimiterType = "ValuePreceding" + } else if upperDir == "FOLLOWING" { + delim.WindowDelimiterType = "ValueFollowing" + } else { + return nil, fmt.Errorf("expected PRECEDING or FOLLOWING after value, got %s", p.curTok.Literal) + } + p.nextToken() + } + + return delim, nil +} + +// parseWindowClause parses WINDOW Win1 AS (...), Win2 AS (...) +func (p *Parser) parseWindowClause() (*ast.WindowClause, error) { + p.nextToken() // consume WINDOW + + clause := &ast.WindowClause{} + + for { + def := &ast.WindowDefinition{} + + // Parse window name + def.WindowName = p.parseIdentifier() + + // Expect AS + if strings.ToUpper(p.curTok.Literal) != "AS" { + return nil, fmt.Errorf("expected AS after window name, got %s", p.curTok.Literal) + } + p.nextToken() // consume AS + + // Expect ( + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after AS in window definition, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Check if it references another window name + if p.curTok.Type == TokenIdent { + upperPeek := strings.ToUpper(p.peekTok.Literal) + // It's a reference if followed by ) or PARTITION or ORDER + if p.peekTok.Type == TokenRParen || upperPeek == "PARTITION" || upperPeek == "ORDER" { + // Could be a window name reference + if p.peekTok.Type == TokenRParen { + // Just a window name reference: Win1 AS (Win2) + def.RefWindowName = p.parseIdentifier() + } else if upperPeek != "BY" { + // Window name followed by more clauses + def.RefWindowName = p.parseIdentifier() + } + } + } + + // Parse PARTITION BY + if strings.ToUpper(p.curTok.Literal) == "PARTITION" { + p.nextToken() // consume PARTITION + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + } + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + if strings.ToUpper(p.curTok.Literal) == "ORDER" { + break + } + partExpr, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + def.Partitions = append(def.Partitions, partExpr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + + // Parse ORDER BY + if p.curTok.Type == TokenOrder { + orderBy, err := p.parseOrderByClause() + if err != nil { + return nil, err + } + def.OrderByClause = orderBy + } + + // Expect ) + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) in window definition, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + clause.WindowDefinition = append(clause.WindowDefinition, def) + + // Check for comma (more window definitions) + if p.curTok.Type != TokenComma { + break + } + p.nextToken() // consume , + } + + return clause, nil +} + +// parsePivotedTableReference parses PIVOT clause +// Syntax: table PIVOT (aggregate_func(columns) FOR pivot_column IN (value1, value2, ...)) AS alias +func (p *Parser) parsePivotedTableReference(tableRef ast.TableReference) (*ast.PivotedTableReference, error) { + p.nextToken() // consume PIVOT + + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after PIVOT, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + pivoted := &ast.PivotedTableReference{ + TableReference: tableRef, + ForPath: false, + } + + // Parse aggregate function identifier (may be multi-part like dbo.z1.MyAggregate) + aggregateId := &ast.MultiPartIdentifier{} + for { + id := p.parseIdentifier() + aggregateId.Identifiers = append(aggregateId.Identifiers, id) + aggregateId.Count++ + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + } else { + break + } + } + pivoted.AggregateFunctionIdentifier = aggregateId + + // Expect ( for aggregate function parameters + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( for aggregate function parameters, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse value columns (parameters to aggregate function) + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col, err := p.parseColumnReference() + if err != nil { + return nil, err + } + pivoted.ValueColumns = append(pivoted.ValueColumns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after aggregate function parameters, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Expect FOR keyword + if strings.ToUpper(p.curTok.Literal) != "FOR" { + return nil, fmt.Errorf("expected FOR in PIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume FOR + + // Parse pivot column + col, err := p.parseColumnReference() + if err != nil { + return nil, err + } + pivoted.PivotColumn = col + + // Expect IN keyword + if p.curTok.Type != TokenIn { + return nil, fmt.Errorf("expected IN in PIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume IN + + // Expect ( + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after IN, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse IN columns (values) + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + id := p.parseIdentifier() + pivoted.InColumns = append(pivoted.InColumns, id) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after IN values, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Expect ) to close PIVOT clause + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) to close PIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse required alias (AS alias) + if p.curTok.Type == TokenAs { + p.nextToken() + } + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + pivoted.Alias = p.parseIdentifier() + } + + return pivoted, nil +} + +// parseUnpivotedTableReference parses UNPIVOT clause +func (p *Parser) parseUnpivotedTableReference(tableRef ast.TableReference) (*ast.UnpivotedTableReference, error) { + p.nextToken() // consume UNPIVOT + + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after UNPIVOT, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + unpivoted := &ast.UnpivotedTableReference{ + TableReference: tableRef, + NullHandling: "None", + ForPath: false, + } + + // Parse pivot value column + unpivoted.PivotValue = p.parseIdentifier() + + // Expect FOR keyword + if strings.ToUpper(p.curTok.Literal) != "FOR" { + return nil, fmt.Errorf("expected FOR in UNPIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume FOR + + // Parse pivot column + unpivoted.PivotColumn = p.parseIdentifier() + + // Expect IN keyword + if p.curTok.Type != TokenIn { + return nil, fmt.Errorf("expected IN in UNPIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume IN + + // Expect ( + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected ( after IN, got %s", p.curTok.Literal) + } + p.nextToken() // consume ( + + // Parse IN columns + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + col, err := p.parseColumnReference() + if err != nil { + return nil, err + } + unpivoted.InColumns = append(unpivoted.InColumns, col) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) after IN columns, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Expect ) to close UNPIVOT clause + if p.curTok.Type != TokenRParen { + return nil, fmt.Errorf("expected ) to close UNPIVOT clause, got %s", p.curTok.Literal) + } + p.nextToken() // consume ) + + // Parse required alias (AS alias) + if p.curTok.Type == TokenAs { + p.nextToken() + } + if p.curTok.Type == TokenIdent || p.curTok.Type == TokenLBracket { + unpivoted.Alias = p.parseIdentifier() + } + + return unpivoted, nil +} diff --git a/parser/parse_statements.go b/parser/parse_statements.go index af18b5cc..8ebf5aa8 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -479,6 +479,10 @@ func (p *Parser) parseInlineIndexDefinition() (*ast.IndexDefinition, error) { // Implicit NONCLUSTERED COLUMNSTORE indexDef.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredColumnStore"} p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "HASH" { + // Implicit NONCLUSTERED HASH + indexDef.IndexType = &ast.IndexType{IndexTypeKind: "NonClusteredHash"} + p.nextToken() } // Parse column list @@ -747,9 +751,10 @@ func (p *Parser) parseInlineIndexDefinition() (*ast.IndexDefinition, error) { // Parse optional ON filegroup clause if p.curTok.Type == TokenOn { p.nextToken() // consume ON + fgName := p.curTok.Literal fg := &ast.FileGroupOrPartitionScheme{ Name: &ast.IdentifierOrValueExpression{ - Value: p.curTok.Literal, + Value: fgName, Identifier: p.parseIdentifier(), }, } @@ -759,8 +764,9 @@ func (p *Parser) parseInlineIndexDefinition() (*ast.IndexDefinition, error) { // Parse optional FILESTREAM_ON clause if strings.ToUpper(p.curTok.Literal) == "FILESTREAM_ON" { p.nextToken() // consume FILESTREAM_ON + fsName := p.curTok.Literal indexDef.FileStreamOn = &ast.IdentifierOrValueExpression{ - Value: p.curTok.Literal, + Value: fsName, Identifier: p.parseIdentifier(), } } @@ -3643,21 +3649,9 @@ func (p *Parser) parseCreateDatabaseAuditSpecificationStatement() (*ast.CreateDa for { upperLit := strings.ToUpper(p.curTok.Literal) if upperLit == "ADD" || upperLit == "DROP" { - part := &ast.AuditSpecificationPart{ - IsDrop: upperLit == "DROP", - } - p.nextToken() // consume ADD/DROP - if p.curTok.Type == TokenLParen { - p.nextToken() // consume ( - // Parse audit action group reference - groupName := p.curTok.Literal - part.Details = &ast.AuditActionGroupReference{ - Group: convertAuditGroupName(groupName), - } - p.nextToken() // consume group name - if p.curTok.Type == TokenRParen { - p.nextToken() // consume ) - } + part, err := p.parseAuditSpecificationPart(upperLit == "DROP") + if err != nil { + return nil, err } stmt.Parts = append(stmt.Parts, part) if p.curTok.Type == TokenComma { @@ -3718,21 +3712,9 @@ func (p *Parser) parseAlterDatabaseAuditSpecificationStatement() (*ast.AlterData for { upperLit := strings.ToUpper(p.curTok.Literal) if upperLit == "ADD" || upperLit == "DROP" { - part := &ast.AuditSpecificationPart{ - IsDrop: upperLit == "DROP", - } - p.nextToken() // consume ADD/DROP - if p.curTok.Type == TokenLParen { - p.nextToken() // consume ( - // Parse audit action group reference - groupName := p.curTok.Literal - part.Details = &ast.AuditActionGroupReference{ - Group: convertAuditGroupName(groupName), - } - p.nextToken() // consume group name - if p.curTok.Type == TokenRParen { - p.nextToken() // consume ) - } + part, err := p.parseAuditSpecificationPart(upperLit == "DROP") + if err != nil { + return nil, err } stmt.Parts = append(stmt.Parts, part) if p.curTok.Type == TokenComma { @@ -3778,6 +3760,27 @@ func convertAuditGroupName(name string) string { "DATABASE_LOGOUT_GROUP": "DatabaseLogoutGroup", "USER_CHANGE_PASSWORD_GROUP": "UserChangePasswordGroup", "USER_DEFINED_AUDIT_GROUP": "UserDefinedAuditGroup", + "DATABASE_PERMISSION_CHANGE_GROUP": "DatabasePermissionChange", + "SCHEMA_OBJECT_PERMISSION_CHANGE_GROUP": "SchemaObjectPermissionChange", + "DATABASE_ROLE_MEMBER_CHANGE_GROUP": "DatabaseRoleMemberChange", + "APPLICATION_ROLE_CHANGE_PASSWORD_GROUP": "ApplicationRoleChangePassword", + "SCHEMA_OBJECT_ACCESS_GROUP": "SchemaObjectAccess", + "BACKUP_RESTORE_GROUP": "BackupRestore", + "DBCC_GROUP": "Dbcc", + "AUDIT_CHANGE_GROUP": "AuditChange", + "DATABASE_CHANGE_GROUP": "DatabaseChange", + "DATABASE_OBJECT_CHANGE_GROUP": "DatabaseObjectChange", + "DATABASE_PRINCIPAL_CHANGE_GROUP": "DatabasePrincipalChange", + "SCHEMA_OBJECT_CHANGE_GROUP": "SchemaObjectChange", + "DATABASE_PRINCIPAL_IMPERSONATION_GROUP": "DatabasePrincipalImpersonation", + "DATABASE_OBJECT_OWNERSHIP_CHANGE_GROUP": "DatabaseObjectOwnershipChange", + "DATABASE_OWNERSHIP_CHANGE_GROUP": "DatabaseOwnershipChange", + "SCHEMA_OBJECT_OWNERSHIP_CHANGE_GROUP": "SchemaObjectOwnershipChange", + "DATABASE_OBJECT_PERMISSION_CHANGE_GROUP": "DatabaseObjectPermissionChange", + "DATABASE_OPERATION_GROUP": "DatabaseOperation", + "DATABASE_OBJECT_ACCESS_GROUP": "DatabaseObjectAccess", + "BATCH_COMPLETED_GROUP": "BatchCompletedGroup", + "BATCH_STARTED_GROUP": "BatchStartedGroup", } if mapped, ok := groupMap[strings.ToUpper(name)]; ok { return mapped @@ -3785,6 +3788,121 @@ func convertAuditGroupName(name string) string { return capitalizeFirst(strings.ToLower(strings.ReplaceAll(name, "_", " "))) } +// isAuditAction checks if the given word is a database audit action +func isAuditAction(word string) bool { + actions := map[string]bool{ + "SELECT": true, "INSERT": true, "UPDATE": true, "DELETE": true, + "EXECUTE": true, "RECEIVE": true, "REFERENCES": true, + } + return actions[word] +} + +// convertAuditActionKind converts audit action to expected format +func convertAuditActionKind(action string) string { + actionMap := map[string]string{ + "SELECT": "Select", + "INSERT": "Insert", + "UPDATE": "Update", + "DELETE": "Delete", + "EXECUTE": "Execute", + "RECEIVE": "Receive", + "REFERENCES": "References", + } + if mapped, ok := actionMap[action]; ok { + return mapped + } + return capitalizeFirst(strings.ToLower(action)) +} + +// parseAuditSpecificationPart parses an ADD or DROP part of an audit specification +func (p *Parser) parseAuditSpecificationPart(isDrop bool) (*ast.AuditSpecificationPart, error) { + part := &ast.AuditSpecificationPart{ + IsDrop: isDrop, + } + p.nextToken() // consume ADD/DROP + + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + + // Check if it's an action specification (SELECT, INSERT, etc.) or an audit group + firstWord := strings.ToUpper(p.curTok.Literal) + if isAuditAction(firstWord) { + // Parse action specification + spec := &ast.AuditActionSpecification{} + + // Parse actions + for { + actionKind := convertAuditActionKind(strings.ToUpper(p.curTok.Literal)) + spec.Actions = append(spec.Actions, &ast.DatabaseAuditAction{ActionKind: actionKind}) + p.nextToken() + if p.curTok.Type == TokenComma { + p.nextToken() + // Check if next is ON (end of actions) or another action + if strings.ToUpper(p.curTok.Literal) == "ON" { + break + } + } else { + break + } + } + + // Parse ON object + if strings.ToUpper(p.curTok.Literal) == "ON" { + p.nextToken() // consume ON + objIdent := p.parseIdentifier() + spec.TargetObject = &ast.SecurityTargetObject{ + ObjectKind: "NotSpecified", + ObjectName: &ast.SecurityTargetObjectName{ + MultiPartIdentifier: &ast.MultiPartIdentifier{ + Identifiers: []*ast.Identifier{objIdent}, + Count: 1, + }, + }, + } + } + + // Parse BY principals + if strings.ToUpper(p.curTok.Literal) == "BY" { + p.nextToken() // consume BY + for { + principal := &ast.SecurityPrincipal{} + upper := strings.ToUpper(p.curTok.Literal) + if upper == "PUBLIC" { + principal.PrincipalType = "Public" + p.nextToken() + } else if upper == "NULL" { + principal.PrincipalType = "Null" + p.nextToken() + } else { + principal.PrincipalType = "Identifier" + principal.Identifier = p.parseIdentifier() + } + spec.Principals = append(spec.Principals, principal) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + } + part.Details = spec + } else { + // Parse audit action group reference + groupName := p.curTok.Literal + part.Details = &ast.AuditActionGroupReference{ + Group: convertAuditGroupName(groupName), + } + p.nextToken() // consume group name + } + + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + + return part, nil +} + func (p *Parser) parseAuditTarget() (*ast.AuditTarget, error) { target := &ast.AuditTarget{} @@ -7038,11 +7156,31 @@ func (p *Parser) parseBackupStatement() (ast.Statement, error) { fileInfo := &ast.BackupRestoreFileInfo{ ItemKind: "Files", } - expr, err := p.parseScalarExpression() - if err != nil { - return nil, err + // Check for parenthesized list: FILE = ('f1', 'f2') + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + expr, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + fileInfo.Items = append(fileInfo.Items, expr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } else { + expr, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + fileInfo.Items = append(fileInfo.Items, expr) } - fileInfo.Items = append(fileInfo.Items, expr) files = append(files, fileInfo) } else if upperLiteral == "FILEGROUP" { p.nextToken() @@ -7053,11 +7191,31 @@ func (p *Parser) parseBackupStatement() (ast.Statement, error) { fileInfo := &ast.BackupRestoreFileInfo{ ItemKind: "FileGroups", } - expr, err := p.parseScalarExpression() - if err != nil { - return nil, err + // Check for parenthesized list: FILEGROUP = ('fg1', 'fg2') + if p.curTok.Type == TokenLParen { + p.nextToken() + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + expr, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + fileInfo.Items = append(fileInfo.Items, expr) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } else { + expr, err := p.parsePrimaryExpression() + if err != nil { + return nil, err + } + fileInfo.Items = append(fileInfo.Items, expr) } - fileInfo.Items = append(fileInfo.Items, expr) files = append(files, fileInfo) } else { break @@ -7294,6 +7452,52 @@ func (p *Parser) parseBackupStatement() (ast.Statement, error) { option.OptionKind = "NoFormat" case "STATS": option.OptionKind = "Stats" + case "BLOCKSIZE": + option.OptionKind = "BlockSize" + case "BUFFERCOUNT": + option.OptionKind = "BufferCount" + case "DESCRIPTION": + option.OptionKind = "Description" + case "DIFFERENTIAL": + option.OptionKind = "Differential" + case "EXPIREDATE": + option.OptionKind = "ExpireDate" + case "MEDIANAME": + option.OptionKind = "MediaName" + case "MEDIADESCRIPTION": + option.OptionKind = "MediaDescription" + case "RETAINDAYS": + option.OptionKind = "RetainDays" + case "SKIP": + option.OptionKind = "Skip" + case "NOSKIP": + option.OptionKind = "NoSkip" + case "REWIND": + option.OptionKind = "Rewind" + case "NOREWIND": + option.OptionKind = "NoRewind" + case "UNLOAD": + option.OptionKind = "Unload" + case "NOUNLOAD": + option.OptionKind = "NoUnload" + case "RESTART": + option.OptionKind = "Restart" + case "COPY_ONLY": + option.OptionKind = "CopyOnly" + case "NAME": + option.OptionKind = "Name" + case "MAXTRANSFERSIZE": + option.OptionKind = "MaxTransferSize" + case "NO_TRUNCATE": + option.OptionKind = "NoTruncate" + case "NORECOVERY": + option.OptionKind = "NoRecovery" + case "STANDBY": + option.OptionKind = "Standby" + case "NO_LOG": + option.OptionKind = "NoLog" + case "TRUNCATE_ONLY": + option.OptionKind = "TruncateOnly" default: option.OptionKind = optionName } @@ -7888,6 +8092,8 @@ func (p *Parser) parseCreateExternalStatement() (ast.Statement, error) { return p.parseCreateExternalLanguageStatement() case "LIBRARY": return p.parseCreateExternalLibraryStatement() + case "RESOURCE": + return p.parseCreateExternalResourcePoolStatement() } return nil, fmt.Errorf("unexpected token after CREATE EXTERNAL: %s", p.curTok.Literal) } @@ -8486,6 +8692,153 @@ func (p *Parser) parseCreateExternalLibraryStatement() (*ast.CreateExternalLibra return stmt, nil } +func (p *Parser) parseCreateExternalResourcePoolStatement() (*ast.CreateExternalResourcePoolStatement, error) { + // Consume RESOURCE + p.nextToken() + + // Expect POOL + if strings.ToUpper(p.curTok.Literal) != "POOL" { + return nil, fmt.Errorf("expected POOL after RESOURCE, got %s", p.curTok.Literal) + } + p.nextToken() + + stmt := &ast.CreateExternalResourcePoolStatement{} + + // Parse pool name + stmt.Name = p.parseIdentifier() + + // Check for optional WITH clause + if p.curTok.Type == TokenWith || strings.ToUpper(p.curTok.Literal) == "WITH" { + p.nextToken() // consume WITH + + // Expect ( + if p.curTok.Type != TokenLParen { + return nil, fmt.Errorf("expected (, got %s", p.curTok.Literal) + } + p.nextToken() + + // Parse parameters + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + paramName := strings.ToUpper(p.curTok.Literal) + p.nextToken() + + param := &ast.ExternalResourcePoolParameter{} + + switch paramName { + case "MAX_CPU_PERCENT": + param.ParameterType = "MaxCpuPercent" + if p.curTok.Type == TokenEquals { + p.nextToken() + } + val, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = val + case "MAX_MEMORY_PERCENT": + param.ParameterType = "MaxMemoryPercent" + if p.curTok.Type == TokenEquals { + p.nextToken() + } + val, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = val + case "MAX_PROCESSES": + param.ParameterType = "MaxProcesses" + if p.curTok.Type == TokenEquals { + p.nextToken() + } + val, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + param.ParameterValue = val + case "AFFINITY": + param.ParameterType = "Affinity" + affinitySpec := &ast.ExternalResourcePoolAffinitySpecification{} + + // Parse CPU or NUMANODE + affinityType := strings.ToUpper(p.curTok.Literal) + p.nextToken() + if affinityType == "CPU" { + affinitySpec.AffinityType = "Cpu" + } else if affinityType == "NUMANODE" { + affinitySpec.AffinityType = "NumaNode" + } + + // Expect = + if p.curTok.Type == TokenEquals { + p.nextToken() + } + + // Check for AUTO or range list + if strings.ToUpper(p.curTok.Literal) == "AUTO" { + affinitySpec.IsAuto = true + p.nextToken() + } else { + // Parse range list: (1) or (1 TO 5, 6 TO 7) + if p.curTok.Type == TokenLParen { + p.nextToken() + } + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + fromVal, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + rangeItem := &ast.LiteralRange{From: fromVal} + + // Check for TO + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() + toVal, err := p.parseScalarExpression() + if err != nil { + return nil, err + } + rangeItem.To = toVal + } + + affinitySpec.PoolAffinityRanges = append(affinitySpec.PoolAffinityRanges, rangeItem) + + // Check for comma + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + param.AffinitySpecification = affinitySpec + } + + stmt.ExternalResourcePoolParameters = append(stmt.ExternalResourcePoolParameters, param) + + // Check for comma + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + + // Consume ) + if p.curTok.Type == TokenRParen { + p.nextToken() + } + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseCreateEventSessionStatement() (*ast.CreateEventSessionStatement, error) { p.nextToken() // consume EVENT if strings.ToUpper(p.curTok.Literal) != "SESSION" { @@ -9968,8 +10321,10 @@ func (p *Parser) parseCreateLoginStatement() (*ast.CreateLoginStatement, error) if p.curTok.Type == TokenFrom { p.nextToken() // consume FROM - // Check for EXTERNAL PROVIDER - if strings.ToUpper(p.curTok.Literal) == "EXTERNAL" { + upper := strings.ToUpper(p.curTok.Literal) + switch upper { + case "EXTERNAL": + // FROM EXTERNAL PROVIDER p.nextToken() // consume EXTERNAL if strings.ToUpper(p.curTok.Literal) == "PROVIDER" { p.nextToken() // consume PROVIDER @@ -9980,11 +10335,109 @@ func (p *Parser) parseCreateLoginStatement() (*ast.CreateLoginStatement, error) // Parse WITH options if p.curTok.Type == TokenWith { p.nextToken() // consume WITH - source.Options = p.parseExternalLoginOptions() + source.Options = p.parsePrincipalOptions() + } + + stmt.Source = source + + case "WINDOWS": + // FROM WINDOWS + p.nextToken() // consume WINDOWS + + source := &ast.WindowsCreateLoginSource{} + + // Parse WITH options + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + source.Options = p.parsePrincipalOptions() + } + + stmt.Source = source + + case "CERTIFICATE": + // FROM CERTIFICATE certname + p.nextToken() // consume CERTIFICATE + + source := &ast.CertificateCreateLoginSource{ + Certificate: p.parseIdentifier(), + } + + // Parse WITH CREDENTIAL option + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenCredential || strings.ToUpper(p.curTok.Literal) == "CREDENTIAL" { + p.nextToken() // consume CREDENTIAL + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + source.Credential = p.parseIdentifier() + } + } + + stmt.Source = source + + case "ASYMMETRIC": + // FROM ASYMMETRIC KEY keyname + p.nextToken() // consume ASYMMETRIC + if p.curTok.Type == TokenKey || strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + + source := &ast.AsymmetricKeyCreateLoginSource{ + Key: p.parseIdentifier(), + } + + // Parse WITH CREDENTIAL option + if p.curTok.Type == TokenWith { + p.nextToken() // consume WITH + if p.curTok.Type == TokenCredential || strings.ToUpper(p.curTok.Literal) == "CREDENTIAL" { + p.nextToken() // consume CREDENTIAL + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + source.Credential = p.parseIdentifier() + } } stmt.Source = source } + } else if p.curTok.Type == TokenWith { + // WITH PASSWORD = '...' + p.nextToken() // consume WITH + + source := &ast.PasswordCreateLoginSource{} + + // Parse PASSWORD = 'value' [HASHED] [MUST_CHANGE] + if strings.ToUpper(p.curTok.Literal) == "PASSWORD" { + p.nextToken() // consume PASSWORD + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + + // Parse password value (string or binary) + source.Password = p.parsePasswordValue() + + // Parse optional flags and other options + for p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF && strings.ToUpper(p.curTok.Literal) != "GO" { + upper := strings.ToUpper(p.curTok.Literal) + if upper == "HASHED" { + source.Hashed = true + p.nextToken() + } else if upper == "MUST_CHANGE" { + source.MustChange = true + p.nextToken() + } else if p.curTok.Type == TokenComma { + p.nextToken() + // Parse remaining options + source.Options = append(source.Options, p.parsePrincipalOptions()...) + break + } else { + break + } + } + } + + stmt.Source = source } // Skip optional semicolon @@ -9995,7 +10448,37 @@ func (p *Parser) parseCreateLoginStatement() (*ast.CreateLoginStatement, error) return stmt, nil } -func (p *Parser) parseExternalLoginOptions() []ast.PrincipalOption { +func (p *Parser) parsePasswordValue() ast.ScalarExpression { + if p.curTok.Type == TokenString { + value := p.curTok.Literal + isNational := false + if len(value) > 0 && (value[0] == 'N' || value[0] == 'n') && len(value) > 1 && value[1] == '\'' { + isNational = true + value = value[2 : len(value)-1] + } else if len(value) >= 2 && value[0] == '\'' && value[len(value)-1] == '\'' { + value = value[1 : len(value)-1] + } + p.nextToken() + return &ast.StringLiteral{ + LiteralType: "String", + IsNational: isNational, + IsLargeObject: false, + Value: value, + } + } else if p.curTok.Type == TokenBinary { + value := p.curTok.Literal + p.nextToken() + return &ast.BinaryLiteral{ + LiteralType: "Binary", + IsLargeObject: false, + Value: value, + } + } + // Return nil if not a recognized password value + return nil +} + +func (p *Parser) parsePrincipalOptions() []ast.PrincipalOption { var options []ast.PrincipalOption for { @@ -10037,6 +10520,33 @@ func (p *Parser) parseExternalLoginOptions() []ast.PrincipalOption { OptionKind: "DefaultLanguage", Identifier: p.parseIdentifier(), }) + case "CHECK_EXPIRATION": + // CHECK_EXPIRATION = ON/OFF + optState := "On" + if strings.ToUpper(p.curTok.Literal) == "OFF" { + optState = "Off" + } + options = append(options, &ast.OnOffPrincipalOption{ + OptionKind: "CheckExpiration", + OptionState: optState, + }) + p.nextToken() + case "CHECK_POLICY": + // CHECK_POLICY = ON/OFF + optState := "On" + if strings.ToUpper(p.curTok.Literal) == "OFF" { + optState = "Off" + } + options = append(options, &ast.OnOffPrincipalOption{ + OptionKind: "CheckPolicy", + OptionState: optState, + }) + p.nextToken() + case "CREDENTIAL": + options = append(options, &ast.IdentifierPrincipalOption{ + OptionKind: "Credential", + Identifier: p.parseIdentifier(), + }) default: // Unknown option, skip value if p.curTok.Type != TokenComma && p.curTok.Type != TokenSemicolon && p.curTok.Type != TokenEOF { @@ -10237,10 +10747,75 @@ func (p *Parser) parseCreateIndexOptions() []ast.IndexOption { OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), }) case "ONLINE": - options = append(options, &ast.OnlineIndexOption{ + onlineOpt := &ast.OnlineIndexOption{ OptionKind: "Online", OptionState: p.capitalizeFirst(strings.ToLower(valueStr)), - }) + } + // Check for optional (WAIT_AT_LOW_PRIORITY (...)) + if valueStr == "ON" && p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + if strings.ToUpper(p.curTok.Literal) == "WAIT_AT_LOW_PRIORITY" { + p.nextToken() // consume WAIT_AT_LOW_PRIORITY + lowPriorityOpt := &ast.OnlineIndexLowPriorityLockWaitOption{} + if p.curTok.Type == TokenLParen { + p.nextToken() // consume ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + optName := strings.ToUpper(p.curTok.Literal) + if optName == "MAX_DURATION" { + p.nextToken() // consume MAX_DURATION + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + durVal, _ := p.parsePrimaryExpression() + unit := "Minutes" + if strings.ToUpper(p.curTok.Literal) == "MINUTES" { + p.nextToken() + } else if strings.ToUpper(p.curTok.Literal) == "SECONDS" { + unit = "Seconds" + p.nextToken() + } + lowPriorityOpt.Options = append(lowPriorityOpt.Options, &ast.LowPriorityLockWaitMaxDurationOption{ + MaxDuration: durVal, + Unit: unit, + OptionKind: "MaxDuration", + }) + } else if optName == "ABORT_AFTER_WAIT" { + p.nextToken() // consume ABORT_AFTER_WAIT + if p.curTok.Type == TokenEquals { + p.nextToken() // consume = + } + abortType := "None" + switch strings.ToUpper(p.curTok.Literal) { + case "NONE": + abortType = "None" + case "SELF": + abortType = "Self" + case "BLOCKERS": + abortType = "Blockers" + } + p.nextToken() + lowPriorityOpt.Options = append(lowPriorityOpt.Options, &ast.LowPriorityLockWaitAbortAfterWaitOption{ + AbortAfterWait: abortType, + OptionKind: "AbortAfterWait", + }) + } else { + break + } + if p.curTok.Type == TokenComma { + p.nextToken() + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + onlineOpt.LowPriorityLockWaitOption = lowPriorityOpt + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + options = append(options, onlineOpt) case "ALLOW_ROW_LOCKS": options = append(options, &ast.IndexStateOption{ OptionKind: "AllowRowLocks", @@ -10319,6 +10894,48 @@ func (p *Parser) parseCreateIndexOptions() []ast.IndexOption { } } options = append(options, opt) + case "XML_COMPRESSION": + // Parse XML_COMPRESSION = ON/OFF [ON PARTITIONS(range)] + isCompressed := "On" + if valueStr == "OFF" { + isCompressed = "Off" + } + opt := &ast.XmlCompressionOption{ + IsCompressed: isCompressed, + OptionKind: "XmlCompression", + } + // Check for optional ON PARTITIONS(range) + 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 ( + for p.curTok.Type != TokenRParen && p.curTok.Type != TokenEOF { + partRange := &ast.CompressionPartitionRange{} + // Parse From value + partRange.From = &ast.IntegerLiteral{LiteralType: "Integer", Value: p.curTok.Literal} + p.nextToken() + // Check for TO keyword indicating a range + if strings.ToUpper(p.curTok.Literal) == "TO" { + p.nextToken() // consume TO + partRange.To = &ast.IntegerLiteral{LiteralType: "Integer", Value: p.curTok.Literal} + p.nextToken() + } + opt.PartitionRanges = append(opt.PartitionRanges, partRange) + if p.curTok.Type == TokenComma { + p.nextToken() + } else { + break + } + } + if p.curTok.Type == TokenRParen { + p.nextToken() // consume ) + } + } + } + } + options = append(options, opt) default: // Generic handling for other options if valueStr == "ON" || valueStr == "OFF" { diff --git a/parser/testdata/BackupStatementTests/metadata.json b/parser/testdata/BackupStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BackupStatementTests/metadata.json +++ b/parser/testdata/BackupStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_DatabaseAuditSpecificationStatementTests/metadata.json b/parser/testdata/Baselines100_DatabaseAuditSpecificationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_DatabaseAuditSpecificationStatementTests/metadata.json +++ b/parser/testdata/Baselines100_DatabaseAuditSpecificationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines100_FromClauseTests100/metadata.json b/parser/testdata/Baselines100_FromClauseTests100/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines100_FromClauseTests100/metadata.json +++ b/parser/testdata/Baselines100_FromClauseTests100/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines110_OverClauseTests110/metadata.json b/parser/testdata/Baselines110_OverClauseTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines110_OverClauseTests110/metadata.json +++ b/parser/testdata/Baselines110_OverClauseTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_CreateIndexStatementTests120/metadata.json b/parser/testdata/Baselines120_CreateIndexStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_CreateIndexStatementTests120/metadata.json +++ b/parser/testdata/Baselines120_CreateIndexStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines120_CreateTableTests120/metadata.json b/parser/testdata/Baselines120_CreateTableTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines120_CreateTableTests120/metadata.json +++ b/parser/testdata/Baselines120_CreateTableTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines130_CreateExternalResourcePoolStatementTests130/metadata.json b/parser/testdata/Baselines130_CreateExternalResourcePoolStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines130_CreateExternalResourcePoolStatementTests130/metadata.json +++ b/parser/testdata/Baselines130_CreateExternalResourcePoolStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_DatabaseAuditSpecificationStatementTests150/metadata.json b/parser/testdata/Baselines150_DatabaseAuditSpecificationStatementTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_DatabaseAuditSpecificationStatementTests150/metadata.json +++ b/parser/testdata/Baselines150_DatabaseAuditSpecificationStatementTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines150_FromClauseTests150/metadata.json b/parser/testdata/Baselines150_FromClauseTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines150_FromClauseTests150/metadata.json +++ b/parser/testdata/Baselines150_FromClauseTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_BuiltInFunctionTests160/metadata.json b/parser/testdata/Baselines160_BuiltInFunctionTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_BuiltInFunctionTests160/metadata.json +++ b/parser/testdata/Baselines160_BuiltInFunctionTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateIndexStatementTests160/metadata.json b/parser/testdata/Baselines160_CreateIndexStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateIndexStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateIndexStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_CreateTableTests160/metadata.json b/parser/testdata/Baselines160_CreateTableTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_CreateTableTests160/metadata.json +++ b/parser/testdata/Baselines160_CreateTableTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_OpenRowsetBulkStatementTests160/metadata.json b/parser/testdata/Baselines160_OpenRowsetBulkStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_OpenRowsetBulkStatementTests160/metadata.json +++ b/parser/testdata/Baselines160_OpenRowsetBulkStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_TrimFunctionTests160/metadata.json b/parser/testdata/Baselines160_TrimFunctionTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_TrimFunctionTests160/metadata.json +++ b/parser/testdata/Baselines160_TrimFunctionTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines160_WindowClauseTests160/metadata.json b/parser/testdata/Baselines160_WindowClauseTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines160_WindowClauseTests160/metadata.json +++ b/parser/testdata/Baselines160_WindowClauseTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_DropStatementsTests2/metadata.json b/parser/testdata/Baselines90_DropStatementsTests2/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_DropStatementsTests2/metadata.json +++ b/parser/testdata/Baselines90_DropStatementsTests2/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/Baselines90_LoginStatementTests/metadata.json b/parser/testdata/Baselines90_LoginStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/Baselines90_LoginStatementTests/metadata.json +++ b/parser/testdata/Baselines90_LoginStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_BackupStatementTests/metadata.json b/parser/testdata/BaselinesCommon_BackupStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_BackupStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_BackupStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BaselinesCommon_RestoreStatementTests/metadata.json b/parser/testdata/BaselinesCommon_RestoreStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BaselinesCommon_RestoreStatementTests/metadata.json +++ b/parser/testdata/BaselinesCommon_RestoreStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/BuiltInFunctionTests160/metadata.json b/parser/testdata/BuiltInFunctionTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/BuiltInFunctionTests160/metadata.json +++ b/parser/testdata/BuiltInFunctionTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateExternalResourcePoolStatementTests130/metadata.json b/parser/testdata/CreateExternalResourcePoolStatementTests130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateExternalResourcePoolStatementTests130/metadata.json +++ b/parser/testdata/CreateExternalResourcePoolStatementTests130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateIndexStatementTests120/metadata.json b/parser/testdata/CreateIndexStatementTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests120/metadata.json +++ b/parser/testdata/CreateIndexStatementTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateIndexStatementTests160/metadata.json b/parser/testdata/CreateIndexStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateIndexStatementTests160/metadata.json +++ b/parser/testdata/CreateIndexStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateTableTests120/metadata.json b/parser/testdata/CreateTableTests120/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateTableTests120/metadata.json +++ b/parser/testdata/CreateTableTests120/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/CreateTableTests160/metadata.json b/parser/testdata/CreateTableTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/CreateTableTests160/metadata.json +++ b/parser/testdata/CreateTableTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DatabaseAuditSpecificationStatementTests/metadata.json b/parser/testdata/DatabaseAuditSpecificationStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DatabaseAuditSpecificationStatementTests/metadata.json +++ b/parser/testdata/DatabaseAuditSpecificationStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DatabaseAuditSpecificationStatementTests150/metadata.json b/parser/testdata/DatabaseAuditSpecificationStatementTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DatabaseAuditSpecificationStatementTests150/metadata.json +++ b/parser/testdata/DatabaseAuditSpecificationStatementTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/DropStatementsTests2/metadata.json b/parser/testdata/DropStatementsTests2/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/DropStatementsTests2/metadata.json +++ b/parser/testdata/DropStatementsTests2/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/FromClauseTests150/metadata.json b/parser/testdata/FromClauseTests150/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/FromClauseTests150/metadata.json +++ b/parser/testdata/FromClauseTests150/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/FunctionStatementTests/metadata.json b/parser/testdata/FunctionStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/FunctionStatementTests/metadata.json +++ b/parser/testdata/FunctionStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/LoginStatementTests/metadata.json b/parser/testdata/LoginStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/LoginStatementTests/metadata.json +++ b/parser/testdata/LoginStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/OpenRowsetBulkStatementTests160/metadata.json b/parser/testdata/OpenRowsetBulkStatementTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/OpenRowsetBulkStatementTests160/metadata.json +++ b/parser/testdata/OpenRowsetBulkStatementTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/OverClauseTests110/metadata.json b/parser/testdata/OverClauseTests110/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/OverClauseTests110/metadata.json +++ b/parser/testdata/OverClauseTests110/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/RestoreStatementTests/metadata.json b/parser/testdata/RestoreStatementTests/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/RestoreStatementTests/metadata.json +++ b/parser/testdata/RestoreStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/TrimFunctionTests160/metadata.json b/parser/testdata/TrimFunctionTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/TrimFunctionTests160/metadata.json +++ b/parser/testdata/TrimFunctionTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/UniqueInlineIndex130/metadata.json b/parser/testdata/UniqueInlineIndex130/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/UniqueInlineIndex130/metadata.json +++ b/parser/testdata/UniqueInlineIndex130/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} diff --git a/parser/testdata/WindowClauseTests160/metadata.json b/parser/testdata/WindowClauseTests160/metadata.json index ccffb5b9..0967ef42 100644 --- a/parser/testdata/WindowClauseTests160/metadata.json +++ b/parser/testdata/WindowClauseTests160/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{}