blob: 399146fcb75a9de23e87f13e8a9acfc8e89db868 [file]
package backup
/*
* This file contains structs and functions related to backing up metadata on the
* coordinator for objects that connect to external data (external tables and external
* protocols).
*/
import (
"database/sql"
"fmt"
"strings"
"github.com/apache/cloudberry-backup/toc"
"github.com/apache/cloudberry-backup/utils"
)
const (
// Type of external table
READABLE = iota
READABLE_WEB
WRITABLE
WRITABLE_WEB
// Protocol external table is using
FILE
GPFDIST
GPHDFS
HTTP
S3
)
type ExternalTableDefinition struct {
Oid uint32
Type int
Protocol int
Location sql.NullString
ExecLocation string
FormatType string
FormatOpts string
Command string
RejectLimit int
RejectLimitType string
ErrTableName string
ErrTableSchema string
LogErrors bool
LogErrPersist bool
Encoding string
Writable bool
URIs []string
}
func PrintExternalTableCreateStatement(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, table Table) {
start := metadataFile.ByteCount
tableTypeStrMap := map[int]string{
READABLE: "READABLE EXTERNAL",
READABLE_WEB: "READABLE EXTERNAL WEB",
WRITABLE: "WRITABLE EXTERNAL",
WRITABLE_WEB: "WRITABLE EXTERNAL WEB",
}
extTableDef := table.ExtTableDef
extTableDef.Type, extTableDef.Protocol = DetermineExternalTableCharacteristics(extTableDef)
metadataFile.MustPrintf("\n\nCREATE %s TABLE %s (\n", tableTypeStrMap[extTableDef.Type], table.FQN())
printColumnDefinitions(metadataFile, table.ColumnDefs, "")
metadataFile.MustPrintf(") ")
PrintExternalTableStatements(metadataFile, table.FQN(), extTableDef)
if extTableDef.Writable {
metadataFile.MustPrintf("\n%s", table.DistPolicy.Policy)
}
metadataFile.MustPrintf(";")
if objToc != nil {
section, entry := table.GetMetadataEntry()
tier := globalTierMap[table.GetUniqueID()]
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, tier)
}
}
func DetermineExternalTableCharacteristics(extTableDef ExternalTableDefinition) (int, int) {
isWritable := extTableDef.Writable
var tableType int
tableProtocol := -1
if !extTableDef.Location.Valid ||
extTableDef.Location.String == "" { // EXTERNAL WEB tables may have EXECUTE instead of LOCATION
tableProtocol = HTTP
if isWritable {
tableType = WRITABLE_WEB
} else {
tableType = READABLE_WEB
}
} else {
/*
* All data sources must use the same protocol, so we can use Location to determine
* the table's protocol even though it only holds one data source URI.
*/
isWeb := strings.HasPrefix(extTableDef.Location.String, "http")
if isWeb && isWritable {
tableType = WRITABLE_WEB
} else if isWeb && !isWritable {
tableType = READABLE_WEB
} else if !isWeb && isWritable {
tableType = WRITABLE
} else {
tableType = READABLE
}
prefix := extTableDef.Location.String[0:strings.Index(extTableDef.Location.String, "://")]
switch prefix {
case "file":
tableProtocol = FILE
case "gpfdist":
tableProtocol = GPFDIST
case "gpfdists":
tableProtocol = GPFDIST
case "gphdfs":
tableProtocol = GPHDFS
case "http":
tableProtocol = HTTP
case "https":
tableProtocol = HTTP
case "s3":
tableProtocol = S3
}
}
return tableType, tableProtocol
}
func generateExecuteStatement(extTableDef ExternalTableDefinition) string {
var executeStatement string
extTableDef.Command = strings.Replace(extTableDef.Command, `'`, `''`, -1)
executeStatement += fmt.Sprintf("EXECUTE '%s'", extTableDef.Command)
execType := strings.Split(extTableDef.ExecLocation, ":")
switch execType[0] {
case "ALL_SEGMENTS": // Default case, don't print anything else
case "HOST":
executeStatement += fmt.Sprintf(" ON HOST '%s'", execType[1])
case "MASTER_ONLY":
executeStatement += " ON MASTER"
case "COORDINATOR_ONLY":
executeStatement += " ON COORDINATOR"
case "PER_HOST":
executeStatement += " ON HOST"
case "SEGMENT_ID":
executeStatement += fmt.Sprintf(" ON SEGMENT %s", execType[1])
case "TOTAL_SEGS":
executeStatement += fmt.Sprintf(" ON %s", execType[1])
}
return executeStatement
}
/*
* This function is adapted from dumputils.c
*
* Escape backslashes and apostrophes in EXTERNAL TABLE format strings.
* Returns a list of unquoted keyword and escaped quoted string tokens
*
* The fmtopts field of a pg_exttable tuple has an odd encoding -- it is
* partially parsed and contains "string" values that aren't legal SQL.
* Each string value is delimited by apostrophes and is usually, but not
* always, a single character. The fmtopts field is typically something
* like {delimiter '\x09' null '\N' escape '\'} or
* {delimiter ',' null '' escape '\' quote '''}. Each backslash and
* apostrophe in a string must be escaped and each string must be
* prepended with an 'E' denoting an "escape syntax" string.
*
* Usage note: A field value containing an apostrophe followed by a space
* will throw this algorithm off -- it presumes no embedded spaces.
*/
func tokenizeAndEscapeFormatOpts(formatOpts string) []string {
inString := false
resultList := make([]string, 0)
currString := ""
for i := 0; i < len(formatOpts); i++ {
switch formatOpts[i] {
case '\'':
if inString {
/*
* Escape apostrophes *within* the string. If the
* apostrophe is at the end of the source string or is
* followed by a space, it is presumed to be a closing
* apostrophe and is not escaped.
*/
if (i+1) == len(formatOpts) || formatOpts[i+1] == ' ' {
inString = false
} else {
currString += "\\"
}
} else {
currString = "E"
inString = true
}
case '\\':
currString += "\\"
case ' ':
if !inString {
resultList = append(resultList, currString)
currString = ""
continue
}
}
currString += string(formatOpts[i])
}
resultList = append(resultList, currString)
return resultList
}
/*
* Format options to use `a = b` format because this format is required
* when using CUSTOM format.
*
* We do this for CUSTOM, AVRO and PARQUET, but not CSV or TEXT because
* CSV and TEXT have some multi-word options that are difficult
* to parse into this format
*/
func makeCustomFormatOpts(tokens []string) string {
var key string
var value string
resultOpts := make([]string, 0)
for i := 0; i < len(tokens)-1; i += 2 {
key = tokens[i]
value = tokens[i+1]
opt := fmt.Sprintf(`%s = %s`, key, value)
resultOpts = append(resultOpts, opt)
}
return strings.Join(resultOpts, ", ")
}
func GenerateFormatStatement(extTableDef ExternalTableDefinition) string {
var formatStatement string
formatType := ""
switch extTableDef.FormatType {
case "t":
formatType = "TEXT"
case "c":
formatType = "CSV"
case "b":
formatType = "CUSTOM"
case "a":
formatType = "AVRO"
case "p":
formatType = "PARQUET"
}
formatStatement += fmt.Sprintf("FORMAT '%s'", formatType)
if extTableDef.FormatOpts != "" {
formatTokens := tokenizeAndEscapeFormatOpts(strings.TrimSpace(extTableDef.FormatOpts))
formatOptsString := ""
if formatType == "TEXT" || formatType == "CSV" {
formatOptsString = strings.Join(formatTokens, " ")
} else {
formatOptsString = makeCustomFormatOpts(formatTokens)
}
formatStatement += fmt.Sprintf(" (%s)", formatOptsString)
}
return formatStatement
}
func generateLogErrorStatement(extTableDef ExternalTableDefinition) string {
logErrorStatement := ""
if extTableDef.LogErrors {
if extTableDef.LogErrPersist {
logErrorStatement += "\nLOG ERRORS PERSISTENTLY"
} else {
logErrorStatement += "\nLOG ERRORS"
}
} else if extTableDef.ErrTableName != "" && extTableDef.ErrTableSchema != "" {
errTableFQN := utils.MakeFQN(extTableDef.ErrTableSchema, extTableDef.ErrTableName)
logErrorStatement += fmt.Sprintf("\nLOG ERRORS INTO %s", errTableFQN)
}
if extTableDef.RejectLimit != 0 {
logErrorStatement += fmt.Sprintf("\nSEGMENT REJECT LIMIT %d ", extTableDef.RejectLimit)
switch extTableDef.RejectLimitType {
case "r":
logErrorStatement += "ROWS"
case "p":
logErrorStatement += "PERCENT"
}
}
return logErrorStatement
}
func PrintExternalTableStatements(metadataFile *utils.FileWithByteCount, tableName string, extTableDef ExternalTableDefinition) {
if extTableDef.Type != WRITABLE_WEB {
if len(extTableDef.URIs) > 0 {
metadataFile.MustPrintf("LOCATION (\n\t'%s'\n)", strings.Join(extTableDef.URIs, "',\n\t'"))
}
}
if extTableDef.Type == READABLE || (extTableDef.Type == WRITABLE_WEB && extTableDef.Protocol == S3) {
if extTableDef.ExecLocation == "COORDINATOR_ONLY" {
metadataFile.MustPrintf(" ON COORDINATOR")
} else if extTableDef.ExecLocation == "COORDINATOR_ONLY" {
metadataFile.MustPrintf(" ON COORDINATOR")
}
}
if extTableDef.Type == READABLE_WEB || extTableDef.Type == WRITABLE_WEB {
if extTableDef.Command != "" {
metadataFile.MustPrint(generateExecuteStatement(extTableDef))
}
}
metadataFile.MustPrintln()
metadataFile.MustPrint(GenerateFormatStatement(extTableDef))
metadataFile.MustPrintln()
metadataFile.MustPrintf("ENCODING '%s'", extTableDef.Encoding)
if extTableDef.Type == READABLE || extTableDef.Type == READABLE_WEB {
metadataFile.MustPrint(generateLogErrorStatement(extTableDef))
}
}
func PrintCreateExternalProtocolStatement(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, protocol ExternalProtocol, funcInfoMap map[uint32]FunctionInfo, protoMetadata ObjectMetadata) {
start := metadataFile.ByteCount
funcOidList := []uint32{protocol.ReadFunction, protocol.WriteFunction, protocol.Validator}
hasUserDefinedFunc := false
tier := globalTierMap[protocol.GetUniqueID()]
for _, funcOid := range funcOidList {
if funcInfo, ok := funcInfoMap[funcOid]; ok && !funcInfo.IsInternal {
hasUserDefinedFunc = true
}
}
if hasUserDefinedFunc {
protocolFunctions := make([]string, 0)
if protocol.ReadFunction != 0 {
protocolFunctions = append(protocolFunctions, fmt.Sprintf("readfunc = %s", funcInfoMap[protocol.ReadFunction].QualifiedName))
}
if protocol.WriteFunction != 0 {
protocolFunctions = append(protocolFunctions, fmt.Sprintf("writefunc = %s", funcInfoMap[protocol.WriteFunction].QualifiedName))
}
if protocol.Validator != 0 {
protocolFunctions = append(protocolFunctions, fmt.Sprintf("validatorfunc = %s", funcInfoMap[protocol.Validator].QualifiedName))
}
metadataFile.MustPrintf("\n\nCREATE ")
if protocol.Trusted {
metadataFile.MustPrintf("TRUSTED ")
}
metadataFile.MustPrintf("PROTOCOL %s (%s);\n", protocol.Name, strings.Join(protocolFunctions, ", "))
section, entry := protocol.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, tier)
}
PrintObjectMetadata(metadataFile, objToc, protoMetadata, protocol, "", tier)
}
func PrintExchangeExternalPartitionStatements(metadataFile *utils.FileWithByteCount, objToc *toc.TOC, extPartitions []PartitionInfo, partInfoMap map[uint32]PartitionInfo, tables []Table) {
tableNameMap := make(map[uint32]string, len(tables))
for _, table := range tables {
tableNameMap[table.Oid] = table.FQN()
}
for _, externalPartition := range extPartitions {
extPartRelationName := tableNameMap[externalPartition.RelationOid]
if extPartRelationName == "" {
continue // Not included in the list of tables to back up
}
parentRelationName := utils.MakeFQN(externalPartition.ParentSchema, externalPartition.ParentRelationName)
start := metadataFile.ByteCount
alterPartitionStr := ""
maxParentTier := []uint32{0, 0}
currentPartition := externalPartition
for currentPartition.PartitionParentRuleOid != 0 {
parent := partInfoMap[currentPartition.PartitionParentRuleOid]
if parent.PartitionName == "" {
alterPartitionStr = fmt.Sprintf("ALTER PARTITION FOR (RANK(%d)) ", parent.PartitionRank) + alterPartitionStr
} else {
// Keep track of deepest tier for partition parents, to ensure that the exchange
// statement is always run after the parent exists
parentTier, ok := globalTierMap[UniqueID{ClassID: PG_CLASS_OID, Oid: parent.RelationOid}]
if ok && parentTier[0] > maxParentTier[0] {
maxParentTier = parentTier
}
alterPartitionStr = fmt.Sprintf("ALTER PARTITION %s ", parent.PartitionName) + alterPartitionStr
}
currentPartition = parent
}
metadataFile.MustPrintf("\n\nALTER TABLE %s %s", parentRelationName, alterPartitionStr)
if externalPartition.PartitionName == "" {
metadataFile.MustPrintf("EXCHANGE PARTITION FOR (RANK(%d)) ", externalPartition.PartitionRank)
} else {
metadataFile.MustPrintf("EXCHANGE PARTITION %s ", externalPartition.PartitionName)
}
metadataFile.MustPrintf("WITH TABLE %s WITHOUT VALIDATION;", extPartRelationName)
metadataFile.MustPrintf("\n\nDROP TABLE %s;", extPartRelationName)
section, entry := externalPartition.GetMetadataEntry()
objToc.AddMetadataEntry(section, entry, start, metadataFile.ByteCount, []uint32{maxParentTier[0] + 1, maxParentTier[1]})
}
}