package backup

/*
 * This file contains structs and functions related to executing specific
 * queries to gather metadata for the objects handled in predata_relations.go.
 */

import (
	"database/sql"
	"fmt"
	"strconv"
	"strings"

	"github.com/apache/cloudberry-backup/options"
	"github.com/apache/cloudberry-backup/toc"
	"github.com/apache/cloudberry-go-libs/dbconn"
	"github.com/apache/cloudberry-go-libs/gplog"
)

type Table struct {
	Relation
	TableDefinition
}

func (t Table) SkipDataBackup() bool {
	def := t.TableDefinition
	return def.IsExternal || (def.ForeignDef != ForeignTableDefinition{})
}

func (t Table) GetMetadataEntry() (string, toc.MetadataEntry) {
	objectType := toc.OBJ_TABLE
	if (t.ForeignDef != ForeignTableDefinition{}) {
		objectType = toc.OBJ_FOREIGN_TABLE
	}
	if t.IsExternal && ((connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB()) {
		objectType = toc.OBJ_FOREIGN_TABLE
	}
	referenceObject := ""
	if t.AttachPartitionInfo != (AttachPartitionInfo{}) {
		referenceObject = t.AttachPartitionInfo.Parent
	}

	return "predata",
		toc.MetadataEntry{
			Schema:          t.Schema,
			Name:            t.Name,
			ObjectType:      objectType,
			ReferenceObject: referenceObject,
			StartByte:       0,
			EndByte:         0,
		}
}

/*
 * Extract all the unique schemas out from a Table array.
 */
func createAlteredPartitionSchemaSet(tables []Table) map[string]bool {
	partitionAlteredSchemas := make(map[string]bool)
	for _, table := range tables {
		for _, alteredPartitionRelation := range table.PartitionAlteredSchemas {
			partitionAlteredSchemas[alteredPartitionRelation.NewSchema] = true
		}
	}

	return partitionAlteredSchemas
}

type TableDefinition struct {
	DistPolicy              DistPolicy
	PartDef                 string
	PartTemplateDef         string
	StorageOpts             string
	TablespaceName          string
	ColumnDefs              []ColumnDefinition
	IsExternal              bool
	ExtTableDef             ExternalTableDefinition
	PartitionLevelInfo      PartitionLevelInfo
	TableType               string
	IsUnlogged              bool
	ForeignDef              ForeignTableDefinition
	Inherits                []string
	ReplicaIdentity         string
	PartitionAlteredSchemas []AlteredPartitionRelation
	AccessMethodName        string
	PartitionKeyDef         string
	AttachPartitionInfo     AttachPartitionInfo
	ForceRowSecurity        bool
}

/*
 * This function calls all the functions needed to gather the metadata for a
 * single table and assembles the metadata into ColumnDef and TableDef structs
 * for more convenient handling in the PrintCreateTableStatement() function.
 */
func ConstructDefinitionsForTables(connectionPool *dbconn.DBConn, tableRelations []Relation) []Table {
	tables := make([]Table, 0)

	gplog.Info("Gathering additional table metadata")
	columnDefs := GetColumnDefinitions(connectionPool)
	distributionPolicies := GetDistributionPolicies(connectionPool, tableRelations)
	partitionDefs, partTemplateDefs := GetPartitionDetails(connectionPool)
	tablespaceNames, storageOptions := GetTableStorage(connectionPool)
	extTableDefs := GetExternalTableDefinitions(connectionPool)
	partTableMap := GetPartitionTableMap(connectionPool)
	tableTypeMap := GetTableType(connectionPool)
	accessMethodMap := GetTableAccessMethod(connectionPool)
	unloggedTableMap := GetUnloggedTables(connectionPool)
	foreignTableDefs := GetForeignTableDefinitions(connectionPool)
	inheritanceMap := GetTableInheritance(connectionPool, tableRelations)
	replicaIdentityMap := GetTableReplicaIdentity(connectionPool)
	partitionAlteredSchemaMap := GetPartitionAlteredSchema(connectionPool)
	partitionKeyDefs := GetPartitionKeyDefs(connectionPool)
	attachPartitionInfo := GetAttachPartitionInfo(connectionPool)
	forceRowSecurity := GetForceRowSecurity(connectionPool)

	gplog.Verbose("Constructing table definition map")
	for _, tableRel := range tableRelations {
		oid := tableRel.Oid
		tableDef := TableDefinition{
			DistPolicy:              distributionPolicies[oid],
			PartDef:                 partitionDefs[oid],
			PartTemplateDef:         partTemplateDefs[oid],
			StorageOpts:             storageOptions[oid],
			TablespaceName:          tablespaceNames[oid],
			ColumnDefs:              columnDefs[oid],
			IsExternal:              extTableDefs[oid].Oid != 0,
			ExtTableDef:             extTableDefs[oid],
			PartitionLevelInfo:      partTableMap[oid],
			TableType:               tableTypeMap[oid],
			IsUnlogged:              unloggedTableMap[oid],
			ForeignDef:              foreignTableDefs[oid],
			Inherits:                inheritanceMap[oid],
			ReplicaIdentity:         replicaIdentityMap[oid],
			PartitionAlteredSchemas: partitionAlteredSchemaMap[oid],
			AccessMethodName:        accessMethodMap[oid],
			PartitionKeyDef:         partitionKeyDefs[oid],
			AttachPartitionInfo:     attachPartitionInfo[oid],
			ForceRowSecurity:        forceRowSecurity[oid],
		}
		if tableDef.Inherits == nil {
			tableDef.Inherits = []string{}
		}
		tables = append(tables, Table{tableRel, tableDef})
	}
	return tables
}

/*
 * This returns a map of all parent partition tables and leaf partition tables;
 * "p" indicates a parent table, "l" indicates a leaf table, and "i" indicates
 * an intermediate table.
 */

type PartitionLevelInfo struct {
	Oid      uint32
	Level    string
	RootName string
}

func GetPartitionTableMap(connectionPool *dbconn.DBConn) map[uint32]PartitionLevelInfo {
	before7Query := `
		SELECT pc.oid AS oid,
			'p' AS level,
			'' AS rootname
		FROM pg_partition p
			JOIN pg_class pc ON p.parrelid = pc.oid
		UNION ALL
		SELECT r.parchildrelid AS oid,
			CASE WHEN p.parlevel = levels.pl THEN 'l' ELSE 'i' END AS level,
			quote_ident(cparent.relname) AS rootname
		FROM pg_partition p
			JOIN pg_partition_rule r ON p.oid = r.paroid
			JOIN pg_class cparent ON cparent.oid = p.parrelid
			JOIN (SELECT parrelid AS relid, max(parlevel) AS pl
				FROM pg_partition GROUP BY parrelid) AS levels ON p.parrelid = levels.relid
		WHERE r.parchildrelid != 0`

	atLeast7Query := `
		SELECT c.oid,
			CASE WHEN p.partrelid IS NOT NULL AND c.relispartition = false THEN ''
				ELSE rc.relname
			END AS rootname,
			CASE WHEN p.partrelid IS NOT NULL AND c.relispartition = false THEN 'p'
				WHEN p.partrelid IS NOT NULL AND c.relispartition = true THEN 'i'
				ELSE 'l'
			END AS level
		FROM pg_class c
			LEFT JOIN pg_partitioned_table p ON c.oid = p.partrelid
			LEFT JOIN pg_class rc ON pg_partition_root(c.oid) = rc.oid
		WHERE c.relispartition = true OR c.relkind = 'p'`

	query := ""
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		query = before7Query
	} else {
		query = atLeast7Query
	}

	results := make([]PartitionLevelInfo, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)

	resultMap := make(map[uint32]PartitionLevelInfo)
	for _, result := range results {
		resultMap[result.Oid] = result
	}

	return resultMap
}

