1113 lines
38 KiB
Go
1113 lines
38 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitea.com/kingbase/gokb"
|
|
)
|
|
|
|
const metadataTimeout = 15 * time.Second
|
|
|
|
// Escape '_' so only Kingbase internal SYS_/XLOG_ prefixes are hidden; names
|
|
// such as SYSTEMS and SYSLOG may be user-created schemas in MySQL mode.
|
|
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
|
|
}
|
|
|
|
type databaseInfo struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type tableInfo struct {
|
|
Name string `json:"name"`
|
|
TableType string `json:"table_type"`
|
|
Comment *string `json:"comment"`
|
|
}
|
|
|
|
type objectInfo struct {
|
|
Name string `json:"name"`
|
|
ObjectType string `json:"object_type"`
|
|
Schema string `json:"schema"`
|
|
Comment *string `json:"comment"`
|
|
Valid *bool `json:"valid,omitempty"`
|
|
}
|
|
|
|
type metadataListConstraints struct {
|
|
Filter string
|
|
Limit int
|
|
Offset int
|
|
ObjectTypes []string
|
|
}
|
|
|
|
type columnInfo struct {
|
|
Name string `json:"name"`
|
|
DataType string `json:"data_type"`
|
|
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 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")
|
|
if !mode.postgresCatalog {
|
|
if mode.compatibilityMode != "" {
|
|
mode.mysqlCompat = mode.compatibilityMode == "mysql"
|
|
} else {
|
|
mode.mysqlCompat = supportsBacktickIdentifiers(db)
|
|
}
|
|
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 {
|
|
var databaseMode string
|
|
switch err := db.QueryRow("SELECT setting FROM sys_catalog.sys_settings WHERE LOWER(name) = 'database_mode'").Scan(&databaseMode); {
|
|
case err == nil:
|
|
// Treat database_mode as authoritative when the server exposes it. This
|
|
// avoids misclassifying Oracle-compatible servers that also publish
|
|
// sql_mode for MySQL syntax toggles such as ANSI_QUOTES.
|
|
return strings.ToLower(strings.TrimSpace(databaseMode))
|
|
case errors.Is(err, sql.ErrNoRows):
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func supportsBacktickIdentifiers(db *sql.DB) bool {
|
|
var value int
|
|
return db.QueryRow("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 `"`
|
|
}
|
|
|
|
func (s *server) connectionInfo() (map[string]any, error) {
|
|
db, err := s.requireDB()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var database, username, version, schema string
|
|
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,
|
|
"compatibilityMode": s.mode.compatibilityMode, "mysql_compat_mode": s.mode.mysqlCompat,
|
|
"identifierQuote": s.identifierQuote(),
|
|
}, nil
|
|
}
|
|
|
|
func (s *server) listDatabases() ([]databaseInfo, error) {
|
|
queries := []string{
|
|
"SELECT datname FROM sys_catalog.sys_database WHERE NOT datistemplate AND datallowconn ORDER BY datname",
|
|
"SELECT datname FROM pg_catalog.pg_database WHERE NOT datistemplate AND datallowconn ORDER BY datname",
|
|
"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 (s *server) listSchemas(visible []string) ([]string, error) {
|
|
query := "SELECT nspname FROM sys_catalog.sys_namespace WHERE nspname NOT LIKE 'sys_temp_%' AND nspname NOT LIKE 'sys_toast_temp_%' ORDER BY nspname"
|
|
if s.mode.postgresCatalog {
|
|
query = "SELECT nspname FROM pg_catalog.pg_namespace WHERE nspname NOT LIKE 'pg_temp_%' AND nspname NOT LIKE 'pg_toast_temp_%' ORDER BY nspname"
|
|
} else if s.mode.mysqlCompat {
|
|
query = kingbaseMySQLCompatListSchemasSQL
|
|
}
|
|
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"
|
|
}
|
|
query := fmt.Sprintf(`SELECT 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,
|
|
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.relkind IN ('r','p','v','m','f') ORDER BY c.relname`, catalog, catalogPrefix(catalog), catalog, catalogPrefix(catalog), quoteLiteral(effective))
|
|
rows, err := s.metadataQuery(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
result := []tableInfo{}
|
|
for rows.Next() {
|
|
var name, kind string
|
|
var comment sql.NullString
|
|
if err := rows.Scan(&name, &kind, &comment); err != nil {
|
|
return nil, err
|
|
}
|
|
item := tableInfo{Name: name, TableType: normalizeTableType(kind), Comment: nullStringPtr(comment)}
|
|
if constraintsMatch(constraints, item.Name, item.TableType) {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return pageTables(result, constraints), rows.Err()
|
|
}
|
|
|
|
func (s *server) getTableComment(schema, table string) (*string, error) {
|
|
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
|
|
}
|
|
return nil, err
|
|
}
|
|
return nullStringPtr(comment), nil
|
|
}
|
|
|
|
func (s *server) listObjects(schema string, constraints metadataListConstraints) ([]objectInfo, error) {
|
|
effective, err := s.effectiveSchema(schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tables, err := s.listTables(effective, metadataListConstraints{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]objectInfo, 0, len(tables))
|
|
for _, table := range tables {
|
|
result = append(result, objectInfo{Name: table.Name, ObjectType: table.TableType, Schema: effective, Comment: table.Comment})
|
|
}
|
|
if !s.mode.mysqlCompat {
|
|
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()
|
|
}
|
|
}
|
|
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) 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)
|
|
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) {
|
|
effective, err := s.effectiveSchema(schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
primary, _ := s.primaryKeys(effective, table)
|
|
if s.mode.mysqlCompat {
|
|
return s.informationSchemaColumns(effective, table, primary)
|
|
}
|
|
catalog, prefix := "sys_catalog", "sys"
|
|
if s.mode.postgresCatalog {
|
|
catalog, prefix = "pg_catalog", "pg"
|
|
return s.queryCatalogColumns(effective, table, primary, catalog, prefix, "pg_get_expr")
|
|
}
|
|
expression := "sys_get_expr"
|
|
if s.usePgDefaultExpression {
|
|
expression = "pg_get_expr"
|
|
}
|
|
result, err := s.queryCatalogColumns(effective, table, primary, 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
|
|
return s.queryCatalogColumns(effective, table, primary, catalog, prefix, "pg_get_expr")
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
func (s *server) queryCatalogColumns(
|
|
schema, table string,
|
|
primary map[string]bool,
|
|
catalog, prefix, expression string,
|
|
) ([]columnInfo, error) {
|
|
query := fmt.Sprintf(`SELECT a.attname, format_type(a.atttypid, a.atttypmod), 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,
|
|
a.attidentity
|
|
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 n.nspname = %s AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, expression, 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()
|
|
result := []columnInfo{}
|
|
for rows.Next() {
|
|
var name, dataType string
|
|
var nullable bool
|
|
var defaultValue, comment, identity sql.NullString
|
|
var precision, scale, length sql.NullInt64
|
|
if err := rows.Scan(&name, &dataType, &nullable, &defaultValue, &comment, &precision, &scale, &length, &identity); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, columnInfo{Name: name, DataType: dataType, IsNullable: nullable, ColumnDefault: nullStringPtr(defaultValue), IsPrimaryKey: primary[strings.ToLower(name)], 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
|
|
}
|
|
if s.mode.sqlServerIdentity {
|
|
s.applyIdentityMetadata(schema, table, result)
|
|
}
|
|
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 (s *server) informationSchemaColumns(schema, table string, primary map[string]bool) ([]columnInfo, error) {
|
|
query := `SELECT c.column_name, c.data_type, 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 = ` + quoteLiteral(schema) + ` AND c.table_name = ` + quoteLiteral(table) + ` ORDER BY c.ordinal_position`
|
|
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 defaultValue, comment sql.NullString
|
|
var precision, scale, length sql.NullInt64
|
|
if err := rows.Scan(&name, &dataType, &nullable, &defaultValue, &comment, &precision, &scale, &length); err != nil {
|
|
return nil, err
|
|
}
|
|
if parsed := boundedVarcharLength(dataType); parsed != nil && !length.Valid {
|
|
length = sql.NullInt64{Int64: int64(*parsed), Valid: true}
|
|
}
|
|
result = append(result, columnInfo{Name: name, DataType: dataType, 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 (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"
|
|
}
|
|
query := kingbaseListIndexesQuery(catalog, prefix, effective, table)
|
|
rows, err := s.metadataQuery(query)
|
|
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 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
|
|
}
|
|
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)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
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"
|
|
}
|
|
query := 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 NOT tg.tgisinternal ORDER BY tg.tgname`, 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 := []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) {
|
|
effective, err := s.effectiveSchema(schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source := ""
|
|
kind := strings.ToUpper(objectType)
|
|
if kind == "VIEW" || kind == "MATERIALIZED_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"
|
|
}
|
|
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", function, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
|
|
err = s.requireDBQueryRow(query, &source)
|
|
}
|
|
} 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"
|
|
}
|
|
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", function, catalog, prefix, catalog, prefix, quoteLiteral(effective), quoteLiteral(name))
|
|
err = s.requireDBQueryRow(query, &source)
|
|
}
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"name": name, "object_type": objectType, "schema": effective, "source": source}, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
tableComment, _ := s.getTableComment(effective, table)
|
|
ddl := 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 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, columnDDLDefinition(column))
|
|
if column.IsPrimaryKey {
|
|
primary = append(primary, quoteIdentifier(column.Name))
|
|
}
|
|
}
|
|
if len(primary) > 0 {
|
|
definitions = append(definitions, "PRIMARY KEY ("+strings.Join(primary, ", ")+")")
|
|
}
|
|
qualifiedTable := quoteIdentifier(schema) + "." + quoteIdentifier(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 + "." + quoteIdentifier(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 "+quoteIdentifier(effective)+"."+quoteIdentifier(name)+" IS "+quoteLiteral(comment.String))
|
|
}
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func (s *server) listTriggerDefinitions(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"
|
|
}
|
|
triggerDefinitionFunction := kingbaseCatalogFunction(catalog, "sys_get_triggerdef", "pg_get_triggerdef")
|
|
query := fmt.Sprintf(`SELECT %s(tg.oid, true)
|
|
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 NOT tg.tgisinternal ORDER BY tg.tgname`, triggerDefinitionFunction, 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 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 columnDDLDefinition(column columnInfo) string {
|
|
definition := quoteIdentifier(column.Name) + " " + column.DataType
|
|
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 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()", ¤t); 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":
|
|
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{"varchar", "character varying"} {
|
|
if strings.HasPrefix(lower, prefix) {
|
|
value := strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(lower, prefix), ")"))
|
|
value = strings.TrimPrefix(value, "(")
|
|
if number, err := strconv.Atoi(strings.TrimSpace(value)); 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 {
|
|
switch normalizeTableType(kind) {
|
|
case "TABLE", "VIEW", "MATERIALIZED_VIEW", "FOREIGN_TABLE":
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|
|
for _, allowed := range constraints.ObjectTypes {
|
|
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
|
|
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
|
|
}
|