package backup

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

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

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

type Function struct {
	Oid               uint32
	Schema            string
	Name              string
	ReturnsSet        bool `db:"proretset"`
	FunctionBody      string
	BinaryPath        string
	Arguments         sql.NullString
	IdentArgs         sql.NullString
	ResultType        sql.NullString
	Volatility        string  `db:"provolatile"`
	IsStrict          bool    `db:"proisstrict"`
	IsLeakProof       bool    `db:"proleakproof"`
	IsSecurityDefiner bool    `db:"prosecdef"`
	Config            string  `db:"proconfig"`
	Cost              float32 `db:"procost"`
	NumRows           float32 `db:"prorows"`
	DataAccess        string  `db:"prodataaccess"`
	Language          string
	Kind              string `db:"prokind"`     // GPDB 7+
	PlannerSupport    string `db:"prosupport"`  // GPDB 7+
	IsWindow          bool   `db:"proiswindow"` // before 7
	ExecLocation      string `db:"proexeclocation"`
	Parallel          string `db:"proparallel"` // GPDB 7+
	TransformTypes    string // GPDB 7+
}

func (f Function) GetMetadataEntry() (string, toc.MetadataEntry) {
	nameWithArgs := fmt.Sprintf("%s(%s)", f.Name, f.IdentArgs.String)
	return "predata",
		toc.MetadataEntry{
			Schema:          f.Schema,
			Name:            nameWithArgs,
			ObjectType:      toc.OBJ_FUNCTION,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (f Function) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_PROC_OID, Oid: f.Oid}
}

func (f Function) FQN() string {
	/*
	 * We need to include arguments to differentiate functions with the same name
	 */
	return fmt.Sprintf("%s.%s(%s)", f.Schema, f.Name, f.IdentArgs.String)
}