type ColumnDefinition struct {
	Oid                   uint32 `db:"attrelid"`
	Num                   int    `db:"attnum"`
	Name                  string
	NotNull               bool `db:"attnotnull"`
	HasDefault            bool `db:"atthasdef"`
	Type                  string
	Encoding              string
	StatTarget            int `db:"attstattarget"`
	StorageType           string
	DefaultVal            string
	Comment               string
	Privileges            sql.NullString
	Kind                  string
	Options               string
	FdwOptions            string
	Collation             string
	SecurityLabelProvider string
	SecurityLabel         string
	AttGenerated          string
	IsInherited           bool
}

var storageTypeCodes = map[string]string{
	"e": "EXTERNAL",
	"m": "MAIN",
	"p": "PLAIN",
	"x": "EXTENDED",
}

var attGeneratedCodes = map[string]string{
	"s": "STORED",
}

func GetColumnDefinitions(connectionPool *dbconn.DBConn) map[uint32][]ColumnDefinition {
	// This query is adapted from the getTableAttrs() function in pg_dump.c.
	// Optimize Get column definitions to avoid child partitions
	// Include child partitions that are also external tables
	gplog.Verbose("Getting column definitions")
	results := make([]ColumnDefinition, 0)

	before6Query := fmt.Sprintf(`
	SELECT a.attrelid,
		a.attnum,
		quote_ident(a.attname) AS name,
		a.attnotnull,
		a.atthasdef,
		pg_catalog.format_type(t.oid,a.atttypmod) AS type,
		coalesce(pg_catalog.array_to_string(e.attoptions, ','), '') AS encoding,
		a.attstattarget,
		CASE WHEN a.attstorage != t.typstorage THEN a.attstorage ELSE '' END AS storagetype,
		coalesce('('||pg_catalog.pg_get_expr(ad.adbin, ad.adrelid)||')', '') AS defaultval,
		coalesce(d.description, '') AS comment
	FROM pg_catalog.pg_attribute a
		JOIN pg_class c ON a.attrelid = c.oid
		JOIN pg_namespace n ON c.relnamespace = n.oid
		LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
		LEFT JOIN pg_catalog.pg_type t ON a.atttypid = t.oid
		LEFT JOIN pg_catalog.pg_attribute_encoding e ON e.attrelid = a.attrelid AND e.attnum = a.attnum
		LEFT JOIN pg_description d ON d.objoid = a.attrelid AND d.classoid = 'pg_class'::regclass AND d.objsubid = a.attnum
	WHERE %s
		AND NOT EXISTS (
			SELECT 1 FROM 
				(SELECT parchildrelid FROM pg_partition_rule EXCEPT SELECT reloid FROM pg_exttable) par 
			WHERE par.parchildrelid = c.oid)
		AND c.reltype <> 0
		AND a.attnum > 0::pg_catalog.int2
		AND a.attisdropped = 'f'
	ORDER BY a.attrelid, a.attnum`, relationAndSchemaFilterClause())

	// GPDB 6 adds multiple additional column attributes and options including fdwoptions and security labels.
	// Add capture logic for all of these.
	version6Query := fmt.Sprintf(`
	SELECT a.attrelid,
		a.attnum,
		quote_ident(a.attname) AS name,
		a.attnotnull,
		a.atthasdef,
		pg_catalog.format_type(t.oid,a.atttypmod) AS type,
		coalesce(pg_catalog.array_to_string(e.attoptions, ','), '') AS encoding,
		a.attstattarget,
		CASE WHEN a.attstorage != t.typstorage THEN a.attstorage ELSE '' END AS storagetype,
		coalesce('('||pg_catalog.pg_get_expr(ad.adbin, ad.adrelid)||')', '') AS defaultval,
		coalesce(d.description, '') AS comment,
		CASE
			WHEN a.attacl IS NULL THEN NULL
			WHEN array_upper(a.attacl, 1) = 0 THEN a.attacl[0]
			ELSE unnest(a.attacl) END AS privileges,
		CASE
			WHEN a.attacl IS NULL THEN ''
			WHEN array_upper(a.attacl, 1) = 0 THEN 'Empty'
			ELSE ''
		END AS kind,
		coalesce(pg_catalog.array_to_string(a.attoptions, ','), '') AS options,
		coalesce(array_to_string(ARRAY(SELECT option_name || ' ' || quote_literal(option_value) FROM pg_options_to_table(attfdwoptions) ORDER BY option_name), ', '), '') AS fdwoptions,
		CASE WHEN a.attcollation <> t.typcollation THEN quote_ident(cn.nspname) || '.' || quote_ident(coll.collname) ELSE '' END AS collation,
		coalesce(sec.provider,'') AS securitylabelprovider,
		coalesce(sec.label,'') AS securitylabel
	FROM pg_catalog.pg_attribute a
		JOIN pg_class c ON a.attrelid = c.oid
		JOIN pg_namespace n ON c.relnamespace = n.oid
		LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
		LEFT JOIN pg_catalog.pg_type t ON a.atttypid = t.oid
		LEFT JOIN pg_catalog.pg_attribute_encoding e ON e.attrelid = a.attrelid AND e.attnum = a.attnum
		LEFT JOIN pg_description d ON d.objoid = a.attrelid AND d.classoid = 'pg_class'::regclass AND d.objsubid = a.attnum
		LEFT JOIN pg_collation coll ON a.attcollation = coll.oid
		LEFT JOIN pg_namespace cn ON coll.collnamespace = cn.oid
		LEFT JOIN pg_seclabel sec ON sec.objoid = a.attrelid AND sec.classoid = 'pg_class'::regclass AND sec.objsubid = a.attnum
	WHERE %s
		AND NOT EXISTS (
			SELECT 1 FROM 
				(SELECT parchildrelid FROM pg_partition_rule EXCEPT SELECT reloid FROM pg_exttable) par 
			WHERE par.parchildrelid = c.oid)
		AND c.reltype <> 0
		AND a.attnum > 0::pg_catalog.int2
		AND a.attisdropped = 'f'
	ORDER BY a.attrelid, a.attnum`, relationAndSchemaFilterClause())

	// In GPDB7+ we do not want to exclude child partitions, they function as separate tables.
	// Cannot use unnest() in CASE statements anymore in GPDB 7+ so convert
	// it to a LEFT JOIN LATERAL. We do not use LEFT JOIN LATERAL for GPDB 6
	// because the CASE unnest() logic is more performant.
	atLeast7Query := fmt.Sprintf(`
	SELECT a.attrelid,
		a.attnum,
		quote_ident(a.attname) AS name,
		a.attnotnull,
		a.atthasdef,
		pg_catalog.format_type(t.oid,a.atttypmod) AS type,
		coalesce(pg_catalog.array_to_string(e.attoptions, ','), '') AS encoding,
		a.attstattarget,
		CASE WHEN a.attstorage != t.typstorage THEN a.attstorage ELSE '' END AS storagetype,
		coalesce('('||pg_catalog.pg_get_expr(ad.adbin, ad.adrelid)||')', '') AS defaultval,
		coalesce(d.description, '') AS comment,
		a.attgenerated,
		ljl_unnest AS privileges,
		CASE
			WHEN a.attacl IS NULL THEN ''
			WHEN array_upper(a.attacl, 1) = 0 THEN 'Empty'
			ELSE ''
		END AS kind,
		coalesce(pg_catalog.array_to_string(a.attoptions, ','), '') AS options,
		coalesce(array_to_string(ARRAY(SELECT option_name || ' ' || quote_literal(option_value) FROM pg_options_to_table(attfdwoptions) ORDER BY option_name), ', '), '') AS fdwoptions,
		CASE WHEN a.attcollation <> t.typcollation THEN quote_ident(cn.nspname) || '.' || quote_ident(coll.collname) ELSE '' END AS collation,
		coalesce(sec.provider,'') AS securitylabelprovider,
		coalesce(sec.label,'') AS securitylabel,
		(a.attinhcount > 0) AS isinherited
	FROM pg_catalog.pg_attribute a
		JOIN pg_class c ON a.attrelid = c.oid
		JOIN pg_namespace n ON c.relnamespace = n.oid
		LEFT JOIN pg_catalog.pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
		LEFT JOIN pg_catalog.pg_type t ON a.atttypid = t.oid
		LEFT JOIN pg_catalog.pg_attribute_encoding e ON e.attrelid = a.attrelid AND e.attnum = a.attnum
		LEFT JOIN pg_description d ON d.objoid = a.attrelid AND d.classoid = 'pg_class'::regclass AND d.objsubid = a.attnum
		LEFT JOIN pg_collation coll ON a.attcollation = coll.oid
		LEFT JOIN pg_namespace cn ON coll.collnamespace = cn.oid
		LEFT JOIN pg_seclabel sec ON sec.objoid = a.attrelid AND sec.classoid = 'pg_class'::regclass AND sec.objsubid = a.attnum
		LEFT JOIN LATERAL unnest(a.attacl) ljl_unnest ON a.attacl IS NOT NULL AND array_length(a.attacl, 1) != 0
	WHERE %s
		AND c.reltype <> 0
		AND a.attnum > 0::pg_catalog.int2
		AND a.attisdropped = 'f'
	ORDER BY a.attrelid, a.attnum`, relationAndSchemaFilterClause())

	query := ``
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		query = before6Query
	} else if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("6") {
		query = version6Query
	} else {
		query = atLeast7Query
	}

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32][]ColumnDefinition)
	for _, result := range results {
		result.StorageType = storageTypeCodes[result.StorageType]
		if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
			result.AttGenerated = attGeneratedCodes[result.AttGenerated]
		}
		resultMap[result.Oid] = append(resultMap[result.Oid], result)
	}
	return resultMap
}

