Merge pull request #9924 from dolthub/nicktobey/global-tables

Implement dolt_global_tables system table
This commit is contained in:
Nick Tobey
2025-10-07 09:59:40 -07:00
committed by GitHub
15 changed files with 1127 additions and 653 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ require (
github.com/dolthub/dolt-mcp v0.2.2-0.20250917171427-13e4520d1c36
github.com/dolthub/eventsapi_schema v0.0.0-20250915094920-eadfd39051ca
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2
github.com/dolthub/go-mysql-server v0.20.1-0.20251003202417-9979526e55c8
github.com/dolthub/go-mysql-server v0.20.1-0.20251006192807-f7a3e3850abc
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63
github.com/edsrzf/mmap-go v1.2.0
github.com/esote/minmaxheap v1.0.0
+2 -2
View File
@@ -213,8 +213,8 @@ github.com/dolthub/fslock v0.0.3 h1:iLMpUIvJKMKm92+N1fmHVdxJP5NdyDK5bK7z7Ba2s2U=
github.com/dolthub/fslock v0.0.3/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0=
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 h1:zxMsH7RLiG+dlZ/y0LgJHTV26XoiSJcuWq+em6t6VVc=
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE=
github.com/dolthub/go-mysql-server v0.20.1-0.20251003202417-9979526e55c8 h1:r54ksXOt1SDgztJsHU3r+W9ZYZjYUTIguGYUcrM9bMk=
github.com/dolthub/go-mysql-server v0.20.1-0.20251003202417-9979526e55c8/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8=
github.com/dolthub/go-mysql-server v0.20.1-0.20251006192807-f7a3e3850abc h1:kEbuDqqQ++5R/ExeKcNcQEe7MKYSn2ZJE2lBxoYQqjw=
github.com/dolthub/go-mysql-server v0.20.1-0.20251006192807-f7a3e3850abc/go.mod h1:EeYR0apo+8j2Dyxmn2ghkPlirO2S5mT1xHBrA+Efys8=
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI=
github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q=
github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718 h1:lT7hE5k+0nkBdj/1UOSFwjWpNxf+LCApbRHgnCA17XE=
+2 -40
View File
@@ -18,7 +18,6 @@ import (
"context"
"fmt"
"io"
"regexp"
"strings"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
@@ -203,43 +202,6 @@ func IdentifyIgnoredTables(ctx context.Context, roots Roots, tables []TableName)
return ignoredTables, nil
}
// compilePattern takes a dolt_ignore pattern and generate a Regexp that matches against the same table names as the pattern.
func compilePattern(pattern string) (*regexp.Regexp, error) {
pattern = "^" + regexp.QuoteMeta(pattern) + "$"
pattern = strings.Replace(pattern, "\\?", ".", -1)
pattern = strings.Replace(pattern, "\\*", ".*", -1)
pattern = strings.Replace(pattern, "%", ".*", -1)
return regexp.Compile(pattern)
}
// getMoreSpecificPatterns takes a dolt_ignore pattern and generates a Regexp that matches against all patterns
// that are "more specific" than it. (a pattern A is more specific than a pattern B if all names that match A also
// match pattern B, but not vice versa.)
func getMoreSpecificPatterns(lessSpecific string) (*regexp.Regexp, error) {
pattern := "^" + regexp.QuoteMeta(lessSpecific) + "$"
// A ? can expand to any character except for a * or %, since that also has special meaning in patterns.
pattern = strings.Replace(pattern, "\\?", "[^\\*%]", -1)
pattern = strings.Replace(pattern, "\\*", ".*", -1)
pattern = strings.Replace(pattern, "%", ".*", -1)
return regexp.Compile(pattern)
}
// normalizePattern generates an equivalent pattern, such that all equivalent patterns have the same normalized pattern.
// It accomplishes this by replacing all * with %, and removing multiple adjacent %.
// This will get a lot harder to implement once we support escaped characters in patterns.
func normalizePattern(pattern string) string {
pattern = strings.Replace(pattern, "*", "%", -1)
for {
newPattern := strings.Replace(pattern, "%%", "%", -1)
if newPattern == pattern {
break
}
pattern = newPattern
}
return pattern
}
func resolveConflictingPatterns(trueMatches, falseMatches []string, tableName TableName) (IgnoreResult, error) {
trueMatchesToRemove := map[string]struct{}{}
falseMatchesToRemove := map[string]struct{}{}
@@ -314,11 +276,11 @@ func (ip *IgnorePatterns) IsTableNameIgnored(tableName TableName) (IgnoreResult,
for _, patternIgnore := range *ip {
pattern := patternIgnore.Pattern
ignore := patternIgnore.Ignore
patternRegExp, err := compilePattern(pattern)
matchesPattern, err := MatchTablePattern(pattern, tableName.Name)
if err != nil {
return ErrorOccurred, err
}
if patternRegExp.MatchString(tableName.Name) {
if matchesPattern {
if ignore {
trueMatches = append(trueMatches, pattern)
} else {
@@ -0,0 +1,101 @@
// Copyright 2025 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 doltdb
import (
"context"
"fmt"
"io"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
"github.com/dolthub/dolt/go/store/types"
"github.com/dolthub/dolt/go/store/val"
)
// GetNonlocalTablesRef is a function that reads the "ref" column from dolt_nonlocal_tables. This is used to handle the Doltgres extended string type.
var GetNonlocalTablesRef = getNonlocalTablesRef
// GetNonlocalTablesNameColumn is a function that reads the "table_name" column from dolt_nonlocal_tables. This is used to handle the Doltgres extended string type.
var GetNonlocalTablesNameColumn = getNonlocalTablesNameColumn
func getNonlocalTablesNameColumn(_ context.Context, keyDesc val.TupleDesc, keyTuple val.Tuple) (string, error) {
key, ok := keyDesc.GetString(0, keyTuple)
if !ok {
return "", fmt.Errorf("failed to read global table")
}
return key, nil
}
type NonlocalTableEntry struct {
Ref string
NewTableName string
Options string
}
func getNonlocalTablesRef(_ context.Context, valDesc val.TupleDesc, valTuple val.Tuple) (result NonlocalTableEntry) {
result.Ref, _ = valDesc.GetString(0, valTuple)
result.NewTableName, _ = valDesc.GetString(1, valTuple)
result.Options, _ = valDesc.GetString(2, valTuple)
return result
}
func GetGlobalTablePatterns(ctx context.Context, root RootValue, schema string, cb func(string)) error {
table_name := TableName{Name: NonlocalTableName, Schema: schema}
table, found, err := root.GetTable(ctx, table_name)
if err != nil {
return err
}
if !found {
// dolt_global_tables doesn't exist, so don't filter any tables.
return nil
}
index, err := table.GetRowData(ctx)
if table.Format() == types.Format_LD_1 {
// dolt_global_tables is not supported for the legacy storage format.
return nil
}
if err != nil {
return err
}
ignoreTableSchema, err := table.GetSchema(ctx)
if err != nil {
return err
}
m := durable.MapFromIndex(index)
keyDesc, _ := ignoreTableSchema.GetMapDescriptors(m.NodeStore())
ignoreTableMap, err := m.IterAll(ctx)
if err != nil {
return err
}
for {
keyTuple, _, err := ignoreTableMap.Next(ctx)
if err == io.EOF {
break
}
if err != nil {
return err
}
globalTableName, err := GetNonlocalTablesNameColumn(ctx, keyDesc, keyTuple)
if err != nil {
return err
}
cb(globalTableName)
}
return nil
}
@@ -217,6 +217,19 @@ const (
QueryCatalogDescriptionCol = "description"
)
const (
// NonlocalTableName is the name of the query catalog table
NonlocalTableName = "dolt_nonlocal_tables"
NonlocalTableTableNameCol = "table_name"
NonlocalTableRefCol = "target_ref"
NonlocalTablesRefTableCol = "ref_table"
NonlocalTablesOptionsCol = "options"
)
const (
// SchemasTableName is the name of the dolt schema fragment table
SchemasTableName = "dolt_schemas"
@@ -378,6 +391,8 @@ var GetStashesTableName = func() string {
var GetQueryCatalogTableName = func() string { return DoltQueryCatalogTableName }
var GetNonlocalTablesTableName = func() string { return NonlocalTableName }
var GetTestsTableName = func() string {
return TestsTableName
}
@@ -0,0 +1,103 @@
// Copyright 2025 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 doltdb
import (
"regexp"
"strings"
"github.com/dolthub/go-mysql-server/sql"
)
// compilePattern takes a dolt_ignore pattern and generate a Regexp that matches against the same table names as the pattern.
func compilePattern(pattern string) (*regexp.Regexp, error) {
pattern = "^" + regexp.QuoteMeta(pattern) + "$"
pattern = strings.Replace(pattern, "\\?", ".", -1)
pattern = strings.Replace(pattern, "\\*", ".*", -1)
pattern = strings.Replace(pattern, "%", ".*", -1)
return regexp.Compile(pattern)
}
func patternContainsSpecialCharacters(pattern string) bool {
return strings.ContainsAny(pattern, "?*%")
}
// MatchTablePattern returns whether a table name matches a table name pattern
func MatchTablePattern(pattern string, table string) (bool, error) {
re, err := compilePattern(pattern)
if err != nil {
return false, err
}
return re.MatchString(table), nil
}
// GetMatchingTables returns all tables that match a pattern
func GetMatchingTables(ctx *sql.Context, root RootValue, schemaName string, pattern string) (results []string, err error) {
// If the pattern doesn't contain any special characters, look up that name.
if !patternContainsSpecialCharacters(pattern) {
_, exists, err := root.GetTable(ctx, TableName{Name: pattern, Schema: schemaName})
if err != nil {
return nil, err
}
if exists {
return []string{pattern}, nil
} else {
return nil, nil
}
}
tables, err := root.GetTableNames(ctx, schemaName, true)
if err != nil {
return nil, err
}
// Otherwise, iterate over each table on the branch to see if they match.
for _, tbl := range tables {
matches, err := MatchTablePattern(pattern, tbl)
if err != nil {
return nil, err
}
if matches {
results = append(results, tbl)
}
}
return results, nil
}
// getMoreSpecificPatterns takes a dolt_ignore pattern and generates a Regexp that matches against all patterns
// that are "more specific" than it. (a pattern A is more specific than a pattern B if all names that match A also
// match pattern B, but not vice versa.)
func getMoreSpecificPatterns(lessSpecific string) (*regexp.Regexp, error) {
pattern := "^" + regexp.QuoteMeta(lessSpecific) + "$"
// A ? can expand to any character except for a * or %, since that also has special meaning in patterns.
pattern = strings.Replace(pattern, "\\?", "[^\\*%]", -1)
pattern = strings.Replace(pattern, "\\*", ".*", -1)
pattern = strings.Replace(pattern, "%", ".*", -1)
return regexp.Compile(pattern)
}
// normalizePattern generates an equivalent pattern, such that all equivalent patterns have the same normalized pattern.
// It accomplishes this by replacing all * with %, and removing multiple adjacent %.
// This will get a lot harder to implement once we support escaped characters in patterns.
func normalizePattern(pattern string) string {
pattern = strings.Replace(pattern, "*", "%", -1)
for {
newPattern := strings.Replace(pattern, "%%", "%", -1)
if newPattern == pattern {
break
}
pattern = newPattern
}
return pattern
}
+127 -6
View File
@@ -38,6 +38,7 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/branch_control"
"github.com/dolthub/dolt/go/libraries/doltcore/diff"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb/durable"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/doltcore/env/actions/commitwalk"
"github.com/dolthub/dolt/go/libraries/doltcore/rebase"
@@ -59,6 +60,14 @@ var ErrInvalidTableName = errors.NewKind("Invalid table name %s.")
var ErrReservedTableName = errors.NewKind("Invalid table name %s. Table names beginning with `dolt_` are reserved for internal use")
var ErrReservedDiffTableName = errors.NewKind("Invalid table name %s. Table names beginning with `__DATABASE__` are reserved for internal use")
var ErrSystemTableAlter = errors.NewKind("Cannot alter table %s: system tables cannot be dropped or altered")
var ErrInvalidNonlocalTableOptions = errors.NewKind("Invalid nonlocal table options %s: only valid value is 'immediate'.")
type readNonlocalTablesFlag bool
const (
doReadNonlocalTables readNonlocalTablesFlag = true
dontReadNonlocalTables readNonlocalTablesFlag = false
)
// Database implements sql.Database for a dolt DB.
type Database struct {
@@ -268,6 +277,10 @@ func (db Database) GetGlobalState() globalstate.GlobalState {
// GetTableInsensitive is used when resolving tables in queries. It returns a best-effort case-insensitive match for
// the table name given.
func (db Database) GetTableInsensitive(ctx *sql.Context, tblName string) (sql.Table, bool, error) {
return db.getTableInsensitive(ctx, tblName, doReadNonlocalTables)
}
func (db Database) getTableInsensitive(ctx *sql.Context, tblName string, readNonlocalTables readNonlocalTablesFlag) (sql.Table, bool, error) {
// We start by first checking whether the input table is a temporary table. Temporary tables with name `x` take
// priority over persisted tables of name `x`.
ds := dsess.DSessFromSess(ctx.Session)
@@ -280,11 +293,15 @@ func (db Database) GetTableInsensitive(ctx *sql.Context, tblName string) (sql.Ta
return nil, false, err
}
return db.getTableInsensitive(ctx, nil, ds, root, tblName, "")
return db.getTableInsensitiveWithRoot(ctx, nil, ds, root, tblName, "", readNonlocalTables)
}
func (db Database) GetTableInsensitiveAsOf(ctx *sql.Context, tableName string, asOf interface{}) (sql.Table, bool, error) {
return db.getTableInsensitiveAsOf(ctx, tableName, asOf, true)
}
// GetTableInsensitiveAsOf implements sql.VersionedDatabase
func (db Database) GetTableInsensitiveAsOf(ctx *sql.Context, tableName string, asOf interface{}) (sql.Table, bool, error) {
func (db Database) getTableInsensitiveAsOf(ctx *sql.Context, tableName string, asOf interface{}, readNonlocalTables readNonlocalTablesFlag) (sql.Table, bool, error) {
if asOf == nil {
return db.GetTableInsensitive(ctx, tableName)
}
@@ -297,7 +314,7 @@ func (db Database) GetTableInsensitiveAsOf(ctx *sql.Context, tableName string, a
sess := dsess.DSessFromSess(ctx.Session)
table, ok, err := db.getTableInsensitive(ctx, head, sess, root, tableName, asOf)
table, ok, err := db.getTableInsensitiveWithRoot(ctx, head, sess, root, tableName, asOf, readNonlocalTables)
if err != nil {
return nil, false, err
}
@@ -307,7 +324,7 @@ func (db Database) GetTableInsensitiveAsOf(ctx *sql.Context, tableName string, a
if doltdb.IsReadOnlySystemTable(doltdb.TableName{Name: tableName, Schema: db.schemaName}) {
// currently, system tables do not need to be "locked to root"
// see comment below in getTableInsensitive
// see comment below in getTableInsensitiveWithRoot
return table, ok, nil
}
@@ -330,9 +347,19 @@ func (db Database) GetTableInsensitiveAsOf(ctx *sql.Context, tableName string, a
}
}
func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds *dsess.DoltSession, root doltdb.RootValue, tblName string, asOf interface{}) (sql.Table, bool, error) {
func (db Database) getTableInsensitiveWithRoot(ctx *sql.Context, head *doltdb.Commit, ds *dsess.DoltSession, root doltdb.RootValue, tblName string, asOf interface{}, readNonlocalTables readNonlocalTablesFlag) (sql.Table, bool, error) {
lwrName := strings.ToLower(tblName)
if readNonlocalTables {
nonlocalTable, exists, err := db.getNonlocalTable(ctx, root, lwrName)
if err != nil {
return nil, false, err
}
if exists {
return nonlocalTable, true, nil
}
}
// TODO: these tables that cache a root value at construction time should not, they need to get it from the session
// at runtime
switch {
@@ -476,7 +503,7 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds
}
}
srcTable, ok, err := db.getTableInsensitive(ctx, head, ds, root, tname.Name, asOf)
srcTable, ok, err := db.getTableInsensitiveWithRoot(ctx, head, ds, root, tname.Name, asOf, readNonlocalTables)
if err != nil {
return nil, false, err
} else if !ok {
@@ -868,6 +895,17 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds
versionableTable := backingTable.(dtables.VersionableTable)
dt, found = dtables.NewQueryCatalogTable(ctx, versionableTable), true
}
case doltdb.NonlocalTableName, doltdb.GetNonlocalTablesTableName():
backingTable, _, err := db.getTable(ctx, root, doltdb.NonlocalTableName)
if err != nil {
return nil, false, err
}
if backingTable == nil {
dt, found = dtables.NewEmptyNonlocalTablesTable(ctx), true
} else {
versionableTable := backingTable.(dtables.VersionableTable)
dt, found = dtables.NewNonlocallTablesTable(ctx, versionableTable), true
}
case doltdb.GetTestsTableName():
backingTable, _, err := db.getTable(ctx, root, doltdb.GetTestsTableName())
if err != nil {
@@ -905,6 +943,89 @@ func (db Database) getTableInsensitive(ctx *sql.Context, head *doltdb.Commit, ds
return resolveOverriddenNonexistentTable(ctx, tblName, db)
}
// getNonlocalTable checks whether the table name maps onto a table in another root via the dolt_nonlocal_tables system table
func (db Database) getNonlocalTable(ctx *sql.Context, root doltdb.RootValue, lwrName string) (sql.Table, bool, error) {
_, nonlocalsTable, nonlocalsTableExists, err := db.resolveUserTable(ctx, root, doltdb.GetNonlocalTablesTableName())
if err != nil {
return nil, false, err
}
if !nonlocalsTableExists {
return nil, false, nil
}
index, err := nonlocalsTable.GetRowData(ctx)
if err != nil {
return nil, false, err
}
nonlocalTablesSchema, err := nonlocalsTable.GetSchema(ctx)
if err != nil {
return nil, false, err
}
m := durable.MapFromIndex(index)
keyDesc, valueDesc := nonlocalTablesSchema.GetMapDescriptors(m.NodeStore())
nonlocalTablesMap, err := m.IterAll(ctx)
if err != nil {
return nil, false, err
}
var nonlocalTableEntry doltdb.NonlocalTableEntry
// check if there's an entry for this table. If so, resolve that reference.
for {
keyTuple, valueTuple, err := nonlocalTablesMap.Next(ctx)
if err == io.EOF {
break
}
if err != nil {
return nil, false, err
}
globalsEntryTableName, err := doltdb.GetNonlocalTablesNameColumn(ctx, keyDesc, keyTuple)
if err != nil {
return nil, false, err
}
globalsEntryTableName = strings.ToLower(globalsEntryTableName)
matchesPattern, err := doltdb.MatchTablePattern(globalsEntryTableName, lwrName)
if err != nil {
return nil, false, err
}
if !matchesPattern {
continue
}
nonlocalTableEntry = doltdb.GetNonlocalTablesRef(ctx, valueDesc, valueTuple)
if nonlocalTableEntry.NewTableName == "" {
nonlocalTableEntry.NewTableName = lwrName
}
if nonlocalTableEntry.Ref == "" {
nonlocalTableEntry.Ref = db.revision
}
if nonlocalTableEntry.Options != "immediate" {
return nil, false, ErrInvalidNonlocalTableOptions.New(nonlocalTableEntry.Options)
}
// If the ref is a branch, we get the working set, not the head.
_, exists, err := isBranch(ctx, db, nonlocalTableEntry.Ref)
if exists {
referencedBranch, err := RevisionDbForBranch(ctx, db, nonlocalTableEntry.Ref, db.requestedName)
if err != nil {
return nil, false, err
}
return referencedBranch.(Database).getTableInsensitive(ctx, nonlocalTableEntry.NewTableName, dontReadNonlocalTables)
} else {
// If we couldn't resolve it as a database revision, treat it as a noms ref.
// This lets us resolve branch heads like 'heads/$branchName' or remotes refs like '$remote/$branchName'
return db.getTableInsensitiveAsOf(ctx, nonlocalTableEntry.NewTableName, nonlocalTableEntry.Ref, false)
}
}
return nil, false, nil
}
// getRootsForBranch uses the specified |ddb| to look up a branch named |branch|, and return the
// Roots for it. If there is no branch named |branch|, then an error is returned.
func getRootsForBranch(ctx *sql.Context, ddb *doltdb.DoltDB, branch string) (doltdb.Roots, error) {
+37 -169
View File
@@ -15,17 +15,12 @@
package dtables
import (
"io"
"github.com/dolthub/go-mysql-server/sql"
sqlTypes "github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/vitess/go/sqltypes"
"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/index"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve"
"github.com/dolthub/dolt/go/store/hash"
)
var _ sql.Table = (*DocsTable)(nil)
@@ -37,25 +32,28 @@ var _ sql.IndexAddressableTable = (*DocsTable)(nil)
// DocsTable is the system table that stores Dolt docs, such as LICENSE and README.
type DocsTable struct {
backingTable VersionableTable
UserSpaceSystemTable
}
// NewDocsTable creates a DocsTable
func NewDocsTable(_ *sql.Context, backingTable VersionableTable) sql.Table {
return &DocsTable{backingTable: backingTable}
return &DocsTable{
UserSpaceSystemTable: UserSpaceSystemTable{
backingTable: backingTable,
tableName: getDoltDocsTableName(),
schema: GetDocsSchema(),
},
}
}
// NewEmptyDocsTable creates a DocsTable
func NewEmptyDocsTable(_ *sql.Context) sql.Table {
return &DocsTable{}
}
func (dt *DocsTable) Name() string {
return doltdb.GetDocTableName()
}
func (dt *DocsTable) String() string {
return doltdb.GetDocTableName()
return &DocsTable{
UserSpaceSystemTable: UserSpaceSystemTable{
tableName: getDoltDocsTableName(),
schema: GetDocsSchema(),
},
}
}
const defaultStringsLen = 16383 / 16
@@ -71,39 +69,27 @@ func getDoltDocsSchema() sql.Schema {
}
}
// Schema is a sql.Table interface function that gets the sql.Schema of the dolt_docs system table.
func (dt *DocsTable) Schema() sql.Schema {
return GetDocsSchema()
}
func (dt *DocsTable) Collation() sql.CollationID {
return sql.Collation_Default
}
// Partitions is a sql.Table interface function that returns a partition of the data.
func (dt *DocsTable) Partitions(context *sql.Context) (sql.PartitionIter, error) {
func (dt *DocsTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
if dt.backingTable == nil {
// no backing table; return an empty iter.
return index.SinglePartitionIterFromNomsMap(nil), nil
return dt, nil
}
return dt.backingTable.Partitions(context)
backingTableLockedToRoot, err := dt.backingTable.LockedToRoot(ctx, root)
if err != nil {
return nil, err
}
return &docsTableAsOf{backingTableLockedToRoot}, nil
}
func (dt *DocsTable) PartitionRows(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
var rowIter sql.RowIter
if dt.backingTable == nil {
// no backing table; empty iter.
rowIter = sql.RowsToRowIter()
} else {
var err error
rowIter, err = dt.backingTable.PartitionRows(ctx, partition)
if err != nil && err != io.EOF {
return nil, err
}
underlyingIter, err := dt.UserSpaceSystemTable.PartitionRows(ctx, partition)
if err != nil {
return nil, err
}
return makeDoltDocRows(ctx, underlyingIter)
}
rows, err := sql.RowIterToRows(ctx, rowIter)
func makeDoltDocRows(ctx *sql.Context, underlyingIter sql.RowIter) (sql.RowIter, error) {
rows, err := sql.RowIterToRows(ctx, underlyingIter)
if err != nil {
return nil, err
@@ -129,99 +115,7 @@ func (dt *DocsTable) PartitionRows(ctx *sql.Context, partition sql.Partition) (s
})
}
rowIter = sql.RowsToRowIter(rows...)
return rowIter, nil
}
// Replacer returns a RowReplacer for this table. The RowReplacer will have Insert and optionally Delete called once
// for each row, followed by a call to Close() when all rows have been processed.
func (dt *DocsTable) Replacer(ctx *sql.Context) sql.RowReplacer {
return newDocsWriter(dt)
}
// Updater returns a RowUpdater for this table. The RowUpdater will have Update called once for each row to be
// updated, followed by a call to Close() when all rows have been processed.
func (dt *DocsTable) Updater(ctx *sql.Context) sql.RowUpdater {
return newDocsWriter(dt)
}
// Inserter returns an Inserter for this table. The Inserter will get one call to Insert() for each row to be
// inserted, and will end with a call to Close() to finalize the insert operation.
func (dt *DocsTable) Inserter(*sql.Context) sql.RowInserter {
return newDocsWriter(dt)
}
// Deleter returns a RowDeleter for this table. The RowDeleter will get one call to Delete for each row to be deleted,
// and will end with a call to Close() to finalize the delete operation.
func (dt *DocsTable) Deleter(*sql.Context) sql.RowDeleter {
return newDocsWriter(dt)
}
func (dt *DocsTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
if dt.backingTable == nil {
return dt, nil
}
return dt.backingTable.LockedToRoot(ctx, root)
}
// IndexedAccess implements IndexAddressableTable, but DocsTables has no indexes.
// Thus, this should never be called.
func (dt *DocsTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
panic("Unreachable")
}
// GetIndexes implements IndexAddressableTable, but DocsTables has no indexes.
func (dt *DocsTable) GetIndexes(ctx *sql.Context) ([]sql.Index, error) {
return nil, nil
}
func (dt *DocsTable) PreciseMatch() bool {
return true
}
var _ sql.RowReplacer = (*docsWriter)(nil)
var _ sql.RowUpdater = (*docsWriter)(nil)
var _ sql.RowInserter = (*docsWriter)(nil)
var _ sql.RowDeleter = (*docsWriter)(nil)
type docsWriter struct {
it *DocsTable
errDuringStatementBegin error
prevHash *hash.Hash
tableWriter dsess.TableWriter
}
func newDocsWriter(it *DocsTable) *docsWriter {
return &docsWriter{it, nil, nil, nil}
}
// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process
// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close
// is called.
func (iw *docsWriter) Insert(ctx *sql.Context, r sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Insert(ctx, r)
}
// Update the given row. Provides both the old and new rows.
func (iw *docsWriter) Update(ctx *sql.Context, old sql.Row, new sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Update(ctx, old, new)
}
// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for
// each row to process for the delete operation, which may involve many rows. After all rows have been processed,
// Close is called.
func (iw *docsWriter) Delete(ctx *sql.Context, r sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Delete(ctx, r)
return sql.RowsToRowIter(rows...), nil
}
func getDoltDocsTableName() doltdb.TableName {
@@ -231,40 +125,14 @@ func getDoltDocsTableName() doltdb.TableName {
return doltdb.TableName{Name: doltdb.GetDocTableName()}
}
// StatementBegin is called before the first operation of a statement. Integrators should mark the state of the data
// in some way that it may be returned to in the case of an error.
func (iw *docsWriter) StatementBegin(ctx *sql.Context) {
name := getDoltDocsTableName()
prevHash, tableWriter, err := createWriteableSystemTable(ctx, name, iw.it.Schema())
type docsTableAsOf struct {
sql.IndexAddressableTable
}
func (dt *docsTableAsOf) PartitionRows(ctx *sql.Context, partition sql.Partition) (sql.RowIter, error) {
underlyingIter, err := dt.IndexAddressableTable.PartitionRows(ctx, partition)
if err != nil {
iw.errDuringStatementBegin = err
return nil, err
}
iw.prevHash = prevHash
iw.tableWriter = tableWriter
}
// DiscardChanges is called if a statement encounters an error, and all current changes since the statement beginning
// should be discarded.
func (iw *docsWriter) DiscardChanges(ctx *sql.Context, errorEncountered error) error {
if iw.tableWriter != nil {
return iw.tableWriter.DiscardChanges(ctx, errorEncountered)
}
return nil
}
// StatementComplete is called after the last operation of the statement, indicating that it has successfully completed.
// The mark set in StatementBegin may be removed, and a new one should be created on the next StatementBegin.
func (iw *docsWriter) StatementComplete(ctx *sql.Context) error {
if iw.tableWriter != nil {
return iw.tableWriter.StatementComplete(ctx)
}
return nil
}
// Close finalizes the delete operation, persisting the result.
func (iw docsWriter) Close(ctx *sql.Context) error {
if iw.tableWriter != nil {
return iw.tableWriter.Close(ctx)
}
return nil
return makeDoltDocRows(ctx, underlyingIter)
}
@@ -15,39 +15,12 @@
package dtables
import (
"fmt"
"github.com/dolthub/go-mysql-server/sql"
sqlTypes "github.com/dolthub/go-mysql-server/sql/types"
"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/index"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
"github.com/dolthub/dolt/go/store/hash"
)
var _ sql.Table = (*IgnoreTable)(nil)
var _ sql.UpdatableTable = (*IgnoreTable)(nil)
var _ sql.DeletableTable = (*IgnoreTable)(nil)
var _ sql.InsertableTable = (*IgnoreTable)(nil)
var _ sql.ReplaceableTable = (*IgnoreTable)(nil)
var _ sql.IndexAddressableTable = (*IgnoreTable)(nil)
// IgnoreTable is the system table that stores patterns for table names that should not be committed.
type IgnoreTable struct {
backingTable VersionableTable
schemaName string
}
func (i *IgnoreTable) Name() string {
return doltdb.IgnoreTableName
}
func (i *IgnoreTable) String() string {
return doltdb.IgnoreTableName
}
func doltIgnoreSchema() sql.Schema {
return []*sql.Column{
{Name: "pattern", Type: sqlTypes.Text, Source: doltdb.IgnoreTableName, PrimaryKey: true},
@@ -59,239 +32,25 @@ func doltIgnoreSchema() sql.Schema {
// by Doltgres to update the dolt_ignore schema using Doltgres types.
var GetDoltIgnoreSchema = doltIgnoreSchema
// Schema is a sql.Table interface function that gets the sql.Schema of the dolt_ignore system table.
func (i *IgnoreTable) Schema() sql.Schema {
return GetDoltIgnoreSchema()
}
func (i *IgnoreTable) Collation() sql.CollationID {
return sql.Collation_Default
}
// Partitions is a sql.Table interface function that returns a partition of the data.
func (i *IgnoreTable) Partitions(context *sql.Context) (sql.PartitionIter, error) {
if i.backingTable == nil {
// no backing table; return an empty iter.
return index.SinglePartitionIterFromNomsMap(nil), nil
}
return i.backingTable.Partitions(context)
}
func (i *IgnoreTable) PartitionRows(context *sql.Context, partition sql.Partition) (sql.RowIter, error) {
if i.backingTable == nil {
// no backing table; return an empty iter.
return sql.RowsToRowIter(), nil
}
return i.backingTable.PartitionRows(context, partition)
}
// NewIgnoreTable creates an IgnoreTable
// NewIgnoreTable creates a dolt_ignore table
func NewIgnoreTable(_ *sql.Context, backingTable VersionableTable, schemaName string) sql.Table {
return &IgnoreTable{backingTable: backingTable, schemaName: schemaName}
return &UserSpaceSystemTable{
backingTable: backingTable,
tableName: doltdb.TableName{
Name: doltdb.IgnoreTableName,
Schema: schemaName,
},
schema: GetDoltIgnoreSchema(),
}
}
// NewEmptyIgnoreTable creates an IgnoreTable
// NewEmptyIgnoreTable creates an empty dolt_ignore table
func NewEmptyIgnoreTable(_ *sql.Context, schemaName string) sql.Table {
return &IgnoreTable{schemaName: schemaName}
}
// Replacer returns a RowReplacer for this table. The RowReplacer will have Insert and optionally Delete called once
// for each row, followed by a call to Close() when all rows have been processed.
func (it *IgnoreTable) Replacer(ctx *sql.Context) sql.RowReplacer {
return newIgnoreWriter(it)
}
// Updater returns a RowUpdater for this table. The RowUpdater will have Update called once for each row to be
// updated, followed by a call to Close() when all rows have been processed.
func (it *IgnoreTable) Updater(ctx *sql.Context) sql.RowUpdater {
return newIgnoreWriter(it)
}
// Inserter returns an Inserter for this table. The Inserter will get one call to Insert() for each row to be
// inserted, and will end with a call to Close() to finalize the insert operation.
func (it *IgnoreTable) Inserter(*sql.Context) sql.RowInserter {
return newIgnoreWriter(it)
}
// Deleter returns a RowDeleter for this table. The RowDeleter will get one call to Delete for each row to be deleted,
// and will end with a call to Close() to finalize the delete operation.
func (it *IgnoreTable) Deleter(*sql.Context) sql.RowDeleter {
return newIgnoreWriter(it)
}
func (it *IgnoreTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
if it.backingTable == nil {
return it, nil
return &UserSpaceSystemTable{
tableName: doltdb.TableName{
Name: doltdb.IgnoreTableName,
Schema: schemaName,
},
schema: GetDoltIgnoreSchema(),
}
return it.backingTable.LockedToRoot(ctx, root)
}
// IndexedAccess implements IndexAddressableTable, but IgnoreTables has no indexes.
// Thus, this should never be called.
func (it *IgnoreTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
panic("Unreachable")
}
// GetIndexes implements IndexAddressableTable, but IgnoreTables has no indexes.
func (it *IgnoreTable) GetIndexes(ctx *sql.Context) ([]sql.Index, error) {
return nil, nil
}
func (i *IgnoreTable) PreciseMatch() bool {
return true
}
var _ sql.RowReplacer = (*ignoreWriter)(nil)
var _ sql.RowUpdater = (*ignoreWriter)(nil)
var _ sql.RowInserter = (*ignoreWriter)(nil)
var _ sql.RowDeleter = (*ignoreWriter)(nil)
type ignoreWriter struct {
it *IgnoreTable
errDuringStatementBegin error
prevHash *hash.Hash
tableWriter dsess.TableWriter
}
func newIgnoreWriter(it *IgnoreTable) *ignoreWriter {
return &ignoreWriter{it, nil, nil, nil}
}
// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process
// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close
// is called.
func (iw *ignoreWriter) Insert(ctx *sql.Context, r sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Insert(ctx, r)
}
// Update the given row. Provides both the old and new rows.
func (iw *ignoreWriter) Update(ctx *sql.Context, old sql.Row, new sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Update(ctx, old, new)
}
// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for
// each row to process for the delete operation, which may involve many rows. After all rows have been processed,
// Close is called.
func (iw *ignoreWriter) Delete(ctx *sql.Context, r sql.Row) error {
if err := iw.errDuringStatementBegin; err != nil {
return err
}
return iw.tableWriter.Delete(ctx, r)
}
// StatementBegin is called before the first operation of a statement. Integrators should mark the state of the data
// in some way that it may be returned to in the case of an error.
func (iw *ignoreWriter) StatementBegin(ctx *sql.Context) {
name := doltdb.TableName{Name: doltdb.IgnoreTableName, Schema: iw.it.schemaName}
prevHash, tableWriter, err := createWriteableSystemTable(ctx, name, iw.it.Schema())
if err != nil {
iw.errDuringStatementBegin = err
return
}
iw.prevHash = prevHash
iw.tableWriter = tableWriter
}
// DiscardChanges is called if a statement encounters an error, and all current changes since the statement beginning
// should be discarded.
func (iw *ignoreWriter) DiscardChanges(ctx *sql.Context, errorEncountered error) error {
if iw.tableWriter != nil {
return iw.tableWriter.DiscardChanges(ctx, errorEncountered)
}
return nil
}
// StatementComplete is called after the last operation of the statement, indicating that it has successfully completed.
// The mark set in StatementBegin may be removed, and a new one should be created on the next StatementBegin.
func (iw *ignoreWriter) StatementComplete(ctx *sql.Context) error {
if iw.tableWriter != nil {
return iw.tableWriter.StatementComplete(ctx)
}
return nil
}
// Close finalizes the delete operation, persisting the result.
func (iw ignoreWriter) Close(ctx *sql.Context) error {
if iw.tableWriter != nil {
return iw.tableWriter.Close(ctx)
}
return nil
}
// CreateWriteableSystemTable is a helper function that creates a writeable system table (dolt_ignore, dolt_docs...) if it does not exist
// Then returns the hash of the previous working root, and a TableWriter.
func createWriteableSystemTable(ctx *sql.Context, tblName doltdb.TableName, tblSchema sql.Schema) (*hash.Hash, dsess.TableWriter, error) {
dbName := ctx.GetCurrentDatabase()
dSess := dsess.DSessFromSess(ctx.Session)
roots, _ := dSess.GetRoots(ctx, dbName)
dbState, ok, err := dSess.LookupDbState(ctx, dbName)
if err != nil {
return nil, nil, err
}
if !ok {
return nil, nil, fmt.Errorf("no root value found in session")
}
prevHash, err := roots.Working.HashOf()
if err != nil {
return nil, nil, err
}
found, err := roots.Working.HasTable(ctx, tblName)
if err != nil {
return nil, nil, err
}
if !found {
sch := sql.NewPrimaryKeySchema(tblSchema)
doltSch, err := sqlutil.ToDoltSchema(ctx, roots.Working, tblName, sch, roots.Head, sql.Collation_Default)
if err != nil {
return nil, nil, err
}
// underlying table doesn't exist. Record this, then create the table.
newRootValue, err := doltdb.CreateEmptyTable(ctx, roots.Working, tblName, doltSch)
if err != nil {
return nil, nil, err
}
if dbState.WorkingSet() == nil {
return nil, nil, doltdb.ErrOperationNotSupportedInDetachedHead
}
// We use WriteSession.SetWorkingSet instead of DoltSession.SetWorkingRoot because we want to avoid modifying the root
// until the end of the transaction, but we still want the WriteSession to be able to find the newly
// created table.
if ws := dbState.WriteSession(); ws != nil {
err = ws.SetWorkingSet(ctx, dbState.WorkingSet().WithWorkingRoot(newRootValue))
if err != nil {
return nil, nil, err
}
} else {
return nil, nil, fmt.Errorf("could not create dolt_ignore table, database does not allow writing")
}
}
var tableWriter dsess.TableWriter
if ws := dbState.WriteSession(); ws != nil {
tableWriter, err = ws.GetTableWriter(ctx, tblName, dbName, dSess.SetWorkingRoot, false)
if err != nil {
return nil, nil, err
}
tableWriter.StatementBegin(ctx)
} else {
return nil, nil, fmt.Errorf("could not create dolt_ignore table, database does not allow writing")
}
return &prevHash, tableWriter, nil
}
@@ -0,0 +1,58 @@
// Copyright 2025 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 dtables
import (
"github.com/dolthub/go-mysql-server/sql"
sqlTypes "github.com/dolthub/go-mysql-server/sql/types"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve"
)
func doltNonlocalTablesSchema() sql.Schema {
return []*sql.Column{
{Name: doltdb.NonlocalTableTableNameCol, Type: sqlTypes.VarChar, Source: doltdb.GetNonlocalTablesTableName(), PrimaryKey: true},
{Name: doltdb.NonlocalTableRefCol, Type: sqlTypes.VarChar, Source: doltdb.GetNonlocalTablesTableName(), Nullable: true},
{Name: doltdb.NonlocalTablesRefTableCol, Type: sqlTypes.VarChar, Source: doltdb.GetNonlocalTablesTableName(), Nullable: true},
{Name: doltdb.NonlocalTablesOptionsCol, Type: sqlTypes.VarChar, Source: doltdb.GetNonlocalTablesTableName(), Nullable: true},
}
}
var GetDoltNonlocalTablesSchema = doltNonlocalTablesSchema
// NewNonlocallTablesTable creates a new dolt_table_aliases table
func NewNonlocallTablesTable(_ *sql.Context, backingTable VersionableTable) sql.Table {
return &UserSpaceSystemTable{
backingTable: backingTable,
tableName: getDoltNonlocalTablesName(),
schema: GetDoltNonlocalTablesSchema(),
}
}
// NewEmptyNonlocalTablesTable creates an empty dolt_table_aliases table
func NewEmptyNonlocalTablesTable(_ *sql.Context) sql.Table {
return &UserSpaceSystemTable{
tableName: getDoltNonlocalTablesName(),
schema: GetDoltNonlocalTablesSchema(),
}
}
func getDoltNonlocalTablesName() doltdb.TableName {
if resolve.UseSearchPath {
return doltdb.TableName{Schema: doltdb.DoltNamespace, Name: doltdb.GetNonlocalTablesTableName()}
}
return doltdb.TableName{Name: doltdb.GetNonlocalTablesTableName()}
}
@@ -19,33 +19,9 @@ import (
sqlTypes "github.com/dolthub/go-mysql-server/sql/types"
"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/index"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/resolve"
"github.com/dolthub/dolt/go/store/hash"
)
var _ sql.Table = (*QueryCatalogTable)(nil)
var _ sql.UpdatableTable = (*QueryCatalogTable)(nil)
var _ sql.DeletableTable = (*QueryCatalogTable)(nil)
var _ sql.InsertableTable = (*QueryCatalogTable)(nil)
var _ sql.ReplaceableTable = (*QueryCatalogTable)(nil)
var _ VersionableTable = (*QueryCatalogTable)(nil)
var _ sql.IndexAddressableTable = (*QueryCatalogTable)(nil)
// QueryCatalogTable is the system table that stores saved queries.
type QueryCatalogTable struct {
backingTable VersionableTable
}
func (qct *QueryCatalogTable) Name() string {
return doltdb.DoltQueryCatalogTableName
}
func (qct *QueryCatalogTable) String() string {
return doltdb.DoltQueryCatalogTableName
}
func doltQueryCatalogSchema() sql.Schema {
return []*sql.Column{
{Name: doltdb.QueryCatalogIdCol, Type: sqlTypes.LongText, Source: doltdb.GetQueryCatalogTableName(), PrimaryKey: true},
@@ -58,139 +34,21 @@ func doltQueryCatalogSchema() sql.Schema {
var GetDoltQueryCatalogSchema = doltQueryCatalogSchema
func (qct *QueryCatalogTable) Schema() sql.Schema {
return GetDoltQueryCatalogSchema()
}
func (qct *QueryCatalogTable) Collation() sql.CollationID {
return sql.Collation_Default
}
func (qct *QueryCatalogTable) Partitions(context *sql.Context) (sql.PartitionIter, error) {
if qct.backingTable == nil {
// no backing table; return an empty iter.
return index.SinglePartitionIterFromNomsMap(nil), nil
}
return qct.backingTable.Partitions(context)
}
func (qct *QueryCatalogTable) PartitionRows(context *sql.Context, partition sql.Partition) (sql.RowIter, error) {
if qct.backingTable == nil {
// no backing table; return an empty iter.
return sql.RowsToRowIter(), nil
}
return qct.backingTable.PartitionRows(context, partition)
}
// NewQueryCatalogTable creates a QueryCatalogTable
func NewQueryCatalogTable(_ *sql.Context, backingTable VersionableTable) sql.Table {
return &QueryCatalogTable{backingTable: backingTable}
return &UserSpaceSystemTable{
backingTable: backingTable,
tableName: getDoltQueryCatalogTableName(),
schema: GetDoltQueryCatalogSchema(),
}
}
// NewEmptyQueryCatalogTable creates an QueryCatalogTable
// NewEmptyQueryCatalogTable creates an empty QueryCatalogTable
func NewEmptyQueryCatalogTable(_ *sql.Context) sql.Table {
return &QueryCatalogTable{}
}
func (qct *QueryCatalogTable) Replacer(_ *sql.Context) sql.RowReplacer {
return newQueryCatalogWriter(qct)
}
// Updater returns a RowUpdater for this table. The RowUpdater will have Update called once for each row to be
// updated, followed by a call to Close() when all rows have been processed.
func (qct *QueryCatalogTable) Updater(_ *sql.Context) sql.RowUpdater {
return newQueryCatalogWriter(qct)
}
// Inserter returns an Inserter for this table. The Inserter will get one call to Insert() for each row to be
// inserted, and will end with a call to Close() to finalize the insert operation.
func (qct *QueryCatalogTable) Inserter(*sql.Context) sql.RowInserter {
return newQueryCatalogWriter(qct)
}
// Deleter returns a RowDeleter for this table. The RowDeleter will get one call to Delete for each row to be deleted,
// and will end with a call to Close() to finalize the delete operation.
func (qct *QueryCatalogTable) Deleter(*sql.Context) sql.RowDeleter {
return newQueryCatalogWriter(qct)
}
func (qct *QueryCatalogTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
if qct.backingTable == nil {
return qct, nil
return &UserSpaceSystemTable{
tableName: getDoltQueryCatalogTableName(),
schema: GetDoltQueryCatalogSchema(),
}
return qct.backingTable.LockedToRoot(ctx, root)
}
// IndexedAccess implements IndexAddressableTable, but QueryCatalogTable has no indexes.
// Thus, this should never be called.
func (qct *QueryCatalogTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
panic("Unreachable")
}
// GetIndexes implements IndexAddressableTable, but QueryCatalogTable has no indexes.
func (qct *QueryCatalogTable) GetIndexes(ctx *sql.Context) ([]sql.Index, error) {
return nil, nil
}
func (qct *QueryCatalogTable) PreciseMatch() bool {
return true
}
var _ sql.RowReplacer = (*queryCatalogWriter)(nil)
var _ sql.RowUpdater = (*queryCatalogWriter)(nil)
var _ sql.RowInserter = (*queryCatalogWriter)(nil)
var _ sql.RowDeleter = (*queryCatalogWriter)(nil)
type queryCatalogWriter struct {
qct *QueryCatalogTable
errDuringStatementBegin error
prevHash *hash.Hash
tableWriter dsess.TableWriter
}
func newQueryCatalogWriter(qct *QueryCatalogTable) *queryCatalogWriter {
return &queryCatalogWriter{qct, nil, nil, nil}
}
// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process
// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close
// is called.
func (qw *queryCatalogWriter) Insert(ctx *sql.Context, r sql.Row) error {
if err := qw.errDuringStatementBegin; err != nil {
return err
}
return qw.tableWriter.Insert(ctx, r)
}
// Update the given row. Provides both the old and new rows.
func (qw *queryCatalogWriter) Update(ctx *sql.Context, old sql.Row, new sql.Row) error {
if err := qw.errDuringStatementBegin; err != nil {
return err
}
return qw.tableWriter.Update(ctx, old, new)
}
// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for
// each row to process for the delete operation, which may involve many rows. After all rows have been processed,
// Close is called.
func (qw *queryCatalogWriter) Delete(ctx *sql.Context, r sql.Row) error {
if err := qw.errDuringStatementBegin; err != nil {
return err
}
return qw.tableWriter.Delete(ctx, r)
}
// StatementBegin is called before the first operation of a statement. Integrators should mark the state of the data
// in some way that it may be returned to in the case of an error.
func (qw *queryCatalogWriter) StatementBegin(ctx *sql.Context) {
name := getDoltQueryCatalogTableName()
prevHash, tableWriter, err := createWriteableSystemTable(ctx, name, qw.qct.Schema())
if err != nil {
qw.errDuringStatementBegin = err
return
}
qw.prevHash = prevHash
qw.tableWriter = tableWriter
}
func getDoltQueryCatalogTableName() doltdb.TableName {
@@ -199,29 +57,3 @@ func getDoltQueryCatalogTableName() doltdb.TableName {
}
return doltdb.TableName{Name: doltdb.GetQueryCatalogTableName()}
}
// DiscardChanges is called if a statement encounters an error, and all current changes since the statement beginning
// should be discarded.
func (qw *queryCatalogWriter) DiscardChanges(ctx *sql.Context, errorEncountered error) error {
if qw.tableWriter != nil {
return qw.tableWriter.DiscardChanges(ctx, errorEncountered)
}
return nil
}
// StatementComplete is called after the last operation of the statement, indicating that it has successfully completed.
// The mark set in StatementBegin may be removed, and a new one should be created on the next StatementBegin.
func (qw *queryCatalogWriter) StatementComplete(ctx *sql.Context) error {
if qw.tableWriter != nil {
return qw.tableWriter.StatementComplete(ctx)
}
return nil
}
// Close finalizes the delete operation, persisting the result.
func (qw *queryCatalogWriter) Close(ctx *sql.Context) error {
if qw.tableWriter != nil {
return qw.tableWriter.Close(ctx)
}
return nil
}
@@ -0,0 +1,276 @@
// Copyright 2025 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 dtables
import (
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"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/index"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
"github.com/dolthub/dolt/go/store/hash"
)
var _ sql.Table = (*UserSpaceSystemTable)(nil)
var _ sql.UpdatableTable = (*UserSpaceSystemTable)(nil)
var _ sql.DeletableTable = (*UserSpaceSystemTable)(nil)
var _ sql.InsertableTable = (*UserSpaceSystemTable)(nil)
var _ sql.ReplaceableTable = (*UserSpaceSystemTable)(nil)
var _ sql.IndexAddressableTable = (*UserSpaceSystemTable)(nil)
// A UserSpaceSystemTable is a system table backed by a normal table in storage.
// Like other system tables, it always exists. If the backing table doesn't exist, then reads return an empty table,
// and writes will create the table.
type UserSpaceSystemTable struct {
backingTable VersionableTable
tableName doltdb.TableName
schema sql.Schema
}
func (bst *UserSpaceSystemTable) Name() string {
return bst.tableName.Name
}
func (bst *UserSpaceSystemTable) String() string {
return bst.tableName.Name
}
func (bst *UserSpaceSystemTable) Schema() sql.Schema {
return bst.schema
}
func (bst *UserSpaceSystemTable) Collation() sql.CollationID {
return sql.Collation_Default
}
// Partitions is a sql.Table interface function that returns a partition of the data.
func (bst *UserSpaceSystemTable) Partitions(context *sql.Context) (sql.PartitionIter, error) {
if bst.backingTable == nil {
// no backing table; return an empty iter.
return index.SinglePartitionIterFromNomsMap(nil), nil
}
return bst.backingTable.Partitions(context)
}
func (bst *UserSpaceSystemTable) PartitionRows(context *sql.Context, partition sql.Partition) (sql.RowIter, error) {
if bst.backingTable == nil {
// no backing table; return an empty iter.
return sql.RowsToRowIter(), nil
}
return bst.backingTable.PartitionRows(context, partition)
}
// Replacer returns a RowReplacer for this table. The RowReplacer will have Insert and optionally Delete called once
// for each row, followed by a call to Close() when all rows have been processed.
func (bst *UserSpaceSystemTable) Replacer(ctx *sql.Context) sql.RowReplacer {
return newBackedSystemTableWriter(bst)
}
// Updater returns a RowUpdater for this table. The RowUpdater will have Update called once for each row to be
// updated, followed by a call to Close() when all rows have been processed.
func (bst *UserSpaceSystemTable) Updater(ctx *sql.Context) sql.RowUpdater {
return newBackedSystemTableWriter(bst)
}
// Inserter returns an Inserter for this table. The Inserter will get one call to Insert() for each row to be
// inserted, and will end with a call to Close() to finalize the insert operation.
func (bst *UserSpaceSystemTable) Inserter(*sql.Context) sql.RowInserter {
return newBackedSystemTableWriter(bst)
}
// Deleter returns a RowDeleter for this table. The RowDeleter will get one call to Delete for each row to be deleted,
// and will end with a call to Close() to finalize the delete operation.
func (bst *UserSpaceSystemTable) Deleter(*sql.Context) sql.RowDeleter {
return newBackedSystemTableWriter(bst)
}
func (bst *UserSpaceSystemTable) LockedToRoot(ctx *sql.Context, root doltdb.RootValue) (sql.IndexAddressableTable, error) {
if bst.backingTable == nil {
return bst, nil
}
return bst.backingTable.LockedToRoot(ctx, root)
}
// IndexedAccess implements IndexAddressableTable, but UserSpaceSystemTable has no indexes.
// Thus, this should never be called.
func (bst *UserSpaceSystemTable) IndexedAccess(ctx *sql.Context, lookup sql.IndexLookup) sql.IndexedTable {
panic("Unreachable")
}
// GetIndexes implements IndexAddressableTable, but IgnoreTables has no indexes.
func (bst *UserSpaceSystemTable) GetIndexes(ctx *sql.Context) ([]sql.Index, error) {
return nil, nil
}
func (bst *UserSpaceSystemTable) PreciseMatch() bool {
return true
}
var _ sql.RowReplacer = (*backedSystemTableWriter)(nil)
var _ sql.RowUpdater = (*backedSystemTableWriter)(nil)
var _ sql.RowInserter = (*backedSystemTableWriter)(nil)
var _ sql.RowDeleter = (*backedSystemTableWriter)(nil)
type backedSystemTableWriter struct {
bst *UserSpaceSystemTable
errDuringStatementBegin error
prevHash *hash.Hash
tableWriter dsess.TableWriter
}
func newBackedSystemTableWriter(bst *UserSpaceSystemTable) *backedSystemTableWriter {
return &backedSystemTableWriter{bst, nil, nil, nil}
}
// Insert inserts the row given, returning an error if it cannot. Insert will be called once for each row to process
// for the insert operation, which may involve many rows. After all rows in an operation have been processed, Close
// is called.
func (bstw *backedSystemTableWriter) Insert(ctx *sql.Context, r sql.Row) error {
if err := bstw.errDuringStatementBegin; err != nil {
return err
}
return bstw.tableWriter.Insert(ctx, r)
}
// Update the given row. Provides both the old and new rows.
func (bstw *backedSystemTableWriter) Update(ctx *sql.Context, old sql.Row, new sql.Row) error {
if err := bstw.errDuringStatementBegin; err != nil {
return err
}
return bstw.tableWriter.Update(ctx, old, new)
}
// Delete deletes the given row. Returns ErrDeleteRowNotFound if the row was not found. Delete will be called once for
// each row to process for the delete operation, which may involve many rows. After all rows have been processed,
// Close is called.
func (bstw *backedSystemTableWriter) Delete(ctx *sql.Context, r sql.Row) error {
if err := bstw.errDuringStatementBegin; err != nil {
return err
}
return bstw.tableWriter.Delete(ctx, r)
}
// StatementBegin is called before the first operation of a statement. Integrators should mark the state of the data
// in some way that it may be returned to in the case of an error.
func (bstw *backedSystemTableWriter) StatementBegin(ctx *sql.Context) {
prevHash, tableWriter, err := createWriteableSystemTable(ctx, bstw.bst.tableName, bstw.bst.Schema())
if err != nil {
bstw.errDuringStatementBegin = err
return
}
bstw.prevHash = prevHash
bstw.tableWriter = tableWriter
}
// DiscardChanges is called if a statement encounters an error, and all current changes since the statement beginning
// should be discarded.
func (bstw *backedSystemTableWriter) DiscardChanges(ctx *sql.Context, errorEncountered error) error {
if bstw.tableWriter != nil {
return bstw.tableWriter.DiscardChanges(ctx, errorEncountered)
}
return nil
}
// StatementComplete is called after the last operation of the statement, indicating that it has successfully completed.
// The mark set in StatementBegin may be removed, and a new one should be created on the next StatementBegin.
func (bstw *backedSystemTableWriter) StatementComplete(ctx *sql.Context) error {
if bstw.tableWriter != nil {
return bstw.tableWriter.StatementComplete(ctx)
}
return nil
}
// Close finalizes the delete operation, persisting the result.
func (bstw backedSystemTableWriter) Close(ctx *sql.Context) error {
if bstw.tableWriter != nil {
return bstw.tableWriter.Close(ctx)
}
return nil
}
// CreateWriteableSystemTable is a helper function that creates a writeable system table (dolt_ignore, dolt_docs...) if it does not exist
// Then returns the hash of the previous working root, and a TableWriter.
func createWriteableSystemTable(ctx *sql.Context, tblName doltdb.TableName, tblSchema sql.Schema) (*hash.Hash, dsess.TableWriter, error) {
dbName := ctx.GetCurrentDatabase()
dSess := dsess.DSessFromSess(ctx.Session)
roots, _ := dSess.GetRoots(ctx, dbName)
dbState, ok, err := dSess.LookupDbState(ctx, dbName)
if err != nil {
return nil, nil, err
}
if !ok {
return nil, nil, fmt.Errorf("no root value found in session")
}
prevHash, err := roots.Working.HashOf()
if err != nil {
return nil, nil, err
}
found, err := roots.Working.HasTable(ctx, tblName)
if err != nil {
return nil, nil, err
}
if !found {
sch := sql.NewPrimaryKeySchema(tblSchema)
doltSch, err := sqlutil.ToDoltSchema(ctx, roots.Working, tblName, sch, roots.Head, sql.Collation_Default)
if err != nil {
return nil, nil, err
}
// underlying table doesn't exist. Record this, then create the table.
newRootValue, err := doltdb.CreateEmptyTable(ctx, roots.Working, tblName, doltSch)
if err != nil {
return nil, nil, err
}
if dbState.WorkingSet() == nil {
return nil, nil, doltdb.ErrOperationNotSupportedInDetachedHead
}
// We use WriteSession.SetWorkingSet instead of DoltSession.SetWorkingRoot because we want to avoid modifying the root
// until the end of the transaction, but we still want the WriteSession to be able to find the newly
// created table.
if ws := dbState.WriteSession(); ws != nil {
err = ws.SetWorkingSet(ctx, dbState.WorkingSet().WithWorkingRoot(newRootValue))
if err != nil {
return nil, nil, err
}
} else {
return nil, nil, fmt.Errorf("could not create %s table, database does not allow writing", tblName)
}
}
var tableWriter dsess.TableWriter
if ws := dbState.WriteSession(); ws != nil {
tableWriter, err = ws.GetTableWriter(ctx, tblName, dbName, dSess.SetWorkingRoot, false)
if err != nil {
return nil, nil, err
}
tableWriter.StatementBegin(ctx)
} else {
return nil, nil, fmt.Errorf("could not create %s table, database does not allow writing", tblName)
}
return &prevHash, tableWriter, nil
}
+1 -1
View File
@@ -658,7 +658,7 @@ type WritableDoltTable struct {
var _ doltTableInterface = (*WritableDoltTable)(nil)
// WritableDoltTableWrapper is an interface that allows a table to be returned as an sql.Table, but actually be a wrapped
// fake table. Specifically, databases.getTableInsensitive will returns an sql.Table, and there are cases where we
// fake table. Specifically, databases.getTableInsensitiveWithRoot will returns an sql.Table, and there are cases where we
// want to return a table that hasn't been materialized yet.
type WritableDoltTableWrapper interface {
// Unwrap returns the underlying WritableDoltTable, nil returns are expected when the wrapped table hasn't been materialized
@@ -143,6 +143,7 @@ SKIP_SERVER_TESTS=$(cat <<-EOM
~import-no-header-psv.bats~
~admin-conjoin.bats~
~admin-archive-inspect.bats~
~nonlocal.bats~
EOM
)
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env bats
load $BATS_TEST_DIRNAME/helper/common.bash
setup() {
setup_common
}
teardown() {
assert_feature_version
teardown_common
}
@test "nonlocal: basic case" {
dolt checkout -b other
dolt sql <<SQL
CALL dolt_checkout('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
CALL dolt_checkout('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_branch", "main", "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias_branch;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
# Nonlocal tables appear in "show create", but the output matches the aliased table.
run dolt sql -q "show create table table_alias_branch"
[ "$status" -eq 0 ]
[[ "$output" =~ "aliased_table" ]] || false
}
@test "nonlocal: branch name reflects the working set of the referenced branch" {
dolt checkout -b other
dolt sql <<SQL
CALL dolt_checkout('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
CALL dolt_checkout('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_branch", "main", "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias_branch;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
}
@test "nonlocal: branch ref reflects the committed version of the parent" {
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ('amzmapqt');
CALL DOLT_COMMIT('-Am', 'create table');
INSERT INTO aliased_table VALUES ('eesekkgo');
CALL DOLT_CHECKOUT('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_branch_ref", "heads/main", "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias_branch_ref;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
! [[ "$output" =~ "eesekkgo" ]] || false
}
@test "nonlocal: tag and hash" {
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
CALL DOLT_COMMIT('-Am', 'commit');
CALL DOLT_TAG('v1.0');
CALL DOLT_CHECKOUT('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_tag", "v1.0", "aliased_table", "immediate"),
("table_alias_tag_ref", "tags/v1.0", "aliased_table", "immediate"),
("table_alias_hash", DOLT_HASHOF('v1.0'), "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias_tag_ref;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
run dolt sql -q "select * from table_alias_tag;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
run dolt sql -q "select * from table_alias_hash;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
# These nonlocal tables are read-only because they reference a read-only ref
run dolt sql <<SQL
INSERT INTO table_alias_tag_ref VALUES ("eesekkgo");
SQL
[ "$status" -eq 1 ]
[[ "$output" =~ "table doesn't support INSERT INTO" ]] || false
run dolt sql <<SQL
INSERT INTO table_alias_tag VALUES ("eesekkgo");
SQL
[ "$status" -eq 1 ]
[[ "$output" =~ "table doesn't support INSERT INTO" ]] || false
run dolt sql <<SQL
INSERT INTO table_alias_hash VALUES ("eesekkgo");
SQL
[ "$status" -eq 1 ]
[[ "$output" =~ "table doesn't support INSERT INTO" ]] || false
}
@test "nonlocal: remote ref" {
mkdir child
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
CALL DOLT_COMMIT('-Am', 'create table');
CALL DOLT_REMOTE('add', 'remote_db', 'file://./remote');
CALL DOLT_PUSH('remote_db', 'main');
-- drop table so it is only accessible from the remote ref
DROP TABLE aliased_table;
CALL DOLT_COMMIT('-am', 'drop table');
CALL DOLT_CHECKOUT('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_remote_branch", "remote_db/main", "aliased_table", "immediate"),
("table_alias_remote_ref", "remotes/remote_db/main", "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias_remote_branch;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
run dolt sql -q "select * from table_alias_remote_ref;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
}
@test "nonlocal: default ref" {
# If unspecified, the ref defaults to the current HEAD.
# This allows one table to alias another table on the same branch.
dolt sql <<SQL
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
INSERT INTO dolt_nonlocal_tables(table_name, ref_table, options) VALUES
("table_alias", "aliased_table", "immediate");
SQL
run dolt sql -q "select * from table_alias;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
}
@test "nonlocal: default table_name" {
# If unspecified, the parent table name defaults to the same table name as the child
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE table_alias (pk char(8) PRIMARY KEY);
INSERT INTO table_alias VALUES ("amzmapqt");
CALL DOLT_CHECKOUT('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("table_alias", "main", "immediate");
SQL
run dolt sql -q "select * from table_alias;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
}
@test "nonlocal: wildcard table_name" {
# The wildcard syntax matches the wildcard syntax used in dolt_ignore
dolt checkout -b other
dolt sql <<SQL
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_*", "main", "immediate");
CALL DOLT_CHECKOUT('main');
CREATE TABLE nonlocal_table1 (pk char(8) PRIMARY KEY);
CREATE TABLE nonlocal_table2 (pk char(8) PRIMARY KEY);
CREATE TABLE not_nonlocal_table (pk char(8) PRIMARY KEY);
INSERT INTO nonlocal_table1 VALUES ("amzmapqt");
INSERT INTO nonlocal_table2 VALUES ("eesekkgo");
INSERT INTO not_nonlocal_table VALUES ("pzdxwmbd");
SQL
run dolt sql -q "select * from nonlocal_table1;"
[ "$status" -eq 0 ]
[[ "$output" =~ "amzmapqt" ]] || false
run dolt sql -q "select * from nonlocal_table2;"
[ "$status" -eq 0 ]
[[ "$output" =~ "eesekkgo" ]] || false
run dolt sql -q "select * from not_nonlocal_table;"
[ "$status" -eq 1 ]
[[ "$output" =~ "table not found" ]] || false
}
@test "nonlocal: a transaction that tries to update multiple branches fails as expected" {
run dolt sql <<SQL
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
CALL DOLT_CHECKOUT('-b', 'other');
CREATE TABLE local_table (pk char(8) PRIMARY KEY);
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("nonlocal_table", "main", "aliased_table", "immediate");
set autocommit = 0;
INSERT INTO local_table VALUES ("amzmapqt");
INSERT INTO nonlocal_table VALUES ("eesekkgo");
COMMIT;
SQL
[ "$status" -eq 1 ]
[[ "$output" =~ "Cannot commit changes on more than one branch / database" ]] || false
}
@test "nonlocal: test foreign keys" {
# Currently, foreign keys cannot be added to nonlocal tables
dolt checkout -b other
run dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("nonlocal_table", "main", "aliased_table", "immediate");
set autocommit = 0;
INSERT INTO nonlocal_table VALUES ("eesekkgo");
SQL
run dolt sql <<SQL
CREATE TABLE local_table (pk char(8) PRIMARY KEY, FOREIGN KEY (pk) REFERENCES nonlocal_table(pk));
SQL
[ "$status" -eq 1 ]
[[ "$output" =~ "table not found: nonlocal_table" ]] || false
}
@test "nonlocal: trying to dolt_add a nonlocal table returns the appropriate warning" {
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE nonlocal_table (pk char(8) PRIMARY KEY);
SQL
dolt sql <<SQL
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_table", "main", "immediate");
INSERT INTO nonlocal_table values ('ghdsgerg');
SQL
run dolt add nonlocal_table
[ "$status" -eq 1 ]
[[ "$output" =~ "the table(s) nonlocal_table do not exist" ]] || false
}
@test "nonlocal: dolt_add('.') doesn't add nonlocal tables" {
dolt checkout -b other
dolt sql <<SQL
CALL DOLT_CHECKOUT('main');
CREATE TABLE test_table (pk char(8) PRIMARY KEY);
SQL
dolt sql <<SQL
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("test_table", "main", "immediate");
INSERT INTO test_table values ('ghdsgerg');
CALL DOLT_ADD('.');
SQL
run dolt sql -q "SELECT * FROM dolt_status"
[ "$status" -eq 0 ]
echo "$output"
! [[ "$output" =~ "test_table" ]] || false
run dolt sql -q "CALL DOLT_CHECKOUT('main'); SELECT * FROM dolt_status"
[ "$status" -eq 0 ]
[[ "$output" =~ "test_table | false" ]] || false
}
@test "nonlocal: self-referrential nonlocal tables in the same branch as their target are effectively ignored" {
dolt sql <<SQL
CREATE TABLE nonlocal_table (pk char(8) PRIMARY KEY);
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_table", "main", "immediate");
INSERT INTO nonlocal_table values ('ghdsgerg');
SQL
dolt add nonlocal_table
run dolt sql -q "select * from dolt_status"
[ "$status" -eq 0 ]
echo "$output"
[[ "$output" =~ "nonlocal_table | true" ]] || false
# Unstage nonlocal_table but keep it in the working set
dolt reset HEAD
dolt add .
run dolt sql -q "select * from dolt_status"
[ "$status" -eq 0 ]
[[ "$output" =~ "nonlocal_table | true" ]] || false
}
@test "nonlocal: invalid options detected" {
dolt sql <<SQL
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_table", "main", "invalid");
SQL
run dolt sql -q "select * from nonlocal_table;"
[ "$status" -eq 1 ]
echo "$output"
[[ "$output" =~ "Invalid nonlocal table options" ]] || false
}
# The below tests are convenience features but not necessary for the MVP
@test "nonlocal: nonlocal tables appear in show_tables" {
skip
dolt checkout -b other
dolt sql <<SQL
CALL dolt_checkout('main');
CREATE TABLE aliased_table (pk char(8) PRIMARY KEY);
INSERT INTO aliased_table VALUES ("amzmapqt");
CALL dolt_checkout('other');
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, ref_table, options) VALUES
("table_alias_branch", "main", "aliased_table", "immediate");
SQL
# Nonlocal tables should appear in "show tables"
run dolt sql -q "show tables"
[ "$status" -eq 0 ]
[[ "$output" =~ "table_alias_branch" ]] || false
}
@test "nonlocal: creating a nonlocal table creates it on the appropriate branch" {
skip
dolt checkout -b other
dolt sql <<SQL
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_table", "main", "immediate");
CREATE TABLE nonlocal_table (pk char(8) PRIMARY KEY);
INSERT INTO nonlocal_table VALUES ("amzmapqt");
SQL
run dolt ls main
[ "$status" -eq 0 ]
[[ "$output" =~ "nonlocal_table" ]] || false
}
@test "nonlocal: adding an existing table to nonlocal tables errors" {
skip
dolt checkout -b other
run dolt sql <<SQL
CREATE TABLE nonlocal_table (pk char(8) PRIMARY KEY);
INSERT INTO dolt_nonlocal_tables(table_name, target_ref, options) VALUES
("nonlocal_table", "main", "immediate");
SQL
[ "$status" -eq 0 ]
[[ "$output" =~ "cannot make nonlocal table nonlocal_table, table already exists on branch other" ]] || false
}