Files

3330 lines
120 KiB
Go

package main
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
"gitea.com/kingbase/gokb"
)
const metadataTimeout = 15 * time.Second
const (
kingbaseListDatabasesSQL = "SELECT datname FROM sys_catalog.sys_database WHERE datallowconn AND LOWER(datname) NOT IN ('template0', 'template1') ORDER BY datname"
kingbaseListDatabasesPostgresSQL = "SELECT datname FROM pg_catalog.pg_database WHERE datallowconn AND LOWER(datname) NOT IN ('template0', 'template1') ORDER BY datname"
)
// Escape '_' so only Kingbase internal SYS_/XLOG_ prefixes are hidden; use a
// non-backslash escape because MySQL mode treats backslash as a string escape.
const kingbaseMySQLCompatListSchemasSQL = `SELECT schema_name FROM information_schema.schemata WHERE UPPER(schema_name) <> 'INFORMATION_SCHEMA' AND UPPER(schema_name) NOT LIKE 'SYS#_%' ESCAPE '#' AND UPPER(schema_name) NOT LIKE 'XLOG#_%' ESCAPE '#' ORDER BY schema_name`
var kingbaseDataTypes = []string{
"bigint", "bigserial", "bit", "bit varying", "boolean", "bytea", "char", "character",
"character varying", "date", "decimal", "double precision", "integer", "interval", "json",
"jsonb", "money", "numeric", "real", "smallint", "smallserial", "serial", "text", "time",
"time with time zone", "timestamp", "timestamp with time zone", "uuid", "varchar", "xml",
}
type kingbaseMode struct {
compatibilityMode string
postgresCatalog bool
mysqlCompat bool
sqlServerIdentity bool
legacyV7 bool
}
var kingbaseReleasePattern = regexp.MustCompile(`(?i)\bV0*([0-9]+)R`)
func detectKingbaseV7(db *sql.DB) bool {
var version string
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := db.QueryRowContext(ctx, "SELECT version()").Scan(&version); err != nil {
return false
}
match := kingbaseReleasePattern.FindStringSubmatch(version)
if len(match) != 2 {
return false
}
major, err := strconv.Atoi(match[1])
return err == nil && major == 7
}
type databaseInfo struct {
Name string `json:"name"`
}
type tableInfo struct {
Name string `json:"name"`
TableType string `json:"table_type"`
Comment *string `json:"comment"`
ParentSchema *string `json:"parent_schema,omitempty"`
ParentName *string `json:"parent_name,omitempty"`
}
type pgPartitionBound struct {
Kind string `json:"kind"`
From []string `json:"from,omitempty"`
To []string `json:"to,omitempty"`
Values []string `json:"values,omitempty"`
Modulus *int `json:"modulus,omitempty"`
Remainder *int `json:"remainder,omitempty"`
}
type pgPartitionNode struct {
Schema string `json:"schema"`
Name string `json:"name"`
Strategy string `json:"strategy,omitempty"`
KeyDefinition string `json:"keyDefinition,omitempty"`
Bound *pgPartitionBound `json:"bound,omitempty"`
BoundDefinition string `json:"boundDefinition,omitempty"`
IsLeaf bool `json:"isLeaf"`
Children []pgPartitionNode `json:"children"`
}
type pgTablePartitioning struct {
IsPartitioned bool `json:"isPartitioned"`
IsPartition bool `json:"isPartition"`
Parent string `json:"parent,omitempty"`
ParentSchema string `json:"parentSchema,omitempty"`
ParentTable string `json:"parentTable,omitempty"`
OwnBound *pgPartitionBound `json:"ownBound,omitempty"`
Strategy string `json:"strategy,omitempty"`
KeyDefinition string `json:"keyDefinition,omitempty"`
KeyColumns []string `json:"keyColumns"`
KeyExpression string `json:"keyExpression,omitempty"`
DefaultPartition string `json:"defaultPartition,omitempty"`
Partitions []pgPartitionNode `json:"partitions"`
}
type partitionRelation struct {
OID int64
Schema string
Name string
ParentOID sql.NullInt64
ParentSchema sql.NullString
ParentName sql.NullString
Relkind string
Bound sql.NullString
KeyDefinition sql.NullString
}
type objectInfo struct {
Name string `json:"name"`
ObjectType string `json:"object_type"`
Schema string `json:"schema"`
ParentSchema *string `json:"parent_schema,omitempty"`
ParentName *string `json:"parent_name,omitempty"`
Comment *string `json:"comment"`
Valid *bool `json:"valid,omitempty"`
CustomTypeKind *string `json:"custom_type_kind,omitempty"`
HasMembers *bool `json:"has_members,omitempty"`
}
type metadataListConstraints struct {
Filter string
Limit int
Offset int
ObjectTypes []string
}
type columnInfo struct {
Name string `json:"name"`
DataType string `json:"data_type"`
ResolvedSchema *string `json:"resolved_schema,omitempty"`
FullDataType string `json:"-"`
IsNullable bool `json:"is_nullable"`
ColumnDefault *string `json:"column_default"`
IsPrimaryKey bool `json:"is_primary_key"`
Extra *string `json:"extra"`
Comment *string `json:"comment"`
NumericPrecision *int `json:"numeric_precision"`
NumericScale *int `json:"numeric_scale"`
CharacterMaximumLength *int `json:"character_maximum_length"`
}
type indexInfo struct {
Name string `json:"name"`
Columns []string `json:"columns"`
IsUnique bool `json:"is_unique"`
IsPrimary bool `json:"is_primary"`
Filter *string `json:"filter"`
IndexType *string `json:"index_type"`
IncludedColumns []string `json:"included_columns"`
Comment *string `json:"comment"`
}
func (i indexInfo) MarshalJSON() ([]byte, error) {
type alias indexInfo
value := alias(i)
if value.Columns == nil {
value.Columns = []string{}
}
if value.IncludedColumns == nil {
value.IncludedColumns = []string{}
}
return json.Marshal(value)
}
type foreignKeyInfo struct {
Name string `json:"name"`
Column string `json:"column"`
RefTable string `json:"ref_table"`
RefColumn string `json:"ref_column"`
}
type constraintInfo struct {
Name string `json:"name"`
ConstraintType string `json:"constraint_type"`
Definition string `json:"definition"`
Columns []string `json:"columns"`
RefSchema *string `json:"ref_schema,omitempty"`
RefTable *string `json:"ref_table,omitempty"`
RefColumns []string `json:"ref_columns"`
MatchType *string `json:"match_type,omitempty"`
OnUpdate *string `json:"on_update,omitempty"`
OnDelete *string `json:"on_delete,omitempty"`
Deferrable bool `json:"deferrable"`
InitiallyDeferred bool `json:"initially_deferred"`
Enabled bool `json:"enabled"`
Valid bool `json:"valid"`
}
type triggerInfo struct {
Name string `json:"name"`
Event string `json:"event"`
Timing string `json:"timing"`
}
func detectKingbaseMode(db *sql.DB, configuredMySQL bool) kingbaseMode {
if configuredMySQL {
return kingbaseMode{compatibilityMode: "mysql", mysqlCompat: true}
}
mode := kingbaseMode{compatibilityMode: detectDatabaseMode(db)}
mode.postgresCatalog = !catalogExists(db, "sys_catalog.sys_namespace") && catalogExists(db, "pg_catalog.pg_namespace")
// Catalog availability and the SQL compatibility mode are independent.
if mode.compatibilityMode != "" {
mode.mysqlCompat = mode.compatibilityMode == "mysql"
} else if !mode.postgresCatalog {
mode.mysqlCompat = supportsBacktickIdentifiers(db)
}
if !mode.postgresCatalog {
mode.sqlServerIdentity = !mode.mysqlCompat && catalogExists(db, "sys.identity_columns")
}
return mode
}
func catalogExists(db *sql.DB, catalog string) bool {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT 1 FROM "+catalog+" WHERE 1 = 0")
if err != nil {
return false
}
return rows.Close() == nil
}
func detectMySQLCompatMode(db *sql.DB) bool {
if databaseMode := detectDatabaseMode(db); databaseMode != "" {
return databaseMode == "mysql"
}
return supportsBacktickIdentifiers(db)
}
func detectDatabaseMode(db *sql.DB) string {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
for _, query := range []string{
"SELECT setting FROM sys_catalog.sys_settings WHERE LOWER(name) = 'database_mode'",
"SELECT setting FROM pg_catalog.pg_settings WHERE LOWER(name) = 'database_mode'",
"SHOW database_mode",
} {
var databaseMode string
if err := db.QueryRowContext(ctx, query).Scan(&databaseMode); err == nil {
// The reported mode wins over syntax probes, including unknown future modes.
if mode := strings.ToLower(strings.TrimSpace(databaseMode)); mode != "" {
return mode
}
}
}
return ""
}
func supportsBacktickIdentifiers(db *sql.DB) bool {
var value int
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return db.QueryRowContext(ctx, "SELECT 1 AS `dbx_identifier_probe`").Scan(&value) == nil
}
func (s *server) identifierQuote() string {
// Kingbase MySQL compatibility mode follows MySQL identifier quoting;
// other modes retain the PostgreSQL-compatible double quote.
if s.mode.mysqlCompat {
return "`"
}
return `"`
}
// quoteDDLIdentifier quotes an identifier inside generated DDL using the mode's
// identifier quote. MySQL compatibility mode uses backticks (and backtick
// escaping); everything else keeps the PostgreSQL-compatible double quote, so
// schema/table names containing hyphens or other special characters render as
// valid SQL instead of being parsed as operators or bare tokens.
func (s *server) quoteDDLIdentifier(value string) string {
return s.quoteIdentifier(value)
}
func (s *server) connectionInfo() (map[string]any, error) {
db, err := s.requireDB()
if err != nil {
return nil, err
}
var database, username, version string
var schema sql.NullString
err = db.QueryRow("SELECT current_database(), current_user, version(), current_schema()").Scan(&database, &username, &version, &schema)
if err != nil {
return nil, err
}
return map[string]any{
"database": database, "username": username, "version": version, "schema": schema.String,
"compatibilityMode": s.mode.compatibilityMode, "mysql_compat_mode": s.mode.mysqlCompat,
"identifierQuote": s.identifierQuote(),
"databaseInfo": map[string]string{
"productName": "KingbaseES", "productVersion": version, "currentDatabase": database,
"compatibilityMode": s.mode.compatibilityMode,
},
}, nil
}
func (s *server) listDatabases() ([]databaseInfo, error) {
queries := []string{
kingbaseListDatabasesSQL,
kingbaseListDatabasesPostgresSQL,
"SELECT current_database()",
}
for _, query := range queries {
rows, err := s.metadataQuery(query)
if err != nil {
continue
}
result := []databaseInfo{}
for rows.Next() {
var name string
if rows.Scan(&name) == nil {
result = append(result, databaseInfo{Name: name})
}
}
err = rows.Err()
_ = rows.Close()
if err == nil && len(result) > 0 {
return result, nil
}
}
return []databaseInfo{{Name: s.params.Database}}, nil
}
func kingbaseListSchemasSQL(mode kingbaseMode, showSystemSchemas bool) string {
if mode.mysqlCompat {
if showSystemSchemas {
return "SELECT schema_name FROM information_schema.schemata ORDER BY schema_name"
}
return kingbaseMySQLCompatListSchemasSQL
}
if mode.postgresCatalog {
if showSystemSchemas {
return "SELECT nspname FROM pg_catalog.pg_namespace ORDER BY nspname"
}
return "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg_temp_%' AND nspname NOT LIKE 'pg_toast_temp_%' ORDER BY nspname"
}
if showSystemSchemas {
return "SELECT nspname FROM sys_catalog.sys_namespace ORDER BY nspname"
}
return "SELECT nspname FROM sys_catalog.sys_namespace WHERE nspname NOT LIKE 'sys_temp_%' AND nspname NOT LIKE 'sys_toast_temp_%' ORDER BY nspname"
}
func (s *server) listSchemas(visible []string, showSystemSchemas bool) ([]string, error) {
query := kingbaseListSchemasSQL(s.mode, showSystemSchemas)
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
allowed := stringSet(visible)
result := []string{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
if len(allowed) == 0 || allowed[strings.ToLower(name)] {
result = append(result, name)
}
}
return result, rows.Err()
}
func (s *server) listTables(schema string, constraints metadataListConstraints) ([]tableInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
if !constraintsAllowsTableLike(constraints) {
return []tableInfo{}, nil
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
includeCatalogMetadata := !s.catalogOIDUnsupported
rows, err := s.queryTables(effective, catalog, includeCatalogMetadata)
if err != nil && includeCatalogMetadata && isUndefinedColumn(err, "c.oid") {
s.catalogOIDUnsupported = true
rows, err = s.queryTables(effective, catalog, false)
}
if err != nil {
return nil, err
}
defer rows.Close()
result := []tableInfo{}
for rows.Next() {
var name, kind string
var comment, parentSchema, parentName sql.NullString
if err := rows.Scan(&name, &kind, &comment, &parentSchema, &parentName); err != nil {
return nil, err
}
item := tableInfo{
Name: name, TableType: normalizeTableType(kind), Comment: nullStringPtr(comment),
ParentSchema: nullStringPtr(parentSchema), ParentName: nullStringPtr(parentName),
}
if constraintsMatch(constraints, item.Name, item.TableType) {
result = append(result, item)
}
}
return pageTables(result, constraints), rows.Err()
}
func (s *server) queryTables(schema, catalog string, includeCatalogMetadata bool) (*sql.Rows, error) {
commentExpression := "NULL AS table_comment"
parentSchemaExpression := "NULL AS parent_schema"
parentNameExpression := "NULL AS parent_name"
inheritanceJoins := ""
if includeCatalogMetadata {
commentExpression = "obj_description(c.oid) AS table_comment"
parentSchemaExpression = "CASE WHEN pc.relkind = 'p' THEN pn.nspname ELSE NULL END AS parent_schema"
parentNameExpression = "CASE WHEN pc.relkind = 'p' THEN pc.relname ELSE NULL END AS parent_name"
prefix := catalogPrefix(catalog)
inheritanceJoins = fmt.Sprintf(`
LEFT JOIN %s.%s_inherits i ON i.inhrelid = c.oid
LEFT JOIN %s.%s_class pc ON pc.oid = i.inhparent
LEFT JOIN %s.%s_namespace pn ON pn.oid = pc.relnamespace`, catalog, prefix, catalog, prefix, catalog, prefix)
}
prefix := catalogPrefix(catalog)
// DISTINCT collapses the rows fanned out by the pg_inherits join when a
// table has several legacy INHERITS parents (each such row is identical —
// only declarative partition parents fill the parent columns).
query := fmt.Sprintf(`SELECT DISTINCT c.relname,
CASE c.relkind WHEN 'r' THEN 'TABLE' WHEN 'p' THEN 'TABLE' WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'MATERIALIZED_VIEW' WHEN 'f' THEN 'FOREIGN_TABLE' ELSE 'TABLE' END,
%s,
%s,
%s
FROM %s.%s_class c
JOIN %s.%s_namespace n ON n.oid = c.relnamespace%s
WHERE n.nspname = %s AND c.relkind IN ('r','p','v','m','f')
ORDER BY c.relname`, commentExpression, parentSchemaExpression, parentNameExpression, catalog, prefix, catalog, prefix, inheritanceJoins, quoteLiteral(schema))
return s.metadataQuery(query)
}
func (s *server) getTableComment(schema, table string) (*string, error) {
if s.catalogOIDUnsupported {
return nil, nil
}
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
prefix := catalogPrefix(catalog)
query := fmt.Sprintf(`SELECT obj_description(c.oid)
FROM %s.%s_class c
JOIN %s.%s_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = %s AND c.relname = %s AND c.relkind IN ('r','p','v','m','f')
LIMIT 1`, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(table))
var comment sql.NullString
if err := s.requireDBQueryRow(query, &comment); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
if isUndefinedColumn(err, "c.oid") {
s.catalogOIDUnsupported = true
return nil, nil
}
return nil, err
}
return nullStringPtr(comment), nil
}
// listCustomTypes lists user-defined types visible in the given schema.
//
// Only explicitly created types are returned: base types (b), standalone
// composite types (c), domains (d), enums (e), ranges (r) and multiranges (m).
// Relation auto-generated row types (table/view/materialized view/foreign
// table/partitioned table) are excluded via `typrelid = 0 OR relkind = 'c'`,
// and array companion types are excluded via `typelem = 0`. MySQL
// compatibility mode has no pg_type catalog contract and returns nothing.
//
// The comment join scopes description entries to the type catalog itself via
// a regclass cast. `t.tableoid` cannot be used because Kingbase's native
// sys_type catalog has no tableoid system column. Kingbase and Vastbase both
// key COMMENT ON TYPE entries with the pg_type identity (oid 1247) even when
// the server is in sys_catalog compatibility mode, so the filter always
// references pg_catalog.pg_type.
func (s *server) listCustomTypes(schema string) ([]objectInfo, error) {
if s.mode.mysqlCompat {
return []objectInfo{}, nil
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
prefix := catalogPrefix(catalog)
query := fmt.Sprintf(`SELECT t.typname, d.description, t.typtype::text,
CASE
WHEN t.typtype = 'c' THEN EXISTS (
SELECT 1 FROM %s.%s_attribute a
WHERE a.attrelid = t.typrelid AND a.attnum > 0 AND NOT a.attisdropped
)
WHEN t.typtype = 'e' THEN EXISTS (
SELECT 1 FROM %s.%s_enum e WHERE e.enumtypid = t.oid
)
ELSE false
END AS has_members
FROM %s.%s_type t
JOIN %s.%s_namespace n ON n.oid = t.typnamespace
LEFT JOIN %s.%s_class c ON c.oid = t.typrelid
LEFT JOIN %s.%s_description d ON d.objoid = t.oid AND d.classoid = 'pg_catalog.pg_type'::regclass AND d.objsubid = 0
WHERE n.nspname = %s
AND t.typtype IN ('b','c','d','e','r','m')
AND t.typisdefined
AND t.typelem = 0
AND (t.typrelid = 0 OR c.relkind = 'c')
AND n.nspname <> 'pg_catalog'
AND n.nspname <> 'information_schema'
AND n.nspname NOT LIKE 'pg_toast%%'
AND n.nspname NOT LIKE 'pg_temp%%'
ORDER BY t.typname`, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := []objectInfo{}
for rows.Next() {
var name, kindCode string
var comment sql.NullString
var hasMembers bool
if err := rows.Scan(&name, &comment, &kindCode, &hasMembers); err != nil {
return nil, err
}
kind, ok := customTypeKindFromCode(kindCode)
if !ok {
continue
}
kindValue := string(kind)
result = append(result, objectInfo{Name: name, ObjectType: "TYPE", Schema: schema, Comment: nullStringPtr(comment), CustomTypeKind: &kindValue, HasMembers: &hasMembers})
}
return result, rows.Err()
}
func isSystemSchema(schema string) bool {
return schema == "pg_catalog" || schema == "information_schema" || strings.HasPrefix(schema, "pg_toast") || strings.HasPrefix(schema, "pg_temp")
}
type customTypeKind string
const (
customTypeKindBase customTypeKind = "base"
customTypeKindComposite customTypeKind = "composite"
customTypeKindDomain customTypeKind = "domain"
customTypeKindEnum customTypeKind = "enum"
customTypeKindRange customTypeKind = "range"
customTypeKindMultirange customTypeKind = "multirange"
)
type customTypeMember struct {
Name string `json:"name"`
DataType string `json:"dataType"`
Ordinal int32 `json:"ordinal"`
Nullable *bool `json:"nullable,omitempty"`
Default *string `json:"default,omitempty"`
Comment *string `json:"comment,omitempty"`
EnumValue *string `json:"enumValue,omitempty"`
}
type customTypeDomainConstraint struct {
Name string `json:"name"`
Definition string `json:"definition"`
}
type customTypeProperties struct {
BaseType *string `json:"baseType,omitempty"`
NotNull *bool `json:"notNull,omitempty"`
Default *string `json:"default,omitempty"`
Collation *string `json:"collation,omitempty"`
DomainConstraints []customTypeDomainConstraint `json:"domainConstraints"`
RangeSubtype *string `json:"rangeSubtype,omitempty"`
RangeMultirangeName *string `json:"rangeMultirangeName,omitempty"`
RangeCanonicalFunction *string `json:"rangeCanonicalFunction,omitempty"`
RangeSubtypeDiffFunction *string `json:"rangeSubtypeDiffFunction,omitempty"`
RangeSubtypeOpclass *string `json:"rangeSubtypeOpclass,omitempty"`
InputFunction *string `json:"inputFunction,omitempty"`
OutputFunction *string `json:"outputFunction,omitempty"`
ReceiveFunction *string `json:"receiveFunction,omitempty"`
SendFunction *string `json:"sendFunction,omitempty"`
AnalyzeFunction *string `json:"analyzeFunction,omitempty"`
Internallength *int32 `json:"internallength,omitempty"`
PassedByValue *bool `json:"passedByValue,omitempty"`
Alignment *string `json:"alignment,omitempty"`
Storage *string `json:"storage,omitempty"`
}
type customTypeDdl struct {
SQL string `json:"sql"`
Complete bool `json:"complete"`
Warnings []string `json:"warnings,omitempty"`
}
type customTypeDetails struct {
Name string `json:"name"`
Schema string `json:"schema"`
Kind customTypeKind `json:"kind"`
Comment *string `json:"comment,omitempty"`
Members []customTypeMember `json:"members"`
Properties customTypeProperties `json:"properties"`
DDL *customTypeDdl `json:"ddl,omitempty"`
}
// customTypeCatalogQueries carries catalog-aware SQL fragments for type
// details. Kingbase exposes pg_get_expr/pg_get_constraintdef under the sys_
// prefix in system-catalog mode, so the function names follow the catalog.
type customTypeCatalogQueries struct {
general string
enumMembers string
compositeMembers string
domainBaseType string
domainRenderedDefault string
domainConstraints string
rangeAttributes string
rangeAttributesForMultirange string
rangeMultirange string
collationName string
}
// qualifiedCatalogTypeExpression keeps user-defined type references usable
// outside the current search_path while retaining format_type's typmod output
// for built-in pg_catalog types.
func qualifiedCatalogTypeExpression(typeAlias, namespaceAlias, elementAlias, elementNamespaceAlias, oidExpression, typmodExpression string) string {
return fmt.Sprintf(`CASE
WHEN %s.typelem <> 0 AND %s.nspname NOT IN ('pg_catalog', 'sys_catalog')
THEN quote_ident(%s.nspname) || '.' || quote_ident(%s.typname) || '[]'
WHEN %s.nspname NOT IN ('pg_catalog', 'sys_catalog')
THEN quote_ident(%s.nspname) || '.' || quote_ident(%s.typname)
WHEN %s.typname = 'json'
THEN 'json'
WHEN %s.typname = '_json'
THEN 'json[]'
ELSE format_type(%s, %s)
END`, typeAlias, elementNamespaceAlias, elementNamespaceAlias, elementAlias, namespaceAlias, namespaceAlias, typeAlias, typeAlias, typeAlias, oidExpression, typmodExpression)
}
func customTypeCatalogQueriesFor(catalog, prefix, schema, name string) customTypeCatalogQueries {
getExpr := prefix + "_get_expr"
getConstraintDef := prefix + "_get_constraintdef"
typeTable := catalog + "." + prefix + "_type"
namespaceTable := catalog + "." + prefix + "_namespace"
classTable := catalog + "." + prefix + "_class"
descriptionTable := catalog + "." + prefix + "_description"
procTable := catalog + "." + prefix + "_proc"
collationTable := catalog + "." + prefix + "_collation"
enumTable := catalog + "." + prefix + "_enum"
attributeTable := catalog + "." + prefix + "_attribute"
attrdefTable := catalog + "." + prefix + "_attrdef"
constraintTable := catalog + "." + prefix + "_constraint"
rangeTable := catalog + "." + prefix + "_range"
opclassTable := catalog + "." + prefix + "_opclass"
quotedSchema := quoteLiteral(schema)
quotedName := quoteLiteral(name)
compositeMemberType := qualifiedCatalogTypeExpression("at", "atn", "elem", "elem_n", "a.atttypid", "a.atttypmod")
domainBaseType := qualifiedCatalogTypeExpression("t", "n", "elem", "elem_n", "t.oid", "%[2]d::int4")
rangeSubtype := qualifiedCatalogTypeExpression("st", "stn", "elem", "elem_n", "r.rngsubtype", "NULL::integer")
return customTypeCatalogQueries{
general: fmt.Sprintf(`SELECT t.oid, t.typtype::text, t.typisdefined,
t.typbasetype, t.typnotnull, t.typrelid, t.typelem, t.typcollation,
t.typdefaultbin, t.typdefault, t.typlen, t.typbyval,
t.typalign::text, t.typstorage::text, t.typtypmod,
pi.proname, po.proname, pr.proname, ps.proname, pa.proname,
d.description,
CASE WHEN t.typrelid != 0 THEN (SELECT c.relkind::text FROM %s c WHERE c.oid = t.typrelid) END,
CASE WHEN cl.oid IS NULL THEN NULL ELSE quote_ident(ncl.nspname) || '.' || quote_ident(cl.collname) END
FROM %s t
JOIN %s n ON n.oid = t.typnamespace
LEFT JOIN %s d ON d.objoid = t.oid AND d.classoid = 'pg_catalog.pg_type'::regclass AND d.objsubid = 0
LEFT JOIN %s pi ON pi.oid = t.typinput
LEFT JOIN %s po ON po.oid = t.typoutput
LEFT JOIN %s pr ON pr.oid = t.typreceive
LEFT JOIN %s ps ON ps.oid = t.typsend
LEFT JOIN %s pa ON pa.oid = t.typanalyze
LEFT JOIN %s cl ON cl.oid = t.typcollation
LEFT JOIN %s ncl ON ncl.oid = cl.collnamespace
WHERE n.nspname = %s AND t.typname = %s`, classTable, typeTable, namespaceTable, descriptionTable, procTable, procTable, procTable, procTable, procTable, collationTable, namespaceTable, quotedSchema, quotedName),
enumMembers: fmt.Sprintf(`SELECT e.enumlabel, e.enumsortorder
FROM %s e
WHERE e.enumtypid = %%d ORDER BY e.enumsortorder`, enumTable),
compositeMembers: fmt.Sprintf(`SELECT a.attname, %s, a.attnum,
NOT a.attnotnull, a.atthasdef, %s(ad.adbin, ad.adrelid), col_description(%%d, a.attnum)
FROM %s a
JOIN %s at ON at.oid = a.atttypid
JOIN %s atn ON atn.oid = at.typnamespace
LEFT JOIN %s elem ON elem.oid = at.typelem
LEFT JOIN %s elem_n ON elem_n.oid = elem.typnamespace
LEFT JOIN %s ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
WHERE a.attrelid = %%d AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, compositeMemberType, getExpr, attributeTable, typeTable, namespaceTable, typeTable, namespaceTable, attrdefTable),
domainBaseType: fmt.Sprintf(`SELECT %s
FROM %s t
JOIN %s n ON n.oid = t.typnamespace
LEFT JOIN %s elem ON elem.oid = t.typelem
LEFT JOIN %s elem_n ON elem_n.oid = elem.typnamespace
WHERE t.oid = %%[1]d`, domainBaseType, typeTable, namespaceTable, typeTable, namespaceTable),
domainRenderedDefault: fmt.Sprintf(`SELECT %s(t.typdefaultbin, 0)
FROM %s t WHERE t.oid = %%d`, getExpr, typeTable),
domainConstraints: fmt.Sprintf(`SELECT c.conname, %s(c.oid, true) FROM %s c WHERE c.contypid = %%d ORDER BY c.conname`, getConstraintDef, constraintTable),
rangeAttributes: fmt.Sprintf(`SELECT %s, quote_ident(ncan.nspname) || '.' || quote_ident(pcan.proname), quote_ident(ndiff.nspname) || '.' || quote_ident(pdiff.proname), quote_ident(nopc.nspname) || '.' || quote_ident(opc.opcname)
FROM %s r
JOIN %s st ON st.oid = r.rngsubtype
JOIN %s stn ON stn.oid = st.typnamespace
LEFT JOIN %s elem ON elem.oid = st.typelem
LEFT JOIN %s elem_n ON elem_n.oid = elem.typnamespace
LEFT JOIN %s pcan ON pcan.oid = r.rngcanonical
LEFT JOIN %s pdiff ON pdiff.oid = r.rngsubdiff
LEFT JOIN %s opc ON opc.oid = r.rngsubopc
LEFT JOIN %s ncan ON ncan.oid = pcan.pronamespace
LEFT JOIN %s ndiff ON ndiff.oid = pdiff.pronamespace
LEFT JOIN %s nopc ON nopc.oid = opc.opcnamespace
WHERE r.rngtypid = %%d`, rangeSubtype, rangeTable, typeTable, namespaceTable, typeTable, namespaceTable, procTable, procTable, opclassTable, namespaceTable, namespaceTable, namespaceTable),
rangeAttributesForMultirange: fmt.Sprintf(`SELECT %s, quote_ident(ncan.nspname) || '.' || quote_ident(pcan.proname), quote_ident(ndiff.nspname) || '.' || quote_ident(pdiff.proname), quote_ident(nopc.nspname) || '.' || quote_ident(opc.opcname)
FROM %s r
JOIN %s st ON st.oid = r.rngsubtype
JOIN %s stn ON stn.oid = st.typnamespace
LEFT JOIN %s elem ON elem.oid = st.typelem
LEFT JOIN %s elem_n ON elem_n.oid = elem.typnamespace
LEFT JOIN %s pcan ON pcan.oid = r.rngcanonical
LEFT JOIN %s pdiff ON pdiff.oid = r.rngsubdiff
LEFT JOIN %s opc ON opc.oid = r.rngsubopc
LEFT JOIN %s ncan ON ncan.oid = pcan.pronamespace
LEFT JOIN %s ndiff ON ndiff.oid = pdiff.pronamespace
LEFT JOIN %s nopc ON nopc.oid = opc.opcnamespace
WHERE r.rngmultitypid = %%d`, rangeSubtype, rangeTable, typeTable, namespaceTable, typeTable, namespaceTable, procTable, procTable, opclassTable, namespaceTable, namespaceTable, namespaceTable),
rangeMultirange: fmt.Sprintf(`SELECT mt.typname
FROM %s r
JOIN %s mt ON mt.oid = r.rngmultitypid
WHERE r.rngtypid = %%d`, rangeTable, typeTable),
collationName: fmt.Sprintf(`SELECT quote_ident(ncl.nspname) || '.' || quote_ident(cl.collname) FROM %s cl JOIN %s ncl ON ncl.oid = cl.collnamespace WHERE cl.oid = %%d`, collationTable, namespaceTable),
}
}
// getTypeDetails returns read-only details of a user-defined type. MySQL
// compatibility mode is explicitly unsupported instead of running PostgreSQL
// catalog SQL against a MySQL-mode server.
func (s *server) getTypeDetails(schema, name string) (*customTypeDetails, error) {
if s.mode.mysqlCompat {
return nil, errors.New("type details are not supported in MySQL compatibility mode")
}
schema = strings.TrimSpace(schema)
name = strings.TrimSpace(name)
if schema == "" || name == "" {
return nil, errors.New("schema and type name are required")
}
if isSystemSchema(schema) {
return nil, fmt.Errorf("system schema %s is not supported for custom type details", schema)
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
prefix := catalogPrefix(catalog)
queries := customTypeCatalogQueriesFor(catalog, prefix, schema, name)
rows, err := s.metadataQuery(queries.general)
if err != nil {
return nil, fmt.Errorf("failed to locate custom type %s.%s: %w", schema, name, err)
}
defer rows.Close()
if !rows.Next() {
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("failed to read type %s.%s: %w", schema, name, err)
}
return nil, fmt.Errorf("custom type %s.%s does not exist", schema, name)
}
var oid, typbasetype, typrelid, typelem, typcollation int64
var typtype, typalign, typstorage string
var typisdefined, typnotnull, typbyval bool
var typdefaultbin, typdefault, inputFn, outputFn, receiveFn, sendFn, analyzeFn, comment, collname, relkind sql.NullString
var typlen sql.NullInt64
var typtypmod int64
if err := rows.Scan(&oid, &typtype, &typisdefined, &typbasetype, &typnotnull, &typrelid, &typelem, &typcollation, &typdefaultbin, &typdefault, &typlen, &typbyval, &typalign, &typstorage, &typtypmod, &inputFn, &outputFn, &receiveFn, &sendFn, &analyzeFn, &comment, &relkind, &collname); err != nil {
return nil, fmt.Errorf("failed to read type %s.%s: %w", schema, name, err)
}
if err := rows.Close(); err != nil {
return nil, err
}
if !typisdefined {
return nil, fmt.Errorf("custom type %s.%s is not fully defined", schema, name)
}
if typelem != 0 {
return nil, fmt.Errorf("custom type %s.%s is an array companion type", schema, name)
}
kind, ok := customTypeKindFromCode(typtype)
if !ok {
return nil, fmt.Errorf("custom type %s.%s is a pseudo type (typtype=%s)", schema, name, typtype)
}
if relkind.Valid && relkind.String != "" && relkind.String != "c" {
return nil, fmt.Errorf("%s.%s is the auto-generated row type of a relation, not an independent custom type", schema, name)
}
properties := customTypeCommonProperties(inputFn, outputFn, receiveFn, sendFn, analyzeFn, typlen, typbyval, typalign, typstorage)
properties.DomainConstraints = []customTypeDomainConstraint{}
details := &customTypeDetails{
Name: name,
Schema: schema,
Kind: kind,
Comment: nullStringPtr(comment),
Members: []customTypeMember{},
Properties: properties,
}
var warnings []string
switch kind {
case customTypeKindEnum:
details.Members, err = s.customTypeEnumMembers(queries.enumMembers, oid)
if err != nil {
return nil, err
}
case customTypeKindComposite:
details.Members, err = s.customTypeCompositeMembers(queries.compositeMembers, typrelid)
if err != nil {
return nil, err
}
case customTypeKindDomain:
warnings = append(warnings, s.customTypeDomainAttributes(queries, &details.Properties, oid, typbasetype, typtypmod, typnotnull, typdefaultbin, typdefault, typcollation, collname)...)
case customTypeKindRange:
warnings = append(warnings, s.customTypeRangeAttributes(queries, &details.Properties, oid, false)...)
case customTypeKindMultirange:
warnings = append(warnings, s.customTypeRangeAttributes(queries, &details.Properties, oid, true)...)
case customTypeKindBase:
}
details.DDL = s.buildCustomTypeDDL(schema, name, kind, inputFn, &details.Members, &details.Properties, warnings)
return details, nil
}
func customTypeKindFromCode(code string) (customTypeKind, bool) {
switch code {
case "b":
return customTypeKindBase, true
case "c":
return customTypeKindComposite, true
case "d":
return customTypeKindDomain, true
case "e":
return customTypeKindEnum, true
case "r":
return customTypeKindRange, true
case "m":
return customTypeKindMultirange, true
default:
return "", false
}
}
func customTypeCommonProperties(inputFn, outputFn, receiveFn, sendFn, analyzeFn sql.NullString, typlen sql.NullInt64, typbyval bool, typalign, typstorage string) customTypeProperties {
properties := customTypeProperties{}
properties.InputFunction = nullStringPtr(inputFn)
properties.OutputFunction = nullStringPtr(outputFn)
properties.ReceiveFunction = nullStringPtr(receiveFn)
properties.SendFunction = nullStringPtr(sendFn)
properties.AnalyzeFunction = nullStringPtr(analyzeFn)
if typlen.Valid && typlen.Int64 > 0 {
value := int32(typlen.Int64)
properties.Internallength = &value
}
properties.PassedByValue = &typbyval
if typalign != "" {
properties.Alignment = &typalign
}
if typstorage != "" {
properties.Storage = &typstorage
}
return properties
}
func (s *server) customTypeEnumMembers(sqlTemplate string, oid int64) ([]customTypeMember, error) {
rows, err := s.metadataQuery(fmt.Sprintf(sqlTemplate, oid))
if err != nil {
return nil, fmt.Errorf("failed to read enum values: %w", err)
}
defer rows.Close()
var members []customTypeMember
index := 0
for rows.Next() {
var label string
var sortOrder float64
if err := rows.Scan(&label, &sortOrder); err != nil {
return nil, err
}
// enumsortorder is float4; ALTER TYPE ... ADD VALUE BEFORE/AFTER can
// yield fractional values. Use the ORDER BY position for a unique key.
index++
members = append(members, customTypeMember{Ordinal: int32(index), EnumValue: &label})
}
return members, rows.Err()
}
func (s *server) customTypeCompositeMembers(sqlTemplate string, typrelid int64) ([]customTypeMember, error) {
rows, err := s.metadataQuery(fmt.Sprintf(sqlTemplate, typrelid, typrelid))
if err != nil {
return nil, fmt.Errorf("failed to read composite fields: %w", err)
}
defer rows.Close()
var members []customTypeMember
for rows.Next() {
var member customTypeMember
var hasDefault bool
var comment sql.NullString
if err := rows.Scan(&member.Name, &member.DataType, &member.Ordinal, &member.Nullable, &hasDefault, &member.Default, &comment); err != nil {
return nil, err
}
if !hasDefault {
member.Default = nil
}
member.Comment = nullStringPtr(comment)
members = append(members, member)
}
return members, rows.Err()
}
func (s *server) customTypeDomainAttributes(queries customTypeCatalogQueries, properties *customTypeProperties, oid, typbasetype, typtypmod int64, typnotnull bool, typdefaultbin, typdefault sql.NullString, typcollation int64, collname sql.NullString) []string {
var warnings []string
if base := s.singleStringQuery(fmt.Sprintf(queries.domainBaseType, typbasetype, typtypmod)); base != "" {
properties.BaseType = &base
}
properties.NotNull = &typnotnull
defaultValue, defaultWarnings := resolveCustomTypeDomainDefault(typdefaultbin, typdefault, func() (string, error) {
return s.singleStringQueryResult(fmt.Sprintf(queries.domainRenderedDefault, oid))
})
properties.Default = defaultValue
warnings = append(warnings, defaultWarnings...)
if typcollation != 0 {
if collname.Valid && collname.String != "" {
properties.Collation = &collname.String
} else if value := s.singleStringQuery(fmt.Sprintf(queries.collationName, typcollation)); value != "" {
properties.Collation = &value
}
}
rows, err := s.metadataQuery(fmt.Sprintf(queries.domainConstraints, oid))
if err != nil {
warnings = append(warnings, fmt.Sprintf("domain constraints could not be read: %v", err))
return warnings
}
for rows.Next() {
var constraint customTypeDomainConstraint
if err := rows.Scan(&constraint.Name, &constraint.Definition); err != nil {
warnings = append(warnings, fmt.Sprintf("domain constraints could not be decoded: %v", err))
break
}
if constraint.Definition != "" {
properties.DomainConstraints = append(properties.DomainConstraints, constraint)
}
}
if err := rows.Err(); err != nil {
warnings = append(warnings, fmt.Sprintf("domain constraints could not be read: %v", err))
}
if err := rows.Close(); err != nil {
warnings = append(warnings, fmt.Sprintf("domain constraints could not be closed: %v", err))
}
return warnings
}
func (s *server) customTypeRangeAttributes(queries customTypeCatalogQueries, properties *customTypeProperties, oid int64, isMultirange bool) []string {
var warnings []string
// pg_range.rngtypid stores the RANGE oid; a multirange view resolves its
// owning range through rngmultitypid instead.
rangeTemplate := queries.rangeAttributes
if isMultirange {
rangeTemplate = queries.rangeAttributesForMultirange
}
rows, err := s.metadataQuery(fmt.Sprintf(rangeTemplate, oid))
if err != nil {
return []string{fmt.Sprintf("range attributes could not be read: %v", err)}
}
if rows.Next() {
var subtype, canonical, subdiff, opclass sql.NullString
if err := rows.Scan(&subtype, &canonical, &subdiff, &opclass); err != nil {
warnings = append(warnings, fmt.Sprintf("range attributes could not be decoded: %v", err))
} else {
properties.RangeSubtype = nullStringPtr(subtype)
properties.RangeCanonicalFunction = nullStringPtr(canonical)
properties.RangeSubtypeDiffFunction = nullStringPtr(subdiff)
properties.RangeSubtypeOpclass = nullStringPtr(opclass)
}
} else if err := rows.Err(); err != nil {
warnings = append(warnings, fmt.Sprintf("range attributes could not be read: %v", err))
} else {
warnings = append(warnings, "range attributes returned no rows")
}
if err := rows.Close(); err != nil {
warnings = append(warnings, fmt.Sprintf("range attributes could not be closed: %v", err))
}
// Optional PG 13+ multirange companion; older kernels have no column.
if multirangeRows, err := s.metadataQuery(fmt.Sprintf(queries.rangeMultirange, oid)); err == nil {
if multirangeRows.Next() {
var name string
if scanErr := multirangeRows.Scan(&name); scanErr != nil {
warnings = append(warnings, fmt.Sprintf("multirange companion could not be decoded: %v", scanErr))
} else if name != "" {
properties.RangeMultirangeName = &name
}
} else if rowsErr := multirangeRows.Err(); rowsErr != nil {
warnings = append(warnings, fmt.Sprintf("multirange companion could not be read: %v", rowsErr))
}
if closeErr := multirangeRows.Close(); closeErr != nil {
warnings = append(warnings, fmt.Sprintf("multirange companion could not be closed: %v", closeErr))
}
} else {
warnings = append(warnings, fmt.Sprintf("multirange companion could not be read: %v", err))
}
return warnings
}
func (s *server) singleStringQuery(query string) string {
value, _ := s.singleStringQueryResult(query)
return value
}
func (s *server) singleStringQueryResult(query string) (string, error) {
rows, err := s.metadataQuery(query)
if err != nil {
return "", err
}
defer rows.Close()
if !rows.Next() {
return "", rows.Err()
}
var value sql.NullString
if err := rows.Scan(&value); err != nil {
return "", err
}
if !value.Valid {
return "", nil
}
return value.String, nil
}
func resolveCustomTypeDomainDefault(typdefaultbin, typdefault sql.NullString, render func() (string, error)) (*string, []string) {
if typdefault.Valid && typdefault.String != "" {
value := typdefault.String
return &value, nil
}
if !typdefaultbin.Valid || typdefaultbin.String == "" {
return nil, nil
}
value, err := render()
if err != nil {
return nil, []string{fmt.Sprintf("default value could not be rendered; the generated DDL is incomplete: %v", err)}
}
if value == "" {
return nil, []string{"default value could not be rendered; the generated DDL is incomplete"}
}
return &value, nil
}
func (s *server) quoteCatalogIdentifier(value string) string {
value = strings.TrimSpace(value)
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) && strings.Contains(value, `"."`) {
return value
}
if separator := strings.LastIndexByte(value, '.'); separator >= 0 {
return s.quoteDDLIdentifier(value[:separator]) + "." + s.quoteDDLIdentifier(value[separator+1:])
}
return s.quoteDDLIdentifier(value)
}
// buildCustomTypeDDL generates normalized CREATE TYPE text. complete is only
// true when the text can be executed standalone; multiranges and base types
// are marked incomplete with visible warnings.
func (s *server) buildCustomTypeDDL(schema, name string, kind customTypeKind, inputFn sql.NullString, members *[]customTypeMember, properties *customTypeProperties, warnings []string) *customTypeDdl {
qualified := s.quoteDDLIdentifier(schema) + "." + s.quoteDDLIdentifier(name)
switch kind {
case customTypeKindEnum:
values := make([]string, 0, len(*members))
for _, member := range *members {
if member.EnumValue != nil {
values = append(values, quoteLiteral(*member.EnumValue))
}
}
return &customTypeDdl{
SQL: fmt.Sprintf("CREATE TYPE %s AS ENUM (%s);", qualified, strings.Join(values, ", ")),
Complete: true,
Warnings: warnings,
}
case customTypeKindComposite:
fields := make([]string, 0, len(*members))
comments := make([]string, 0, len(*members))
for _, member := range *members {
fields = append(fields, s.quoteDDLIdentifier(member.Name)+" "+member.DataType)
if member.Comment != nil {
comments = append(comments, fmt.Sprintf("COMMENT ON COLUMN %s.%s IS %s;", qualified, s.quoteDDLIdentifier(member.Name), quoteLiteral(*member.Comment)))
}
}
sql := fmt.Sprintf("CREATE TYPE %s AS (\n %s\n);", qualified, strings.Join(fields, ",\n "))
if len(comments) > 0 {
sql = sql + "\n" + strings.Join(comments, "\n")
}
return &customTypeDdl{SQL: sql, Complete: true, Warnings: warnings}
case customTypeKindDomain:
complete := true
base := "unknown"
if properties.BaseType != nil && *properties.BaseType != "" {
base = *properties.BaseType
} else {
complete = false
warnings = append(warnings, "base type could not be resolved; the generated DDL is incomplete")
}
parts := []string{fmt.Sprintf("CREATE DOMAIN %s AS %s", qualified, base)}
if properties.Collation != nil && *properties.Collation != "" {
parts = append(parts, "COLLATE "+s.quoteCatalogIdentifier(*properties.Collation))
}
if properties.Default != nil && *properties.Default != "" {
parts = append(parts, "DEFAULT "+*properties.Default)
}
if properties.NotNull != nil && *properties.NotNull {
parts = append(parts, "NOT NULL")
}
for _, constraint := range properties.DomainConstraints {
body := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(constraint.Definition), "CHECK"))
constraintName := constraint.Name
if constraintName == "" {
constraintName = name + "_check"
}
parts = append(parts, fmt.Sprintf("CONSTRAINT %s CHECK %s", s.quoteDDLIdentifier(constraintName), body))
}
for _, warning := range warnings {
if strings.Contains(warning, "domain constraints") || strings.Contains(warning, "default value could not be rendered") {
complete = false
}
}
return &customTypeDdl{SQL: strings.Join(parts, "\n ") + ";", Complete: complete, Warnings: warnings}
case customTypeKindRange:
var args []string
if properties.RangeSubtype != nil && *properties.RangeSubtype != "" {
args = append(args, "subtype = "+*properties.RangeSubtype)
}
if properties.RangeSubtypeOpclass != nil && *properties.RangeSubtypeOpclass != "" {
args = append(args, "subtype_opclass = "+s.quoteCatalogIdentifier(*properties.RangeSubtypeOpclass))
}
if properties.RangeCanonicalFunction != nil && *properties.RangeCanonicalFunction != "" {
args = append(args, "canonical = "+s.quoteCatalogIdentifier(*properties.RangeCanonicalFunction))
}
if properties.RangeSubtypeDiffFunction != nil && *properties.RangeSubtypeDiffFunction != "" {
args = append(args, "subtype_diff = "+s.quoteCatalogIdentifier(*properties.RangeSubtypeDiffFunction))
}
if properties.RangeMultirangeName != nil && *properties.RangeMultirangeName != "" {
args = append(args, "multirange_type_name = "+s.quoteCatalogIdentifier(*properties.RangeMultirangeName))
}
if properties.RangeSubtype == nil || *properties.RangeSubtype == "" {
result := &customTypeDdl{
SQL: fmt.Sprintf("CREATE TYPE %s AS RANGE (subtype = unknown);", qualified),
Complete: false,
Warnings: append(warnings, "range attributes could not be resolved"),
}
return result
}
return &customTypeDdl{
SQL: fmt.Sprintf("CREATE TYPE %s AS RANGE (\n %s\n);", qualified, strings.Join(args, ",\n ")),
Complete: true,
Warnings: warnings,
}
case customTypeKindMultirange:
return &customTypeDdl{
SQL: "",
Complete: false,
Warnings: append(append([]string{}, warnings...), fmt.Sprintf("%s is the auto-generated multirange companion of a range type; it has no standalone CREATE statement", qualified)),
}
default: // customTypeKindBase
inputName := "unknown"
if inputFn.Valid && inputFn.String != "" {
inputName = inputFn.String
}
return &customTypeDdl{
SQL: fmt.Sprintf("CREATE TYPE %s; -- base type attributes require manual reconstruction", qualified),
Complete: false,
Warnings: append(append([]string{}, warnings...), fmt.Sprintf("%s is a base type; its input/output functions (%s) cannot be rebuilt from catalogs", qualified, inputName)),
}
}
}
func (s *server) getTablePartitioning(schema, table string) (pgTablePartitioning, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return pgTablePartitioning{}, err
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
prefix := catalogPrefix(catalog)
partKeyFunction := kingbaseCatalogFunction(catalog, "sys_get_partkeydef", "pg_get_partkeydef")
boundFunction := kingbaseCatalogFunction(catalog, "sys_get_expr", "pg_get_expr")
rows, err := s.queryPartitionTree(catalog, prefix, boundFunction, partKeyFunction, effective, table)
if err != nil {
return pgTablePartitioning{}, err
}
defer rows.Close()
relations := make([]partitionRelation, 0)
for rows.Next() {
var relation partitionRelation
if err := rows.Scan(&relation.OID, &relation.Schema, &relation.Name, &relation.ParentOID, &relation.ParentSchema, &relation.ParentName, &relation.Relkind, &relation.Bound, &relation.KeyDefinition); err != nil {
return pgTablePartitioning{}, err
}
relations = append(relations, relation)
}
if err := rows.Err(); err != nil {
return pgTablePartitioning{}, err
}
if len(relations) == 0 {
return emptyPgTablePartitioning(), nil
}
rootIndex := -1
for index, relation := range relations {
if relation.Schema == effective && relation.Name == table {
rootIndex = index
break
}
}
if rootIndex < 0 {
return emptyPgTablePartitioning(), nil
}
root := relations[rootIndex]
result := emptyPgTablePartitioning()
result.IsPartitioned = root.Relkind == "p"
result.IsPartition = root.ParentOID.Valid
if root.ParentSchema.Valid && root.ParentName.Valid {
result.ParentSchema, result.ParentTable = root.ParentSchema.String, root.ParentName.String
result.Parent = root.ParentSchema.String + "." + root.ParentName.String
}
if root.KeyDefinition.Valid {
result.KeyDefinition = root.KeyDefinition.String
result.Strategy = partitionKindFromKeyDefinition(root.KeyDefinition.String)
result.KeyColumns = partitionKeyColumns(root.KeyDefinition.String)
}
result.OwnBound = parseKingbasePartitionBound(root.Bound)
children := make(map[int64][]partitionRelation)
for index, relation := range relations {
if index == rootIndex || !relation.ParentOID.Valid {
continue
}
children[relation.ParentOID.Int64] = append(children[relation.ParentOID.Int64], relation)
}
result.Partitions = buildKingbasePartitionNodes(root.OID, children)
for _, node := range result.Partitions {
if node.Bound != nil && node.Bound.Kind == "default" {
result.DefaultPartition = node.Name
break
}
}
return result, nil
}
// partitionTreeQueryOptions is the catalog-compatibility tier for the
// partition-tree query. Kingbase derivatives differ in which catalog columns and
// deparser functions exist, so `queryPartitionTree` retries progressively
// simpler variants instead of failing the whole RPC.
type partitionTreeQueryOptions struct {
relispartition bool
partitionKey bool
}
// Partition-tree query variants, most faithful first. `relispartition`
// distinguishes declarative partitions from legacy INHERITS children (without
// it, an inherited table would be mistaken for a partition and offered invalid
// DETACH DDL); `partitionKey` needs sys_get_partkeydef/pg_get_partkeydef.
func partitionTreeQueryVariants() []partitionTreeQueryOptions {
return []partitionTreeQueryOptions{
{relispartition: true, partitionKey: true},
{relispartition: false, partitionKey: true},
{relispartition: false, partitionKey: false},
}
}
func (s *server) queryPartitionTree(catalog, prefix, boundFunction, partKeyFunction, schema, table string) (*sql.Rows, error) {
var lastErr error
for _, options := range partitionTreeQueryVariants() {
rows, err := s.metadataQuery(buildPartitionTreeQuery(catalog, prefix, boundFunction, partKeyFunction, schema, table, options))
if err == nil {
return rows, nil
}
lastErr = err
if options.relispartition && isUndefinedColumn(err, "relispartition") {
continue
}
if options.partitionKey && isUndefinedFunction(err, "get_partkeydef") {
continue
}
return nil, err
}
return nil, lastErr
}
// buildPartitionTreeQuery renders the recursive partition-tree query. Bounds are
// read with the two-argument deparser (sys_get_expr(relpartbound, oid)), which
// Kingbase exposes on every supported release; the three-argument pretty form is
// not guaranteed on sys_catalog. The recursion carries a path array and filters
// on relispartition so a corrupted catalog cannot loop forever and a legacy
// INHERITS child is never reported as a partition.
func buildPartitionTreeQuery(catalog, prefix, boundFunction, partKeyFunction, schema, table string, options partitionTreeQueryOptions) string {
classTable := fmt.Sprintf("%s.%s_class", catalog, prefix)
namespaceTable := fmt.Sprintf("%s.%s_namespace", catalog, prefix)
inheritsTable := fmt.Sprintf("%s.%s_inherits", catalog, prefix)
anchorColumns := "c.oid::bigint AS oid, n.nspname AS schema_name, c.relname AS table_name,\n i.inhparent::bigint AS parent_oid, pn.nspname AS parent_schema, pc.relname AS parent_name,\n c.relkind::text AS relkind, ARRAY[c.oid::bigint] AS path"
anchorInheritsJoin := fmt.Sprintf("LEFT JOIN %s i ON i.inhrelid = c.oid", inheritsTable)
recursiveChildJoin := fmt.Sprintf("JOIN %s c ON c.oid = i.inhrelid", classTable)
recursivePredicate := "NOT c.oid = ANY(tree.path)"
if options.relispartition {
anchorInheritsJoin = fmt.Sprintf("LEFT JOIN %s i ON i.inhrelid = c.oid AND c.relispartition", inheritsTable)
recursiveChildJoin = fmt.Sprintf("JOIN %s c ON c.oid = i.inhrelid AND c.relispartition", classTable)
} else {
// Older catalogs lack relispartition. A declarative partition's parent
// always has relkind 'p' (traditional INHERITS parents are relkind 'r'),
// so scope the parent edge to partitioned parents instead.
anchorColumns = "c.oid::bigint AS oid, n.nspname AS schema_name, c.relname AS table_name,\n CASE WHEN pc.relkind = 'p' THEN i.inhparent::bigint END AS parent_oid,\n CASE WHEN pc.relkind = 'p' THEN pn.nspname END AS parent_schema,\n CASE WHEN pc.relkind = 'p' THEN pc.relname END AS parent_name,\n c.relkind::text AS relkind, ARRAY[c.oid::bigint] AS path"
recursivePredicate = "tree.relkind = 'p' AND NOT c.oid = ANY(tree.path)"
}
boundExpression := fmt.Sprintf("%s(c.relpartbound, c.oid)", boundFunction)
partKeyExpression := "CAST(NULL AS text)"
if options.partitionKey {
partKeyExpression = fmt.Sprintf("CASE WHEN t.relkind = 'p' THEN %s(c.oid) ELSE NULL END", partKeyFunction)
}
return fmt.Sprintf(`WITH RECURSIVE tree AS (
SELECT %s
FROM %s c
JOIN %s n ON n.oid = c.relnamespace
%s
LEFT JOIN %s pc ON pc.oid = i.inhparent
LEFT JOIN %s pn ON pn.oid = pc.relnamespace
WHERE n.nspname = %s AND c.relname = %s AND c.relkind IN ('r','p','f')
UNION ALL
SELECT c.oid::bigint, n.nspname, c.relname, tree.oid, tree.schema_name, tree.table_name, c.relkind::text,
tree.path || c.oid::bigint
FROM %s i
%s
JOIN %s n ON n.oid = c.relnamespace
JOIN tree ON tree.oid = i.inhparent
WHERE %s
)
SELECT t.oid, t.schema_name, t.table_name, t.parent_oid, t.parent_schema, t.parent_name, t.relkind,
%s AS partition_bound,
%s AS partition_key
FROM tree t
JOIN %s c ON c.oid = t.oid
ORDER BY t.oid`,
anchorColumns,
classTable, namespaceTable, anchorInheritsJoin,
classTable, namespaceTable,
quoteLiteral(schema), quoteLiteral(table),
inheritsTable,
recursiveChildJoin,
namespaceTable,
recursivePredicate,
boundExpression, partKeyExpression,
classTable,
)
}
// pgTablePartitionStatus mirrors the native table_partition_status_core result.
type pgTablePartitionStatus struct {
IsPartitionedParent bool `json:"isPartitionedParent"`
IsPartition bool `json:"isPartition"`
}
// getTablePartitionStatus answers the cheap "does this table participate in
// declarative partitioning?" probe without materializing the whole tree. It
// keys the parent edge on relkind 'p' (as listTables does), so it works even on
// releases whose catalog lacks relispartition, and a legacy INHERITS child is
// never reported as a partition.
func (s *server) getTablePartitionStatus(schema, table string) (pgTablePartitionStatus, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return pgTablePartitionStatus{}, err
}
catalog := "sys_catalog"
if s.mode.postgresCatalog {
catalog = "pg_catalog"
}
prefix := catalogPrefix(catalog)
query := fmt.Sprintf(`SELECT c.relkind::text,
EXISTS (
SELECT 1 FROM %s.%s_inherits i
JOIN %s.%s_class pc ON pc.oid = i.inhparent
WHERE i.inhrelid = c.oid AND pc.relkind = 'p'
)
FROM %s.%s_class c
JOIN %s.%s_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = %s AND c.relname = %s AND c.relkind IN ('r','p','f')`,
catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return pgTablePartitionStatus{}, err
}
defer rows.Close()
if !rows.Next() {
return pgTablePartitionStatus{}, rows.Err()
}
var relkind string
var isPartition bool
if err := rows.Scan(&relkind, &isPartition); err != nil {
return pgTablePartitionStatus{}, err
}
return pgTablePartitionStatus{IsPartitionedParent: relkind == "p", IsPartition: isPartition}, nil
}
func emptyPgTablePartitioning() pgTablePartitioning {
return pgTablePartitioning{KeyColumns: []string{}, Partitions: []pgPartitionNode{}}
}
func partitionKeyColumns(definition string) []string {
start := strings.Index(definition, "(")
if start < 0 {
return []string{}
}
body, _, ok := takeParenGroup(definition[start:])
if !ok {
return []string{}
}
columns := make([]string, 0)
for _, item := range splitTopLevelCommas(body) {
if column, ok := simplePartitionKeyColumn(item); ok {
columns = append(columns, column)
}
}
return columns
}
func simplePartitionKeyColumn(value string) (string, bool) {
value = strings.TrimSpace(value)
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
return strings.ReplaceAll(value[1:len(value)-1], "\"\"", "\""), true
}
if value == "" {
return "", false
}
for index, character := range value {
if index == 0 {
if character != '_' && !unicode.IsLetter(character) {
return "", false
}
continue
}
if character != '_' && character != '$' && !unicode.IsLetter(character) && !unicode.IsDigit(character) {
return "", false
}
}
return value, true
}
func partitionKindFromKeyDefinition(definition string) string {
fields := strings.Fields(strings.TrimSpace(definition))
if len(fields) == 0 {
return ""
}
switch strings.ToLower(fields[0]) {
case "range", "list", "hash":
return strings.ToLower(fields[0])
default:
return ""
}
}
func buildKingbasePartitionNodes(parentOID int64, children map[int64][]partitionRelation) []pgPartitionNode {
relations := children[parentOID]
sort.SliceStable(relations, func(i, j int) bool { return relations[i].Name < relations[j].Name })
nodes := make([]pgPartitionNode, 0, len(relations))
for _, relation := range relations {
childNodes := buildKingbasePartitionNodes(relation.OID, children)
node := pgPartitionNode{Schema: relation.Schema, Name: relation.Name, IsLeaf: len(childNodes) == 0, Children: childNodes}
if relation.KeyDefinition.Valid {
node.KeyDefinition = relation.KeyDefinition.String
node.Strategy = partitionKindFromKeyDefinition(relation.KeyDefinition.String)
}
node.BoundDefinition = relation.Bound.String
node.Bound = parseKingbasePartitionBound(relation.Bound)
nodes = append(nodes, node)
}
return nodes
}
func parseKingbasePartitionBound(value sql.NullString) *pgPartitionBound {
if !value.Valid || strings.TrimSpace(value.String) == "" {
return nil
}
definition := strings.TrimSpace(value.String)
if strings.EqualFold(definition, "DEFAULT") {
return &pgPartitionBound{Kind: "default"}
}
rest, ok := stripASCIICasePrefix(definition, "FOR VALUES")
if !ok {
return nil
}
rest = strings.TrimSpace(rest)
if body, matched := stripASCIICasePrefix(rest, "FROM"); matched {
from, tail, ok := takeParenGroup(body)
if !ok {
return nil
}
toBody, matched := stripASCIICasePrefix(strings.TrimSpace(tail), "TO")
if !matched {
return nil
}
to, _, ok := takeParenGroup(toBody)
if !ok {
return nil
}
return &pgPartitionBound{Kind: "range", From: splitBoundItems(from), To: splitBoundItems(to)}
}
if body, matched := stripASCIICasePrefix(rest, "IN"); matched {
values, _, ok := takeParenGroup(body)
if !ok {
return nil
}
return &pgPartitionBound{Kind: "list", Values: splitBoundItems(values)}
}
if body, matched := stripASCIICasePrefix(rest, "WITH"); matched {
options, _, ok := takeParenGroup(body)
if !ok {
return nil
}
modulus, remainder := -1, -1
for _, option := range splitTopLevelCommas(options) {
fields := strings.Fields(option)
if len(fields) != 2 {
return nil
}
number, err := strconv.Atoi(fields[1])
if err != nil {
return nil
}
switch strings.ToLower(fields[0]) {
case "modulus":
modulus = number
case "remainder":
remainder = number
}
}
if modulus < 0 || remainder < 0 {
return nil
}
return &pgPartitionBound{Kind: "hash", Modulus: &modulus, Remainder: &remainder}
}
// Keep the raw catalog definition when a Kingbase version renders a bound
// shape that this parser does not recognize.
return nil
}
// stripASCIICasePrefix removes a case-insensitive prefix, requiring a token
// boundary so `IN` never matches `INTO` and `TO` never matches `TOAST`.
func stripASCIICasePrefix(input, prefix string) (string, bool) {
if len(input) < len(prefix) || !strings.EqualFold(input[:len(prefix)], prefix) {
return "", false
}
rest := input[len(prefix):]
if rest == "" {
return rest, true
}
runeValue, _ := utf8.DecodeRuneInString(rest)
if unicode.IsSpace(runeValue) || runeValue == '(' {
return rest, true
}
return "", false
}
// takeParenGroup splits `( ... )` off the front of `input`, honoring single
// quotes, doubled-single-quote escapes, double quotes, and nested parentheses.
// It returns the inner text and the remainder after the closing parenthesis.
func takeParenGroup(input string) (string, string, bool) {
input = strings.TrimLeftFunc(input, unicode.IsSpace)
if input == "" || input[0] != '(' {
return "", "", false
}
depth := 0
inSingle := false
inDouble := false
for index := 0; index < len(input); index++ {
switch ch := input[index]; {
case ch == '\'' && !inDouble:
if inSingle && index+1 < len(input) && input[index+1] == '\'' {
index++
continue
}
inSingle = !inSingle
case ch == '"' && !inSingle:
inDouble = !inDouble
case ch == '(' && !inSingle && !inDouble:
depth++
case ch == ')' && !inSingle && !inDouble:
depth--
if depth == 0 {
return input[1:index], input[index+1:], true
}
}
}
return "", "", false
}
func splitBoundItems(input string) []string {
items := []string{}
for _, item := range splitTopLevelCommas(input) {
if item != "" {
items = append(items, item)
}
}
return items
}
// splitTopLevelCommas splits on top-level commas only; commas inside quotes,
// nested parentheses, or doubled-single-quote escapes stay in the item.
func splitTopLevelCommas(input string) []string {
items := []string{}
var current strings.Builder
depth := 0
inSingle := false
inDouble := false
for index := 0; index < len(input); index++ {
switch ch := input[index]; {
case ch == '\'' && !inDouble:
if inSingle && index+1 < len(input) && input[index+1] == '\'' {
current.WriteString("''")
index++
continue
}
inSingle = !inSingle
current.WriteByte(ch)
case ch == '"' && !inSingle:
inDouble = !inDouble
current.WriteByte(ch)
case ch == '(' && !inSingle && !inDouble:
depth++
current.WriteByte(ch)
case ch == ')' && !inSingle && !inDouble:
depth--
current.WriteByte(ch)
case ch == ',' && !inSingle && !inDouble && depth == 0:
items = append(items, strings.TrimSpace(current.String()))
current.Reset()
default:
current.WriteByte(ch)
}
}
items = append(items, strings.TrimSpace(current.String()))
return items
}
func (s *server) listObjects(schema string, constraints metadataListConstraints) ([]objectInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
result := []objectInfo{}
if constraintsAllowsTableLike(constraints) {
tables, err := s.listTables(effective, metadataListConstraints{})
if err != nil {
return nil, err
}
for _, table := range tables {
result = append(result, objectInfo{
Name: table.Name, ObjectType: table.TableType, Schema: effective, Comment: table.Comment,
ParentSchema: table.ParentSchema, ParentName: table.ParentName,
})
}
}
if constraintsAllowRoutines(constraints) {
var query string
if s.mode.mysqlCompat {
query = fmt.Sprintf(`SELECT ROUTINE_NAME, ROUTINE_TYPE, ROUTINE_COMMENT
FROM information_schema.routines
WHERE ROUTINE_SCHEMA = %s
ORDER BY ROUTINE_NAME`, quoteLiteral(effective))
} else {
catalog := "sys_catalog"
function := "sys"
if s.mode.postgresCatalog {
catalog, function = "pg_catalog", "pg"
}
query = fmt.Sprintf(`SELECT p.proname, CASE WHEN p.prorettype = 2278 THEN 'PROCEDURE' ELSE 'FUNCTION' END, d.description
FROM %s.%s_proc p JOIN %s.%s_namespace n ON n.oid = p.pronamespace
LEFT JOIN %s.%s_description d ON d.objoid = p.oid AND d.objsubid = 0
WHERE n.nspname = %s ORDER BY p.proname`, catalog, function, catalog, function, catalog, function, quoteLiteral(effective))
}
rows, queryErr := s.metadataQuery(query)
if queryErr == nil {
for rows.Next() {
var name, kind string
var comment sql.NullString
if rows.Scan(&name, &kind, &comment) == nil {
result = append(result, objectInfo{Name: name, ObjectType: kind, Schema: effective, Comment: nullStringPtr(comment)})
}
}
_ = rows.Close()
}
}
if constraintsAllowTriggers(constraints) {
triggers, triggerErr := s.listTriggerObjects(effective)
if triggerErr != nil {
return nil, fmt.Errorf("list triggers in schema %q: %w", effective, triggerErr)
}
result = append(result, triggers...)
}
if constraintsAllowTypes(constraints) {
types, typesErr := s.listCustomTypes(effective)
if typesErr != nil {
// A type catalog failure is a real fault: surfacing it lets the user
// distinguish an incomplete “all objects” view from an actually
// empty schema, instead of silently dropping the type group.
return nil, fmt.Errorf("list custom types in schema %q: %w", effective, typesErr)
}
result = append(result, types...)
}
filtered := result[:0]
for _, item := range result {
if constraintsMatch(constraints, item.Name, item.ObjectType) {
filtered = append(filtered, item)
}
}
sort.SliceStable(filtered, func(i, j int) bool {
if objectOrder(filtered[i].ObjectType) != objectOrder(filtered[j].ObjectType) {
return objectOrder(filtered[i].ObjectType) < objectOrder(filtered[j].ObjectType)
}
return filtered[i].Name < filtered[j].Name
})
return pageObjects(filtered, constraints), nil
}
func (s *server) listTriggerObjects(schema string) ([]objectInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
queryForCatalog := func() string {
internalPredicate := "NOT tg.tgisinternal"
if s.triggerInternalUnsupported {
internalPredicate = "tg.tgkind <> 'c'"
}
return fmt.Sprintf(`SELECT tg.tgname, c.relname, d.description
FROM %s.%s_trigger tg JOIN %s.%s_class c ON c.oid = tg.tgrelid
JOIN %s.%s_namespace n ON n.oid = c.relnamespace
LEFT JOIN %s.%s_description d ON d.objoid = tg.oid AND d.objsubid = 0
WHERE n.nspname = %s AND %s ORDER BY c.relname, tg.tgname`, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(effective), internalPredicate)
}
rows, err := s.metadataQuery(queryForCatalog())
if err != nil && !s.triggerInternalUnsupported && isUndefinedColumn(err, "tgisinternal") {
s.triggerInternalUnsupported = true
rows, err = s.metadataQuery(queryForCatalog())
}
if err != nil {
return nil, err
}
defer rows.Close()
result := []objectInfo{}
for rows.Next() {
var name, parentName string
var comment sql.NullString
if err := rows.Scan(&name, &parentName, &comment); err != nil {
return nil, err
}
result = append(result, objectInfo{
Name: name, ObjectType: "TRIGGER", Schema: effective,
ParentSchema: stringPtr(effective), ParentName: stringPtr(parentName), Comment: nullStringPtr(comment),
})
}
return result, rows.Err()
}
func (s *server) completionAssistantSearch(request completionAssistantRequest) (completionAssistantResponse, error) {
limit := request.MaxResults
if limit <= 0 || limit > 1000 {
limit = 100
}
kinds := stringSet(request.ObjectKinds)
candidates := make([]completionAssistantCandidate, 0, limit+1)
if kinds["column"] && request.ParentName != "" {
schema := request.ParentSchema
if schema == "" {
schema = request.Schema
}
columns, err := s.getColumns(schema, request.ParentName)
if err != nil {
return completionAssistantResponse{}, err
}
for _, column := range columns {
if !completionNameMatches(column.Name, request) {
continue
}
dataType := column.DataType
candidates = append(candidates, completionAssistantCandidate{
Name: column.Name, Kind: "COLUMN", Schema: stringPtr(schema), ParentSchema: stringPtr(schema),
ParentName: stringPtr(request.ParentName), Comment: column.Comment, DataType: &dataType,
})
}
} else {
schemas := []string{request.Schema}
if request.GlobalSearch {
visible, err := s.listSchemas(nil, false)
if err != nil {
return completionAssistantResponse{}, err
}
schemas = visible
}
objectTypes := request.ObjectKinds
for _, schema := range schemas {
objects, err := s.listObjects(schema, metadataListConstraints{ObjectTypes: objectTypes})
if err != nil {
return completionAssistantResponse{}, err
}
for _, object := range objects {
if !completionNameMatches(object.Name, request) {
continue
}
candidates = append(candidates, completionAssistantCandidate{Name: object.Name, Kind: object.ObjectType, Schema: stringPtr(object.Schema), Comment: object.Comment})
if len(candidates) > limit {
return completionAssistantResponse{Candidates: candidates[:limit], Incomplete: true}, nil
}
}
}
}
incomplete := len(candidates) > limit
if incomplete {
candidates = candidates[:limit]
}
if candidates == nil {
candidates = []completionAssistantCandidate{}
}
return completionAssistantResponse{Candidates: candidates, Incomplete: incomplete}, nil
}
func completionNameMatches(name string, request completionAssistantRequest) bool {
mask := request.Mask
if mask == "" {
return true
}
if !request.CaseSensitive {
name = strings.ToLower(name)
mask = strings.ToLower(mask)
}
if strings.EqualFold(request.MatchMode, "contains") {
return strings.Contains(name, mask)
}
return strings.HasPrefix(name, mask)
}
func (s *server) getColumns(schema, table string) ([]columnInfo, error) {
if s.mode.mysqlCompat {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
primary, _ := s.primaryKeys(effective, table)
return s.informationSchemaColumns(effective, table, primary)
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
result, err := s.queryCatalogColumns(schema, table, catalog, prefix, "pg_get_expr")
return s.finishCatalogColumns(schema, table, result, err)
}
expression := "sys_get_expr"
if s.usePgDefaultExpression {
expression = "pg_get_expr"
}
result, err := s.queryCatalogColumns(schema, table, catalog, prefix, expression)
if err != nil && expression == "sys_get_expr" && isUndefinedFunction(err, expression) {
// Some V8R6 PostgreSQL-mode databases keep sys_catalog while adbin is
// pg_node_tree. Cache the compatible function after the exact failure.
s.usePgDefaultExpression = true
result, err = s.queryCatalogColumns(schema, table, catalog, prefix, "pg_get_expr")
}
return s.finishCatalogColumns(schema, table, result, err)
}
func (s *server) finishCatalogColumns(schema, table string, result []columnInfo, err error) ([]columnInfo, error) {
if err != nil || len(result) == 0 {
return result, err
}
resolvedSchema := strings.TrimSpace(schema)
if result[0].ResolvedSchema != nil {
resolvedSchema = *result[0].ResolvedSchema
}
primary, _ := s.primaryKeys(resolvedSchema, table)
for index := range result {
result[index].IsPrimaryKey = primary[strings.ToLower(result[index].Name)]
}
if s.mode.sqlServerIdentity {
s.applyIdentityMetadata(resolvedSchema, table, result)
}
return result, nil
}
func (s *server) queryCatalogColumns(
schema, table string,
catalog, prefix, expression string,
) ([]columnInfo, error) {
identityExpression := "a.attidentity"
if s.catalogIdentityUnsupported {
identityExpression = "CAST(NULL AS varchar(1)) AS attidentity"
}
relationPredicate := fmt.Sprintf("n.nspname = %s AND c.relname = %s", quoteLiteral(schema), quoteLiteral(table))
if strings.TrimSpace(schema) == "" {
visibilityFunction := kingbaseCatalogFunction(catalog, "sys_table_is_visible", "pg_table_is_visible")
relationPredicate = fmt.Sprintf("c.relname = %s AND %s(c.oid)", quoteLiteral(table), visibilityFunction)
}
query := fmt.Sprintf(`SELECT n.nspname, a.attname,
CASE
WHEN t.typname = 'json' THEN 'json'
WHEN t.typname = '_json' THEN 'json[]'
ELSE format_type(a.atttypid, a.atttypmod)
END, NOT a.attnotnull,
%s(ad.adbin, ad.adrelid), col_description(a.attrelid, a.attnum),
CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN ((a.atttypmod - 4) >> 16) & 65535 END,
CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN (a.atttypmod - 4) & 65535 END,
CASE WHEN t.typname IN ('varchar','bpchar') AND a.atttypmod > 0 THEN a.atttypmod - 4 END,
%s
FROM %s.%s_attribute a JOIN %s.%s_type t ON t.oid = a.atttypid
JOIN %s.%s_class c ON c.oid = a.attrelid JOIN %s.%s_namespace n ON n.oid = c.relnamespace
LEFT JOIN %s.%s_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
WHERE %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, expression, identityExpression, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, relationPredicate)
rows, err := s.metadataQuery(query)
if err != nil && !s.catalogIdentityUnsupported && isUndefinedColumn(err, "attidentity") {
s.catalogIdentityUnsupported = true
return s.queryCatalogColumns(schema, table, catalog, prefix, expression)
}
if err != nil {
return nil, err
}
defer rows.Close()
result := []columnInfo{}
for rows.Next() {
var resolvedSchema, name, dataType string
var nullable bool
var defaultValue, comment, identity sql.NullString
var precision, scale, length sql.NullInt64
if err := rows.Scan(&resolvedSchema, &name, &dataType, &nullable, &defaultValue, &comment, &precision, &scale, &length, &identity); err != nil {
return nil, err
}
result = append(result, columnInfo{Name: name, DataType: dataType, ResolvedSchema: stringPtr(resolvedSchema), IsNullable: nullable, ColumnDefault: nullStringPtr(defaultValue), Extra: kingbaseIdentityClause(identity.String), Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)})
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
func isUndefinedFunction(err error, functionName string) bool {
var driverError *gokb.Error
undefined := errors.As(err, &driverError) && string(driverError.Code) == "42883"
normalized := strings.ToLower(err.Error())
undefined = undefined || strings.Contains(normalized, "does not exist") || strings.Contains(normalized, "不存在")
return undefined && strings.Contains(normalized, strings.ToLower(functionName))
}
func isUndefinedColumn(err error, columnName string) bool {
var driverError *gokb.Error
undefined := errors.As(err, &driverError) && string(driverError.Code) == "42703"
normalized := strings.ToLower(err.Error())
undefined = undefined || strings.Contains(normalized, "does not exist") || strings.Contains(normalized, "不存在")
return undefined && strings.Contains(normalized, strings.ToLower(columnName))
}
func (s *server) informationSchemaColumns(schema, table string, primary map[string]bool) ([]columnInfo, error) {
// Cache the optional information_schema capabilities for this connection so
// subsequent table metadata requests do not repeat known failing probes.
includeColumnType := !s.infoColumnTypeUnsupported
includeUdtName := !s.infoUdtNameUnsupported
for {
result, err := s.queryInformationSchemaColumns(schema, table, primary, includeColumnType, includeUdtName)
if err == nil {
return result, nil
}
switch {
case includeColumnType && isUndefinedColumn(err, "column_type"):
includeColumnType = false
s.infoColumnTypeUnsupported = true
case includeUdtName && isUndefinedColumn(err, "udt_name"):
includeUdtName = false
s.infoUdtNameUnsupported = true
default:
return nil, err
}
}
}
func (s *server) queryInformationSchemaColumns(schema, table string, primary map[string]bool, includeColumnType, includeUdtName bool) ([]columnInfo, error) {
var fullDataTypeExpression string
switch {
case includeColumnType && includeUdtName:
fullDataTypeExpression = `CASE
WHEN UPPER(TRIM(c.data_type)) IN ('USER-DEFINED', 'USER_DEFINED')
AND UPPER(COALESCE(NULLIF(TRIM(c.column_type), ''), 'USER-DEFINED')) IN ('USER-DEFINED', 'USER_DEFINED')
THEN c.udt_name
ELSE c.column_type
END`
case includeColumnType:
fullDataTypeExpression = "c.column_type"
case includeUdtName:
fullDataTypeExpression = `CASE
WHEN UPPER(TRIM(c.data_type)) IN ('USER-DEFINED', 'USER_DEFINED') THEN c.udt_name
END AS column_type`
default:
fullDataTypeExpression = "NULL AS column_type"
}
query := fmt.Sprintf(`SELECT c.column_name, c.data_type, %s, c.is_nullable, c.column_default,
col_description(a.attrelid, a.attnum), c.numeric_precision, c.numeric_scale, c.character_maximum_length
FROM information_schema.columns c
LEFT JOIN sys_catalog.sys_namespace n ON n.nspname = c.table_schema
LEFT JOIN sys_catalog.sys_class rel ON rel.relnamespace = n.oid AND rel.relname = c.table_name
LEFT JOIN sys_catalog.sys_attribute a ON a.attrelid = rel.oid AND a.attname = c.column_name AND a.attnum > 0 AND NOT a.attisdropped
WHERE c.table_schema = %s AND c.table_name = %s ORDER BY c.ordinal_position`, fullDataTypeExpression, quoteLiteral(schema), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := []columnInfo{}
for rows.Next() {
var name, dataType, nullable string
var fullDataType, defaultValue, comment sql.NullString
var precision, scale, length sql.NullInt64
if err := rows.Scan(&name, &dataType, &fullDataType, &nullable, &defaultValue, &comment, &precision, &scale, &length); err != nil {
return nil, err
}
if !length.Valid || length.Int64 < 0 {
if parsed := boundedVarcharLength(dataType); parsed != nil {
length = sql.NullInt64{Int64: int64(*parsed), Valid: true}
} else if parsed := boundedVarcharLength(fullDataType.String); parsed != nil {
length = sql.NullInt64{Int64: int64(*parsed), Valid: true}
}
}
result = append(result, columnInfo{Name: name, DataType: resolvedInformationSchemaDataType(dataType, fullDataType.String), ResolvedSchema: stringPtr(schema), FullDataType: fullDataType.String, IsNullable: strings.EqualFold(nullable, "YES"), ColumnDefault: nullStringPtr(defaultValue), IsPrimaryKey: primary[strings.ToLower(name)], Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)})
}
return result, rows.Err()
}
func resolvedInformationSchemaDataType(dataType, fullDataType string) string {
if !isUserDefinedDataTypeMarker(dataType) {
return dataType
}
resolved := strings.TrimSpace(fullDataType)
if resolved == "" || isUserDefinedDataTypeMarker(resolved) {
return dataType
}
return resolved
}
func isUserDefinedDataTypeMarker(dataType string) bool {
switch strings.ToUpper(strings.TrimSpace(dataType)) {
case "USER-DEFINED", "USER_DEFINED":
return true
default:
return false
}
}
func (s *server) listIndexes(schema, table string) ([]indexInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
if s.indexOrdinalityUnsupported {
return s.listIndexesWithoutOrdinality(catalog, prefix, effective, table)
}
rows, err := s.metadataQuery(kingbaseListIndexesQuery(catalog, prefix, effective, table))
if err != nil && isUnsupportedWithOrdinality(err) {
s.indexOrdinalityUnsupported = true
return s.listIndexesWithoutOrdinality(catalog, prefix, effective, table)
}
if err != nil {
return nil, err
}
defer rows.Close()
byName := map[string]*indexInfo{}
order := []string{}
for rows.Next() {
var name, kind, column string
var unique, primary bool
var ordinal int
if err := rows.Scan(&name, &kind, &unique, &primary, &column, &ordinal); err != nil {
return nil, err
}
item := byName[name]
if item == nil {
item = &indexInfo{Name: name, IsUnique: unique, IsPrimary: primary, IndexType: stringPtr(kind), Columns: []string{}, IncludedColumns: []string{}}
byName[name] = item
order = append(order, name)
}
item.Columns = append(item.Columns, column)
}
result := make([]indexInfo, 0, len(order))
for _, name := range order {
result = append(result, *byName[name])
}
return result, rows.Err()
}
func isUnsupportedWithOrdinality(err error) bool {
if err == nil {
return false
}
var driverError *gokb.Error
isSyntaxError := errors.As(err, &driverError) && string(driverError.Code) == "42601"
normalized := strings.ToLower(err.Error())
return (isSyntaxError || strings.Contains(normalized, "syntax error") || strings.Contains(normalized, "语法错误")) && strings.Contains(normalized, "with ordinality")
}
func (s *server) listIndexesWithoutOrdinality(catalog, prefix, schema, table string) ([]indexInfo, error) {
query := fmt.Sprintf(`SELECT i.relname, am.amname, ix.indisunique, ix.indisprimary, ix.indkey
FROM %s.%s_index ix JOIN %s.%s_class t ON t.oid = ix.indrelid
JOIN %s.%s_class i ON i.oid = ix.indexrelid JOIN %s.%s_namespace n ON n.oid = t.relnamespace
JOIN %s.%s_am am ON am.oid = i.relam
WHERE n.nspname = %s AND t.relname = %s ORDER BY i.relname`, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
type rawIndex struct {
name, kind string
unique, primary bool
attributeNums []int
}
rawIndexes := []rawIndex{}
for rows.Next() {
var item rawIndex
var raw any
if err := rows.Scan(&item.name, &item.kind, &item.unique, &item.primary, &raw); err != nil {
return nil, err
}
item.attributeNums, err = parseCatalogAttributeNumbers(raw)
if err != nil {
return nil, fmt.Errorf("failed to parse index %s columns: %w", item.name, err)
}
rawIndexes = append(rawIndexes, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(rawIndexes) == 0 {
return []indexInfo{}, nil
}
attributes, err := s.relationAttributesByNumber(catalog, prefix, schema, table)
if err != nil {
return nil, err
}
result := make([]indexInfo, 0, len(rawIndexes))
for _, raw := range rawIndexes {
item := indexInfo{Name: raw.name, IsUnique: raw.unique, IsPrimary: raw.primary, IndexType: stringPtr(raw.kind), Columns: []string{}, IncludedColumns: []string{}}
for _, number := range raw.attributeNums {
if name := attributes[number]; name != "" {
item.Columns = append(item.Columns, name)
}
}
result = append(result, item)
}
return result, nil
}
func parseCatalogAttributeNumbers(raw any) ([]int, error) {
var value string
switch typed := raw.(type) {
case nil:
return []int{}, nil
case string:
value = typed
case []byte:
value = string(typed)
default:
value = fmt.Sprint(typed)
}
value = strings.TrimSpace(strings.Trim(value, "{}[]"))
if value == "" {
return []int{}, nil
}
parts := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ' ' || r == '\t' })
result := make([]int, 0, len(parts))
for _, part := range parts {
number, err := strconv.Atoi(strings.TrimSpace(part))
if err != nil {
return nil, fmt.Errorf("invalid attribute number %q", part)
}
result = append(result, number)
}
return result, nil
}
func (s *server) relationAttributesByNumber(catalog, prefix, schema, table string) (map[int]string, error) {
query := fmt.Sprintf(`SELECT a.attnum, a.attname
FROM %s.%s_attribute a JOIN %s.%s_class c ON c.oid = a.attrelid
JOIN %s.%s_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = %s AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped`, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := map[int]string{}
for rows.Next() {
var number int
var name string
if err := rows.Scan(&number, &name); err != nil {
return nil, err
}
result[number] = name
}
return result, rows.Err()
}
func kingbaseListIndexesQuery(catalog, prefix, schema, table string) string {
return fmt.Sprintf(`SELECT i.relname, am.amname, ix.indisunique, ix.indisprimary, a.attname, pos.n
FROM %s.%s_index ix JOIN %s.%s_class t ON t.oid = ix.indrelid
JOIN %s.%s_class i ON i.oid = ix.indexrelid JOIN %s.%s_namespace n ON n.oid = t.relnamespace
JOIN %s.%s_am am ON am.oid = i.relam
JOIN unnest(ix.indkey) WITH ORDINALITY AS pos(attnum,n) ON true
JOIN %s.%s_attribute a ON a.attrelid = t.oid AND a.attnum = pos.attnum
WHERE n.nspname = %s AND t.relname = %s ORDER BY i.relname, pos.n`, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table))
}
func kingbaseCatalogFunction(catalog, sysFunction, postgresFunction string) string {
// Kingbase compatibility modes expose different catalog-qualified deparser names.
if catalog == "pg_catalog" {
return "pg_catalog." + postgresFunction
}
return "sys_catalog." + sysFunction
}
func (s *server) listForeignKeys(schema, table string) ([]foreignKeyInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
if s.mode.legacyV7 && !s.mode.mysqlCompat {
return s.listForeignKeysFromCatalog(effective, table)
}
query := `SELECT fk.constraint_name, fk.column_name, pk.table_name, pk.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage fk ON fk.constraint_schema = tc.constraint_schema AND fk.constraint_name = tc.constraint_name AND fk.table_schema = tc.table_schema AND fk.table_name = tc.table_name
JOIN information_schema.referential_constraints rc ON rc.constraint_schema = tc.constraint_schema AND rc.constraint_name = tc.constraint_name
JOIN information_schema.key_column_usage pk ON pk.constraint_schema = rc.unique_constraint_schema AND pk.constraint_name = rc.unique_constraint_name AND pk.ordinal_position = fk.position_in_unique_constraint
WHERE tc.table_schema = ` + quoteLiteral(effective) + ` AND tc.table_name = ` + quoteLiteral(table) + ` AND tc.constraint_type = 'FOREIGN KEY' ORDER BY fk.constraint_name, fk.ordinal_position`
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := []foreignKeyInfo{}
for rows.Next() {
var item foreignKeyInfo
if err := rows.Scan(&item.Name, &item.Column, &item.RefTable, &item.RefColumn); err != nil {
return nil, err
}
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
func (s *server) listForeignKeysFromCatalog(schema, table string) ([]foreignKeyInfo, error) {
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
query := fmt.Sprintf(`SELECT c.conname, c.conkey, c.confkey, rn.nspname, rt.relname
FROM %s.%s_constraint c JOIN %s.%s_class t ON t.oid = c.conrelid
JOIN %s.%s_namespace n ON n.oid = t.relnamespace
JOIN %s.%s_class rt ON rt.oid = c.confrelid
JOIN %s.%s_namespace rn ON rn.oid = rt.relnamespace
WHERE c.contype = 'f' AND n.nspname = %s AND t.relname = %s ORDER BY c.conname`, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
type rawForeignKey struct {
name, refSchema, refTable string
columns, refColumns []int
}
rawKeys := []rawForeignKey{}
for rows.Next() {
var item rawForeignKey
var columnsRaw, refColumnsRaw any
if err := rows.Scan(&item.name, &columnsRaw, &refColumnsRaw, &item.refSchema, &item.refTable); err != nil {
return nil, err
}
item.columns, err = parseCatalogAttributeNumbers(columnsRaw)
if err != nil {
return nil, fmt.Errorf("failed to parse foreign key %s columns: %w", item.name, err)
}
item.refColumns, err = parseCatalogAttributeNumbers(refColumnsRaw)
if err != nil {
return nil, fmt.Errorf("failed to parse foreign key %s referenced columns: %w", item.name, err)
}
rawKeys = append(rawKeys, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(rawKeys) == 0 {
return []foreignKeyInfo{}, nil
}
localAttributes, err := s.relationAttributesByNumber(catalog, prefix, schema, table)
if err != nil {
return nil, err
}
refAttributes := map[string]map[int]string{}
result := []foreignKeyInfo{}
for _, raw := range rawKeys {
key := raw.refSchema + "\x00" + raw.refTable
attributes := refAttributes[key]
if attributes == nil {
attributes, err = s.relationAttributesByNumber(catalog, prefix, raw.refSchema, raw.refTable)
if err != nil {
return nil, err
}
refAttributes[key] = attributes
}
for i, number := range raw.columns {
if i >= len(raw.refColumns) {
break
}
column, refColumn := localAttributes[number], attributes[raw.refColumns[i]]
if column != "" && refColumn != "" {
result = append(result, foreignKeyInfo{Name: raw.name, Column: column, RefTable: raw.refTable, RefColumn: refColumn})
}
}
}
return result, nil
}
func kingbaseConstraintFunctionName(catalog string) string {
if catalog == "pg_catalog" {
return "pg_get_constraintdef"
}
return "sys_get_constraintdef"
}
func kingbaseConstraintsQuery(catalog, prefix, schema, table string, definitionUnsupported, validatedUnsupported, statusUnsupported bool) string {
definitionExpression := fmt.Sprintf("COALESCE(%s(c.oid, true), '')", kingbaseCatalogFunction(catalog, "sys_get_constraintdef", "pg_get_constraintdef"))
validExpression := "COALESCE(CAST(c.convalidated AS text), 'T')"
statusExpression := "COALESCE(CAST(c.constatus AS text), 'E')"
if definitionUnsupported {
definitionExpression = "''"
}
if validatedUnsupported {
validExpression = "true"
}
if statusUnsupported {
statusExpression = "'E'"
}
return fmt.Sprintf(`SELECT COALESCE(c.conname, ''), c.contype::text, %s, c.conkey,
rn.nspname, rt.relname, c.confkey, c.confmatchtype::text, c.confupdtype::text, c.confdeltype::text,
c.condeferrable, c.condeferred, %s, %s
FROM %s.%s_constraint c
JOIN %s.%s_class t ON t.oid = c.conrelid
JOIN %s.%s_namespace n ON n.oid = t.relnamespace
LEFT JOIN %s.%s_class rt ON rt.oid = c.confrelid
LEFT JOIN %s.%s_namespace rn ON rn.oid = rt.relnamespace
WHERE n.nspname = %s AND t.relname = %s AND t.relkind IN ('r', 'p', 'f')
ORDER BY COALESCE(c.conname, '')`, definitionExpression, validExpression, statusExpression, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table))
}
func (s *server) listConstraints(schema, table string) ([]constraintInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
definitionUnsupported := s.constraintDefinitionUnsupported
validatedUnsupported := s.mode.legacyV7 || s.constraintValidatedUnsupported
statusUnsupported := s.mode.legacyV7 || s.constraintStatusUnsupported
var rows *sql.Rows
for {
query := kingbaseConstraintsQuery(catalog, prefix, effective, table, definitionUnsupported, validatedUnsupported, statusUnsupported)
rows, err = s.metadataQuery(query)
if err == nil {
break
}
changed := false
if !definitionUnsupported && isUndefinedFunction(err, kingbaseConstraintFunctionName(catalog)) {
definitionUnsupported = true
s.constraintDefinitionUnsupported = true
changed = true
}
if !validatedUnsupported && isUndefinedColumn(err, "convalidated") {
validatedUnsupported = true
s.constraintValidatedUnsupported = true
changed = true
}
if !statusUnsupported && isUndefinedColumn(err, "constatus") {
statusUnsupported = true
s.constraintStatusUnsupported = true
changed = true
}
if !changed {
return nil, err
}
}
defer rows.Close()
type rawConstraint struct {
name, kind, definition string
columns, refColumns []int
refSchema, refTable sql.NullString
matchType, onUpdate, onDelete sql.NullString
deferrable, initiallyDeferred bool
valid, enabled bool
}
raw := []rawConstraint{}
for rows.Next() {
var item rawConstraint
var columnsRaw, refColumnsRaw, validRaw, statusRaw any
if err := rows.Scan(&item.name, &item.kind, &item.definition, &columnsRaw, &item.refSchema, &item.refTable, &refColumnsRaw, &item.matchType, &item.onUpdate, &item.onDelete, &item.deferrable, &item.initiallyDeferred, &validRaw, &statusRaw); err != nil {
return nil, err
}
item.valid = parseConstraintEnabled(validRaw)
item.enabled = parseConstraintEnabled(statusRaw)
item.columns, err = parseCatalogAttributeNumbers(columnsRaw)
if err != nil {
return nil, fmt.Errorf("failed to parse constraint %s columns: %w", item.name, err)
}
item.refColumns, err = parseCatalogAttributeNumbers(refColumnsRaw)
if err != nil {
return nil, fmt.Errorf("failed to parse constraint %s referenced columns: %w", item.name, err)
}
raw = append(raw, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
attributes, err := s.relationAttributesByNumber(catalog, prefix, effective, table)
if err != nil {
return nil, err
}
refAttributes := map[string]map[int]string{}
result := make([]constraintInfo, 0, len(raw))
for _, item := range raw {
constraint := constraintInfo{
Name: item.name, ConstraintType: kingbaseConstraintTypeName(item.kind), Definition: item.definition,
Columns: []string{}, RefColumns: []string{}, Deferrable: item.deferrable,
// FK details are retained for API completeness and future unified
// constraint/FK presentations. The current UI renders FK rows through
// list_foreign_keys, so these fields are not currently displayed in the
// Constraints tab.
InitiallyDeferred: item.initiallyDeferred, Enabled: item.enabled, Valid: item.valid,
}
for _, number := range item.columns {
if name := attributes[number]; name != "" {
constraint.Columns = append(constraint.Columns, name)
}
}
if item.refSchema.Valid {
constraint.RefSchema = stringPtr(item.refSchema.String)
}
if item.refTable.Valid {
constraint.RefTable = stringPtr(item.refTable.String)
}
if strings.EqualFold(strings.TrimSpace(item.kind), "f") && item.refSchema.Valid && item.refTable.Valid {
key := item.refSchema.String + "\x00" + item.refTable.String
ref := refAttributes[key]
if ref == nil {
ref, err = s.relationAttributesByNumber(catalog, prefix, item.refSchema.String, item.refTable.String)
if err != nil {
return nil, err
}
refAttributes[key] = ref
}
for _, number := range item.refColumns {
if name := ref[number]; name != "" {
constraint.RefColumns = append(constraint.RefColumns, name)
}
}
constraint.MatchType = kingbaseConstraintMatchType(item.matchType)
constraint.OnUpdate = kingbaseConstraintAction(item.onUpdate)
constraint.OnDelete = kingbaseConstraintAction(item.onDelete)
}
result = append(result, constraint)
}
return result, nil
}
func parseConstraintEnabled(raw any) bool {
if raw == nil {
return true
}
if enabled, ok := raw.(bool); ok {
return enabled
}
if bytes, ok := raw.([]byte); ok {
raw = string(bytes)
}
value := strings.ToLower(strings.TrimSpace(fmt.Sprint(raw)))
if value == "" {
return true
}
switch value {
case "1", "t", "true", "y", "yes", "e", "enabled", "enable", "on":
return true
case "0", "f", "false", "d", "disabled", "disable", "off":
return false
default:
// Unknown catalog states should not make the whole metadata request fail.
return true
}
}
func kingbaseConstraintTypeName(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case "p":
return "PRIMARY KEY"
case "f":
return "FOREIGN KEY"
case "u":
return "UNIQUE"
case "c":
return "CHECK"
case "t":
return "CONSTRAINT TRIGGER"
case "x":
return "EXCLUDE"
case "n":
return "NOT NULL"
default:
return strings.TrimSpace(value)
}
}
func kingbaseConstraintMatchType(value sql.NullString) *string {
if !value.Valid {
return nil
}
var result string
switch strings.ToLower(strings.TrimSpace(value.String)) {
case "f":
result = "FULL"
case "p":
result = "PARTIAL"
case "s":
result = "SIMPLE"
default:
return nil
}
return &result
}
func kingbaseConstraintAction(value sql.NullString) *string {
if !value.Valid {
return nil
}
var result string
switch strings.ToLower(strings.TrimSpace(value.String)) {
case "a":
result = "NO ACTION"
case "r":
result = "RESTRICT"
case "c":
result = "CASCADE"
case "n":
result = "SET NULL"
case "d":
result = "SET DEFAULT"
default:
return nil
}
return &result
}
func (s *server) listTriggers(schema, table string) ([]triggerInfo, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
queryForCatalog := func() string {
internalPredicate := "NOT tg.tgisinternal"
if s.triggerInternalUnsupported {
internalPredicate = "tg.tgkind <> 'c'"
}
return fmt.Sprintf(`SELECT tg.tgname,
trim(trailing ',' FROM (CASE WHEN (tg.tgtype & 4) <> 0 THEN 'INSERT,' ELSE '' END || CASE WHEN (tg.tgtype & 8) <> 0 THEN 'DELETE,' ELSE '' END || CASE WHEN (tg.tgtype & 16) <> 0 THEN 'UPDATE,' ELSE '' END || CASE WHEN (tg.tgtype & 32) <> 0 THEN 'TRUNCATE,' ELSE '' END)), tg.tgtype
FROM %s.%s_trigger tg JOIN %s.%s_class c ON c.oid = tg.tgrelid JOIN %s.%s_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = %s AND c.relname = %s AND %s ORDER BY tg.tgname`, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(table), internalPredicate)
}
rows, err := s.metadataQuery(queryForCatalog())
if err != nil && !s.triggerInternalUnsupported && isUndefinedColumn(err, "tgisinternal") {
s.triggerInternalUnsupported = true
rows, err = s.metadataQuery(queryForCatalog())
}
if err != nil {
return nil, err
}
defer rows.Close()
result := []triggerInfo{}
for rows.Next() {
var name, event string
var triggerType int
if err := rows.Scan(&name, &event, &triggerType); err != nil {
return nil, err
}
result = append(result, triggerInfo{Name: name, Event: event, Timing: decodeTriggerTiming(triggerType)})
}
return result, rows.Err()
}
func (s *server) getObjectSource(schema, name, objectType string) (map[string]any, error) {
return s.getObjectSourceForRelation(schema, name, objectType, "")
}
func (s *server) getObjectSourceForRelation(schema, name, objectType, relationName string) (map[string]any, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
source := ""
kind := strings.ToUpper(objectType)
if kind == "VIEW" {
if s.mode.mysqlCompat {
err = s.requireDBQueryRow("SELECT view_definition FROM information_schema.views WHERE table_schema = "+quoteLiteral(effective)+" AND table_name = "+quoteLiteral(name), &source)
} else {
catalog, prefix, function := "sys_catalog", "sys", "sys_get_viewdef"
if s.mode.postgresCatalog {
catalog, prefix, function = "pg_catalog", "pg", "pg_get_viewdef"
} else if s.usePgViewDefinition {
function = "pg_get_viewdef"
}
querySource := func(definitionFunction string) error {
query := fmt.Sprintf("SELECT %s(c.oid) FROM %s.%s_class c JOIN %s.%s_namespace n ON n.oid=c.relnamespace WHERE n.nspname=%s AND c.relname=%s LIMIT 1", definitionFunction, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
return s.requireDBQueryRow(query, &source)
}
err = querySource(function)
if err != nil && function == "sys_get_viewdef" && isUndefinedFunction(err, function) {
s.usePgViewDefinition = true
err = querySource("pg_get_viewdef")
}
}
} else if kind == "MATERIALIZED_VIEW" {
source, err = s.getMaterializedViewSource(effective, name)
} else if kind == "FUNCTION" || kind == "PROCEDURE" {
catalog, prefix, function := "sys_catalog", "sys", "sys_get_functiondef"
if s.mode.postgresCatalog {
catalog, prefix, function = "pg_catalog", "pg", "pg_get_functiondef"
} else if s.usePgFunctionDefinition {
function = "pg_get_functiondef"
}
querySource := func(definitionFunction string) error {
query := fmt.Sprintf("SELECT %s(p.oid) FROM %s.%s_proc p JOIN %s.%s_namespace n ON n.oid=p.pronamespace WHERE n.nspname=%s AND p.proname=%s ORDER BY CASE WHEN p.prorettype=2278 THEN 0 ELSE 1 END LIMIT 1", definitionFunction, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
return s.requireDBQueryRow(query, &source)
}
queryLegacySource := func() error {
legacyFunction := "GET_FUNC_DDL"
if kind == "PROCEDURE" {
legacyFunction = "GET_PROCEDURE_DDL"
}
query := fmt.Sprintf("SELECT DBMS_METADATA.%s(CAST(%s AS varchar(128)), CAST(%s AS varchar(128)))", legacyFunction, quoteLiteral(name), quoteLiteral(effective))
return s.requireDBQueryRow(query, &source)
}
if s.useLegacyRoutineDefinition {
err = queryLegacySource()
} else {
err = querySource(function)
if err != nil && function == "sys_get_functiondef" && isUndefinedFunction(err, function) {
s.usePgFunctionDefinition = true
function = "pg_get_functiondef"
err = querySource(function)
}
if err != nil && !s.mode.postgresCatalog && function == "pg_get_functiondef" && isUndefinedFunction(err, function) {
s.useLegacyRoutineDefinition = true
err = queryLegacySource()
}
}
} else if kind == "TRIGGER" {
definitions, triggerErr := s.listTriggerDefinitionsFor(effective, relationName, name)
if triggerErr != nil {
err = triggerErr
} else if len(definitions) == 1 {
source = definitions[0]
} else if len(definitions) > 1 {
err = fmt.Errorf("trigger %q is ambiguous in schema %q; relation_name is required", name, effective)
} else {
err = sql.ErrNoRows
}
}
if err != nil && err != sql.ErrNoRows {
return nil, err
}
result := map[string]any{"name": name, "object_type": objectType, "schema": effective, "source": source}
if kind == "TRIGGER" {
result["editable"] = false
}
return result, nil
}
func (s *server) getMaterializedViewSource(schema, name string) (string, error) {
catalog, prefix, function := "sys_catalog", "sys", "sys_get_viewdef"
if s.mode.postgresCatalog {
catalog, prefix, function = "pg_catalog", "pg", "pg_get_viewdef"
} else if s.usePgViewDefinition {
function = "pg_get_viewdef"
}
querySource := func(definitionFunction string) (string, error) {
query := fmt.Sprintf("SELECT %s(c.oid) FROM %s.%s_class c JOIN %s.%s_namespace n ON n.oid=c.relnamespace WHERE n.nspname=%s AND c.relname=%s AND c.relkind = 'm' LIMIT 1", definitionFunction, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(name))
var source sql.NullString
if err := s.requireDBQueryRow(query, &source); err != nil {
return "", err
}
if !source.Valid || strings.TrimSpace(source.String) == "" {
return "", sql.ErrNoRows
}
return source.String, nil
}
source, err := querySource(function)
if err == nil {
return source, nil
}
if function == "sys_get_viewdef" {
if isUndefinedFunction(err, function) {
s.usePgViewDefinition = true
} else if !errors.Is(err, sql.ErrNoRows) {
return "", err
}
source, err = querySource("pg_get_viewdef")
if err == nil {
return source, nil
}
}
if errors.Is(err, sql.ErrNoRows) {
return "", fmt.Errorf("materialized view %q.%q returned an empty source definition", schema, name)
}
return "", err
}
func (s *server) getTableDDL(schema, table string) (string, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return "", err
}
columns, err := s.getColumns(effective, table)
if err != nil {
return "", err
}
if len(columns) == 0 {
if visibleColumns, fallbackErr := s.getColumns("", table); fallbackErr == nil && len(visibleColumns) > 0 {
columns = visibleColumns
if columns[0].ResolvedSchema != nil && *columns[0].ResolvedSchema != "" {
effective = *columns[0].ResolvedSchema
}
}
}
tableComment, _ := s.getTableComment(effective, table)
ddl := s.renderTableDDL(effective, table, columns, tableComment)
ddl, err = s.appendTableIndexDDL(effective, table, ddl)
if err != nil {
return "", err
}
ddl, err = s.appendTableTriggerDDL(effective, table, ddl)
if err != nil {
return "", err
}
return ddl, nil
}
func (s *server) renderTableDDL(schema, table string, columns []columnInfo, tableComment *string) string {
definitions := make([]string, 0, len(columns)+1)
primary := []string{}
for _, column := range columns {
definitions = append(definitions, s.columnDDLDefinition(column))
if column.IsPrimaryKey {
primary = append(primary, s.quoteDDLIdentifier(column.Name))
}
}
if len(primary) > 0 {
definitions = append(definitions, "PRIMARY KEY ("+strings.Join(primary, ", ")+")")
}
qualifiedTable := s.quoteDDLIdentifier(schema) + "." + s.quoteDDLIdentifier(table)
ddl := "CREATE TABLE " + qualifiedTable + " (\n " + strings.Join(definitions, ",\n ") + "\n);"
if tableComment != nil && strings.TrimSpace(*tableComment) != "" {
ddl += "\nCOMMENT ON TABLE " + qualifiedTable + " IS " + quoteLiteral(*tableComment) + ";"
}
for _, column := range columns {
if column.Comment == nil || strings.TrimSpace(*column.Comment) == "" {
continue
}
ddl += "\nCOMMENT ON COLUMN " + qualifiedTable + "." + s.quoteDDLIdentifier(column.Name) + " IS " + quoteLiteral(*column.Comment) + ";"
}
return ddl
}
func (s *server) appendTableIndexDDL(schema, table, ddl string) (string, error) {
definitions, err := s.listIndexDefinitions(schema, table)
if err != nil {
return "", err
}
return appendDDLStatements(ddl, definitions), nil
}
func (s *server) appendTableTriggerDDL(schema, table, ddl string) (string, error) {
definitions, err := s.listTriggerDefinitions(schema, table)
if err != nil {
return "", err
}
return appendDDLStatements(ddl, definitions), nil
}
func (s *server) listIndexDefinitions(schema, table string) ([]string, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
indexDefinitionFunction := kingbaseCatalogFunction(catalog, "sys_get_indexdef", "pg_get_indexdef")
query := fmt.Sprintf(`SELECT i.relname, %s(ix.indexrelid, 0, true), obj_description(i.oid)
FROM %s.%s_index ix JOIN %s.%s_class t ON t.oid = ix.indrelid
JOIN %s.%s_class i ON i.oid = ix.indexrelid JOIN %s.%s_namespace n ON n.oid = t.relnamespace
WHERE n.nspname = %s AND t.relname = %s AND NOT ix.indisprimary ORDER BY i.relname`, indexDefinitionFunction, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(table))
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var name string
var definition string
var comment sql.NullString
if err := rows.Scan(&name, &definition, &comment); err != nil {
return nil, err
}
if strings.TrimSpace(definition) != "" {
result = append(result, definition)
}
if comment.Valid && strings.TrimSpace(comment.String) != "" {
result = append(result, "COMMENT ON INDEX "+s.quoteDDLIdentifier(effective)+"."+s.quoteDDLIdentifier(name)+" IS "+quoteLiteral(comment.String))
}
}
return result, rows.Err()
}
func (s *server) listTriggerDefinitions(schema, table string) ([]string, error) {
return s.listTriggerDefinitionsFor(schema, table, "")
}
func (s *server) listTriggerDefinitionsFor(schema, table, trigger string) ([]string, error) {
effective, err := s.effectiveSchema(schema)
if err != nil {
return nil, err
}
catalog, prefix := "sys_catalog", "sys"
if s.mode.postgresCatalog {
catalog, prefix = "pg_catalog", "pg"
}
triggerDefinitionFunction := kingbaseCatalogFunction(catalog, "sys_get_triggerdef", "pg_get_triggerdef")
queryForSignature := func(includePretty bool) string {
arguments := "tg.oid"
if includePretty {
arguments += ", true"
}
internalPredicate := "NOT tg.tgisinternal"
if s.triggerInternalUnsupported {
internalPredicate = "tg.tgkind <> 'c'"
}
relationFilter := ""
if strings.TrimSpace(table) != "" {
relationFilter = " AND c.relname = " + quoteLiteral(table)
}
triggerFilter := ""
if strings.TrimSpace(trigger) != "" {
triggerFilter = " AND tg.tgname = " + quoteLiteral(trigger)
}
return fmt.Sprintf(`SELECT %s(%s)
FROM %s.%s_trigger tg JOIN %s.%s_class c ON c.oid = tg.tgrelid JOIN %s.%s_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = %s%s%s AND %s ORDER BY c.relname, tg.tgname`, triggerDefinitionFunction, arguments, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(effective), relationFilter, triggerFilter, internalPredicate)
}
var rows *sql.Rows
for {
includePretty := !s.triggerPrettyUnsupported
rows, err = s.metadataQuery(queryForSignature(includePretty))
if err == nil {
break
}
if includePretty && isUndefinedFunction(err, triggerDefinitionFunction) {
s.triggerPrettyUnsupported = true
continue
}
if !s.triggerInternalUnsupported && isUndefinedColumn(err, "tgisinternal") {
s.triggerInternalUnsupported = true
continue
}
break
}
if err != nil {
return nil, err
}
defer rows.Close()
result := []string{}
for rows.Next() {
var definition string
if err := rows.Scan(&definition); err != nil {
return nil, err
}
if strings.TrimSpace(definition) != "" {
result = append(result, definition)
}
}
return result, rows.Err()
}
func appendDDLStatements(ddl string, statements []string) string {
for _, statement := range statements {
ddl = appendDDLStatement(ddl, statement)
}
return ddl
}
func appendDDLStatement(ddl, statement string) string {
ddl = strings.TrimRight(ddl, "\r\n\t ")
statement = ensureStatementTerminator(statement)
if statement == "" {
return ddl
}
if ddl == "" {
return statement
}
if !strings.HasSuffix(ddl, ";") {
ddl += ";"
}
return ddl + "\n\n" + statement
}
func ensureStatementTerminator(statement string) string {
trimmed := strings.TrimSpace(statement)
if trimmed == "" || strings.HasSuffix(trimmed, ";") {
return trimmed
}
return trimmed + ";"
}
func (s *server) columnDDLDefinition(column columnInfo) string {
definition := s.quoteDDLIdentifier(column.Name) + " " + columnDDLDataType(column)
if column.Extra != nil && *column.Extra != "" {
// Identity clauses belong immediately after the data type in both
// PostgreSQL-compatible and SQL Server-compatible Kingbase modes.
definition += " " + *column.Extra
}
if !column.IsNullable {
definition += " NOT NULL"
}
if column.ColumnDefault != nil && *column.ColumnDefault != "" {
definition += " DEFAULT " + *column.ColumnDefault
}
return definition
}
func columnDDLDataType(column columnInfo) string {
if fullDataType := strings.TrimSpace(column.FullDataType); fullDataType != "" {
return fullDataType
}
dataType := strings.TrimSpace(column.DataType)
if strings.Contains(dataType, "(") {
return dataType
}
normalized := strings.Join(strings.Fields(strings.ToLower(dataType)), " ")
switch normalized {
case "varchar", "character varying", "char", "character":
if column.CharacterMaximumLength != nil && *column.CharacterMaximumLength > 0 {
return fmt.Sprintf("%s(%d)", dataType, *column.CharacterMaximumLength)
}
case "numeric", "decimal":
if column.NumericPrecision != nil && *column.NumericPrecision > 0 {
if column.NumericScale != nil {
return fmt.Sprintf("%s(%d,%d)", dataType, *column.NumericPrecision, *column.NumericScale)
}
return fmt.Sprintf("%s(%d)", dataType, *column.NumericPrecision)
}
}
return dataType
}
func kingbaseIdentityClause(code string) *string {
var clause string
switch strings.ToLower(strings.TrimSpace(code)) {
case "a":
clause = "GENERATED ALWAYS AS IDENTITY"
case "d":
clause = "GENERATED BY DEFAULT AS IDENTITY"
default:
return nil
}
return &clause
}
func (s *server) getExplainInfo(sqlText string) (string, error) {
rows, err := s.metadataQuery("EXPLAIN " + trimStatementSQL(sqlText))
if err != nil {
return "", err
}
defer rows.Close()
lines := []string{}
for rows.Next() {
var line string
if err := rows.Scan(&line); err != nil {
return "", err
}
lines = append(lines, line)
}
return strings.Join(lines, "\n"), rows.Err()
}
func (s *server) metadataQuery(query string) (*sql.Rows, error) {
db, err := s.requireDB()
if err != nil {
return nil, err
}
// These are bounded, internally generated statements. Calling Query without
// arguments keeps gokb on its single-round-trip simple-query path.
return db.Query(query)
}
func (s *server) requireDBQueryRow(query string, destination ...any) error {
db, err := s.requireDB()
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), metadataTimeout)
defer cancel()
return db.QueryRowContext(ctx, query).Scan(destination...)
}
func (s *server) effectiveSchema(schema string) (string, error) {
if strings.TrimSpace(schema) != "" {
return strings.TrimSpace(schema), nil
}
var current sql.NullString
if err := s.requireDBQueryRow("SELECT current_schema()", &current); err == nil && current.Valid && current.String != "" {
return current.String, nil
}
if s.params.Username != "" {
return s.params.Username, nil
}
return "public", nil
}
func (s *server) primaryKeys(schema, table string) (map[string]bool, error) {
query := `SELECT kcu.column_name FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON kcu.constraint_schema=tc.constraint_schema AND kcu.constraint_name=tc.constraint_name AND kcu.table_schema=tc.table_schema AND kcu.table_name=tc.table_name
WHERE tc.table_schema=` + quoteLiteral(schema) + ` AND tc.table_name=` + quoteLiteral(table) + ` AND tc.constraint_type='PRIMARY KEY' ORDER BY kcu.ordinal_position`
rows, err := s.metadataQuery(query)
if err != nil {
return nil, err
}
defer rows.Close()
result := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
result[strings.ToLower(name)] = true
}
return result, rows.Err()
}
func (s *server) applyIdentityMetadata(schema, table string, columns []columnInfo) {
query := `SELECT a.attname, ic.seed_value, ic.increment_value FROM sys.identity_columns ic
JOIN sys_catalog.sys_class c ON c.oid=ic.object_id JOIN sys_catalog.sys_namespace n ON n.oid=c.relnamespace
JOIN sys_catalog.sys_attribute a ON a.attrelid=c.oid AND a.attnum=ic.column_id
WHERE n.nspname=` + quoteLiteral(schema) + ` AND c.relname=` + quoteLiteral(table)
rows, err := s.metadataQuery(query)
if err != nil {
s.mode.sqlServerIdentity = false
return
}
defer rows.Close()
byName := map[string]*columnInfo{}
for i := range columns {
byName[strings.ToLower(columns[i].Name)] = &columns[i]
}
for rows.Next() {
var name string
var seed, increment sql.NullString
if rows.Scan(&name, &seed, &increment) == nil {
if column := byName[strings.ToLower(name)]; column != nil {
extra := "IDENTITY"
if seed.Valid && increment.Valid {
extra = "IDENTITY(" + seed.String + "," + increment.String + ")"
}
column.Extra = &extra
}
}
}
}
func catalogPrefix(catalog string) string {
if catalog == "pg_catalog" {
return "pg"
}
return "sys"
}
func normalizeTableType(value string) string {
normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(value), " ", "_"))
switch normalized {
case "BASE_TABLE", "PARTITIONED_TABLE":
return "TABLE"
case "MATERIALIZED_VIEW", "FOREIGN_TABLE", "VIEW", "TABLE", "TRIGGER", "TYPE", "TYPE_BODY":
return normalized
default:
return "TABLE"
}
}
func decodeTriggerTiming(triggerType int) string {
if triggerType&(1<<6) != 0 {
return "INSTEAD OF"
}
if triggerType&(1<<1) != 0 {
return "BEFORE"
}
return "AFTER"
}
func boundedVarcharLength(dataType string) *int {
lower := strings.ToLower(strings.TrimSpace(dataType))
for _, prefix := range []string{"character varying", "varchar", "bpchar", "character", "char"} {
if strings.HasPrefix(lower, prefix) {
value := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lower, prefix), ")"))
value = strings.TrimPrefix(value, "(")
fields := strings.Fields(value)
if len(fields) > 0 {
if number, err := strconv.Atoi(fields[0]); err == nil && number >= 0 {
return &number
}
}
}
}
return nil
}
func constraintsAllowsTableLike(constraints metadataListConstraints) bool {
if len(constraints.ObjectTypes) == 0 {
return true
}
for _, kind := range constraints.ObjectTypes {
if routineObjectType(kind) != "" {
continue
}
switch normalizeTableType(kind) {
case "TABLE", "VIEW", "MATERIALIZED_VIEW", "FOREIGN_TABLE":
return true
}
}
return false
}
// constraintsAllowTypes reports whether the object-type filter asks for
// user-defined types (or leaves the filter open). normalizeTableType treats
// "TYPE" and "TYPE_BODY" as first-class kinds, so table-like constraints never
// match them and a dedicated type request does not scan relations.
func constraintsAllowTypes(constraints metadataListConstraints) bool {
if len(constraints.ObjectTypes) == 0 {
return true
}
for _, kind := range constraints.ObjectTypes {
switch normalizeTableType(kind) {
case "TYPE", "TYPE_BODY":
return true
}
}
return false
}
// constraintsAllowRoutines reports whether the object-type filter asks for
// procedures or functions (or leaves the filter open).
func constraintsAllowRoutines(constraints metadataListConstraints) bool {
if len(constraints.ObjectTypes) == 0 {
return true
}
for _, kind := range constraints.ObjectTypes {
if routineObjectType(kind) != "" {
return true
}
}
return false
}
// Schema triggers are opt-in because legacy unfiltered object calls predate
// this object family. Sidebar requests include explicit object types, while
// completion callers keep their existing bounded result set.
func constraintsAllowTriggers(constraints metadataListConstraints) bool {
for _, kind := range constraints.ObjectTypes {
if normalizeTableType(kind) == "TRIGGER" {
return true
}
}
return false
}
func routineObjectType(value string) string {
upper := strings.ToUpper(strings.TrimSpace(value))
switch {
case strings.Contains(upper, "PROCEDURE"):
return "PROCEDURE"
case strings.Contains(upper, "FUNCTION"):
return "FUNCTION"
default:
return ""
}
}
func constraintsMatch(constraints metadataListConstraints, name, kind string) bool {
if filter := strings.TrimSpace(constraints.Filter); filter != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(filter)) {
return false
}
if len(constraints.ObjectTypes) == 0 {
return true
}
if routineKind := routineObjectType(kind); routineKind != "" {
for _, allowed := range constraints.ObjectTypes {
if routineObjectType(allowed) == routineKind {
return true
}
}
return false
}
for _, allowed := range constraints.ObjectTypes {
if routineObjectType(allowed) != "" {
continue
}
if strings.EqualFold(normalizeTableType(allowed), normalizeTableType(kind)) || strings.EqualFold(allowed, kind) {
return true
}
}
return false
}
func pageTables(items []tableInfo, constraints metadataListConstraints) []tableInfo {
start, end := pageBounds(len(items), constraints.Offset, constraints.Limit)
return items[start:end]
}
func pageObjects(items []objectInfo, constraints metadataListConstraints) []objectInfo {
start, end := pageBounds(len(items), constraints.Offset, constraints.Limit)
return items[start:end]
}
func pageBounds(length, offset, limit int) (int, int) {
if offset < 0 {
offset = 0
}
if offset > length {
offset = length
}
end := length
if limit > 0 && offset+limit < end {
end = offset + limit
}
return offset, end
}
func objectOrder(kind string) int {
switch strings.ToUpper(kind) {
case "TABLE":
return 0
case "VIEW":
return 1
case "MATERIALIZED_VIEW":
return 2
case "FOREIGN_TABLE":
return 3
case "PROCEDURE":
return 4
case "FUNCTION":
return 5
case "TRIGGER":
return 6
case "TYPE":
return 7
default:
return 9
}
}
func stringSet(values []string) map[string]bool {
result := map[string]bool{}
for _, value := range values {
result[strings.ToLower(value)] = true
}
return result
}
func nullStringPtr(value sql.NullString) *string {
if !value.Valid {
return nil
}
return &value.String
}
func nullIntPtr(value sql.NullInt64) *int {
if !value.Valid {
return nil
}
converted := int(value.Int64)
return &converted
}