package backup

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

import (
	"fmt"

	"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 SessionGUCs struct {
	ClientEncoding string `db:"client_encoding"`
}

func (sg SessionGUCs) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            "",
			ObjectType:      toc.OBJ_SESSION_GUC,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func GetSessionGUCs(connectionPool *dbconn.DBConn) SessionGUCs {
	result := SessionGUCs{}
	query := "SHOW client_encoding;"
	err := connectionPool.Get(&result, query)
	gplog.FatalOnError(err)
	return result
}

type Database struct {
	Oid        uint32
	Name       string
	Tablespace string
	Collate    string
	CType      string
	Encoding   string
}

func (db Database) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            db.Name,
			ObjectType:      toc.OBJ_DATABASE_METADATA,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (db Database) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_DATABASE_OID, Oid: db.Oid}
}

func (db Database) FQN() string {
	return db.Name
}

func GetDefaultDatabaseEncodingInfo(connectionPool *dbconn.DBConn) Database {
	lcQuery := ""
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		lcQuery = "datcollate AS collate, datctype AS ctype,"
	}

	query := fmt.Sprintf(`
	SELECT datname AS name,
		%s
		pg_encoding_to_char(encoding) AS encoding
	FROM pg_database
	WHERE datname = 'template0'`, lcQuery)

	result := Database{}
	err := connectionPool.Get(&result, query)
	gplog.FatalOnError(err)
	return result
}

func GetDatabaseInfo(connectionPool *dbconn.DBConn) Database {
	lcQuery := ""
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		lcQuery = "datcollate AS collate, datctype AS ctype,"
	}
	query := fmt.Sprintf(`
	SELECT d.oid,
		quote_ident(d.datname) AS name,
		quote_ident(t.spcname) AS tablespace,
		%s
		pg_encoding_to_char(d.encoding) AS encoding
	FROM pg_database d
		JOIN pg_tablespace t ON d.dattablespace = t.oid
	WHERE d.datname = '%s'`, lcQuery, utils.EscapeSingleQuotes(connectionPool.DBName))

	result := Database{}
	err := connectionPool.Get(&result, query)
	gplog.FatalOnError(err)
	return result
}

func GetDatabaseGUCs(connectionPool *dbconn.DBConn) []string {
	//We do not want to quote list type config settings such as search_path and DateStyle
	query := `
	SELECT CASE
		WHEN option_name='search_path' OR option_name = 'DateStyle'
		THEN ('SET ' || option_name || ' TO ' || option_value)
		ELSE ('SET ' || option_name || ' TO ''' || option_value || '''')
	END AS string
	FROM pg_options_to_table((%s))`
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		subQuery := fmt.Sprintf("SELECT datconfig FROM pg_database WHERE datname = '%s'", utils.EscapeSingleQuotes(connectionPool.DBName))
		query = fmt.Sprintf(query, subQuery)
	} else {
		subQuery := fmt.Sprintf("SELECT setconfig FROM pg_db_role_setting WHERE setrole = 0 AND setdatabase = (SELECT oid FROM pg_database WHERE datname = '%s')", utils.EscapeSingleQuotes(connectionPool.DBName))
		query = fmt.Sprintf(query, subQuery)
	}
	return dbconn.MustSelectStringSlice(connectionPool, query)
}

type ResourceQueue struct {
	Oid              uint32
	Name             string
	ActiveStatements int
	MaxCost          string
	CostOvercommit   bool
	MinCost          string
	Priority         string
	MemoryLimit      string
}

func (rq ResourceQueue) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            rq.Name,
			ObjectType:      toc.OBJ_RESOURCE_QUEUE,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (rq ResourceQueue) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_RESQUEUE_OID, Oid: rq.Oid}
}

func (rq ResourceQueue) FQN() string {
	return rq.Name
}

func GetResourceQueues(connectionPool *dbconn.DBConn) []ResourceQueue {
	/*
	 * maxcost and mincost are represented as real types in the database, but we round to two decimals
	 * and cast them as text for more consistent formatting. pg_dumpall does this as well.
	 */
	query := `
	SELECT r.oid,
		quote_ident(rsqname) AS name,
		rsqcountlimit AS activestatements,
		ROUND(rsqcostlimit::numeric, 2)::text AS maxcost,
		rsqovercommit AS costovercommit,
		ROUND(rsqignorecostlimit::numeric, 2)::text AS mincost,
		priority_capability.ressetting::text AS priority,
		memory_capability.ressetting::text AS memorylimit
	FROM pg_resqueue r
		JOIN (SELECT resqueueid, ressetting FROM pg_resqueuecapability WHERE restypid = 5) priority_capability
			ON r.oid = priority_capability.resqueueid
		JOIN (SELECT resqueueid, ressetting FROM pg_resqueuecapability WHERE restypid = 6) memory_capability
			ON r.oid = memory_capability.resqueueid`
	results := make([]ResourceQueue, 0)
	err := connectionPool.Select(&results, query)
	gplog.FatalOnError(err)
	return results
}

