Files
2026-07-13 12:32:25 +08:00

468 lines
16 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pgcatalog
import (
"fmt"
"io"
"math"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/index"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/server/functions"
"github.com/dolthub/doltgresql/server/tables"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// PgClassName is a constant to the pg_class name.
const PgClassName = "pg_class"
// InitPgClass handles registration of the pg_class handler.
func InitPgClass() {
tables.AddHandler(PgCatalogName, PgClassName, PgClassHandler{})
}
// PgClassHandler is the handler for the pg_class table.
type PgClassHandler struct{}
var _ tables.Handler = PgClassHandler{}
var _ tables.IndexedTableHandler = PgClassHandler{}
// Name implements the interface tables.Handler.
func (p PgClassHandler) Name() string {
return PgClassName
}
// RowIter implements the interface tables.Handler.
func (p PgClassHandler) RowIter(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
// Use cached data from this session if it exists
pgCatalogCache, err := getPgCatalogCache(ctx)
if err != nil {
return nil, err
}
if pgCatalogCache.pgClasses == nil {
err = cachePgClasses(ctx, pgCatalogCache)
if err != nil {
return nil, err
}
}
if classIdxPart, ok := partition.(inMemIndexPartition); ok {
return &inMemIndexScanIter[*pgClass]{
lookup: classIdxPart.lookup,
rangeConverter: p,
btreeAccess: pgCatalogCache.pgClasses,
rowConverter: pgClassToRow,
}, nil
}
return &pgClassTableScanIter{
classCache: pgCatalogCache.pgClasses,
idx: 0,
}, nil
}
// cachePgClasses caches the pg_class data for the current database in the session.
func cachePgClasses(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error {
var classes []*pgClass
tableHasIndexes := make(map[uint32]struct{})
nameIdx := NewUniqueInMemIndexStorage[*pgClass](lessName)
oidIdx := NewUniqueInMemIndexStorage[*pgClass](lessOid)
err := functions.IterateCurrentDatabase(ctx, functions.Callbacks{
Index: func(ctx *sql.Context, schema functions.ItemSchema, table functions.ItemTable, index functions.ItemIndex) (cont bool, err error) {
tableHasIndexes[id.Cache().ToOID(table.OID.AsId())] = struct{}{}
schemaOid := schema.OID
class := &pgClass{
oid: index.OID.AsId(),
oidNative: id.Cache().ToOID(index.OID.AsId()),
name: formatIndexName(index.Item),
hasIndexes: false,
kind: "i",
schemaOid: schemaOid.AsId(),
schemaOidNative: id.Cache().ToOID(schemaOid.AsId()),
relType: id.Null,
}
nameIdx.Add(class)
oidIdx.Add(class)
classes = append(classes, class)
return true, nil
},
Table: func(ctx *sql.Context, schema functions.ItemSchema, table functions.ItemTable) (cont bool, err error) {
_, hasIndexes := tableHasIndexes[id.Cache().ToOID(table.OID.AsId())]
class := &pgClass{
oid: table.OID.AsId(),
oidNative: id.Cache().ToOID(table.OID.AsId()),
name: table.Item.Name(),
hasIndexes: hasIndexes,
kind: "r",
schemaOid: schema.OID.AsId(),
schemaOidNative: id.Cache().ToOID(schema.OID.AsId()),
relType: id.NewType(table.OID.SchemaName(), table.OID.SchemaName()).AsId(),
}
nameIdx.Add(class)
oidIdx.Add(class)
classes = append(classes, class)
return true, nil
},
View: func(ctx *sql.Context, schema functions.ItemSchema, view functions.ItemView) (cont bool, err error) {
class := &pgClass{
oid: view.OID.AsId(),
oidNative: id.Cache().ToOID(view.OID.AsId()),
name: view.Item.Name,
hasIndexes: false,
kind: "v",
schemaOid: schema.OID.AsId(),
schemaOidNative: id.Cache().ToOID(schema.OID.AsId()),
relType: id.NewType(view.OID.SchemaName(), view.OID.SchemaName()).AsId(),
}
nameIdx.Add(class)
oidIdx.Add(class)
classes = append(classes, class)
return true, nil
},
Sequence: func(ctx *sql.Context, schema functions.ItemSchema, sequence functions.ItemSequence) (cont bool, err error) {
class := &pgClass{
oid: sequence.OID.AsId(),
oidNative: id.Cache().ToOID(sequence.OID.AsId()),
name: sequence.Item.Id.SequenceName(),
hasIndexes: false,
kind: "S",
schemaOid: schema.OID.AsId(),
schemaOidNative: id.Cache().ToOID(schema.OID.AsId()),
relType: id.Null,
}
nameIdx.Add(class)
oidIdx.Add(class)
classes = append(classes, class)
return true, nil
},
})
if err != nil {
return err
}
pgCatalogCache.pgClasses = &pgClassCache{
classes: classes,
nameIdx: nameIdx,
oidIdx: oidIdx,
}
return nil
}
// formatIndexName returns the name of an index for display
func formatIndexName(idx sql.Index) string {
if idx.ID() == "PRIMARY" {
return fmt.Sprintf("%s_pkey", idx.Table())
}
switch idx.(type) {
case *index.BranchNameIndex, *index.CommitIndex:
return fmt.Sprintf("%s_%s_key", idx.Table(), idx.ID())
}
return idx.ID()
// TODO: Unnamed indexes should have below format
// return fmt.Sprintf("%s_%s_key", idx.Table(), idx.ID())
}
// getIndexScanRange implements the interface RangeConverter.
func (p PgClassHandler) getIndexScanRange(rng sql.Range, index sql.Index) (*pgClass, bool, *pgClass, bool) {
var gte, lt *pgClass
var hasLowerBound, hasUpperBound bool
switch index.(pgCatalogInMemIndex).name {
case "pg_class_oid_index":
msrng := rng.(sql.MySQLRange)
oidRng := msrng[0]
if oidRng.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(oidRng.LowerBound)
if lb != nil {
lowerRangeCutKey := lb.(id.Id)
gte = &pgClass{
oidNative: idToOid(lowerRangeCutKey),
}
hasLowerBound = true
}
}
if oidRng.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(oidRng.UpperBound)
if ub != nil {
upperRangeCutKey := ub.(id.Id)
lt = &pgClass{
oidNative: idToOid(upperRangeCutKey) + 1,
}
hasUpperBound = true
}
}
case "pg_class_relname_nsp_index":
msrng := rng.(sql.MySQLRange)
relNameRange := msrng[0]
schemaOidRange := msrng[1]
var relnameLower, relnameUpper string
schemaOidLower := uint32(0)
schemaOidUpper := uint32(math.MaxUint32)
schemaOidUpperSet := false
if relNameRange.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(relNameRange.LowerBound)
if lb != nil {
relnameLower = lb.(string)
hasLowerBound = true
}
}
if relNameRange.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(relNameRange.UpperBound)
if ub != nil {
relnameUpper = ub.(string)
hasUpperBound = true
}
}
if schemaOidRange.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(schemaOidRange.LowerBound)
if lb != nil {
lowerRangeCutKey := lb.(id.Id)
schemaOidLower = idToOid(lowerRangeCutKey)
}
}
if schemaOidRange.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(schemaOidRange.UpperBound)
if ub != nil {
upperRangeCutKey := ub.(id.Id)
schemaOidUpper = idToOid(upperRangeCutKey)
schemaOidUpperSet = true
}
}
if relNameRange.HasLowerBound() || schemaOidRange.HasLowerBound() {
gte = &pgClass{
name: relnameLower,
schemaOidNative: schemaOidLower,
}
}
if relNameRange.HasUpperBound() || schemaOidRange.HasUpperBound() {
// our less-than upper bound depends on whether we have a prefix match or both fields were set
if !schemaOidUpperSet {
relnameUpper = fmt.Sprintf("%s%o", relnameUpper, rune(0))
} else {
schemaOidUpper = schemaOidUpper + 1
}
lt = &pgClass{
name: relnameUpper,
schemaOidNative: schemaOidUpper,
}
}
default:
panic("unknown index name: " + index.(pgCatalogInMemIndex).name)
}
return gte, hasLowerBound, lt, hasUpperBound
}
// idToOid converts an id.Id to its native uint32 OID representation. The type conversion process during index
// building will produce one of two values for comparison against an OID column: either a known OID value, which
// will be an Id of the appropriate type (Table, Namespace, etc), or an unknown value, which will be an oid.Oid.
func idToOid(i id.Id) uint32 {
switch i.Section() {
case id.Section_OID:
return id.Oid(i).OID()
default:
return id.Cache().ToOID(i)
}
}
// PkSchema implements the interface tables.Handler.
func (p PgClassHandler) PkSchema() sql.PrimaryKeySchema {
return sql.PrimaryKeySchema{
Schema: pgClassSchema,
PkOrdinals: nil,
}
}
// Indexes implements tables.IndexedTableHandler.
func (p PgClassHandler) Indexes() ([]sql.Index, error) {
return []sql.Index{
pgCatalogInMemIndex{
name: "pg_class_oid_index",
tblName: "pg_class",
dbName: "pg_catalog",
uniq: true,
columnExprs: []sql.ColumnExpressionType{{Expression: "pg_class.oid", Type: pgtypes.Oid}},
},
pgCatalogInMemIndex{
name: "pg_class_relname_nsp_index",
tblName: "pg_class",
dbName: "pg_catalog",
uniq: true,
columnExprs: []sql.ColumnExpressionType{
{Expression: "pg_class.relname", Type: pgtypes.Name},
{Expression: "pg_class.relnamespace", Type: pgtypes.Oid},
},
},
}, nil
}
// LookupPartitions implements tables.IndexedTableHandler.
func (p PgClassHandler) LookupPartitions(context *sql.Context, lookup sql.IndexLookup) (sql.PartitionIter, error) {
return &inMemIndexPartIter{
part: inMemIndexPartition{
idxName: lookup.Index.(pgCatalogInMemIndex).name,
lookup: lookup,
},
}, nil
}
// pgClassSchema is the schema for pg_class.
var pgClassSchema = sql.Schema{
{Name: "oid", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relname", Type: pgtypes.Name, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relnamespace", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "reltype", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "reloftype", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relowner", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relam", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relfilenode", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "reltablespace", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relpages", Type: pgtypes.Int32, Default: nil, Nullable: false, Source: PgClassName},
{Name: "reltuples", Type: pgtypes.Float32, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relallvisible", Type: pgtypes.Int32, Default: nil, Nullable: false, Source: PgClassName},
{Name: "reltoastrelid", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relhasindex", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relisshared", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relpersistence", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relkind", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relnatts", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relchecks", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relhasrules", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relhastriggers", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relhassubclass", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relrowsecurity", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relforcerowsecurity", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relispopulated", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relreplident", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relispartition", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relrewrite", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relfrozenxid", Type: pgtypes.Xid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relminmxid", Type: pgtypes.Xid, Default: nil, Nullable: false, Source: PgClassName},
{Name: "relacl", Type: pgtypes.TextArray, Default: nil, Nullable: true, Source: PgClassName}, // TODO: type aclitem[]
{Name: "reloptions", Type: pgtypes.TextArray, Default: nil, Nullable: true, Source: PgClassName}, // TODO: collation C
{Name: "relpartbound", Type: pgtypes.Text, Default: nil, Nullable: true, Source: PgClassName}, // TODO: type pg_node_tree, collation C
}
// pgClass represents a row in the pg_class table.
// We store oids in their native format as well so that we can do range scans on them.
type pgClass struct {
oid id.Id
oidNative uint32
name string
schemaOid id.Id
schemaOidNative uint32
hasIndexes bool
kind string // r = ordinary table, i = index, S = sequence, t = TOAST table, v = view, m = materialized view, c = composite type, f = foreign table, p = partitioned table, I = partitioned index
relType id.Id
}
// lessOid is a sort function for pgClass based on oid.
func lessOid(a, b *pgClass) bool {
return a.oidNative < b.oidNative
}
// lessName is a sort function for pgClass based on name, then schemaOid.
func lessName(a, b *pgClass) bool {
if a.name == b.name {
return a.schemaOidNative < b.schemaOidNative
}
return a.name < b.name
}
// pgClassTableScanIter is the sql.RowIter for the pg_class table.
type pgClassTableScanIter struct {
classCache *pgClassCache
idx int
}
var _ sql.RowIter = (*pgClassTableScanIter)(nil)
// Next implements the interface sql.RowIter.
func (iter *pgClassTableScanIter) Next(ctx *sql.Context) (sql.Row, error) {
if iter.idx >= len(iter.classCache.classes) {
return nil, io.EOF
}
iter.idx++
class := iter.classCache.classes[iter.idx-1]
return pgClassToRow(class), nil
}
func pgClassToRow(class *pgClass) sql.Row {
// TODO: this is temporary definition of 'relam' field
var relam = id.Null
if class.kind == "i" {
relam = id.NewAccessMethod("btree").AsId()
} else if class.kind == "r" || class.kind == "t" {
relam = id.NewAccessMethod("heap").AsId()
}
// TODO: Fill in the rest of the pg_class columns
return sql.Row{
class.oid, // oid
class.name, // relname
class.schemaOid, // relnamespace
class.relType, // reltype
id.Null, // reloftype
id.Null, // relowner
relam, // relam
id.Null, // relfilenode
id.Null, // reltablespace
int32(0), // relpages
float32(0), // reltuples
int32(0), // relallvisible
id.Null, // reltoastrelid
class.hasIndexes, // relhasindex
false, // relisshared
"p", // relpersistence
class.kind, // relkind
int16(0), // relnatts
int16(0), // relchecks
false, // relhasrules
false, // relhastriggers
false, // relhassubclass
false, // relrowsecurity
false, // relforcerowsecurity
true, // relispopulated
"d", // relreplident
false, // relispartition
id.Null, // relrewrite
uint32(0), // relfrozenxid
uint32(0), // relminmxid
nil, // relacl
nil, // reloptions
nil, // relpartbound
}
}
// Close implements the interface sql.RowIter.
func (iter *pgClassTableScanIter) Close(ctx *sql.Context) error {
return nil
}