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

492 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 (
"io"
"math"
"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/doltgresql/core/id"
pgparser "github.com/dolthub/doltgresql/postgres/parser/parser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/functions"
"github.com/dolthub/doltgresql/server/tables"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// PgAttributeName is a constant to the pg_attribute name.
const PgAttributeName = "pg_attribute"
// InitPgAttribute handles registration of the pg_attribute handler.
func InitPgAttribute() {
tables.AddHandler(PgCatalogName, PgAttributeName, PgAttributeHandler{})
}
// PgAttributeHandler is the handler for the pg_attribute table.
type PgAttributeHandler struct{}
var _ tables.Handler = PgAttributeHandler{}
var _ tables.IndexedTableHandler = PgAttributeHandler{}
// Name implements the interface tables.Handler.
func (p PgAttributeHandler) Name() string {
return PgAttributeName
}
// RowIter implements the interface tables.Handler.
func (p PgAttributeHandler) 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.pgAttributes == nil {
err = cachePgAttributes(ctx, pgCatalogCache)
if err != nil {
return nil, err
}
}
if attrIdxPart, ok := partition.(inMemIndexPartition); ok {
return &inMemIndexScanIter[*pgAttribute]{
lookup: attrIdxPart.lookup,
rangeConverter: p,
btreeAccess: pgCatalogCache.pgAttributes,
rowConverter: pgAttributeToRow,
}, nil
}
return &pgAttributeTableScanIter{
attributeCache: pgCatalogCache.pgAttributes,
idx: 0,
}, nil
}
// cachePgAttributes caches the pg_attribute data for the current database in the session.
func cachePgAttributes(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error {
var attributes []*pgAttribute
attrelidIdx := NewUniqueInMemIndexStorage[*pgAttribute](lessAttNum)
attrelidAttnameIdx := NewUniqueInMemIndexStorage[*pgAttribute](lessAttName)
// Get the engine for resolving view column types. May be nil in some test environments.
type queryAnalyzer interface {
AnalyzeQuery(*sql.Context, string) (sql.Node, error)
}
var engine queryAnalyzer
if runningServer := sqlserver.GetRunningServer(); runningServer != nil && runningServer.Engine != nil {
engine = runningServer.Engine
}
err := functions.IterateCurrentDatabase(ctx, functions.Callbacks{
Table: func(ctx *sql.Context, _ functions.ItemSchema, table functions.ItemTable) (cont bool, err error) {
for i, col := range table.Item.Schema(ctx) {
typeOid := id.Null
if doltgresType, ok := col.Type.(*pgtypes.DoltgresType); ok {
typeOid = doltgresType.ID.AsId()
} else {
// TODO: Remove once all information_schema tables are converted to use DoltgresType
dt := pgtypes.FromGmsType(col.Type)
typeOid = dt.ID.AsId()
}
generated := ""
if col.Generated != nil {
generated = "s"
}
dimensions := int16(0)
if s, ok := col.Type.(sql.SetType); ok {
dimensions = int16(s.NumberOfElements())
}
hasDefault := col.Default != nil
attr := &pgAttribute{
attrelid: table.OID.AsId(),
attrelidNative: id.Cache().ToOID(table.OID.AsId()),
attname: col.Name,
atttypid: typeOid,
attnum: int16(i + 1),
attndims: dimensions,
attnotnull: !col.Nullable,
atthasdef: hasDefault,
attgenerated: generated,
}
attrelidIdx.Add(attr)
attrelidAttnameIdx.Add(attr)
attributes = append(attributes, attr)
}
return true, nil
},
View: func(ctx *sql.Context, _ functions.ItemSchema, view functions.ItemView) (cont bool, err error) {
if engine == nil {
return true, nil
}
// Get the SELECT body from the view definition.
selectBody := view.Item.TextDefinition
if selectBody == "" {
stmts, parseErr := pgparser.Parse(view.Item.CreateViewStatement)
if parseErr != nil || len(stmts) == 0 {
return true, nil
}
cv, ok := stmts[0].AST.(*tree.CreateView)
if !ok {
return true, nil
}
selectBody = cv.AsSource.String()
}
if selectBody == "" {
return true, nil
}
// Analyze the SELECT statement to get the view's output schema.
analyzed, analyzeErr := engine.AnalyzeQuery(ctx, selectBody)
if analyzeErr != nil {
return true, nil
}
for i, col := range analyzed.Schema(ctx) {
typeOid := id.Null
if doltgresType, ok := col.Type.(*pgtypes.DoltgresType); ok {
typeOid = doltgresType.ID.AsId()
} else {
dt := pgtypes.FromGmsType(col.Type)
typeOid = dt.ID.AsId()
}
attr := &pgAttribute{
attrelid: view.OID.AsId(),
attrelidNative: id.Cache().ToOID(view.OID.AsId()),
attname: col.Name,
atttypid: typeOid,
attnum: int16(i + 1),
}
attrelidIdx.Add(attr)
attrelidAttnameIdx.Add(attr)
attributes = append(attributes, attr)
}
return true, nil
},
})
if err != nil {
return err
}
pgCatalogCache.pgAttributes = &pgAttributeCache{
attributes: attributes,
attrelidIdx: attrelidIdx,
attrelidAttnameIdx: attrelidAttnameIdx,
}
return nil
}
// getIndexScanRange implements the interface RangeConverter.
func (p PgAttributeHandler) getIndexScanRange(rng sql.Range, index sql.Index) (*pgAttribute, bool, *pgAttribute, bool) {
var gte, lt *pgAttribute
var hasLowerBound, hasUpperBound bool
switch index.(pgCatalogInMemIndex).name {
case "pg_attribute_relid_attnum_index":
msrng := rng.(sql.MySQLRange)
oidRng := msrng[0]
attNumRng := msrng[1]
var oidLower, oidUpper id.Id
attnumLower := int16(math.MinInt16)
attnumUpper := int16(math.MaxInt16)
attNumUpperSet := false
if oidRng.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(oidRng.LowerBound)
if lb != nil {
oidLower = lb.(id.Id)
hasLowerBound = true
}
}
if oidRng.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(oidRng.UpperBound)
if ub != nil {
oidUpper = ub.(id.Id)
hasUpperBound = true
}
}
if attNumRng.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(attNumRng.LowerBound)
if lb != nil {
attnumLower = lb.(int16)
}
}
if attNumRng.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(attNumRng.UpperBound)
if ub != nil {
attnumUpper = ub.(int16)
attNumUpperSet = true
}
}
if hasLowerBound {
gte = &pgAttribute{
attrelidNative: idToOid(oidLower),
attnum: attnumLower,
}
}
if hasUpperBound {
// our less-than upper bound depends on whether one or both fields in the range were set
oid := idToOid(oidUpper)
if !attNumUpperSet {
oid += 1
} else {
attnumUpper += 1
}
lt = &pgAttribute{
attrelidNative: oid,
attnum: attnumUpper,
}
}
case "pg_attribute_relid_attnam_index":
msrng := rng.(sql.MySQLRange)
attrelidRange := msrng[0]
attnameRange := msrng[1]
var attrelidLower, attrelidUpper uint32
var attnameLower, attnameUpper string
if attrelidRange.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(attrelidRange.LowerBound)
if lb != nil {
lowerRangeCutKey := lb.(id.Id)
attrelidLower = idToOid(lowerRangeCutKey)
hasLowerBound = true
}
}
if attrelidRange.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(attrelidRange.UpperBound)
if ub != nil {
upperRangeCutKey := ub.(id.Id)
attrelidUpper = idToOid(upperRangeCutKey)
hasUpperBound = true
}
}
if attnameRange.HasLowerBound() {
lb := sql.GetMySQLRangeCutKey(attnameRange.LowerBound)
if lb != nil {
attnameLower = lb.(string)
}
}
if attnameRange.HasUpperBound() {
ub := sql.GetMySQLRangeCutKey(attnameRange.UpperBound)
if ub != nil {
attnameUpper = ub.(string)
}
}
if attrelidRange.HasLowerBound() || attnameRange.HasLowerBound() {
gte = &pgAttribute{
attrelidNative: attrelidLower,
attname: attnameLower,
}
}
if attrelidRange.HasUpperBound() || attnameRange.HasUpperBound() {
// our less-than upper bound depends on whether one or both fields in the range were set
oidUpper := attrelidUpper
if attnameUpper == "" {
oidUpper += 1
} else {
attnameUpper += " "
}
lt = &pgAttribute{
attrelidNative: oidUpper,
attname: attnameUpper,
}
}
default:
panic("unknown index name: " + index.(pgCatalogInMemIndex).name)
}
return gte, hasLowerBound, lt, hasUpperBound
}
// PkSchema implements the interface tables.Handler.
func (p PgAttributeHandler) PkSchema() sql.PrimaryKeySchema {
return sql.PrimaryKeySchema{
Schema: pgAttributeSchema,
PkOrdinals: nil,
}
}
// Indexes implements tables.IndexedTableHandler.
func (p PgAttributeHandler) Indexes() ([]sql.Index, error) {
return []sql.Index{
pgCatalogInMemIndex{
name: "pg_attribute_relid_attnum_index",
tblName: "pg_attribute",
dbName: "pg_catalog",
uniq: true,
columnExprs: []sql.ColumnExpressionType{
{Expression: "pg_attribute.attrelid", Type: pgtypes.Oid},
{Expression: "pg_attribute.attnum", Type: pgtypes.Int16},
},
},
pgCatalogInMemIndex{
name: "pg_attribute_relid_attnam_index",
tblName: "pg_attribute",
dbName: "pg_catalog",
uniq: true,
columnExprs: []sql.ColumnExpressionType{
{Expression: "pg_attribute.attrelid", Type: pgtypes.Oid},
{Expression: "pg_attribute.attname", Type: pgtypes.Name},
},
},
}, nil
}
// LookupPartitions implements tables.IndexedTableHandler.
func (p PgAttributeHandler) LookupPartitions(context *sql.Context, lookup sql.IndexLookup) (sql.PartitionIter, error) {
return &inMemIndexPartIter{
part: inMemIndexPartition{
idxName: lookup.Index.(pgCatalogInMemIndex).name,
lookup: lookup,
},
}, nil
}
// pgAttributeSchema is the schema for pg_attribute.
var pgAttributeSchema = sql.Schema{
{Name: "attrelid", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attname", Type: pgtypes.Name, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "atttypid", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attlen", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attnum", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attcacheoff", Type: pgtypes.Int32, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "atttypmod", Type: pgtypes.Int32, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attndims", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attbyval", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attalign", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attstorage", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attcompression", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attnotnull", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "atthasdef", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "atthasmissing", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attidentity", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attgenerated", Type: pgtypes.InternalChar, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attisdropped", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attislocal", Type: pgtypes.Bool, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attinhcount", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attstattarget", Type: pgtypes.Int16, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attcollation", Type: pgtypes.Oid, Default: nil, Nullable: false, Source: PgAttributeName},
{Name: "attacl", Type: pgtypes.TextArray, Default: nil, Nullable: true, Source: PgAttributeName}, // TODO: type aclitem[]
{Name: "attoptions", Type: pgtypes.TextArray, Default: nil, Nullable: true, Source: PgAttributeName}, // TODO: collation C
{Name: "attfdwoptions", Type: pgtypes.TextArray, Default: nil, Nullable: true, Source: PgAttributeName}, // TODO: collation C
{Name: "attmissingval", Type: pgtypes.AnyArray, Default: nil, Nullable: true, Source: PgAttributeName},
}
// pgAttribute represents a row in the pg_attribute table.
// We store oids in their native format as well so that we can do range scans on them.
type pgAttribute struct {
attrelid id.Id
attrelidNative uint32
attname string
atttypid id.Id
attnum int16
attndims int16
attnotnull bool
atthasdef bool
attgenerated string
}
// lessAttNum is a sort function for pgAttribute based on attrelid.
func lessAttNum(a, b *pgAttribute) bool {
if a.attrelidNative == b.attrelidNative {
return a.attnum < b.attnum
}
return a.attrelidNative < b.attrelidNative
}
// lessAttName is a sort function for pgAttribute based on attrelid, then attname.
func lessAttName(a, b *pgAttribute) bool {
if a.attrelidNative == b.attrelidNative {
return a.attname < b.attname
}
return a.attrelidNative < b.attrelidNative
}
// pgAttributeTableScanIter is the sql.RowIter for the pg_attribute table.
type pgAttributeTableScanIter struct {
attributeCache *pgAttributeCache
idx int
}
var _ sql.RowIter = (*pgAttributeTableScanIter)(nil)
// Next implements the interface sql.RowIter.
func (iter *pgAttributeTableScanIter) Next(ctx *sql.Context) (sql.Row, error) {
if iter.idx >= len(iter.attributeCache.attributes) {
return nil, io.EOF
}
iter.idx++
attr := iter.attributeCache.attributes[iter.idx-1]
return pgAttributeToRow(attr), nil
}
// Close implements the interface sql.RowIter.
func (iter *pgAttributeTableScanIter) Close(ctx *sql.Context) error {
return nil
}
func pgAttributeToRow(attr *pgAttribute) sql.Row {
// TODO: Fill in the rest of the pg_attribute columns
return sql.Row{
attr.attrelid, // attrelid
attr.attname, // attname
attr.atttypid, // atttypid
int16(0), // attlen
attr.attnum, // attnum
int32(-1), // attcacheoff
int32(-1), // atttypmod
attr.attndims, // attndims
false, // attbyval
"i", // attalign
"p", // attstorage
"", // attcompression
attr.attnotnull, // attnotnull
attr.atthasdef, // atthasdef
false, // atthasmissing
"", // attidentity
attr.attgenerated, // attgenerated
false, // attisdropped
true, // attislocal
int16(0), // attinhcount
int16(-1), // attstattarget
id.Null, // attcollation
nil, // attacl
nil, // attoptions
nil, // attfdwoptions
nil, // attmissingval
}
}