func GetFunctions(connectionPool *dbconn.DBConn) []Function {
	excludeImplicitFunctionsClause := ""
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		// This excludes implicitly created functions. Currently this is only range type functions
		excludeImplicitFunctionsClause = `
	AND NOT EXISTS (
		SELECT 1 FROM pg_depend
		WHERE classid = 'pg_proc'::regclass::oid
			AND objid = p.oid AND deptype = 'i')`
	}

	locationAtts := ""
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		locationAtts = "'a' AS proexeclocation,"
	} else if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("6") {
		locationAtts = "proiswindow,proexeclocation,proleakproof,"
	} else {
		locationAtts = "proexeclocation,proleakproof,proparallel,"
	}

	before7Query := fmt.Sprintf(`
		SELECT p.oid,
			quote_ident(nspname) AS schema,
			quote_ident(proname) AS name,
			proretset,
			coalesce(prosrc, '') AS functionbody,
			coalesce(probin, '') AS binarypath,
			pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
			pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs,
			pg_catalog.pg_get_function_result(p.oid) AS resulttype,
			provolatile,
			proisstrict,
			prosecdef,
			%s
			coalesce(array_to_string(ARRAY(SELECT 'SET ' || option_name || ' TO ' || option_value
				FROM pg_options_to_table(proconfig)), ' '), '') AS proconfig,
			procost,
			prorows,
			prodataaccess,
			l.lanname AS language
		FROM pg_proc p
			JOIN pg_catalog.pg_language l ON p.prolang = l.oid
			LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
		WHERE %s
			AND proisagg = 'f'
			AND %s%s
		ORDER BY nspname, proname, identargs`, locationAtts,
		SchemaFilterClause("n"),
		ExtensionFilterClause("p"),
		excludeImplicitFunctionsClause)

	atLeast7Query := fmt.Sprintf(`
		SELECT p.oid,
			quote_ident(nspname) AS schema,
			quote_ident(p.proname) AS name,
			proretset,
			coalesce(prosrc, '') AS functionbody,
			coalesce(probin, '') AS binarypath,
			pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
			pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs,
			pg_catalog.pg_get_function_result(p.oid) AS resulttype,
			provolatile,
			proisstrict,
			prosecdef,
			%s
			coalesce(array_to_string(ARRAY(SELECT 'SET ' || option_name || ' TO ' || option_value
				FROM pg_options_to_table(proconfig)), ' '), '') AS proconfig,
			procost,
			prorows,
			prokind,
			prosupport,
			l.lanname AS language,
            coalesce(array_to_string(ARRAY(SELECT 'FOR TYPE ' || nm.nspname || '.' || typ.typname
                from
                    unnest(p.protrftypes) as trf_unnest
                    left join pg_type typ
                         on trf_unnest = typ.oid
					left join pg_namespace nm
						on typ.typnamespace = nm.oid
                ), ', '), '') AS transformtypes
		FROM pg_proc p
			JOIN pg_catalog.pg_language l ON p.prolang = l.oid
			LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
		WHERE %s
			AND prokind <> 'a'
			AND %s%s
		ORDER BY nspname, proname, identargs`, locationAtts,
		SchemaFilterClause("n"),
		ExtensionFilterClause("p"),
		excludeImplicitFunctionsClause)

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

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

	err = PostProcessFunctionConfigs(results)
	gplog.FatalOnError(err)

	// Process kind value for GPDB7+ window functions, to ensure window
	// attribute is correctly set.
	// Remove all functions that have NULL arguments, NULL identity
	// arguments, or NULL result type. This can happen if the query
	// above is run and a concurrent function drop happens just
	// before the pg_get_function_* functions execute.
	verifiedResults := make([]Function, 0)
	for _, result := range results {
		if ((connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB()) && result.Kind == "w" {
			result.IsWindow = true
		}

		if result.Arguments.Valid && result.IdentArgs.Valid && result.ResultType.Valid {
			verifiedResults = append(verifiedResults, result)
		} else if ((connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB()) &&
			result.Kind == "p" && !result.ResultType.Valid { // GPDB7+ stored procedure
			verifiedResults = append(verifiedResults, result)
		} else {
			gplog.Warn("Function '%s.%s' not backed up, most likely dropped after gpbackup had begun.", result.Schema, result.Name)
		}
	}

	return verifiedResults
}

func PostProcessFunctionConfigs(allFunctions []Function) error {
	setToNameValuePattern := regexp.MustCompile(`^SET (.*) TO (.*)$`)

	for i, function := range allFunctions {
		if function.Config == "" {
			continue
		}

		captures := setToNameValuePattern.FindStringSubmatch(function.Config)
		if len(captures) != 3 {
			return fmt.Errorf("Function config does not match syntax expectations. Function was: %v", function)
		}
		gucName := strings.ToLower(captures[1])
		gucValue := captures[2]
		quotedValue := QuoteGUCValue(gucName, gucValue)

		// write to struct by referencing the slice rather than the readonly 'function' copy
		allFunctions[i].Config = fmt.Sprintf(`SET %s TO %s`, gucName, quotedValue)
	}
	return nil
}

func QuoteGUCValue(name, value string) string {
	/*
	 * GUC tools have one way to stuff many strings into a single string, with it own
	 * system of quotation. SQL quotation is different, so we have to unwind the
	 * GUC quoting system and use the SQL system.
	 * We are modeling this function after SplitGUCList in psql/dumputils.c
	 */
	var result string
	if name == "temp_tablespaces" ||
		name == "session_preload_libraries" ||
		name == "shared_preload_libraries" ||
		name == "local_preload_libraries" ||
		name == "search_path" {
		strSplit := strings.Split(value, ",")
		for i, item := range strSplit {
			item = strings.Trim(item, " ")
			item = UnescapeDoubleQuote(item)
			item = `'` + item + `'`
			strSplit[i] = item
		}
		result = strings.Join(strSplit, ", ")
	} else {
		result = `'` + value + `'`
	}

	return result
}

func UnescapeDoubleQuote(value string) string {
	result := value
	if len(value) > 1 && value[0] == '"' && value[len(value)-1] == '"' {
		result = value[1 : len(value)-1]
		result = strings.Replace(result, `""`, `"`, -1)
	}
	return result
}

func GetFunctionReturnTypes(connectionPool *dbconn.DBConn) map[uint32]Function {
	query := fmt.Sprintf(`
	SELECT p.oid,
		proretset,
		CASE WHEN proretset = 't' THEN 'SETOF ' || format_type(prorettype, NULL)
			ELSE format_type(prorettype, NULL) END AS resulttype
	FROM pg_proc p
		JOIN pg_namespace n ON p.pronamespace = n.oid
	WHERE %s`, SchemaFilterClause("n"))

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

	returnMap := make(map[uint32]Function)
	for _, result := range results {
		returnMap[result.Oid] = result
	}
	return returnMap
}

type Aggregate struct {
	Oid                        uint32
	Schema                     string
	Name                       string
	Arguments                  sql.NullString
	IdentArgs                  sql.NullString
	TransitionFunction         uint32 `db:"aggtransfn"`
	PreliminaryFunction        uint32 `db:"aggprelimfn"`
	CombineFunction            uint32 `db:"aggcombinefn"`
	SerialFunction             uint32 `db:"aggserialfn"`
	DeserialFunction           uint32 `db:"aggdeserialfn"`
	FinalFunction              uint32 `db:"aggfinalfn"`
	FinalFuncExtra             bool
	SortOperator               string
	SortOperatorSchema         string
	Hypothetical               bool   // GPDB < 7
	Kind                       string // GPDB7+
	TransitionDataType         string
	TransitionDataSize         int `db:"aggtransspace"`
	InitialValue               string
	InitValIsNull              bool
	IsOrdered                  bool   `db:"aggordered"`
	MTransitionFunction        uint32 `db:"aggmtransfn"`
	MInverseTransitionFunction uint32 `db:"aggminvtransfn"`
	MTransitionDataType        string
	MTransitionDataSize        int    `db:"aggmtransspace"`
	MFinalFunction             uint32 `db:"aggmfinalfn"`
	MFinalFuncExtra            bool
	MInitialValue              string
	MInitValIsNull             bool
	Finalmodify                string // GPDB7+
	Mfinalmodify               string // GPDB7+
	Parallel                   string // GPDB7+
}

func (a Aggregate) GetMetadataEntry() (string, toc.MetadataEntry) {
	identArgumentsStr := "*"
	if a.IdentArgs.String != "" {
		identArgumentsStr = a.IdentArgs.String
	}
	aggWithArgs := fmt.Sprintf("%s(%s)", a.Name, identArgumentsStr)
	return "predata",
		toc.MetadataEntry{
			Schema:          a.Schema,
			Name:            aggWithArgs,
			ObjectType:      toc.OBJ_AGGREGATE,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (a Aggregate) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_AGGREGATE_OID, Oid: a.Oid}
}

func (a Aggregate) FQN() string {
	identArgumentsStr := "*"
	if a.IdentArgs.String != "" {
		identArgumentsStr = a.IdentArgs.String
	}
	return fmt.Sprintf("%s.%s(%s)", a.Schema, a.Name, identArgumentsStr)
}

func GetAggregates(connectionPool *dbconn.DBConn) []Aggregate {
	version5query := fmt.Sprintf(`
	SELECT p.oid,
		quote_ident(n.nspname) AS schema,
		p.proname AS name,
		pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
		pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs,
		a.aggtransfn::regproc::oid,
		a.aggprelimfn::regproc::oid,
		a.aggfinalfn::regproc::oid,
		coalesce(o.oprname, '') AS sortoperator,
		coalesce(quote_ident(opn.nspname), '') AS sortoperatorschema, 
		format_type(a.aggtranstype, NULL) as transitiondatatype,
		coalesce(a.agginitval, '') AS initialvalue,
		(a.agginitval IS NULL) AS initvalisnull,
		true AS minitvalisnull,
		a.aggordered
	FROM pg_aggregate a
		LEFT JOIN pg_proc p ON a.aggfnoid = p.oid
		LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
		LEFT JOIN pg_operator o ON a.aggsortop = o.oid
		LEFT JOIN pg_namespace opn ON o.oprnamespace = opn.oid
	WHERE %s
		AND %s`,
		SchemaFilterClause("n"), ExtensionFilterClause("p"))

	version6query := fmt.Sprintf(`
	SELECT p.oid,
		quote_ident(n.nspname) AS schema,
		p.proname AS name,
		pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
		pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs,
		a.aggtransfn::regproc::oid,
		a.aggcombinefn::regproc::oid,
		a.aggserialfn::regproc::oid,
		a.aggdeserialfn::regproc::oid,
		a.aggfinalfn::regproc::oid,
		a.aggfinalextra AS finalfuncextra,
		coalesce(o.oprname, '') AS sortoperator,
		coalesce(quote_ident(opn.nspname), '') AS sortoperatorschema, 
		(a.aggkind = 'h') AS hypothetical,
		format_type(a.aggtranstype, NULL) as transitiondatatype,
		aggtransspace,
		coalesce(a.agginitval, '') AS initialvalue,
		(a.agginitval IS NULL) AS initvalisnull,
		a.aggmtransfn::regproc::oid,
		a.aggminvtransfn::regproc::oid,
		a.aggmfinalfn::regproc::oid,
		a.aggmfinalextra AS mfinalfuncextra,
		format_type(a.aggmtranstype, NULL) as mtransitiondatatype,
		aggmtransspace,
		(a.aggminitval IS NULL) AS minitvalisnull,
		coalesce(a.aggminitval, '') AS minitialvalue
	FROM pg_aggregate a
		LEFT JOIN pg_proc p ON a.aggfnoid = p.oid
		LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
		LEFT JOIN pg_operator o ON a.aggsortop = o.oid
		LEFT JOIN pg_namespace opn ON o.oprnamespace = opn.oid
	WHERE %s
		AND %s`,
		SchemaFilterClause("n"), ExtensionFilterClause("p"))

	atLeast7Query := fmt.Sprintf(`
	SELECT p.oid,
		quote_ident(n.nspname) AS schema,
		p.proname AS name,
		p.proparallel as parallel,
		pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
		pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs,
		a.aggtransfn::regproc::oid,
		a.aggcombinefn::regproc::oid,
		a.aggserialfn::regproc::oid,
		a.aggdeserialfn::regproc::oid,
		a.aggfinalfn::regproc::oid,
		a.aggfinalextra AS finalfuncextra,
		coalesce(o.oprname, '') AS sortoperator,
		coalesce(quote_ident(opn.nspname), '') AS sortoperatorschema, 
		aggkind AS kind,
		format_type(a.aggtranstype, NULL) as transitiondatatype,
		aggtransspace,
		coalesce(a.agginitval, '') AS initialvalue,
		(a.agginitval IS NULL) AS initvalisnull,
		a.aggmtransfn::regproc::oid,
		a.aggminvtransfn::regproc::oid,
		a.aggmfinalfn::regproc::oid,
		a.aggmfinalextra AS mfinalfuncextra,
		format_type(a.aggmtranstype, NULL) as mtransitiondatatype,
		aggmtransspace,
		(a.aggminitval IS NULL) AS minitvalisnull,
		coalesce(a.aggminitval, '') AS minitialvalue,
		a.aggfinalmodify AS finalmodify,
		a.aggmfinalmodify AS mfinalmodify
	FROM pg_aggregate a
		LEFT JOIN pg_proc p ON a.aggfnoid = p.oid
		LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
		LEFT JOIN pg_operator o ON a.aggsortop = o.oid
		LEFT JOIN pg_namespace opn ON o.oprnamespace = opn.oid
	WHERE %s
		AND %s`,
		SchemaFilterClause("n"), ExtensionFilterClause("p"))

	aggregates := make([]Aggregate, 0)
	query := ""
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("5") {
		query = version5query
	} else if connectionPool.Version.IsGPDB() && connectionPool.Version.Is("6") {
		query = version6query
	} else {
		query = atLeast7Query
	}
	err := connectionPool.Select(&aggregates, query)
	gplog.FatalOnError(err)
	for i := range aggregates {
		if aggregates[i].MTransitionDataType == "-" {
			aggregates[i].MTransitionDataType = ""
		}
	}

	// Remove all aggregates that have NULL arguments or NULL
	// identity arguments. This can happen if the query above
	// is run and a concurrent aggregate drop happens before
	// the pg_get_function_* functions execute.
	verifiedAggregates := make([]Aggregate, 0)
	for _, aggregate := range aggregates {
		if aggregate.Arguments.Valid && aggregate.IdentArgs.Valid {
			verifiedAggregates = append(verifiedAggregates, aggregate)
		} else {
			gplog.Warn("Aggregate '%s.%s' not backed up, most likely dropped after gpbackup had begun.", aggregate.Schema, aggregate.Name)
		}
	}
	return verifiedAggregates
}

type FunctionInfo struct {
	Oid           uint32
	Name          string
	Schema        string
	QualifiedName string
	Arguments     sql.NullString
	IdentArgs     sql.NullString
	IsInternal    bool
}

func (info FunctionInfo) FQN() string {
	return fmt.Sprintf("%s(%s)", info.QualifiedName, info.IdentArgs.String)
}

func (info FunctionInfo) GetMetadataEntry() (string, toc.MetadataEntry) {
	nameWithArgs := fmt.Sprintf("%s(%s)", info.Name, info.IdentArgs.String)
	return "predata",
		toc.MetadataEntry{
			Schema:          info.Schema,
			Name:            nameWithArgs,
			ObjectType:      toc.OBJ_FUNCTION,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func GetFunctionOidToInfoMap(connectionPool *dbconn.DBConn) map[uint32]FunctionInfo {
	query := `
	SELECT p.oid,
		quote_ident(n.nspname) AS schema,
		quote_ident(p.proname) AS name,
		pg_catalog.pg_get_function_arguments(p.oid) AS arguments,
		pg_catalog.pg_get_function_identity_arguments(p.oid) AS identargs
	FROM pg_proc p
		LEFT JOIN pg_namespace n ON p.pronamespace = n.oid`

	results := make([]FunctionInfo, 0)
	funcMap := make(map[uint32]FunctionInfo)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	for _, funcInfo := range results {
		if !funcInfo.Arguments.Valid || !funcInfo.IdentArgs.Valid {
			gplog.Warn("Function '%s.%s' not backed up, most likely dropped after gpbackup had begun.", funcInfo.Schema, funcInfo.Name)
			continue
		}

		if funcInfo.Schema == "pg_catalog" {
			funcInfo.IsInternal = true
		}
		funcInfo.QualifiedName = utils.MakeFQN(funcInfo.Schema, funcInfo.Name)
		funcMap[funcInfo.Oid] = funcInfo
	}
	return funcMap
}

type Cast struct {
	Oid            uint32
	SourceTypeFQN  string
	TargetTypeFQN  string
	FunctionSchema string
	FunctionName   string
	FunctionArgs   string
	CastContext    string
	CastMethod     string
}

func (c Cast) GetMetadataEntry() (string, toc.MetadataEntry) {
	castStr := fmt.Sprintf("(%s AS %s)", c.SourceTypeFQN, c.TargetTypeFQN)
	filterSchema := "pg_catalog"
	if c.CastMethod == "f" {
		filterSchema = c.FunctionSchema // Use the function's schema to allow restore filtering
	}
	return "predata",
		toc.MetadataEntry{
			Schema:          filterSchema,
			Name:            castStr,
			ObjectType:      toc.OBJ_CAST,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (c Cast) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_CAST_OID, Oid: c.Oid}
}

func (c Cast) FQN() string {
	return fmt.Sprintf("(%s AS %s)", c.SourceTypeFQN, c.TargetTypeFQN)
}

func GetCasts(connectionPool *dbconn.DBConn) []Cast {
	/* This query retrieves all casts where either the source type, the target
	 * type, or the cast function is user-defined.
	 */
	methodStr := ""
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		methodStr = "castmethod,"
	} else {
		methodStr = "CASE WHEN c.castfunc = 0 THEN 'b' ELSE 'f' END AS castmethod,"
	}
	query := fmt.Sprintf(`
	SELECT
		c.oid,
		quote_ident(sn.nspname) || '.' || quote_ident(st.typname) AS sourcetypefqn,
		quote_ident(tn.nspname) || '.' || quote_ident(tt.typname) AS targettypefqn,
		coalesce(quote_ident(n.nspname), '') AS functionschema,
		coalesce(quote_ident(p.proname), '') AS functionname,
		coalesce(pg_get_function_arguments(p.oid), '') AS functionargs,
		%s
		c.castcontext
	FROM pg_cast c
		JOIN pg_type st ON c.castsource = st.oid
		JOIN pg_type tt ON c.casttarget = tt.oid
		JOIN pg_namespace sn ON st.typnamespace = sn.oid
		JOIN pg_namespace tn ON tt.typnamespace = tn.oid
		LEFT JOIN pg_proc p ON c.castfunc = p.oid
		LEFT JOIN pg_description d ON c.oid = d.objoid
		LEFT JOIN pg_namespace n ON p.pronamespace = n.oid
	WHERE ((%s) OR (%s) OR (%s))
		AND %s
	ORDER BY 1, 2`, methodStr,
		SchemaFilterClause("sn"), SchemaFilterClause("tn"),
		SchemaFilterClause("n"), ExtensionFilterClause("c"))

	casts := make([]Cast, 0)
	err := connectionPool.Select(&casts, query)
	gplog.FatalOnError(err)
	return casts
}

type Extension struct {
	Oid    uint32
	Name   string
	Schema string
}

func (e Extension) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            e.Name,
			ObjectType:      toc.OBJ_EXTENSION,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (e Extension) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_EXTENSION_OID, Oid: e.Oid}
}