type DistPolicy struct {
	Oid        uint32
	Policy     string `db:"value"`
	DistByEnum bool   `db:"distbyenum"`
}

func GetDistributionPolicies(connectionPool *dbconn.DBConn, relations interface{}) map[uint32]DistPolicy {
	gplog.Verbose("Getting distribution policies")

	tbloids := "{"
	query := ""

	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		// This query is adapted from the addDistributedBy() function in pg_dump.c.
		query = `
		SELECT p.localoid AS oid,
			'DISTRIBUTED BY (' || string_agg(quote_ident(a.attname) , ', ' ORDER BY index) || ')' AS value	
		FROM (SELECT localoid, unnest(attrnums) AS attnum,
				generate_series(1, array_upper(attrnums, 1)) AS index
				FROM gp_distribution_policy WHERE attrnums IS NOT NULL) p
			JOIN pg_attribute a ON (p.localoid, p.attnum) = (a.attrelid, a.attnum)
		GROUP BY localoid
		UNION ALL
		SELECT p.localoid AS oid, 'DISTRIBUTED RANDOMLY' AS value
		FROM gp_distribution_policy p WHERE attrnums IS NULL`
	} else {
		// Identify relations of interest
		// Tables and Materialized Views can have a distribution policy
		if rels, ok := relations.([]Relation); ok {
			for _, rel := range rels {
				oid := rel.GetUniqueID().Oid
				if len(tbloids) > 1 {
					tbloids += ","
				}
				tbloids += strconv.Itoa(int(oid))
			}
			tbloids += "}"
		} else if rels, ok := relations.([]View); ok {
			for _, rel := range rels {
				oid := rel.GetUniqueID().Oid
				if len(tbloids) > 1 {
					tbloids += ","
				}
				tbloids += strconv.Itoa(int(oid))
			}
			tbloids += "}"
		} else {
			gplog.Fatal(fmt.Errorf("Unable to parse distribution policies"), "")
		}

		query = fmt.Sprintf(`WITH distpol AS (
			SELECT
				localoid AS oid,
				d.distclass,
				pg_catalog.pg_get_table_distributedby(localoid) AS value 
			FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid) 
			JOIN gp_distribution_policy d ON d.localoid=src.tbloid
			)
			SELECT 
			distpol.oid,
			distpol.value,
				CASE WHEN opcintype=%d THEN true ELSE false END AS distbyenum
			FROM distpol 
			LEFT JOIN (
				SELECT localoid, unnest(distclass) AS distclass 
				FROM gp_distribution_policy
				) t 
			ON t.localoid=distpol.oid
			LEFT JOIN pg_opclass opc ON opc.oid=t.distclass;`, tbloids, ENUM_TYPE_OID)
	}

	results := make([]DistPolicy, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]DistPolicy)
	for _, result := range results {
		resultMap[result.Oid] = result
	}
	return resultMap

}