type ResourceGroup struct {
	Oid         uint32 `db:"oid"`
	Name        string `db:"name"`
	Concurrency string `db:"concurrency"`
	Cpuset      string `db:"cpuset"`
}

func (rg ResourceGroup) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            rg.Name,
			ObjectType:      toc.OBJ_RESOURCE_GROUP,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (rg ResourceGroup) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_RESGROUP_OID, Oid: rg.Oid}
}

func (rg ResourceGroup) FQN() string {
	return rg.Name
}

type ResourceGroupBefore7 struct {
	ResourceGroup            // embedded common rg fields+methods
	CPURateLimit      string `db:"cpuratelimit"`
	MemoryLimit       string `db:"memorylimit"`
	MemorySharedQuota string `db:"memorysharedquota"`
	MemorySpillRatio  string `db:"memoryspillratio"`
	MemoryAuditor     string `db:"memoryauditor"`
}

type ResourceGroupAtLeast7 struct {
	ResourceGroup        // embedded common rg fields+methods
	CpuMaxPercent string `db:"cpu_max_percent"`
	CpuWeight     string `db:"cpu_weight"`
}

func GetResourceGroups[T ResourceGroupBefore7 | ResourceGroupAtLeast7](connectionPool *dbconn.DBConn) []T {
	var query string

	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("7") {
		before7SelectClause := ""
		// This is when pg_dumpall was changed to use the actual values
		if connectionPool.Version.AtLeast("5.2.0") {
			before7SelectClause += `
		SELECT g.oid,
			quote_ident(g.rsgname) AS name,
			t1.value AS concurrency,
			t2.value AS cpuratelimit,
			t3.value AS memorylimit,
			t4.value AS memorysharedquota,
			t5.value AS memoryspillratio`
		} else { // GPDB 5.0.0 and 5.1.0
			before7SelectClause += `
		SELECT g.oid,
			quote_ident(g.rsgname) AS name,
			t1.proposed AS concurrency,
			t2.value    AS cpuratelimit,
			t3.proposed AS memorylimit,
			t4.proposed AS memorysharedquota,
			t5.proposed AS memoryspillratio`
		}

		before7FromClause := `
	    FROM pg_resgroup g
	    	JOIN pg_resgroupcapability t1 ON t1.resgroupid = g.oid AND t1.reslimittype = 1
	    	JOIN pg_resgroupcapability t2 ON t2.resgroupid = g.oid AND t2.reslimittype = 2
	    	JOIN pg_resgroupcapability t3 ON t3.resgroupid = g.oid AND t3.reslimittype = 3
	    	JOIN pg_resgroupcapability t4 ON t4.resgroupid = g.oid AND t4.reslimittype = 4
	    	JOIN pg_resgroupcapability t5 ON t5.resgroupid = g.oid AND t5.reslimittype = 5`

		// The reslimittype 6 (memoryauditor) was introduced in GPDB
		// 5.8.0. Default the value to '0' (vmtracker) since there could
		// be a resource group created before 5.8.0 which will not have
		// this memoryauditor field defined.
		if connectionPool.Version.AtLeast("5.8.0") {
			before7SelectClause += `, coalesce(t6.value, '0') AS memoryauditor`
			before7FromClause += ` LEFT JOIN pg_resgroupcapability t6 ON t6.resgroupid = g.oid AND t6.reslimittype = 6`
		}

		// The reslimittype 7 (cpuset) was introduced in GPDB
		// 5.9.0. Default the value to '-1' since there could be a
		// resource group created before 5.9.0 which will not have this
		// cpuset field defined.
		if connectionPool.Version.AtLeast("5.9.0") {
			before7SelectClause += `, coalesce(t7.value, '-1') AS cpuset`
			before7FromClause += ` LEFT JOIN pg_resgroupcapability t7 ON t7.resgroupid = g.oid AND t7.reslimittype = 7`
		}
		query = fmt.Sprintf(`%s %s;`, before7SelectClause, before7FromClause)
	} else { // GPDB7+
		// Resource groups were heavily reworked for GPDB7
		// See: https://github.com/greenplum-db/gpdb/commit/483adea86b50c1759460a6265b3d8e3f4198d92e
		query = `
			SELECT
				g.oid       AS oid,
				g.rsgname   AS name,
				t1.value    AS concurrency,
				t2.value    AS cpu_max_percent,
				t3.value    AS cpu_weight,
				t4.value    AS cpuset
			FROM pg_resgroup g
				JOIN pg_resgroupcapability t1 ON g.oid = t1.resgroupid AND t1.reslimittype = 1
				JOIN pg_resgroupcapability t2 ON g.oid = t2.resgroupid AND t2.reslimittype = 2
				JOIN pg_resgroupcapability t3 ON g.oid = t3.resgroupid AND t3.reslimittype = 3
				LEFT JOIN pg_resgroupcapability t4 ON g.oid = t4.resgroupid AND t4.reslimittype = 4`
	}

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

type TimeConstraint struct {
	Oid       uint32
	StartDay  int
	StartTime string
	EndDay    int
	EndTime   string
}

type Role struct {
	Oid             uint32
	Name            string
	Super           bool `db:"rolsuper"`
	Inherit         bool `db:"rolinherit"`
	CreateRole      bool `db:"rolcreaterole"`
	CreateDB        bool `db:"rolcreatedb"`
	CanLogin        bool `db:"rolcanlogin"`
	Replication     bool `db:"rolreplication"`
	ConnectionLimit int  `db:"rolconnlimit"`
	Password        string
	ValidUntil      string
	ResQueue        string
	ResGroup        string
	Createrexthttp  bool `db:"rolcreaterexthttp"`
	Createrextgpfd  bool `db:"rolcreaterextgpfd"`
	Createwextgpfd  bool `db:"rolcreatewextgpfd"`
	Createrexthdfs  bool `db:"rolcreaterexthdfs"`
	Createwexthdfs  bool `db:"rolcreatewexthdfs"`
	TimeConstraints []TimeConstraint
}

func (r Role) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            r.Name,
			ObjectType:      toc.OBJ_ROLE,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (r Role) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_AUTHID_OID, Oid: r.Oid}
}