func (e Extension) FQN() string {
	return e.Name
}

func GetExtensions(connectionPool *dbconn.DBConn) []Extension {
	results := make([]Extension, 0)

	query := fmt.Sprintf(`
	SELECT e.oid,
		quote_ident(extname) AS name,
		quote_ident(n.nspname) AS schema
	FROM pg_extension e
		JOIN pg_namespace n ON e.extnamespace = n.oid
	WHERE e.oid >= %d`, FIRST_NORMAL_OBJECT_ID)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type ProceduralLanguage struct {
	Oid       uint32
	Name      string
	Owner     string
	IsPl      bool   `db:"lanispl"`
	PlTrusted bool   `db:"lanpltrusted"`
	Handler   uint32 `db:"lanplcallfoid"`
	Inline    uint32 `db:"laninline"`
	Validator uint32 `db:"lanvalidator"`
}

func (pl ProceduralLanguage) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            pl.Name,
			ObjectType:      toc.OBJ_LANGUAGE,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (pl ProceduralLanguage) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_LANGUAGE_OID, Oid: pl.Oid}
}

func (pl ProceduralLanguage) FQN() string {
	return pl.Name
}

func GetProceduralLanguages(connectionPool *dbconn.DBConn) []ProceduralLanguage {
	results := make([]ProceduralLanguage, 0)
	query := fmt.Sprintf(`
	SELECT oid,
		quote_ident(l.lanname) AS name,
		pg_get_userbyid(l.lanowner) AS owner,
		l.lanispl,
		l.lanpltrusted,
		l.lanplcallfoid::regprocedure::oid,
		l.laninline::regprocedure::oid,
		l.lanvalidator::regprocedure::oid
	FROM pg_language l
	WHERE l.lanispl='t'
		AND l.lanname != 'plpgsql'
		AND %s`, ExtensionFilterClause("l"))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type Transform struct {
	Oid           uint32
	TypeNamespace string `db:"typnamespace"`
	TypeName      string `db:"typname"`
	LanguageName  string `db:"lanname"`
	FromSQLFunc   uint32 `db:"trffromsql"`
	ToSQLFunc     uint32 `db:"trftosql"`
}

func (trf Transform) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            "",
			ObjectType:      toc.OBJ_TRANSFORM,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (trf Transform) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_TRANSFORM_OID, Oid: trf.Oid}
}