func GetTableType(connectionPool *dbconn.DBConn) map[uint32]string {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		return map[uint32]string{}
	}
	query := `SELECT oid, reloftype::pg_catalog.regtype AS value FROM pg_class WHERE reloftype != 0`
	return selectAsOidToStringMap(connectionPool, query)
}

func GetTableAccessMethod(connectionPool *dbconn.DBConn) map[uint32]string {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		return map[uint32]string{}
	}
	query := `
	SELECT c.oid, a.amname AS value
	FROM pg_class c
        JOIN pg_am a ON c.relam = a.oid
	WHERE a.amtype = 't'
        AND a.amname != 'heap';`
	return selectAsOidToStringMap(connectionPool, query)
}

func GetTableReplicaIdentity(connectionPool *dbconn.DBConn) map[uint32]string {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		return make(map[uint32]string)
	}

	relkindFilter := "'r', 'm'"
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		relkindFilter = "'r', 'm', 'p'"
	}

	query := fmt.Sprintf(`
	SELECT oid,
		relreplident AS value
	FROM pg_class
	WHERE relkind IN (%s)
		AND oid >= %d`, relkindFilter, FIRST_NORMAL_OBJECT_ID)
	return selectAsOidToStringMap(connectionPool, query)
}

func GetPartitionDetails(connectionPool *dbconn.DBConn) (map[uint32]string, map[uint32]string) {
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		// GPDB7 reworked partition tables significantly, to match
		// how upstream handles them.  These separate details are no
		// longer needed, and instead partition tables are handled as
		// true tables and their information is retrieved during "normal"
		// table queries.
		return make(map[uint32]string), make(map[uint32]string)
	}

	gplog.Info("Getting partition definitions")
	query := fmt.Sprintf(`
	SELECT p.parrelid AS oid,
		pg_get_partition_def(p.parrelid, true, true) AS definition,
		pg_get_partition_template_def(p.parrelid, true, true) AS template
	FROM pg_partition p
		JOIN pg_class c ON p.parrelid = c.oid
		JOIN pg_namespace n ON c.relnamespace = n.oid
	WHERE %s`, relationAndSchemaFilterClause())
	var results []struct {
		Oid        uint32
		Definition string
		Template   sql.NullString
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	partitionDef := make(map[uint32]string)
	partitionTemp := make(map[uint32]string)
	for _, result := range results {
		partitionDef[result.Oid] = result.Definition
		if result.Template.Valid {
			partitionTemp[result.Oid] = result.Template.String
		}
	}
	return partitionDef, partitionTemp
}

