Skip to content

Commit b2cb43e

Browse files
committed
Add comprehensive RESTORE statement support
- Add TAPE device type support in FROM clause - Add FILELISTONLY, VERIFYONLY, LABELONLY, REWINDONLY, HEADERONLY kinds - Add MOVE restore option parsing (MOVE 'file' TO 'path') - Add FILE, MEDIANAME, MEDIAPASSWORD, PASSWORD, STOPAT options - Add ENABLE_BROKER, ERROR_BROKER_CONVERSATIONS, NEW_BROKER options - Add KEEP_REPLICATION, RESTRICTED_USER options - Fix STATS option to handle optional value (STATS or STATS = n) - Update MoveRestoreOption to use ScalarExpression types Enables RestoreStatementTests tests.
1 parent c252399 commit b2cb43e

4 files changed

Lines changed: 172 additions & 69 deletions

File tree

ast/restore_statement.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ func (o *GeneralSetCommandRestoreOption) restoreOptionNode() {}
4949

5050
// MoveRestoreOption represents a MOVE restore option
5151
type MoveRestoreOption struct {
52-
OptionKind string
53-
LogicalFileName *IdentifierOrValueExpression
54-
OSFileName *IdentifierOrValueExpression
52+
OptionKind string
53+
LogicalFileName ScalarExpression
54+
OSFileName ScalarExpression
5555
}
5656

5757
func (o *MoveRestoreOption) restoreOptionNode() {}

parser/marshal.go

Lines changed: 167 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9131,6 +9131,7 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) {
91319131
}
91329132

91339133
stmt := &ast.RestoreStatement{}
9134+
hasDatabaseName := true
91349135

91359136
// Parse restore kind (DATABASE, LOG, etc.)
91369137
switch strings.ToUpper(p.curTok.Literal) {
@@ -9140,74 +9141,95 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) {
91409141
case "LOG":
91419142
stmt.Kind = "TransactionLog"
91429143
p.nextToken()
9144+
case "FILELISTONLY":
9145+
stmt.Kind = "FileListOnly"
9146+
p.nextToken()
9147+
hasDatabaseName = false
9148+
case "VERIFYONLY":
9149+
stmt.Kind = "VerifyOnly"
9150+
p.nextToken()
9151+
hasDatabaseName = false
9152+
case "LABELONLY":
9153+
stmt.Kind = "LabelOnly"
9154+
p.nextToken()
9155+
hasDatabaseName = false
9156+
case "REWINDONLY":
9157+
stmt.Kind = "RewindOnly"
9158+
p.nextToken()
9159+
hasDatabaseName = false
9160+
case "HEADERONLY":
9161+
stmt.Kind = "HeaderOnly"
9162+
p.nextToken()
9163+
hasDatabaseName = false
91439164
default:
91449165
stmt.Kind = "Database"
91459166
}
91469167

9147-
// Parse database name
9148-
dbName := &ast.IdentifierOrValueExpression{}
9149-
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
9150-
// Variable reference
9151-
varRef := &ast.VariableReference{Name: p.curTok.Literal}
9152-
p.nextToken()
9153-
dbName.Value = varRef.Name
9154-
dbName.ValueExpression = varRef
9155-
} else {
9156-
ident := p.parseIdentifier()
9157-
dbName.Value = ident.Value
9158-
dbName.Identifier = ident
9159-
}
9160-
stmt.DatabaseName = dbName
9161-
9162-
// Parse optional FILE = or FILEGROUP = before FROM
9163-
for strings.ToUpper(p.curTok.Literal) == "FILE" || strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
9164-
itemKind := "Files"
9165-
if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
9166-
itemKind = "FileGroups"
9167-
}
9168-
p.nextToken() // consume FILE/FILEGROUP
9169-
if p.curTok.Type != TokenEquals {
9170-
return nil, fmt.Errorf("expected = after FILE/FILEGROUP, got %s", p.curTok.Literal)
9171-
}
9172-
p.nextToken() // consume =
9173-
9174-
fileInfo := &ast.BackupRestoreFileInfo{ItemKind: itemKind}
9175-
// Parse the file name
9176-
var item ast.ScalarExpression
9177-
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
9178-
// Strip surrounding quotes
9179-
val := p.curTok.Literal
9180-
if len(val) >= 2 && ((val[0] == '\'' && val[len(val)-1] == '\'') || (val[0] == '"' && val[len(val)-1] == '"')) {
9181-
val = val[1 : len(val)-1]
9182-
}
9183-
item = &ast.StringLiteral{
9184-
LiteralType: "String",
9185-
Value: val,
9186-
IsNational: p.curTok.Type == TokenNationalString,
9187-
IsLargeObject: false,
9188-
}
9189-
p.nextToken()
9190-
} else if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
9191-
item = &ast.VariableReference{Name: p.curTok.Literal}
9168+
// Parse database name (only for DATABASE and LOG kinds)
9169+
if hasDatabaseName {
9170+
dbName := &ast.IdentifierOrValueExpression{}
9171+
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
9172+
// Variable reference
9173+
varRef := &ast.VariableReference{Name: p.curTok.Literal}
91929174
p.nextToken()
9175+
dbName.Value = varRef.Name
9176+
dbName.ValueExpression = varRef
91939177
} else {
91949178
ident := p.parseIdentifier()
9195-
item = &ast.ColumnReferenceExpression{
9196-
ColumnType: "Regular",
9197-
MultiPartIdentifier: &ast.MultiPartIdentifier{
9198-
Identifiers: []*ast.Identifier{ident},
9199-
Count: 1,
9200-
},
9201-
}
9179+
dbName.Value = ident.Value
9180+
dbName.Identifier = ident
92029181
}
9203-
fileInfo.Items = append(fileInfo.Items, item)
9204-
stmt.Files = append(stmt.Files, fileInfo)
9182+
stmt.DatabaseName = dbName
92059183