func (trf Transform) FQN() string {
	return fmt.Sprintf("FOR %s.%s LANGUAGE %s", trf.TypeNamespace, trf.TypeName, trf.LanguageName)
}

func GetTransforms(connectionPool *dbconn.DBConn) []Transform {
	results := make([]Transform, 0)
	query := `
	SELECT trf.oid,
		quote_ident(ns.nspname) AS typnamespace,
		quote_ident(tp.typname) AS typname,
		l.lanname,
		trf.trffromsql::oid,
		trf.trftosql::oid
	FROM pg_transform trf
		JOIN pg_type tp ON trf.trftype=tp.oid
		JOIN pg_namespace ns ON tp.typnamespace = ns.oid
		JOIN pg_language l ON trf.trflang=l.oid;`

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type Conversion struct {
	Oid                uint32
	Schema             string
	Name               string
	ForEncoding        string
	ToEncoding         string
	ConversionFunction string
	IsDefault          bool `db:"condefault"`
}

func (c Conversion) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          c.Schema,
			Name:            c.Name,
			ObjectType:      toc.OBJ_CONVERSION,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (c Conversion) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_CONVERSION_OID, Oid: c.Oid}
}

func (c Conversion) FQN() string {
	return utils.MakeFQN(c.Schema, c.Name)
}