type AlteredPartitionRelation struct {
	OldSchema string
	NewSchema string
	Name      string
}

/*
 * Partition tables could have child partitions in schemas different
 * than the root partition. We need to keep track of these child
 * partitions and later create ALTER TABLE SET SCHEMA statements for
 * them.
 */
func GetPartitionAlteredSchema(connectionPool *dbconn.DBConn) map[uint32][]AlteredPartitionRelation {
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		return make(map[uint32][]AlteredPartitionRelation)
	}

	gplog.Info("Getting child partitions with altered schema")
	query := `
	SELECT pgp.parrelid AS oid,
		quote_ident(pgn2.nspname) AS oldschema,
		quote_ident(pgn.nspname) AS newschema,
		quote_ident(pgc.relname) AS name
	FROM pg_catalog.pg_partition_rule pgpr
		JOIN pg_catalog.pg_partition pgp ON pgp.oid = pgpr.paroid
		JOIN pg_catalog.pg_class pgc ON pgpr.parchildrelid = pgc.oid
		JOIN pg_catalog.pg_class pgc2 ON pgp.parrelid = pgc2.oid
		JOIN pg_catalog.pg_namespace pgn ON pgc.relnamespace = pgn.oid
		JOIN pg_catalog.pg_namespace pgn2 ON pgc2.relnamespace = pgn2.oid
	WHERE pgc.relnamespace != pgc2.relnamespace`
	var results []struct {
		Oid uint32
		AlteredPartitionRelation
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	partitionAlteredSchemaMap := make(map[uint32][]AlteredPartitionRelation)
	for _, result := range results {
		alteredPartitionRelation := AlteredPartitionRelation{result.OldSchema, result.NewSchema, result.Name}
		partitionAlteredSchemaMap[result.Oid] = append(partitionAlteredSchemaMap[result.Oid], alteredPartitionRelation)
	}
	return partitionAlteredSchemaMap
}