func (r Role) FQN() string {
	return r.Name
}

/*
 * We convert rolvaliduntil to UTC and then append '-00' so that
 * we standardize times to UTC but do not lose time zone information
 * in the timestamp.
 */
func GetRoles(connectionPool *dbconn.DBConn) []Role {
	replicationQuery := ""
	readExtHdfs := "rolcreaterexthdfs,"
	writeExtHdfs := "rolcreatewexthdfs,"
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		replicationQuery = "rolreplication,"
		readExtHdfs = ""
		writeExtHdfs = ""
	}

	var whereClause string
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		whereClause = `
	WHERE rolname !~ '^pg_'`
	}

	query := fmt.Sprintf(`
	SELECT oid,
		quote_ident(rolname) AS name,
		rolsuper,
		rolinherit,
		rolcreaterole,
		rolcreatedb,
		rolcanlogin,
		%s
		rolconnlimit,
		coalesce(rolpassword, '') AS password,
		CASE
			WHEN (rolvaliduntil = 'infinity'::timestamp OR rolvaliduntil = '-infinity'::timestamp)
			THEN timezone('UTC', rolvaliduntil)::text
			ELSE coalesce(timezone('UTC', rolvaliduntil)::text || '-00', '')
		END AS validuntil,
		(SELECT quote_ident(rsqname) FROM pg_resqueue WHERE pg_resqueue.oid = rolresqueue) AS resqueue,
		(SELECT quote_ident(rsgname) FROM pg_resgroup WHERE pg_resgroup.oid = rolresgroup) AS resgroup,
		rolcreaterexthttp,
		%s
		%s
		rolcreaterextgpfd,
		rolcreatewextgpfd
	FROM pg_authid`, replicationQuery, readExtHdfs, writeExtHdfs)

	query += whereClause

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

	constraintsByRole := getTimeConstraintsByRole(connectionPool)
	for idx, role := range roles {
		roles[idx].TimeConstraints = constraintsByRole[role.Oid]
	}

	return roles
}

type RoleGUC struct {
	RoleName string
	DbName   string
	Config   string
}