func GetConversions(connectionPool *dbconn.DBConn) []Conversion {
	results := make([]Conversion, 0)
	query := fmt.Sprintf(`
	SELECT c.oid,
		quote_ident(n.nspname) AS schema,
		quote_ident(c.conname) AS name,
		pg_encoding_to_char(c.conforencoding) AS forencoding,
		pg_encoding_to_char(c.contoencoding) AS toencoding,
		quote_ident(fn.nspname) || '.' || quote_ident(p.proname) AS conversionfunction,
		c.condefault
	FROM pg_conversion c
		JOIN pg_namespace n ON c.connamespace = n.oid
		JOIN pg_proc p ON c.conproc = p.oid
		JOIN pg_namespace fn ON p.pronamespace = fn.oid
	WHERE %s
		AND %s
	ORDER BY n.nspname, c.conname`, SchemaFilterClause("n"), ExtensionFilterClause("c"))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type ForeignDataWrapper struct {
	Oid       uint32
	Name      string
	Handler   uint32
	Validator uint32
	Options   string
}

func (fdw ForeignDataWrapper) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            fdw.Name,
			ObjectType:      toc.OBJ_FOREIGN_DATA_WRAPPER,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (fdw ForeignDataWrapper) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_FOREIGN_DATA_WRAPPER_OID, Oid: fdw.Oid}
}