func GetTableStorage(connectionPool *dbconn.DBConn) (map[uint32]string, map[uint32]string) {
	gplog.Info("Getting storage information")
	query := fmt.Sprintf(`
	SELECT c.oid,
		quote_ident(t.spcname) AS tablespace,
		array_to_string(reloptions, ', ') AS reloptions
	FROM pg_class c
		JOIN pg_namespace n ON c.relnamespace = n.oid
		LEFT JOIN pg_tablespace t ON t.oid = c.reltablespace
	WHERE %s
		AND (t.spcname IS NOT NULL OR reloptions IS NOT NULL)`,
		relationAndSchemaFilterClause())
	var results []struct {
		Oid        uint32
		Tablespace sql.NullString
		RelOptions sql.NullString
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	tableSpaces := make(map[uint32]string)
	relOptions := make(map[uint32]string)
	for _, result := range results {
		if result.Tablespace.Valid {
			tableSpaces[result.Oid] = result.Tablespace.String
		}
		if result.RelOptions.Valid {
			reloptString := result.RelOptions.String

			// Prior to GPDB7, it was possible to indicate fillfactor for an AO table, but it was a
			// no-op. In GPDB7 this was banned server-side. To prevent migration restores from
			// failing in this case, and because they're garbage values anyway, drop fillfactor for
			// AO tables.
			if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") &&
				strings.Contains(reloptString, "appendonly") &&
				strings.Contains(reloptString, "fillfactor") {

				opts := strings.Split(reloptString, ", ")
				newopts := make([]string, 0)
				for _, opt := range opts {
					if !strings.Contains(opt, "fillfactor") {
						newopts = append(newopts, opt)
					}
				}
				reloptString = strings.Join(newopts, ", ")
			}
			relOptions[result.Oid] = reloptString
		}
	}
	return tableSpaces, relOptions
}

func GetUnloggedTables(connectionPool *dbconn.DBConn) map[uint32]bool {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		return map[uint32]bool{}
	}
	query := `SELECT oid FROM pg_class WHERE relpersistence = 'u'`
	var results []struct {
		Oid uint32
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]bool)
	for _, result := range results {
		resultMap[result.Oid] = true
	}
	return resultMap
}