func (rg RoleGUC) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            rg.RoleName,
			ObjectType:      toc.OBJ_ROLE_GUC,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func GetRoleGUCs(connectionPool *dbconn.DBConn) map[string][]RoleGUC {
	selectClause := `
	SELECT rolename,
		dbname,
		CASE
			WHEN option_name='search_path' OR option_name = 'DateStyle'
			THEN ('SET ' || option_name || ' TO ' || option_value)
			ELSE ('SET ' || option_name || ' TO ''' || option_value || '''')
		END AS config`

	var gucsForDBQuery string
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		gucsForDBQuery = `UNION
	SELECT quote_ident(r.rolname) AS rolename,
		quote_ident(d.datname) AS dbname,
		(pg_options_to_table(setconfig)).option_name,
		(pg_options_to_table(setconfig)).option_value
	FROM pg_db_role_setting pgdb
		JOIN pg_database d ON pgdb.setdatabase = d.oid
		JOIN pg_roles r ON pgdb.setrole = r.oid `
	}
	fromClause := fmt.Sprintf(`
	FROM ( SELECT quote_ident(rolname) AS rolename,
			'' AS dbname,
			(pg_options_to_table(rolconfig)).option_name,
			(pg_options_to_table(rolconfig)).option_value
			FROM pg_roles %s) AS options`, gucsForDBQuery)

	var whereClause string
	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		whereClause = `
	WHERE rolename !~ '^pg_'`
	}

	query := selectClause + fromClause + whereClause

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

	resultMap := make(map[string][]RoleGUC)
	for _, result := range results {
		resultMap[result.RoleName] = append(resultMap[result.RoleName], result)
	}

	return resultMap
}

func getTimeConstraintsByRole(connectionPool *dbconn.DBConn) map[uint32][]TimeConstraint {
	timeConstraints := make([]TimeConstraint, 0)
	query := `
	SELECT authid AS oid,
		start_day AS startday,
		start_time::text AS starttime,
		end_day AS endday,
		end_time::text AS endtime
	FROM pg_auth_time_constraint`
	err := connectionPool.Select(&timeConstraints, query)
	gplog.FatalOnError(err)

	constraintsByRole := make(map[uint32][]TimeConstraint)
	for _, constraint := range timeConstraints {
		roleConstraints, ok := constraintsByRole[constraint.Oid]
		if !ok {
			roleConstraints = make([]TimeConstraint, 0)
		}
		constraintsByRole[constraint.Oid] = append(roleConstraints, constraint)
	}

	return constraintsByRole
}

type RoleMember struct {
	Role    string
	Member  string
	Grantor string
	IsAdmin bool
}

func (rm RoleMember) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            rm.Member,
			ObjectType:      toc.OBJ_ROLE_GRANT,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func GetRoleMembers(connectionPool *dbconn.DBConn) []RoleMember {
	var whereClause string

	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("7")) || connectionPool.Version.IsCBDB() {
		whereClause = fmt.Sprintf(`WHERE roleid >= %d`, FIRST_NORMAL_OBJECT_ID)
	} else {
		whereClause = ``
	}

	query := fmt.Sprintf(`
	SELECT quote_ident(pg_get_userbyid(pga.roleid)) AS role,
		quote_ident(pg_get_userbyid(pga.member)) AS member,
		CASE WHEN pg_get_userbyid(pga.grantor) like 'unknown (OID='||pga.grantor::regclass||')'
		THEN '' ELSE quote_ident(pg_get_userbyid(pga.grantor))
		END AS grantor,
		admin_option AS isadmin
	FROM pg_auth_members pga
	% s
	ORDER BY roleid, member`, whereClause)

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

type Tablespace struct {
	Oid              uint32
	Tablespace       string
	FileLocation     string // FILESPACE in 5, LOCATION in 6 and later
	SegmentLocations []string
	Options          string
	// CBDB only
	Spcfilehandlerbin string
	Spcfilehandlersrc string
}

func (t Tablespace) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            t.Tablespace,
			ObjectType:      toc.OBJ_TABLESPACE,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (t Tablespace) GetUniqueID() UniqueID {
	return UniqueID{ClassID: PG_TABLESPACE_OID, Oid: t.Oid}
}

func (t Tablespace) FQN() string {
	return t.Tablespace
}