func (fdw ForeignDataWrapper) FQN() string {
	return fdw.Name
}

func GetForeignDataWrappers(connectionPool *dbconn.DBConn) []ForeignDataWrapper {
	results := make([]ForeignDataWrapper, 0)
	query := fmt.Sprintf(`
	SELECT oid,
		quote_ident(fdwname) AS name,
		fdwvalidator AS validator,
		fdwhandler AS handler,
		array_to_string(ARRAY(
			SELECT pg_catalog.quote_ident(option_name) || ' ' || pg_catalog.quote_literal(option_value)
			FROM pg_options_to_table(fdwoptions) ORDER BY option_name), ', ') AS options
	FROM pg_foreign_data_wrapper
	WHERE oid >= %d AND %s`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause(""))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type ForeignServer struct {
	Oid                uint32
	Name               string
	Type               string
	Version            string
	ForeignDataWrapper string
	Options            string
}

func (fs ForeignServer) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            fs.Name,
			ObjectType:      toc.OBJ_FOREIGN_SERVER,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (fs ForeignServer) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_FOREIGN_SERVER_OID, Oid: fs.Oid}
}

func (fs ForeignServer) FQN() string {
	return fs.Name
}

func GetForeignServers(connectionPool *dbconn.DBConn) []ForeignServer {
	results := make([]ForeignServer, 0)
	query := fmt.Sprintf(`
	SELECT fs.oid,
		quote_ident(fs.srvname) AS name,
		coalesce(fs.srvtype, '') AS type,
		coalesce(fs.srvversion, '') AS version,
		quote_ident(fdw.fdwname) AS foreigndatawrapper,
		array_to_string(ARRAY(
			SELECT pg_catalog.quote_ident(option_name) || ' ' || pg_catalog.quote_literal(option_value)
			FROM pg_options_to_table(fs.srvoptions) ORDER BY option_name), ', ') AS options
	FROM pg_foreign_server fs
		LEFT JOIN pg_foreign_data_wrapper fdw ON fdw.oid = srvfdw
	WHERE fs.oid >= %d AND %s`, FIRST_NORMAL_OBJECT_ID, ExtensionFilterClause("fs"))

	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type UserMapping struct {
	Oid     uint32
	User    string
	Server  string
	Options string
}

func (um UserMapping) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "predata",
		toc.MetadataEntry{
			Schema:          "",
			Name:            um.FQN(),
			ObjectType:      toc.OBJ_USER_MAPPING,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (um UserMapping) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_USER_MAPPING_OID, Oid: um.Oid}
}