type ForeignTableDefinition struct {
	Oid     uint32 `db:"ftrelid"`
	Options string `db:"ftoptions"`
	Server  string `db:"ftserver"`
}

func GetForeignTableDefinitions(connectionPool *dbconn.DBConn) map[uint32]ForeignTableDefinition {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		return map[uint32]ForeignTableDefinition{}
	}

	query := fmt.Sprintf(`
	SELECT ftrelid, quote_ident(fs.srvname) AS ftserver,
		array_to_string(array(
			SELECT quote_ident(option_name) || ' ' || quote_literal(option_value)
			FROM pg_options_to_table(ftoptions) ORDER BY option_name
		), e',    ') AS ftoptions
	FROM pg_foreign_table ft
		JOIN pg_foreign_server fs ON ft.ftserver = fs.oid
	WHERE ft.ftrelid >= %d AND fs.oid >= %d`, FIRST_NORMAL_OBJECT_ID, FIRST_NORMAL_OBJECT_ID)
	results := make([]ForeignTableDefinition, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]ForeignTableDefinition, len(results))
	for _, result := range results {
		resultMap[result.Oid] = result
	}
	return resultMap
}

type Dependency struct {
	Oid              uint32
	ReferencedObject string
}

func GetTableInheritance(connectionPool *dbconn.DBConn, tables []Relation) map[uint32][]string {
	tableFilterStr := ""
	if len(MustGetFlagStringArray(options.INCLUDE_RELATION)) > 0 {
		tableOidList := make([]string, len(tables))
		for i, table := range tables {
			tableOidList[i] = fmt.Sprintf("%d", table.Oid)
		}
		// If we are filtering on tables, we only want to record dependencies on other tables in the list
		if len(tableOidList) > 0 {
			tableFilterStr = fmt.Sprintf("\nAND i.inhrelid IN (%s)", strings.Join(tableOidList, ","))
		}
	}

	query := fmt.Sprintf(`
	SELECT i.inhrelid AS oid,
		quote_ident(n.nspname) || '.' || quote_ident(p.relname) AS referencedobject
	FROM pg_inherits i
		JOIN pg_class p ON i.inhparent = p.oid
		JOIN pg_namespace n ON p.relnamespace = n.oid
	WHERE %s%s
	ORDER BY i.inhrelid, i.inhseqno`,
		ExtensionFilterClause("p"), tableFilterStr)

	results := make([]Dependency, 0)
	resultMap := make(map[uint32][]string)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	for _, result := range results {
		resultMap[result.Oid] = append(resultMap[result.Oid], result.ReferencedObject)
	}
	return resultMap
}