9206-
if p.curTok.Type == TokenComma {
9207-
p.nextToken()
9184+
// Parse optional FILE = or FILEGROUP = before FROM
9185+
for strings.ToUpper(p.curTok.Literal) == "FILE" || strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
9186+
itemKind := "Files"
9187+
if strings.ToUpper(p.curTok.Literal) == "FILEGROUP" {
9188+
itemKind = "FileGroups"
9189+
}
9190+
p.nextToken() // consume FILE/FILEGROUP
9191+
if p.curTok.Type != TokenEquals {
9192+
return nil, fmt.Errorf("expected = after FILE/FILEGROUP, got %s", p.curTok.Literal)
9193+
}
9194+
p.nextToken() // consume =
9195+
9196+
fileInfo := &ast.BackupRestoreFileInfo{ItemKind: itemKind}
9197+
// Parse the file name
9198+
var item ast.ScalarExpression
9199+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
9200+
// Strip surrounding quotes
9201+
val := p.curTok.Literal
9202+
if len(val) >= 2 && ((val[0] == '\'' && val[len(val)-1] == '\'') || (val[0] == '"' && val[len(val)-1] == '"')) {
9203+
val = val[1 : len(val)-1]
9204+
}
9205+
item = &ast.StringLiteral{
9206+
LiteralType: "String",
9207+
Value: val,
9208+
IsNational: p.curTok.Type == TokenNationalString,
9209+
IsLargeObject: false,
9210+
}
9211+
p.nextToken()
9212+
} else if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
9213+
item = &ast.VariableReference{Name: p.curTok.Literal}
9214+
p.nextToken()
9215+
} else {
9216+
ident := p.parseIdentifier()
9217+
item = &ast.ColumnReferenceExpression{
9218+
ColumnType: "Regular",
9219+
MultiPartIdentifier: &ast.MultiPartIdentifier{
9220+
Identifiers: []*ast.Identifier{ident},
9221+
Count: 1,
9222+
},
9223+
}
9224+
}
9225+
fileInfo.Items = append(fileInfo.Items, item)
9226+
stmt.Files = append(stmt.Files, fileInfo)
9227+
9228+
if p.curTok.Type == TokenComma {
9229+
p.nextToken()
9230+
}
92089231
}
92099232
}
9210-
92119233
// Check for optional FROM clause
92129234
if strings.ToUpper(p.curTok.Literal) != "FROM" {
92139235
// No FROM clause - check for WITH clause
@@ -9228,6 +9250,13 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) {
92289250

92299251
// Check for device type
92309252
switch strings.ToUpper(p.curTok.Literal) {
9253+
case "TAPE":
9254+
device.DeviceType = "Tape"
9255+
p.nextToken()
9256+
if p.curTok.Type != TokenEquals {
9257+
return nil, fmt.Errorf("expected = after TAPE, got %s", p.curTok.Literal)
9258+
}
9259+
p.nextToken()
92319260
case "DISK":
92329261
device.DeviceType = "Disk"
92339262
p.nextToken()
@@ -9245,8 +9274,8 @@ func (p *Parser) parseRestoreStatement() (ast.Statement, error) {
92459274
}
92469275

92479276
// Parse device name
9248-
if device.DeviceType == "Disk" || device.DeviceType == "URL" {
9249-
// For DISK and URL, use PhysicalDevice with the string literal directly
9277+
if device.DeviceType == "Disk" || device.DeviceType == "URL" || device.DeviceType == "Tape" {
9278+
// For DISK, URL, and TAPE, use PhysicalDevice with the string literal directly
92509279
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
92519280
// Strip the surrounding quotes from the literal
92529281
val := p.curTok.Literal
@@ -9358,6 +9387,28 @@ parseWithClause:
93589387
}
93599388
stmt.Options = append(stmt.Options, fsOpt)
93609389

9390+
case "MOVE":
9391+
// MOVE 'logical_file_name' TO 'os_file_name'
9392+
opt := &ast.MoveRestoreOption{OptionKind: "Move"}
9393+
// Parse logical file name
9394+
expr, err := p.parseScalarExpression()
9395+
if err != nil {
9396+
return nil, err
9397+
}
9398+
opt.LogicalFileName = expr
9399+
// Expect TO
9400+
if strings.ToUpper(p.curTok.Literal) != "TO" {
9401+
return nil, fmt.Errorf("expected TO after logical file name, got %s", p.curTok.Literal)
9402+
}
9403+
p.nextToken()
9404+
// Parse OS file name
9405+
osExpr, err := p.parseScalarExpression()
9406+
if err != nil {
9407+
return nil, err
9408+
}
9409+
opt.OSFileName = osExpr
9410+
stmt.Options = append(stmt.Options, opt)
9411+
93619412
case "STOPATMARK", "STOPBEFOREMARK":
93629413
opt := &ast.StopRestoreOption{
93639414
OptionKind: "StopAt",
@@ -9399,21 +9450,73 @@ parseWithClause:
93999450
}
94009451
stmt.Options = append(stmt.Options, opt)
94019452

9402-
case "KEEP_TEMPORAL_RETENTION", "NOREWIND", "NOUNLOAD", "STATS",
9453+
case "FILE", "MEDIANAME", "MEDIAPASSWORD", "PASSWORD", "STOPAT":
9454+
// Options that take a scalar expression value
9455+
optKind := optionName
9456+
switch optionName {
9457+
case "MEDIANAME":
9458+
optKind = "MediaName"
9459+
case "MEDIAPASSWORD":
9460+
optKind = "MediaPassword"
9461+
case "PASSWORD":
9462+
optKind = "Password"
9463+
case "STOPAT":
9464+
optKind = "StopAt"
9465+
case "FILE":
9466+
optKind = "File"
9467+
}
9468+
opt := &ast.ScalarExpressionRestoreOption{OptionKind: optKind}
9469+
if p.curTok.Type == TokenEquals {
9470+
p.nextToken()
9471+
expr, err := p.parseScalarExpression()
9472+
if err != nil {
9473+
return nil, err
9474+
}
9475+
opt.Value = expr
9476+
}
9477+
stmt.Options = append(stmt.Options, opt)
9478+
9479+
case "STATS":
9480+
// STATS can optionally have a value: STATS or STATS = 10
9481+
if p.curTok.Type == TokenEquals {
9482+
p.nextToken()
9483+
expr, err := p.parseScalarExpression()
9484+
if err != nil {
9485+
return nil, err
9486+
}
9487+
stmt.Options = append(stmt.Options, &ast.ScalarExpressionRestoreOption{
9488+
OptionKind: "Stats",
9489+
Value: expr,
9490+
})
9491+
} else {
9492+
stmt.Options = append(stmt.Options, &ast.SimpleRestoreOption{OptionKind: "Stats"})
9493+
}
9494+
9495+
case "ENABLE_BROKER", "ERROR_BROKER_CONVERSATIONS", "NEW_BROKER",
9496+
"KEEP_REPLICATION", "RESTRICTED_USER",
9497+
"KEEP_TEMPORAL_RETENTION", "NOREWIND", "NOUNLOAD",
94039498
"RECOVERY", "NORECOVERY", "REPLACE", "RESTART", "REWIND",
94049499
"UNLOAD", "CHECKSUM", "NO_CHECKSUM", "STOP_ON_ERROR",
94059500
"CONTINUE_AFTER_ERROR":
94069501
// Map option names to proper casing
94079502
optKind := optionName
94089503
switch optionName {
9504+
case "ENABLE_BROKER":
9505+
optKind = "EnableBroker"
9506+
case "ERROR_BROKER_CONVERSATIONS":
9507+
optKind = "ErrorBrokerConversations"
9508+
case "NEW_BROKER":
9509+
optKind = "NewBroker"
9510+
case "KEEP_REPLICATION":
9511+
optKind = "KeepReplication"
9512+
case "RESTRICTED_USER":
9513+
optKind = "RestrictedUser"
94099514
case "KEEP_TEMPORAL_RETENTION":
94109515
optKind = "KeepTemporalRetention"
94119516
case "NOREWIND":
94129517
optKind = "NoRewind"
94139518
case "NOUNLOAD":
94149519
optKind = "NoUnload"
9415-
case "STATS":
9416-
optKind = "Stats"
94179520
case "RECOVERY":
94189521
optKind = "Recovery"
94199522
case "NORECOVERY":
@@ -12115,10 +12218,10 @@ func restoreOptionToJSON(o ast.RestoreOption) jsonNode {
1211512218
"OptionKind": opt.OptionKind,
1211612219
}
1211712220
if opt.LogicalFileName != nil {
12118-
node["LogicalFileName"] = identifierOrValueExpressionToJSON(opt.LogicalFileName)
12221+
node["LogicalFileName"] = scalarExpressionToJSON(opt.LogicalFileName)
1211912222
}
1212012223
if opt.OSFileName != nil {
12121-
node["OSFileName"] = identifierOrValueExpressionToJSON(opt.OSFileName)
12224+
node["OSFileName"] = scalarExpressionToJSON(opt.OSFileName)
1212212225
}
1212312226
return node
1212412227
default:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)