blob: 94d509720a1451612065f6a65dcee5a40e1021f2 [file]
package backup
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/apache/cloudberry-backup/toc"
"github.com/apache/cloudberry-backup/utils"
"github.com/apache/cloudberry-go-libs/gplog"
)
/*
* This file contains structs and functions related to backing up global cluster
* metadata on the coordinator that needs to be restored before data is restored,
* such as roles and database configuration.
*/
func PrintSessionGUCs(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, gucs SessionGUCs) {
start := metadataFile.ByteCount
metadataFile.MustPrintf(`
SET client_encoding = '%s';
`, gucs.ClientEncoding)
section, entry := gucs.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
func PrintCreateDatabaseStatement(metadataFile *utils.FileWithByteCount, tocfile *toc.TOC, defaultDB Database, db Database, dbMetadata MetadataMap) {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\n\nCREATE DATABASE %s TEMPLATE template0", db.Name)
if db.Tablespace != "pg_default" {
metadataFile.MustPrintf(" TABLESPACE %s", db.Tablespace)
}
if db.Encoding != "" && (db.Encoding != defaultDB.Encoding) {
metadataFile.MustPrintf(" ENCODING '%s'", db.Encoding)
}
if db.Collate != "" && (db.Collate != defaultDB.Collate) {
metadataFile.MustPrintf(" LC_COLLATE '%s'", db.Collate)
}
if db.CType != "" && (db.CType != defaultDB.CType) {
metadataFile.MustPrintf(" LC_CTYPE '%s'", db.CType)
}
metadataFile.MustPrintf(";")
entry := toc.MetadataEntry{Name: db.Name, ObjectType: toc.OBJ_DATABASE}
tocfile.AddMetadataEntry("global", entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, tocfile, dbMetadata[db.GetUniqueID()], db, "", []uint32{0, 0})
}
func PrintDatabaseGUCs(metadataFile *utils.FileWithByteCount, tocfile *toc.TOC, gucs []string, dbname string) {
for _, guc := range gucs {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\nALTER DATABASE %s %s;", dbname, guc)
entry := toc.MetadataEntry{Name: dbname, ObjectType: toc.OBJ_DATABASE_GUC}
tocfile.AddMetadataEntry("global", entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
func PrintCreateResourceQueueStatements(metadataFile *utils.FileWithByteCount, tocfile *toc.TOC, resQueues []ResourceQueue, resQueueMetadata MetadataMap) {
for _, resQueue := range resQueues {
start := metadataFile.ByteCount
attributes := make([]string, 0)
if resQueue.ActiveStatements != -1 {
attributes = append(attributes, fmt.Sprintf("ACTIVE_STATEMENTS=%d", resQueue.ActiveStatements))
}
maxCostFloat, parseErr := strconv.ParseFloat(resQueue.MaxCost, 64)
gplog.FatalOnError(parseErr)
if maxCostFloat > -1 {
attributes = append(attributes, fmt.Sprintf("MAX_COST=%s", resQueue.MaxCost))
}
if resQueue.CostOvercommit {
attributes = append(attributes, "COST_OVERCOMMIT=TRUE")
}
minCostFloat, parseErr := strconv.ParseFloat(resQueue.MinCost, 64)
gplog.FatalOnError(parseErr)
if minCostFloat > 0 {
attributes = append(attributes, fmt.Sprintf("MIN_COST=%s", resQueue.MinCost))
}
if resQueue.Priority != "medium" {
attributes = append(attributes, fmt.Sprintf("PRIORITY=%s", strings.ToUpper(resQueue.Priority)))
}
if resQueue.MemoryLimit != "-1" {
attributes = append(attributes, fmt.Sprintf("MEMORY_LIMIT='%s'", resQueue.MemoryLimit))
}
action := "CREATE"
if resQueue.Name == "pg_default" {
action = "ALTER"
}
metadataFile.MustPrintf("\n\n%s RESOURCE QUEUE %s WITH (%s);", action, resQueue.Name, strings.Join(attributes, ", "))
section, entry := resQueue.GetMetadataEntry()
tocfile.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, tocfile, resQueueMetadata[resQueue.GetUniqueID()], resQueue, "", []uint32{0, 0})
}
}
func PrintResetResourceGroupStatements(metadataFile *utils.FileWithByteCount, tocfile *toc.TOC) {
/*
* total cpu_rate_limit and memory_limit should less than 100, so clean
* them before we seting new memory_limit and cpu_rate_limit.
*
* Minimal memory_limit is adjusted from 1 to 0 since 5.20, however for
* backward compatibility we still use 1 as the minimal value. The only
* failing case is that default_group has memory_limit=100 and admin_group
* has memory_limit=0, but this should not happen in real world.
*/
type DefSetting struct {
name string
setting string
}
defSettings := make([]DefSetting, 0)
if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
defSettings = append(defSettings, DefSetting{"admin_group", "SET CPU_RATE_LIMIT 1"})
defSettings = append(defSettings, DefSetting{"admin_group", "SET MEMORY_LIMIT 1"})
defSettings = append(defSettings, DefSetting{"default_group", "SET CPU_RATE_LIMIT 1"})
defSettings = append(defSettings, DefSetting{"default_group", "SET MEMORY_LIMIT 1"})
} else { // GPDB7+
defSettings = append(defSettings, DefSetting{"admin_group", "SET CPU_MAX_PERCENT 1"})
defSettings = append(defSettings, DefSetting{"admin_group", "SET CPU_WEIGHT 100"})
defSettings = append(defSettings, DefSetting{"default_group", "SET CPU_MAX_PERCENT 1"})
defSettings = append(defSettings, DefSetting{"default_group", "SET CPU_WEIGHT 100"})
defSettings = append(defSettings, DefSetting{"system_group", "SET CPU_MAX_PERCENT 1"})
defSettings = append(defSettings, DefSetting{"system_group", "SET CPU_WEIGHT 100"})
}
for _, prepare := range defSettings {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s %s;", prepare.name, prepare.setting)
entry := toc.MetadataEntry{Name: prepare.name, ObjectType: toc.OBJ_RESOURCE_GROUP}
tocfile.AddMetadataEntry("global", entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
func PrintCreateResourceGroupStatementsAtLeast7(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, resGroups []ResourceGroupAtLeast7, resGroupMetadata MetadataMap) {
for _, resGroup := range resGroups {
var start uint64
section, entry := resGroup.GetMetadataEntry()
if resGroup.Name == "default_group" || resGroup.Name == "admin_group" || resGroup.Name == "system_group" {
resGroupList := []struct {
setting string
value string
}{
{"CPU_WEIGHT", resGroup.CpuWeight},
{"CONCURRENCY", resGroup.Concurrency},
}
for _, property := range resGroupList {
start = metadataFile.ByteCount
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET %s %s;", resGroup.Name, property.setting, property.value)
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
/* special handling for cpu properties */
// TODO -- why do we handle these separately?
// TODO -- is this still necessary for 7?
start = metadataFile.ByteCount
if !strings.HasPrefix(resGroup.CpuMaxPercent, "-") {
/* cpu rate mode */
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET CPU_MAX_PERCENT %s;", resGroup.Name, resGroup.CpuMaxPercent)
} else {
/* cpuset mode */
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET CPUSET '%s';", resGroup.Name, resGroup.Cpuset)
}
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, objToc, resGroupMetadata[resGroup.GetUniqueID()], resGroup, "", []uint32{0, 0})
} else {
start = metadataFile.ByteCount
attributes := make([]string, 0)
/* special handling for cpu properties */
// TODO -- why do we handle these separately?
// TODO -- is this still necessary for 7?
if !strings.HasPrefix(resGroup.CpuMaxPercent, "-") {
/* cpu rate mode */
attributes = append(attributes, fmt.Sprintf("CPU_MAX_PERCENT=%s", resGroup.CpuMaxPercent))
} else if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("5.9.0")) || connectionPool.Version.IsCBDB() {
/* cpuset mode */
attributes = append(attributes, fmt.Sprintf("CPUSET='%s'", resGroup.Cpuset))
}
attributes = append(attributes, fmt.Sprintf("CPU_WEIGHT=%s", resGroup.CpuWeight))
attributes = append(attributes, fmt.Sprintf("CONCURRENCY=%s", resGroup.Concurrency))
metadataFile.MustPrintf("\n\nCREATE RESOURCE GROUP %s WITH (%s);", resGroup.Name, strings.Join(attributes, ", "))
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, objToc, resGroupMetadata[resGroup.GetUniqueID()], resGroup, "", []uint32{0, 0})
}
}
}
func PrintCreateResourceGroupStatementsBefore7(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, resGroups []ResourceGroupBefore7, resGroupMetadata MetadataMap) {
for _, resGroup := range resGroups {
// temporarily special case for 5x resource groups #temp5xResGroup
memorySpillRatio := resGroup.MemorySpillRatio
if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("5") {
/*
* memory_spill_ratio can be set in absolute value format since 5.20,
* such as '1 MB', it has to be set as a quoted string, otherwise set
* it without quotes.
*/
if _, err := strconv.Atoi(memorySpillRatio); err != nil {
/* memory_spill_ratio is in absolute value format, set it with quotes */
memorySpillRatio = "'" + memorySpillRatio + "'"
}
}
var start uint64
section, entry := resGroup.GetMetadataEntry()
if resGroup.Name == "default_group" || resGroup.Name == "admin_group" {
resGroupList := []struct {
setting string
value string
}{
{"MEMORY_LIMIT", resGroup.MemoryLimit},
{"MEMORY_SHARED_QUOTA", resGroup.MemorySharedQuota},
{"MEMORY_SPILL_RATIO", memorySpillRatio},
{"CONCURRENCY", resGroup.Concurrency},
}
for _, property := range resGroupList {
start = metadataFile.ByteCount
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET %s %s;", resGroup.Name, property.setting, property.value)
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
/* special handling for cpu properties */
start = metadataFile.ByteCount
if !strings.HasPrefix(resGroup.CPURateLimit, "-") {
/* cpu rate mode */
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET CPU_RATE_LIMIT %s;", resGroup.Name, resGroup.CPURateLimit)
} else if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("5.9.0")) || connectionPool.Version.IsCBDB() {
/* cpuset mode */
metadataFile.MustPrintf("\n\nALTER RESOURCE GROUP %s SET CPUSET '%s';", resGroup.Name, resGroup.Cpuset)
}
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, objToc, resGroupMetadata[resGroup.GetUniqueID()], resGroup, "", []uint32{0, 0})
} else {
start = metadataFile.ByteCount
attributes := make([]string, 0)
/* special handling for cpu properties */
if !strings.HasPrefix(resGroup.CPURateLimit, "-") {
/* cpu rate mode */
attributes = append(attributes, fmt.Sprintf("CPU_RATE_LIMIT=%s", resGroup.CPURateLimit))
} else if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("5.9.0")) || connectionPool.Version.IsCBDB() {
/* cpuset mode */
attributes = append(attributes, fmt.Sprintf("CPUSET='%s'", resGroup.Cpuset))
}
/*
* Possible values of memory_auditor:
* - "1": cgroup
* - "0": vmtracker (default)
*/
if resGroup.MemoryAuditor == "1" {
attributes = append(attributes, "MEMORY_AUDITOR=cgroup")
} else if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("5.8.0")) || connectionPool.Version.IsCBDB() {
attributes = append(attributes, "MEMORY_AUDITOR=vmtracker")
}
attributes = append(attributes, fmt.Sprintf("MEMORY_LIMIT=%s", resGroup.MemoryLimit))
attributes = append(attributes, fmt.Sprintf("MEMORY_SHARED_QUOTA=%s", resGroup.MemorySharedQuota))
attributes = append(attributes, fmt.Sprintf("MEMORY_SPILL_RATIO=%s", memorySpillRatio))
attributes = append(attributes, fmt.Sprintf("CONCURRENCY=%s", resGroup.Concurrency))
metadataFile.MustPrintf("\n\nCREATE RESOURCE GROUP %s WITH (%s);", resGroup.Name, strings.Join(attributes, ", "))
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
PrintObjectMetadata(metadataFile, objToc, resGroupMetadata[resGroup.GetUniqueID()], resGroup, "", []uint32{0, 0})
}
}
}
func PrintCreateRoleStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, roles []Role, roleMetadata MetadataMap) {
for _, role := range roles {
start := metadataFile.ByteCount
attrs := make([]string, 0)
if role.Super {
attrs = append(attrs, "SUPERUSER")
} else {
attrs = append(attrs, "NOSUPERUSER")
}
if role.Inherit {
attrs = append(attrs, "INHERIT")
} else {
attrs = append(attrs, "NOINHERIT")
}
if role.CreateRole {
attrs = append(attrs, "CREATEROLE")
} else {
attrs = append(attrs, "NOCREATEROLE")
}
if role.CreateDB {
attrs = append(attrs, "CREATEDB")
} else {
attrs = append(attrs, "NOCREATEDB")
}
if role.CanLogin {
attrs = append(attrs, "LOGIN")
} else {
attrs = append(attrs, "NOLOGIN")
}
if role.Replication {
attrs = append(attrs, "REPLICATION")
/*
* Not adding NOREPLICATION when this is false because that option
* was not supported prior to 6 and NOREPLICATION is the default
*/
}
if role.ConnectionLimit != -1 {
attrs = append(attrs, fmt.Sprintf("CONNECTION LIMIT %d", role.ConnectionLimit))
}
if role.Password != "" {
attrs = append(attrs, fmt.Sprintf("PASSWORD '%s'", role.Password))
}
if role.ValidUntil != "" {
attrs = append(attrs, fmt.Sprintf("VALID UNTIL '%s'", role.ValidUntil))
}
attrs = append(attrs, fmt.Sprintf("RESOURCE QUEUE %s", role.ResQueue))
attrs = append(attrs, fmt.Sprintf("RESOURCE GROUP %s", role.ResGroup))
if role.Createrexthttp {
attrs = append(attrs, "CREATEEXTTABLE (protocol='http')")
}
if role.Createrextgpfd {
attrs = append(attrs, "CREATEEXTTABLE (protocol='gpfdist', type='readable')")
}
if role.Createwextgpfd {
attrs = append(attrs, "CREATEEXTTABLE (protocol='gpfdist', type='writable')")
}
if role.Createrexthdfs {
attrs = append(attrs, "CREATEEXTTABLE (protocol='gphdfs', type='readable')")
}
if role.Createwexthdfs {
attrs = append(attrs, "CREATEEXTTABLE (protocol='gphdfs', type='writable')")
}
metadataFile.MustPrintf(`
CREATE ROLE %s;
ALTER ROLE %s WITH %s;`, role.Name, role.Name, strings.Join(attrs, " "))
section, entry := role.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
if len(role.TimeConstraints) != 0 {
for _, timeConstraint := range role.TimeConstraints {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\nALTER ROLE %s DENY BETWEEN DAY %d TIME '%s' AND DAY %d TIME '%s';", role.Name, timeConstraint.StartDay, timeConstraint.StartTime, timeConstraint.EndDay, timeConstraint.EndTime)
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
PrintObjectMetadata(metadataFile, objToc, roleMetadata[role.GetUniqueID()], role, "", []uint32{0, 0})
}
}
func PrintRoleGUCStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, roleGUCs map[string][]RoleGUC) {
for roleName := range roleGUCs {
for _, roleGUC := range roleGUCs[roleName] {
start := metadataFile.ByteCount
dbString := ""
if roleGUC.DbName != "" {
dbString = fmt.Sprintf("IN DATABASE %s ", roleGUC.DbName)
}
metadataFile.MustPrintf("\n\nALTER ROLE %s %s%s;", roleName, dbString, roleGUC.Config)
section, entry := roleGUC.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
}
func PrintRoleMembershipStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, roleMembers []RoleMember) {
metadataFile.MustPrintf("\n\n")
for _, roleMember := range roleMembers {
start := metadataFile.ByteCount
metadataFile.MustPrintf("\nGRANT %s TO %s", roleMember.Role, roleMember.Member)
if roleMember.IsAdmin {
metadataFile.MustPrintf(" WITH ADMIN OPTION")
}
if roleMember.Grantor != "" {
metadataFile.MustPrintf(" GRANTED BY %s", roleMember.Grantor)
}
metadataFile.MustPrintf(";")
section, entry := roleMember.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}
func PrintCreateTablespaceStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, tablespaces []Tablespace, tablespaceMetadata MetadataMap) {
for _, tablespace := range tablespaces {
start := metadataFile.ByteCount
// Step 1: Use the utility function to parse options into a map.
optionsMap := utils.ParseOptions(tablespace.Options)
// Step 2: Determine the location string, handling remote tablespaces.
locationStr := ""
if remotePath, ok := optionsMap["path"]; ok {
locationStr = fmt.Sprintf("LOCATION %s", remotePath)
delete(optionsMap, "path")
} else {
// Fallback to standard tablespace location logic.
if tablespace.SegmentLocations == nil {
locationStr = fmt.Sprintf("FILESPACE %s", tablespace.FileLocation)
} else if len(tablespace.SegmentLocations) == 0 {
locationStr = fmt.Sprintf("LOCATION %s", tablespace.FileLocation)
} else {
locationStr = fmt.Sprintf("LOCATION %s\n\tWITH (%s)", tablespace.FileLocation, strings.Join(tablespace.SegmentLocations, ", "))
}
}
// Step 3: Separate special options for CREATE: 'server' and 'storage'.
server, hasServer := optionsMap["server"]
if hasServer {
delete(optionsMap, "server")
}
withOptions := []string{}
if storageVal, ok := optionsMap["storage"]; ok {
withOptions = append(withOptions, fmt.Sprintf("storage = %s", storageVal))
delete(optionsMap, "storage")
}
// Step 4: Rebuild the remaining options string for the ALTER statement.
alterOptionsKeys := make([]string, 0, len(optionsMap))
for k := range optionsMap {
alterOptionsKeys = append(alterOptionsKeys, k)
}
sort.Strings(alterOptionsKeys)
alterOptions := make([]string, len(alterOptionsKeys))
for i, k := range alterOptionsKeys {
alterOptions[i] = fmt.Sprintf("%s = %s", k, optionsMap[k])
}
alterOptionsStr := strings.Join(alterOptions, ", ")
// Step 5: Construct and print the CREATE TABLESPACE statement.
metadataFile.MustPrintf("\n\nCREATE TABLESPACE %s %s", tablespace.Tablespace, locationStr)
// Add WITH clause only for options like 'storage', assuming it doesn't conflict with segment location WITH.
if len(withOptions) > 0 {
metadataFile.MustPrintf(" WITH (%s)", strings.Join(withOptions, ", "))
}
if hasServer {
metadataFile.MustPrintf(" SERVER %s", server)
if tablespace.Spcfilehandlerbin != "" && tablespace.Spcfilehandlersrc != "" {
metadataFile.MustPrintf(" HANDLER '%s, %s'", tablespace.Spcfilehandlerbin, tablespace.Spcfilehandlersrc)
}
}
metadataFile.MustPrintf(";")
section, entry := tablespace.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
// Step 6: If there are any remaining "alterable" options, print the ALTER TABLESPACE statement.
if alterOptionsStr != "" {
start = metadataFile.ByteCount
metadataFile.MustPrintf("\n\nALTER TABLESPACE %s SET (%s);\n", tablespace.Tablespace, alterOptionsStr)
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
PrintObjectMetadata(metadataFile, objToc, tablespaceMetadata[tablespace.GetUniqueID()], tablespace, "", []uint32{0, 0})
}
}
func buildStorageOptionsString(optionsMap map[string]string) string {
// Sort the keys for deterministic, consistent backup output.
keys := make([]string, 0, len(optionsMap))
for k := range optionsMap {
keys = append(keys, k)
}
sort.Strings(keys)
optionParts := make([]string, len(keys))
for i, k := range keys {
// Assuming all option values should be single-quoted in the DDL.
optionParts[i] = fmt.Sprintf("%s '%s'", k, optionsMap[k])
}
return strings.Join(optionParts, ", ")
}
func PrintCreateStorageServerStatements(metadataFile *utils.FileWithByteCount, toc *toc.TOC, servers []StorageServer, serverMetadata MetadataMap) {
for _, server := range servers {
start := metadataFile.ByteCount
optionsMap := utils.ParseOptions(server.ServerOptions)
if len(optionsMap) > 0 {
optionsStr := buildStorageOptionsString(optionsMap)
metadataFile.MustPrintf("\n\nCREATE STORAGE SERVER %s OPTIONS(%s);", server.Server, optionsStr)
} else {
// It's possible for a storage server to have no options, though unlikely.
metadataFile.MustPrintf("\n\nCREATE STORAGE SERVER %s;", server.Server)
}
section, entry := server.GetMetadataEntry()
toc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
// TODO: Re-enable metadata printing for STORAGE SERVER once Cloudberry supports
// `ALTER STORAGE SERVER ... OWNER TO ...` and `COMMENT ON STORAGE SERVER ...`.
// These DDLs currently cause a syntax error.
//PrintObjectMetadata(metadataFile, toc, serverMetadata[server.GetUniqueID()], server, "", []uint32{0, 0})
}
}
func PrintCreateStorageUserMappingStatements(metadataFile *utils.FileWithByteCount, toc *toc.TOC, users []StorageUserMapping) {
for _, user := range users {
start := metadataFile.ByteCount
optionsMap := utils.ParseOptions(user.Options)
if len(optionsMap) > 0 {
optionsStr := buildStorageOptionsString(optionsMap)
metadataFile.MustPrintf("\n\nCREATE STORAGE USER MAPPING FOR %s STORAGE SERVER %s OPTIONS (%s);", user.User, user.Server, optionsStr)
} else {
// A user mapping without options may also be possible.
metadataFile.MustPrintf("\n\nCREATE STORAGE USER MAPPING FOR %s STORAGE SERVER %s;", user.User, user.Server)
}
section, entry := user.GetMetadataEntry()
toc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{0, 0})
}
}