// Used to contruct root tables for GPDB 7+, because the root partition must be
// constructed by itself first.
func GetPartitionKeyDefs(connectionPool *dbconn.DBConn) map[uint32]string {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		return make(map[uint32]string, 0)
	}
	query := `
	SELECT
		partrelid AS oid,
		pg_get_partkeydef(partrelid) AS keydef
	FROM
		pg_partitioned_table;`

	var results []struct {
		Oid    uint32
		Keydef string
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]string)
	for _, result := range results {
		resultMap[result.Oid] = result.Keydef
	}
	return resultMap
}

type AttachPartitionInfo struct {
	Oid     uint32
	Relname string
	Parent  string
	Expr    string
}

func GetAttachPartitionInfo(connectionPool *dbconn.DBConn) map[uint32]AttachPartitionInfo {
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		return make(map[uint32]AttachPartitionInfo, 0)
	}

	query := `
	SELECT
		c.oid,
		quote_ident(n.nspname) || '.' || quote_ident(c.relname) AS relname,
		quote_ident(rn.nspname) || '.' || quote_ident(rc.relname) AS parent,
		coalesce(pg_get_expr(c.relpartbound, c.oid), '') AS expr
	FROM pg_class c
		JOIN pg_namespace n ON c.relnamespace = n.oid
		JOIN pg_class rc ON pg_partition_root(c.oid) = rc.oid
		JOIN pg_namespace rn ON rc.relnamespace = rn.oid
	WHERE c.relispartition = 't'`

	results := make([]AttachPartitionInfo, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]AttachPartitionInfo)
	for _, result := range results {
		resultMap[result.Oid] = result
	}
	return resultMap
}

func GetForceRowSecurity(connectionPool *dbconn.DBConn) map[uint32]bool {
	resultMap := make(map[uint32]bool)
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		return resultMap
	}

	query := fmt.Sprintf(`
	SELECT oid
	FROM pg_class
	WHERE relforcerowsecurity = 't'
		AND oid >= %d`, FIRST_NORMAL_OBJECT_ID)

	results := make([]uint32, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)

	for _, oid := range results {
		resultMap[oid] = true
	}
	return resultMap
}

func selectAsOidToStringMap(connectionPool *dbconn.DBConn, query string) map[uint32]string {
	var results []struct {
		Oid   uint32
		Value string
	}
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	resultMap := make(map[uint32]string)
	for _, result := range results {
		resultMap[result.Oid] = result.Value
	}
	return resultMap
}