func (um UserMapping) FQN() string {
	// User mappings don't have a unique name, so we construct an arbitrary identifier
	return fmt.Sprintf("%s ON %s", um.User, um.Server)
}

func GetUserMappings(connectionPool *dbconn.DBConn) []UserMapping {
	query := `
	SELECT um.umid AS oid,
		quote_ident(um.usename) AS user,
		quote_ident(um.srvname) AS server,
		array_to_string(ARRAY(
			SELECT pg_catalog.quote_ident(option_name) || ' ' || pg_catalog.quote_literal(option_value)
			FROM pg_options_to_table(um.umoptions) ORDER BY option_name), ', ') AS options
	FROM pg_user_mappings um
	WHERE um.umid NOT IN (select objid from pg_depend where deptype = 'e')
	ORDER by um.usename`

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

type StatisticExt struct {
	Oid         uint32
	Name        string
	Namespace   string // namespace that statistics object belongs to
	Owner       string
	TableSchema string // schema that table covered by statistics belongs to
	TableName   string // table covered by statistics
	Definition  string
}

func (se StatisticExt) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "postdata",
		toc.MetadataEntry{
			Schema:          se.Namespace,
			Name:            se.Name,
			ObjectType:      toc.OBJ_STATISTICS,
			ReferenceObject: utils.MakeFQN(se.TableSchema, se.TableName),
			StartByte:       0,
			EndByte:         0,
		}
}

func (se StatisticExt) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_STATISTIC_EXT_OID, Oid: se.Oid}
}

func (se StatisticExt) FQN() string {
	return fmt.Sprintf("%s.%s", se.Namespace, se.Name)
}

func GetExtendedStatistics(connectionPool *dbconn.DBConn) []StatisticExt {
	results := make([]StatisticExt, 0)

	query := `
	SELECT 	se.oid,
			stxname AS name,
			regexp_replace(pg_catalog.pg_get_statisticsobjdef(se.oid), '(.* FROM ).*', '\1' || quote_ident(c.relnamespace::regnamespace::text) || '.' || quote_ident(c.relname)) AS definition,
			quote_ident(se.stxnamespace::regnamespace::text) AS namespace,
			se.stxowner::regrole AS owner,
			quote_ident(c.relnamespace::regnamespace::text) AS tableschema,
			quote_ident(c.relname) AS tablename
	FROM pg_catalog.pg_statistic_ext se
	JOIN pg_catalog.pg_class c ON se.stxrelid = c.oid;`
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}
