// 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 core import ( "maps" "slices" "github.com/cockroachdb/errors" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess" "github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/doltgresql/core/casts" "github.com/dolthub/doltgresql/core/extensions" "github.com/dolthub/doltgresql/core/functions" "github.com/dolthub/doltgresql/core/procedures" "github.com/dolthub/doltgresql/core/rootobject/objinterface" "github.com/dolthub/doltgresql/core/sequences" "github.com/dolthub/doltgresql/core/triggers" "github.com/dolthub/doltgresql/core/typecollection" ) // contextValues contains a set of cached data passed alongside the context. This data is considered temporary // and may be refreshed at any point, including during the middle of a query. Callers should not assume that // data stored in contextValues is persisted, and other types of data should not be added to contextValues. type contextValues struct { seqs map[string]*sequences.Collection types map[string]*typecollection.TypeCollection funcs map[string]*functions.Collection procs map[string]*procedures.Collection trigs map[string]*triggers.Collection exts map[string]*extensions.Collection casts map[string]*casts.Collection pgCatalogCache any runner sql.StatementRunner // cache the dateOutputFormat, this is refreshed on SET dateOutputFormat string } // getContextValues accesses the contextValues in the given context. If the context does not have a contextValues, then // it creates one and adds it to the context. func getContextValues(ctx *sql.Context) (*contextValues, error) { if ctx == nil { return nil, errors.New("context is nil") } sess := dsess.DSessFromSess(ctx.Session) if sess.DoltgresSessObj == nil { cv := &contextValues{} sess.DoltgresSessObj = cv return cv, nil } cv, ok := sess.DoltgresSessObj.(*contextValues) if !ok { return nil, errors.Errorf("context contains an unknown values struct of type: %T", sess.DoltgresSessObj) } return cv, nil } // ClearContextValues clears all context values. This is primarily for operations that are directly called from Dolt, as // Dolt does not have the Doltgres concept of context values. Care must be taken to ensure that intermediate state // written to the context values are not overwritten. func ClearContextValues(ctx *sql.Context) { sess := dsess.DSessFromSess(ctx.Session) if sess.DoltgresSessObj != nil { // We want to persist the runner between clears since it's static var runner sql.StatementRunner if cv, ok := sess.DoltgresSessObj.(*contextValues); ok { runner = cv.runner } sess.DoltgresSessObj = &contextValues{ runner: runner, } } } // GetRootFromContext returns the working session's root from the context, along with the session. func GetRootFromContext(ctx *sql.Context) (*dsess.DoltSession, *RootValue, error) { return getRootFromContextForDatabase(ctx, "") } // getRootFromContextForDatabase returns the working session's root from the context for a specific database, along with the session. func getRootFromContextForDatabase(ctx *sql.Context, database string) (*dsess.DoltSession, *RootValue, error) { session := dsess.DSessFromSess(ctx.Session) if len(database) == 0 { database = ctx.GetCurrentDatabase() } state, ok, err := session.LookupDbState(ctx, database) if err != nil { return nil, nil, err } if !ok { return nil, nil, sql.ErrDatabaseNotFound.New(database) } return session, state.WorkingRoot().(*RootValue), nil } // IsContextValid returns whether the context is valid for use with any of the functions in the package. If this is not // false, then there's a high likelihood that the context is being used in a temporary scenario (such as setting up the // database, etc.). func IsContextValid(ctx *sql.Context) bool { if ctx == nil { return false } _, ok := ctx.Session.(*dsess.DoltSession) return ok } // GetPgCatalogCache returns a cache of data for pg_catalog tables. This function should only be used by // pg_catalog table handlers. The catalog cache instance stores generated pg_catalog table data so that // it only has to generated table data once per query. // // TODO: The return type here is currently any, to avoid a package import cycle. This could be cleaned up by // introducing a new interface type, in a package that does not depend on core or pgcatalog packages. func GetPgCatalogCache(ctx *sql.Context) (any, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } return cv.pgCatalogCache, nil } // SetPgCatalogCache sets |pgCatalogCache| as the catalog cache instance for this session. // // TODO: The input type here is currently any, to avoid a package import cycle. This could be cleaned up by // introducing a new interface type, in a package that does not depend on core or pgcatalog packages. func SetPgCatalogCache(ctx *sql.Context, pgCatalogCache any) error { cv, err := getContextValues(ctx) if err != nil { return err } cv.pgCatalogCache = pgCatalogCache return nil } // SetRunnerOnContext sets the given runner within the context values. func SetRunnerOnContext(ctx *sql.Context, runner sql.StatementRunner) error { if runner == nil { return nil } cv, err := getContextValues(ctx) if err != nil { return err } cv.runner = runner return nil } // GetRunnerFromContext returns the sql.StatementRunner from within the context. func GetRunnerFromContext(ctx *sql.Context) (sql.StatementRunner, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } return cv.runner, nil } // GetDoltTableFromContext returns the Dolt table from the context. Returns nil if no table was found. func GetDoltTableFromContext(ctx *sql.Context, tableName doltdb.TableName) (*doltdb.Table, error) { _, root, err := GetRootFromContext(ctx) if err != nil { return nil, err } var table *doltdb.Table if tableName.Schema == "" { _, table, _, err = resolve.Table(ctx, root, tableName.Name) if err != nil { return nil, err } } else { table, _, err = root.GetTable(ctx, tableName) } if err != nil { return nil, err } return table, nil } // GetSqlDatabaseFromContext returns the database from the context. Uses the context's current database if an empty // string is provided. Returns nil if the database was not found. func GetSqlDatabaseFromContext(ctx *sql.Context, database string) (sql.Database, error) { session := dsess.DSessFromSess(ctx.Session) if len(database) == 0 { database = ctx.GetCurrentDatabase() } db, err := session.Provider().Database(ctx, database) if err != nil { if sql.ErrDatabaseNotFound.Is(err) { return nil, nil } return nil, err } return db, nil } // GetSqlTableFromContext returns the table from the context. Uses the context's current database if an empty database // name is provided. Returns nil if no table was found. func GetSqlTableFromContext(ctx *sql.Context, databaseName string, tableName doltdb.TableName) (sql.Table, error) { db, err := GetSqlDatabaseFromContext(ctx, databaseName) if err != nil || db == nil { return nil, err } schemaDb, ok := db.(sql.SchemaDatabase) if !ok { // Fairly confident that Dolt only has database implementations that inherit sql.SchemaDatabase, so only GMS // databases may fail here (like information_schema). In this scenario, we expect that no schema will be passed, // so we'll special-case it here. if len(tableName.Schema) == 0 { tbl, ok, err := db.GetTableInsensitive(ctx, tableName.Name) if err != nil || !ok { return nil, err } return tbl, nil } return nil, nil } var searchPath []string if len(tableName.Schema) == 0 { // If a schema was not provided, then we'll use the search path searchPath, err = SearchPath(ctx) if err != nil { return nil, err } } else { // A specific schema is given, so we'll only use that one for the search path searchPath = []string{tableName.Schema} } for _, schema := range searchPath { db, ok, err = schemaDb.GetSchema(ctx, schema) if err != nil { return nil, err } if !ok { continue } tbl, ok, err := db.GetTableInsensitive(ctx, tableName.Name) if err != nil { return nil, err } if !ok { continue } return tbl, nil } return nil, nil } // SearchPath returns the effective schema search path for the current session func SearchPath(ctx *sql.Context) ([]string, error) { path, err := resolve.SearchPath(ctx) if err != nil { return nil, err } // pg_catalog is *always* implicitly part of the search path as the first element, unless it's specifically // included later. This allows users to override built-in names with user-defined names, but they have to // opt in to that behavior. hasPgCatalog := false for _, schema := range path { if schema == "pg_catalog" { hasPgCatalog = true break } } if !hasPgCatalog { path = append([]string{"pg_catalog"}, path...) } return path, nil } // GetExtensionsCollectionFromContext returns the extensions collection from the given context. Will always return a // collection if no error is returned. func GetExtensionsCollectionFromContext(ctx *sql.Context, database string) (*extensions.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.exts == nil { cv.exts = make(map[string]*extensions.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.exts[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.exts[database], err = extensions.LoadExtensions(ctx, root) if err != nil { return nil, err } } return cv.exts[database], nil } // GetFunctionsCollectionFromContext returns the functions collection from the given context. Will always return a // collection if no error is returned. func GetFunctionsCollectionFromContext(ctx *sql.Context, database string) (*functions.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.funcs == nil { cv.funcs = make(map[string]*functions.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.funcs[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.funcs[database], err = functions.LoadFunctions(ctx, root) if err != nil { return nil, err } } return cv.funcs[database], nil } // GetProceduresCollectionFromContext returns the procedures collection from the given context. Will always return a // collection if no error is returned. func GetProceduresCollectionFromContext(ctx *sql.Context, database string) (*procedures.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.procs == nil { cv.procs = make(map[string]*procedures.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.procs[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.procs[database], err = procedures.LoadProcedures(ctx, root) if err != nil { return nil, err } } return cv.procs[database], nil } // GetSequencesCollectionFromContext returns the given sequence collection from the context for the database // named. If no database is provided, the context's current database is used. // Will always return a collection if no error is returned. func GetSequencesCollectionFromContext(ctx *sql.Context, database string) (*sequences.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.seqs == nil { cv.seqs = make(map[string]*sequences.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.seqs[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.seqs[database], err = sequences.LoadSequences(ctx, root) if err != nil { return nil, err } } return cv.seqs[database], nil } // GetTriggersCollectionFromContext returns the triggers collection from the given context. Will always return a // collection if no error is returned. func GetTriggersCollectionFromContext(ctx *sql.Context, database string) (*triggers.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.trigs == nil { cv.trigs = make(map[string]*triggers.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.trigs[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.trigs[database], err = triggers.LoadTriggers(ctx, root) if err != nil { return nil, err } } return cv.trigs[database], nil } // GetTypesCollectionFromContext returns the given type collection from the context. // Will always return a collection if no error is returned. func GetTypesCollectionFromContext(ctx *sql.Context, database string) (*typecollection.TypeCollection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.types == nil { cv.types = make(map[string]*typecollection.TypeCollection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.types[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.types[database], err = typecollection.LoadTypes(ctx, root) if err != nil { return nil, err } } return cv.types[database], nil } // GetCastsCollectionFromContext returns the given casts collection from the context. // Will always return a collection if no error is returned. func GetCastsCollectionFromContext(ctx *sql.Context, database string) (*casts.Collection, error) { cv, err := getContextValues(ctx) if err != nil { return nil, err } if cv.casts == nil { cv.casts = make(map[string]*casts.Collection) } if len(database) == 0 { database = ctx.GetCurrentDatabase() } if cv.casts[database] == nil { _, root, err := getRootFromContextForDatabase(ctx, database) if err != nil { return nil, err } cv.casts[database], err = casts.LoadCasts(ctx, root) if err != nil { return nil, err } } return cv.casts[database], nil } // CloseContextRootFinalizer finalizes any changes persisted within the context by writing them to the working root. // This should ONLY be called by the ContextRootFinalizer node. func CloseContextRootFinalizer(ctx *sql.Context) error { sess := dsess.DSessFromSess(ctx.Session) if sess.DoltgresSessObj == nil { return nil } cv, ok := sess.DoltgresSessObj.(*contextValues) if !ok { return nil } // We need to update the root for all databases used by this context. This logic parallels what happens during // transaction commit in the dolt/sqle layer, where we check each branch state to see if it's dirty for _, db := range databasesInContext(ctx, cv) { err := updateSessionRootForDatabase(ctx, db, cv) if err != nil { return err } } return nil } // GetDateStyleOutputFormat returns the cached DateOutputFormat func GetDateStyleOutputFormat(ctx *sql.Context) (string, error) { cv, err := getContextValues(ctx) if err != nil { return "", err } return cv.dateOutputFormat, nil } // SetDateStyleOutputFormat cached the provided dateOutputFormat func SetDateStyleOutputFormat(ctx *sql.Context, dateOutputFormat string) error { cv, err := getContextValues(ctx) if err != nil { return err } cv.dateOutputFormat = dateOutputFormat return nil } // updateSessionRootForDatabase updates the root for all changes made to root object collections within the context // values. func updateSessionRootForDatabase(ctx *sql.Context, db string, cv *contextValues) error { session, root, err := getRootFromContextForDatabase(ctx, db) if err != nil { return err } newRoot := root if cv.seqs != nil && cv.seqs[db] != nil { retRoot, err := cv.seqs[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.seqs, db) } if cv.funcs != nil && cv.funcs[db] != nil && cv.funcs[db].DiffersFrom(ctx, root) { retRoot, err := cv.funcs[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.funcs, db) } if cv.procs != nil && cv.procs[db] != nil && cv.procs[db].DiffersFrom(ctx, root) { retRoot, err := cv.procs[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.procs, db) } if cv.trigs != nil && cv.trigs[db] != nil && cv.trigs[db].DiffersFrom(ctx, root) { retRoot, err := cv.trigs[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.trigs, db) } if cv.exts != nil && cv.exts[db] != nil && cv.exts[db].DiffersFrom(ctx, root) { retRoot, err := cv.exts[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.exts, db) } if cv.types != nil && cv.types[db] != nil { retRoot, err := cv.types[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.types, db) } if cv.casts != nil && cv.casts[db] != nil && cv.casts[db].DiffersFrom(ctx, root) { retRoot, err := cv.casts[db].UpdateRoot(ctx, newRoot) if err != nil { return err } newRoot = retRoot.(*RootValue) delete(cv.casts, db) } // Setting the session working root doesn't do a check to see if anything actually changed or not before marking that // branch state dirty, and dolt only allows a single dirty working set per commit. So it's important here to only // update the session root if something actually changed for that db. if err, rootChanged := rootValueChanged(newRoot, root); rootChanged { if err = session.SetWorkingRoot(ctx, db, newRoot); err != nil { // TODO: We need a way to see if the session has a writeable working root // (new interface method on session probably), and avoid setting it if so if errors.Is(err, doltdb.ErrOperationNotSupportedInDetachedHead) { return nil } return err } } else if err != nil { return err } return nil } // rootValueChanged returns whether the new root value is different from the old one func rootValueChanged(newRoot *RootValue, root *RootValue) (error, bool) { if newRoot == root { return nil, false } newHash, err := newRoot.HashOf() if err != nil { return err, false } oldHash, err := root.HashOf() if err != nil { return err, false } if newHash == oldHash { return nil, false } return nil, true } // databasesInContext returns all databases found within the context values. func databasesInContext(ctx *sql.Context, cv *contextValues) []string { dbs := make(map[string]struct{}) if cv.seqs != nil { for db := range cv.seqs { dbs[db] = struct{}{} } } currentDb := ctx.GetCurrentDatabase() if len(currentDb) > 0 { dbs[currentDb] = struct{}{} } return slices.Sorted(maps.Keys(dbs)) } // clear removes the collection from the cache. func (cv *contextValues) clear(objID objinterface.RootObjectID) { switch objID { case objinterface.RootObjectID_None: // Nothing to cache with this case objinterface.RootObjectID_Sequences: cv.seqs = nil case objinterface.RootObjectID_Types: cv.types = nil case objinterface.RootObjectID_Functions: cv.funcs = nil case objinterface.RootObjectID_Triggers: cv.trigs = nil case objinterface.RootObjectID_Extensions: // We don't cache these case objinterface.RootObjectID_Conflicts: // We don't cache these case objinterface.RootObjectID_Procedures: cv.procs = nil case objinterface.RootObjectID_Casts: cv.casts = nil default: panic("unhandled context clear object ID") } }