func GetTablespaces(connectionPool *dbconn.DBConn) []Tablespace {
	before6Query := `
	SELECT t.oid,
		quote_ident(t.spcname) AS tablespace,
		quote_ident(f.fsname) AS filelocation
	FROM pg_tablespace t
		JOIN pg_filespace f ON t.spcfsoid = f.oid
	WHERE spcname != 'pg_default'
		AND spcname != 'pg_global'`
	atLeast6Query := `
	SELECT oid,
		quote_ident(spcname) AS tablespace,
		'''' || pg_catalog.pg_tablespace_location(oid)::text || '''' AS filelocation,
		coalesce(array_to_string(spcoptions, ', '), '') AS options
	FROM pg_tablespace
	WHERE spcname != 'pg_default'
		AND spcname != 'pg_global'`

	cbdbQuery := `
	SELECT oid,
		quote_ident(spcname) AS tablespace,
		CASE
			WHEN spcfilehandlerbin IS NOT NULL THEN ''
			ELSE '''' || pg_catalog.pg_tablespace_location(oid)::text || ''''
		END AS filelocation,
		coalesce(array_to_string(spcoptions, ', '), '') AS options,
		coalesce(spcfilehandlerbin, '') AS spcfilehandlerbin,
		coalesce(spcfilehandlersrc, '') AS spcfilehandlersrc
	FROM pg_tablespace
	WHERE spcname != 'pg_default'
		AND spcname != 'pg_global'`

	results := make([]Tablespace, 0)
	var err error
	if connectionPool.Version.IsGPDB() && connectionPool.Version.Before("6") {
		err = connectionPool.Select(&results, before6Query)
	} else if connectionPool.Version.IsCBDB() {
		err = connectionPool.Select(&results, cbdbQuery)
	} else {
		err = connectionPool.Select(&results, atLeast6Query)
	}
	gplog.FatalOnError(err)

	if (connectionPool.Version.IsGPDB() && connectionPool.Version.AtLeast("6")) || connectionPool.Version.IsCBDB() {
		for i := 0; i < len(results); i++ {
			results[i].SegmentLocations = GetSegmentTablespaces(connectionPool, results[i].Oid)
		}
	}
	return results
}

func GetSegmentTablespaces(connectionPool *dbconn.DBConn, Oid uint32) []string {
	query := fmt.Sprintf(`
	SELECT 'content'::text || gp_segment_id::text || '=''' || tblspc_loc || '''' AS string
	FROM gp_tablespace_segment_location(%d)
	WHERE tblspc_loc != pg_tablespace_location(%d)
	ORDER BY gp_segment_id;`, Oid, Oid)

	return dbconn.MustSelectStringSlice(connectionPool, query)
}

// Potentially expensive query
func GetDBSize(connectionPool *dbconn.DBConn) string {
	size := struct{ DBSize string }{}
	sizeQuery := fmt.Sprintf("SELECT pg_size_pretty(pg_database_size('%s')) as dbsize",
		utils.EscapeSingleQuotes(connectionPool.DBName))
	err := connectionPool.Get(&size, sizeQuery)
	gplog.FatalOnError(err)
	return size.DBSize
}

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

func (sum StorageUserMapping) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            sum.FQN(),
			ObjectType:      toc.OBJ_STORAGE_USER_MAPPING,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (sum StorageUserMapping) GetUniqueID() UniqueID {
	return UniqueID{ClassID: GP_STORAGE_USER_MAPPING_OID, Oid: sum.Oid}
}

func (sum StorageUserMapping) FQN() string {
	return fmt.Sprintf("%s ON %s", sum.User, sum.Server)
}

func GetStorageUserMapping(connectionPool *dbconn.DBConn) []StorageUserMapping {
	usersQuery := `SELECT 
                    u.oid as Oid,
                    quote_ident(pg_get_userbyid(u.umuser)) as User,
				    quote_ident(s.srvname) as Server,
                    coalesce(array_to_string(u.umoptions, ', '), '') AS options
                   FROM gp_storage_user_mapping u join gp_storage_server s on u.umserver = s.oid`

	users := make([]StorageUserMapping, 0)
	if err := connectionPool.Select(&users, usersQuery); err != nil {
		gplog.FatalOnError(err)
	}
	return users
}

type StorageServer struct {
	Oid           uint32
	Server        string
	ServerOwner   string
	ServerOptions string
}

func (ss StorageServer) GetMetadataEntry() (string, toc.MetadataEntry) {
	return "global",
		toc.MetadataEntry{
			Schema:          "",
			Name:            ss.FQN(),
			ObjectType:      toc.OBJ_STORAGE_SERVER,
			ReferenceObject: "",
			StartByte:       0,
			EndByte:         0,
		}
}

func (ss StorageServer) GetUniqueID() UniqueID {
	return UniqueID{ClassID: GP_STORAGE_SERVER_OID, Oid: ss.Oid}
}

func (ss StorageServer) FQN() string {
	return ss.Server
}

func GetStorageServers(connectionPool *dbconn.DBConn) []StorageServer {
	serversQuery := `SELECT 
                    s.oid as Oid,
                    quote_ident(s.srvname) as Server,
                    quote_ident(pg_get_userbyid(s.srvowner)) as ServerOwner,
                    coalesce(array_to_string(s.srvoptions, ', '), '') AS Serveroptions
				FROM gp_storage_server s
				where srvname != 'local_server'`

	servers := make([]StorageServer, 0)
	if err := connectionPool.Select(&servers, serversQuery); err != nil {
		gplog.FatalOnError(err)
	}
	return servers
}
