From 459c45c942206191f63bb717677e1eb6d6c9849d Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Thu, 13 Aug 2026 11:39:11 -0700 Subject: [PATCH 1/4] implemented the final missing pieces required for the last two special-cased psql query handlers --- server/connection_handler.go | 25 +--- server/functions/pg_function_is_visible.go | 42 +++++- server/functions/pg_get_indexdef.go | 48 ++++--- testing/go/functions_test.go | 143 ++++++++++++++++++++- testing/go/psql_test.go | 32 +++++ 5 files changed, 241 insertions(+), 49 deletions(-) diff --git a/server/connection_handler.go b/server/connection_handler.go index 36ce3f7862..e6eb1e9e1a 100644 --- a/server/connection_handler.go +++ b/server/connection_handler.go @@ -490,11 +490,6 @@ func (h *ConnectionHandler) handleMessage(msg pgproto3.Message) (stop, endOfMess // expected as part of this query, in which case the server will send a READY FOR QUERY message back to the client so // that it can send its next query. func (h *ConnectionHandler) handleQuery(message *pgproto3.Query) (endOfMessages bool, err error) { - handled, err := h.handledPSQLCommands(message.String) - if handled || err != nil { - return true, err - } - queries, err := h.convertQuery(message.String) if err != nil { if printErrorStackTraces { @@ -507,6 +502,7 @@ func (h *ConnectionHandler) handleQuery(message *pgproto3.Query) (endOfMessages delete(h.preparedStatements, "") delete(h.portals, "") + var handled bool if len(queries) == 1 { // empty query special case if queries[0].AST == nil { @@ -1376,25 +1372,6 @@ func (h *ConnectionHandler) sendDescribeResponse(fields []pgproto3.FieldDescript } } -// handledPSQLCommands handles the special PSQL commands, such as \l and \dt. -func (h *ConnectionHandler) handledPSQLCommands(statement string) (bool, error) { - statement = strings.ToLower(statement) - // Command: \d table_name - if strings.HasPrefix(statement, "select c.oid,\n n.nspname,\n c.relname\nfrom pg_catalog.pg_class c\n left join pg_catalog.pg_namespace n on n.oid = c.relnamespace\nwhere c.relname operator(pg_catalog.~) '^(") && strings.HasSuffix(statement, ")$' collate pg_catalog.default\n and pg_catalog.pg_table_is_visible(c.oid)\norder by 2, 3;") { - // There are >at least< 15 separate statements sent for this command, which is far too much to validate and - // implement, so we'll just return an error for now - return true, errors.Errorf("PSQL command not yet supported") - } - // Command: \df - if statement == "select n.nspname as \"schema\",\n p.proname as \"name\",\n pg_catalog.pg_get_function_result(p.oid) as \"result data type\",\n pg_catalog.pg_get_function_arguments(p.oid) as \"argument data types\",\n case p.prokind\n when 'a' then 'agg'\n when 'w' then 'window'\n when 'p' then 'proc'\n else 'func'\n end as \"type\"\nfrom pg_catalog.pg_proc p\n left join pg_catalog.pg_namespace n on n.oid = p.pronamespace\nwhere pg_catalog.pg_function_is_visible(p.oid)\n and n.nspname <> 'pg_catalog'\n and n.nspname <> 'information_schema'\norder by 1, 2, 4;" { - return true, h.query(ConvertedQuery{ - String: `SELECT '' AS "Schema", '' AS "Name", '' AS "Result data type", '' AS "Argument data types", '' AS "Type" LIMIT 0;`, - StatementTag: "SELECT", - }) - } - return false, nil -} - // endOfMessages should be called from HandleConnection or a function within HandleConnection. This represents the end // of the message slice, which may occur naturally (all relevant response messages have been sent) or on error. Once // endOfMessages has been called, no further messages should be sent, and the connection loop should wait for the next diff --git a/server/functions/pg_function_is_visible.go b/server/functions/pg_function_is_visible.go index 8e642c82d7..2a7173257f 100644 --- a/server/functions/pg_function_is_visible.go +++ b/server/functions/pg_function_is_visible.go @@ -17,8 +17,8 @@ package functions import ( "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/doltgresql/core" "github.com/dolthub/doltgresql/core/id" - "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -36,7 +36,43 @@ var pg_function_is_visible_oid = framework.Function1{ IsNonDeterministic: true, Strict: true, Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { - // TODO: Functions are not contained within a schema for now, so will be true if function is found - return id.Cache().Exists(val.(id.Id)), nil + oidVal := val.(id.Id) + if oidVal.Section() != id.Section_Function && oidVal.Section() != id.Section_Procedure { + return false, nil + } + // TODO: Postgres additionally checks that the function isn't shadowed by one with the same + // name and argument types in an earlier schema of the search path. + paths, err := core.SearchPath(ctx) + if err != nil { + return false, err + } + schemaName := oidVal.Segment(0) + inPath := false + for _, path := range paths { + if path == schemaName { + inPath = true + break + } + } + if !inPath { + return false, nil + } + // Built-in functions are registered in the OID cache at startup, and don't appear in the + // function and procedure collections that RunCallback iterates over. + if id.Cache().Exists(oidVal) { + return true, nil + } + isVisible := false + err = RunCallback(ctx, oidVal, Callbacks{ + Function: func(ctx *sql.Context, schema ItemSchema, function ItemFunction) (cont bool, err error) { + isVisible = true + return false, nil + }, + Procedure: func(ctx *sql.Context, schema ItemSchema, procedure ItemProcedure) (cont bool, err error) { + isVisible = true + return false, nil + }, + }) + return isVisible, err }, } diff --git a/server/functions/pg_get_indexdef.go b/server/functions/pg_get_indexdef.go index 7d19f92ef3..c6e736f970 100644 --- a/server/functions/pg_get_indexdef.go +++ b/server/functions/pg_get_indexdef.go @@ -18,7 +18,6 @@ import ( "fmt" "strings" - "github.com/cockroachdb/errors" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/plan" @@ -59,12 +58,29 @@ var pg_get_indexdef_oid = framework.Function1{ // buildIndexDef generates a CREATE INDEX DDL statement for the given index. func buildIndexDef(ctx *sql.Context, index sql.Index, table sql.Table, schemaName string) string { name := index.ID() + if name == "PRIMARY" { + // Primary key indexes are displayed with their postgres-convention name, matching pg_class + name = fmt.Sprintf("%s_pkey", index.Table()) + } using := strings.ToLower(index.IndexType()) unique := "" if index.IsUnique() { unique = " UNIQUE" } + colsStr := strings.Join(indexColumnExprs(ctx, index, table), ", ") + + def := fmt.Sprintf("CREATE%s INDEX %s ON %s.%s USING %s (%s)", unique, name, schemaName, index.Table(), using, colsStr) + if pi, ok := index.(sql.PartialIndex); ok && pi.Predicate() != "" { + def += " WHERE (" + pi.Predicate() + ")" + } + return def +} + +// indexColumnExprs returns the rendered text of each column of the given index, in index column +// order. Plain columns render as the bare column name, functional expressions as their original +// SQL text. +func indexColumnExprs(ctx *sql.Context, index sql.Index, table sql.Table) []string { cols := make([]string, len(index.Expressions())) for i, expr := range index.Expressions() { if exprText, ok := RenderHiddenIndexColumnExpr(plan.GetColumnFromIndexExpr(ctx, expr, table)); ok { @@ -79,13 +95,7 @@ func buildIndexDef(ctx *sql.Context, index sql.Index, table sql.Table, schemaNam cols[i] = expr } } - colsStr := strings.Join(cols, ", ") - - def := fmt.Sprintf("CREATE%s INDEX %s ON %s.%s USING %s (%s)", unique, name, schemaName, index.Table(), using, colsStr) - if pi, ok := index.(sql.PartialIndex); ok && pi.Predicate() != "" { - def += " WHERE (" + pi.Predicate() + ")" - } - return def + return cols } // RenderHiddenIndexColumnExpr returns the original SQL text of the functional expression backing @@ -112,23 +122,27 @@ var pg_get_indexdef_oid_integer_bool = framework.Function3{ Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, val1, val2, val3 any) (any, error) { oidVal := val1.(id.Id) colNo := val2.(int32) - pretty := val3.(bool) - if pretty { - return "", errors.Errorf("pretty printing is not yet supported") - } + // The pretty flag only affects the formatting of expressions, which we don't reproduce, so + // we return the same text either way. + result := "" err := RunCallback(ctx, oidVal, Callbacks{ Index: func(ctx *sql.Context, schema ItemSchema, table ItemTable, index ItemIndex) (cont bool, err error) { - exprs := index.Item.Expressions() - if int(colNo) >= len(exprs) { - return false, errors.Errorf("column not found") + if colNo == 0 { + result = buildIndexDef(ctx, index.Item, table.Item, schema.Item.SchemaName()) + return false, nil + } + // A non-zero column number selects just that column's definition, or an empty + // string if the index has no such column. + cols := indexColumnExprs(ctx, index.Item, table.Item) + if colNo >= 1 && int(colNo) <= len(cols) { + result = cols[colNo-1] } - // TODO: make `create index` statement return false, nil }, }) if err != nil { return "", err } - return "", nil + return result, nil }, } diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index 10d044f6c5..050b049da5 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -2033,18 +2033,64 @@ func TestArrayFunctions(t *testing.T) { func TestSchemaVisibilityInquiryFunctions(t *testing.T) { RunScripts(t, []ScriptTest{ { - Skip: true, // TODO: not supported - Name: "pg_function_is_visible", - SetUpScript: []string{}, + Name: "pg_function_is_visible", + SetUpScript: []string{ + "CREATE SCHEMA myschema;", + "SET search_path TO myschema;", + "CREATE FUNCTION myfunc(a int) RETURNS int LANGUAGE sql AS 'SELECT a + 1';", + "CREATE PROCEDURE myproc() LANGUAGE sql AS $$ SELECT 1 $$;", + "CREATE SCHEMA testschema;", + "SET search_path TO testschema;", + "CREATE FUNCTION test_func(a int) RETURNS int LANGUAGE sql AS 'SELECT a + 2';", + }, Assertions: []ScriptTestAssertion{ { - Query: `SELECT pg_function_is_visible(1342177280);`, + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'test_func';`, Expected: []sql.Row{{"t"}}, }, { - Query: `SELECT pg_function_is_visible(22);`, // invalid + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'myfunc';`, Expected: []sql.Row{{"f"}}, }, + { + // Procedures are also subject to visibility checks + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'myproc';`, + Expected: []sql.Row{{"f"}}, + }, + { + Query: `SET search_path = 'myschema';`, + Expected: []sql.Row{}, + }, + { + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'myfunc';`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'myproc';`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `SELECT pg_function_is_visible(p.oid) FROM pg_catalog.pg_proc p WHERE p.proname = 'test_func';`, + Expected: []sql.Row{{"f"}}, + }, + { + // Built-in functions live in pg_catalog, which is always on the search path. + // OID 31 is byteaout. + Query: `SELECT pg_function_is_visible(31);`, + Expected: []sql.Row{{"t"}}, + }, + { + Query: `SELECT pg_function_is_visible(22);`, // not a function OID + Expected: []sql.Row{{"f"}}, + }, + { + Query: `SELECT pg_function_is_visible(845743985);`, // OID does not exist + Expected: []sql.Row{{"f"}}, + }, + { + Query: `SELECT pg_function_is_visible(NULL);`, + Expected: []sql.Row{{nil}}, + }, }, }, { @@ -2477,6 +2523,93 @@ func TestSystemCatalogInformationFunctions(t *testing.T) { }, }, }, + { + Name: "pg_get_indexdef", + SetUpScript: []string{ + `CREATE TABLE idx_test (pk INT PRIMARY KEY, a INT, b INT, c TEXT);`, + `CREATE UNIQUE INDEX idx_ab ON idx_test (a, b);`, + `CREATE INDEX idx_c ON idx_test (c);`, + }, + Assertions: []ScriptTestAssertion{ + { + // OID does not exist + Query: `SELECT pg_get_indexdef(845743985);`, + ExpectedColNames: []string{"pg_get_indexdef"}, + Expected: []sql.Row{{""}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_test_pkey'::regclass::oid);`, + Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_test_pkey ON public.idx_test USING btree (pk)"}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid);`, + Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_ab ON public.idx_test USING btree (a, b)"}}, + }, + { + // A column number of 0 returns the whole definition, same as the one-argument form + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, 0, true);`, + Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_ab ON public.idx_test USING btree (a, b)"}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, 0, false);`, + Expected: []sql.Row{{"CREATE UNIQUE INDEX idx_ab ON public.idx_test USING btree (a, b)"}}, + }, + { + // A non-zero column number returns just that column's definition + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, 1, true);`, + Expected: []sql.Row{{"a"}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, 2, false);`, + Expected: []sql.Row{{"b"}}, + }, + { + // Out-of-range column numbers return an empty string + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, 3, true);`, + Expected: []sql.Row{{""}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, -1, true);`, + Expected: []sql.Row{{""}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_c'::regclass::oid, 1, true);`, + Expected: []sql.Row{{"c"}}, + }, + { + // OID does not exist + Query: `SELECT pg_get_indexdef(845743985, 0, true);`, + Expected: []sql.Row{{""}}, + }, + { + Query: `SELECT pg_get_indexdef(845743985, 1, true);`, + Expected: []sql.Row{{""}}, + }, + { + // NULL arguments produce a NULL result + Query: `SELECT pg_get_indexdef(NULL, 1, true);`, + Expected: []sql.Row{{nil}}, + }, + { + Query: `SELECT pg_get_indexdef('idx_ab'::regclass::oid, NULL, true);`, + Expected: []sql.Row{{nil}}, + }, + { + // The index listing query issued by psql's \d command + Query: `SELECT c2.relname, i.indisprimary, i.indisunique, i.indisclustered, i.indisvalid, pg_catalog.pg_get_indexdef(i.indexrelid, 0, true), + pg_catalog.pg_get_constraintdef(con.oid, true), contype, condeferrable, condeferred, i.indisreplident, c2.reltablespace +FROM pg_catalog.pg_class c, pg_catalog.pg_class c2, pg_catalog.pg_index i + LEFT JOIN pg_catalog.pg_constraint con ON (conrelid = i.indrelid AND conindid = i.indexrelid AND contype IN ('p','u','x')) +WHERE c.oid = 'idx_test'::regclass AND c.oid = i.indrelid AND i.indexrelid = c2.oid +ORDER BY i.indisprimary DESC, c2.relname;`, + Expected: []sql.Row{ + {"idx_test_pkey", "t", "t", "f", "t", "CREATE UNIQUE INDEX idx_test_pkey ON public.idx_test USING btree (pk)", "PRIMARY KEY (pk)", "p", "f", "f", "f", 0}, + {"idx_ab", "f", "t", "f", "t", "CREATE UNIQUE INDEX idx_ab ON public.idx_test USING btree (a, b)", "UNIQUE (a, b)", "u", "f", "f", "f", 0}, + {"idx_c", "f", "f", "f", "t", "CREATE INDEX idx_c ON public.idx_test USING btree (c)", nil, nil, nil, nil, "f", 0}, + }, + }, + }, + }, { Name: "pg_get_function_result", SetUpScript: []string{}, diff --git a/testing/go/psql_test.go b/testing/go/psql_test.go index e311fcb8a2..47a9700e9a 100755 --- a/testing/go/psql_test.go +++ b/testing/go/psql_test.go @@ -94,6 +94,38 @@ func TestPsqlCommands(t *testing.T) { }, }, }, + { + Name: `\df`, + SetUpScript: []string{ + "CREATE FUNCTION add_two(a int, b int) RETURNS int LANGUAGE sql AS 'SELECT a + b';", + "CREATE PROCEDURE noop_proc() LANGUAGE sql AS $$ SELECT 1 $$;", + }, + Assertions: []ScriptTestAssertion{ + { + // The query issued by psql's \df command + Query: `SELECT n.nspname as "Schema", + p.proname as "Name", + pg_catalog.pg_get_function_result(p.oid) as "Result data type", + pg_catalog.pg_get_function_arguments(p.oid) as "Argument data types", + CASE p.prokind + WHEN 'a' THEN 'agg' + WHEN 'w' THEN 'window' + WHEN 'p' THEN 'proc' + ELSE 'func' + END as "Type" +FROM pg_catalog.pg_proc p + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace +WHERE pg_catalog.pg_function_is_visible(p.oid) + AND n.nspname <> 'pg_catalog' + AND n.nspname <> 'information_schema' +ORDER BY 1, 2, 4;`, + Expected: []sql.Row{ + {"public", "add_two", "", "a integer, b integer", "func"}, + {"public", "noop_proc", "", "", "proc"}, + }, + }, + }, + }, { Name: `\d tablename`, SetUpScript: []string{ From a7034ebfd98b018ce290269c7629f1a728749c11 Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Thu, 13 Aug 2026 17:13:17 -0700 Subject: [PATCH 2/4] multiple bug fixes for psql support --- server/ast/aliased_table_expr.go | 68 +++++++++-- server/functions/binary/equal.go | 7 +- server/functions/init.go | 1 + server/functions/oid.go | 4 +- server/functions/pg_get_triggerdef.go | 37 ++++-- server/functions/pg_partition_ancestors.go | 74 ++++++++++++ server/tables/pgcatalog/pg_class.go | 107 ++++++++++++------ testing/go/functions_test.go | 124 ++++++++++++++++++++- testing/go/pgcatalog_test.go | 24 ++++ testing/go/psql_test.go | 28 +++++ 10 files changed, 417 insertions(+), 57 deletions(-) create mode 100644 server/functions/pg_partition_ancestors.go diff --git a/server/ast/aliased_table_expr.go b/server/ast/aliased_table_expr.go index 164aad14f2..91a4dec04f 100644 --- a/server/ast/aliased_table_expr.go +++ b/server/ast/aliased_table_expr.go @@ -15,6 +15,8 @@ package ast import ( + "strings" + "github.com/cockroachdb/errors" vitess "github.com/dolthub/vitess/go/vt/sqlparser" @@ -26,7 +28,9 @@ import ( // nodeAliasedTableExpr handles *tree.AliasedTableExpr nodes. func nodeAliasedTableExpr(ctx *Context, node *tree.AliasedTableExpr) (*vitess.AliasedTableExpr, error) { if node.Ordinality { - return nil, errors.Errorf("ordinality is not yet supported") + if _, ok := node.Expr.(*tree.RowsFromExpr); !ok { + return nil, errors.Errorf("WITH ORDINALITY is only supported for functions") + } } if node.IndexFlags != nil { return nil, errors.Errorf("index flags are not yet supported") @@ -101,16 +105,51 @@ func nodeAliasedTableExpr(ctx *Context, node *tree.AliasedTableExpr) (*vitess.Al } aliasExpr = subquery case *tree.RowsFromExpr: - tableExpr, err := nodeTableExpr(ctx, expr) - if err != nil { - return nil, err - } + var selectStmt vitess.SelectStatement + if node.Ordinality { + // WITH ORDINALITY appends a bigint column numbering the function's result rows, named + // "ordinality" unless renamed by a column alias list. The numbering projection has to + // live one level above the function's expansion, so we expand the function in the + // select list of a wrapped subquery. + items, err := nodeExprs(ctx, expr.Items) + if err != nil { + return nil, err + } + innerExprs := make(vitess.SelectExprs, len(items)) + for i := range items { + innerExprs[i] = &vitess.AliasedExpr{Expr: items[i]} + } + selectStmt = &vitess.Select{ + SelectExprs: vitess.SelectExprs{ + &vitess.StarExpr{}, + &vitess.AliasedExpr{ + Expr: &vitess.FuncExpr{ + Name: vitess.NewColIdent("row_number"), + Over: &vitess.Over{}, + }, + As: vitess.NewColIdent("ordinality"), + }, + }, + From: vitess.TableExprs{ + &vitess.AliasedTableExpr{ + Expr: &vitess.Subquery{Select: &vitess.Select{SelectExprs: innerExprs}}, + As: vitess.NewTableIdent("with_ordinality"), + }, + }, + } + } else { + tableExpr, err := nodeTableExpr(ctx, expr) + if err != nil { + return nil, err + } - // TODO: this should be represented as a table function more directly - subquery := &vitess.Subquery{ - Select: &vitess.Select{ + // TODO: this should be represented as a table function more directly + selectStmt = &vitess.Select{ From: vitess.TableExprs{tableExpr}, - }, + } + } + subquery := &vitess.Subquery{ + Select: selectStmt, } if len(node.As.Cols) > 0 { @@ -125,6 +164,17 @@ func nodeAliasedTableExpr(ctx *Context, node *tree.AliasedTableExpr) (*vitess.Al return nil, errors.Errorf("unhandled table expression: `%T`", expr) } alias := string(node.As.Alias) + if alias == "" && node.Ordinality { + // A derived table needs an alias; the implicit alias of a function called in FROM is the + // function's name, matching Postgres + alias = "with_ordinality" + if rf, ok := node.Expr.(*tree.RowsFromExpr); ok && len(rf.Items) == 1 { + if fe, ok := rf.Items[0].(*tree.FuncExpr); ok { + nameParts := strings.Split(fe.Func.String(), ".") + alias = strings.ToLower(nameParts[len(nameParts)-1]) + } + } + } var asOf *vitess.AsOf if node.AsOf != nil { diff --git a/server/functions/binary/equal.go b/server/functions/binary/equal.go index 508c779849..4e389c0b10 100644 --- a/server/functions/binary/equal.go +++ b/server/functions/binary/equal.go @@ -499,7 +499,12 @@ var oideq = framework.Function2{ // This method doesn't use DoltgresType.Compare because it's on the critical path for many tooling queries that // examine the pg_catalog tables. val1id, val2id := val1.(id.Id), val2.(id.Id) - return val1id == val2id, nil + if val1id == val2id { + return true, nil + } + // Different internal IDs can still map to the same OID: an OID given to us by a client resolves to a raw + // numeric ID unless its assignment is already cached, and a raw OID of 0 is a distinct value from id.Null. + return id.Cache().ToOID(val1id) == id.Cache().ToOID(val2id), nil }, } diff --git a/server/functions/init.go b/server/functions/init.go index 188ae47454..3f2a2fb445 100644 --- a/server/functions/init.go +++ b/server/functions/init.go @@ -169,6 +169,7 @@ func Init() { initPgOpclassIsVisible() initPgOperatorIsVisible() initPgOpfamilyIsVisible() + initPgPartitionAncestors() initPgPostmasterStartTime() initPgRelationIsPublishable() initPgRelationSize() diff --git a/server/functions/oid.go b/server/functions/oid.go index 68099eb9a7..05cac59f86 100644 --- a/server/functions/oid.go +++ b/server/functions/oid.go @@ -114,6 +114,8 @@ var btoidcmp = framework.Function2{ Parameters: [2]*pgtypes.DoltgresType{pgtypes.Oid, pgtypes.Oid}, Strict: true, Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) { - return int32(cmp.Compare(val1.(id.Id), val2.(id.Id))), nil + // Postgres compares OIDs numerically, and different internal IDs can map to the same OID (e.g. a raw + // numeric OID given by a client and the cached ID it refers to) + return int32(cmp.Compare(id.Cache().ToOID(val1.(id.Id)), id.Cache().ToOID(val2.(id.Id)))), nil }, } diff --git a/server/functions/pg_get_triggerdef.go b/server/functions/pg_get_triggerdef.go index 18bf1b240d..cf6cd1e334 100644 --- a/server/functions/pg_get_triggerdef.go +++ b/server/functions/pg_get_triggerdef.go @@ -15,10 +15,12 @@ package functions import ( - "github.com/cockroachdb/errors" + "strings" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/doltgresql/core" + "github.com/dolthub/doltgresql/core/id" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" ) @@ -37,8 +39,7 @@ var pg_get_triggerdef_oid = framework.Function1{ IsNonDeterministic: true, Strict: true, Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { - // TODO: triggers are not supported yet - return "", nil + return getTriggerDef(ctx, val.(id.Id)) }, } @@ -50,11 +51,29 @@ var pg_get_triggerdef_oid_bool = framework.Function2{ IsNonDeterministic: true, Strict: true, Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1, val2 any) (any, error) { - pretty := val2.(bool) - if pretty { - return "", errors.Errorf("pretty printing is not yet supported") - } - // TODO: triggers are not supported yet - return "", nil + // The pretty flag only affects the formatting of expressions, which we don't reproduce, so + // we return the same text either way. + return getTriggerDef(ctx, val1.(id.Id)) }, } + +// getTriggerDef returns the definition of the trigger for the given OID, or an empty string if the OID doesn't refer +// to an existing trigger. Postgres reconstructs the statement from the catalog, whereas we return the statement that +// created the trigger. +func getTriggerDef(ctx *sql.Context, oidVal id.Id) (string, error) { + if oidVal.Section() != id.Section_Trigger { + return "", nil + } + collection, err := core.GetTriggersCollectionFromContext(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return "", err + } + trigger, err := collection.GetTrigger(ctx, id.Trigger(oidVal)) + if err != nil { + return "", err + } + if !trigger.ID.IsValid() { + return "", nil + } + return strings.TrimSuffix(strings.TrimSpace(trigger.Definition), ";"), nil +} diff --git a/server/functions/pg_partition_ancestors.go b/server/functions/pg_partition_ancestors.go new file mode 100644 index 0000000000..a99cee53db --- /dev/null +++ b/server/functions/pg_partition_ancestors.go @@ -0,0 +1,74 @@ +// Copyright 2026 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 functions + +import ( + "io" + + "github.com/dolthub/go-mysql-server/sql" + + "github.com/dolthub/doltgresql/core/id" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initPgPartitionAncestors registers the functions to the catalog. +func initPgPartitionAncestors() { + framework.RegisterFunction(pg_partition_ancestors_regclass) +} + +// pgPartitionAncestorsName is the name of the pg_partition_ancestors function. +const pgPartitionAncestorsName = "pg_partition_ancestors" + +// pg_partition_ancestors_regclass represents the PostgreSQL partitioning information function of the same name. It +// lists the ancestors of the given partition, including the relation itself. Since we don't support table +// partitioning, a relation's only ancestor is itself; relations that don't exist return an empty set, matching +// Postgres. +var pg_partition_ancestors_regclass = framework.Function1{ + Name: pgPartitionAncestorsName, + Return: pgtypes.RowTypeWithReturnType(pgtypes.Regclass), + Parameters: [1]*pgtypes.DoltgresType{pgtypes.Regclass}, + IsNonDeterministic: true, + Strict: true, + SRF: true, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + oidVal := val.(id.Id) + exists := false + err := RunCallback(ctx, oidVal, Callbacks{ + Table: func(ctx *sql.Context, schema ItemSchema, table ItemTable) (cont bool, err error) { + exists = true + return false, nil + }, + Index: func(ctx *sql.Context, schema ItemSchema, table ItemTable, index ItemIndex) (cont bool, err error) { + exists = true + return false, nil + }, + }) + if err != nil { + return nil, err + } + returned := false + return pgtypes.NewSetReturningFunctionRowIter(func(ctx *sql.Context) (sql.Row, error) { + if !exists || returned { + return nil, io.EOF + } + returned = true + return sql.Row{oidVal}, nil + }), nil + }, + OutParams: sql.Schema{ + {Name: "relid", Type: pgtypes.Regclass, Default: nil, Nullable: false, Source: pgPartitionAncestorsName}, + }, +} diff --git a/server/tables/pgcatalog/pg_class.go b/server/tables/pgcatalog/pg_class.go index 3e501b9b45..1e4c877141 100644 --- a/server/tables/pgcatalog/pg_class.go +++ b/server/tables/pgcatalog/pg_class.go @@ -22,7 +22,9 @@ import ( "github.com/dolthub/dolt/go/libraries/doltcore/sqle/index" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/doltgresql/core" "github.com/dolthub/doltgresql/core/id" + "github.com/dolthub/doltgresql/core/triggers" "github.com/dolthub/doltgresql/server/functions" "github.com/dolthub/doltgresql/server/tables" pgtypes "github.com/dolthub/doltgresql/server/types" @@ -84,7 +86,22 @@ func cachePgClasses(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { nameIdx := NewUniqueInMemIndexStorage[*pgClass](lessName) oidIdx := NewUniqueInMemIndexStorage[*pgClass](lessOid) - err := functions.IterateCurrentDatabase(ctx, functions.Callbacks{ + // Tables with triggers report relhastriggers, which clients like psql rely on to decide + // whether to look for triggers and foreign keys (which Postgres enforces via system triggers). + triggerTables := make(map[id.Table]struct{}) + trigCollection, err := core.GetTriggersCollectionFromContext(ctx, ctx.GetCurrentDatabase()) + if err != nil { + return err + } + err = trigCollection.IterateTriggers(ctx, func(t triggers.Trigger) (stop bool, err error) { + triggerTables[id.NewTable(t.ID.SchemaName(), t.ID.TableName())] = struct{}{} + return false, nil + }) + if err != nil { + return err + } + + err = functions.IterateCurrentDatabase(ctx, functions.Callbacks{ Index: func(ctx *sql.Context, schema functions.ItemSchema, table functions.ItemTable, index functions.ItemIndex) (cont bool, err error) { tableHasIndexes[id.Cache().ToOID(table.OID.AsId())] = struct{}{} schemaOid := schema.OID @@ -105,11 +122,32 @@ func cachePgClasses(ctx *sql.Context, pgCatalogCache *pgCatalogCache) error { }, Table: func(ctx *sql.Context, schema functions.ItemSchema, table functions.ItemTable) (cont bool, err error) { _, hasIndexes := tableHasIndexes[id.Cache().ToOID(table.OID.AsId())] + _, hasTriggers := triggerTables[table.OID] + if !hasTriggers { + // Postgres enforces foreign keys with system triggers on both the referencing and + // the referenced table, so both report relhastriggers + if fkTable, ok := table.Item.(sql.ForeignKeyTable); ok { + declared, err := fkTable.GetDeclaredForeignKeys(ctx) + if err != nil { + return false, err + } + if len(declared) > 0 { + hasTriggers = true + } else { + referenced, err := fkTable.GetReferencedForeignKeys(ctx) + if err != nil { + return false, err + } + hasTriggers = len(referenced) > 0 + } + } + } class := &pgClass{ oid: table.OID.AsId(), oidNative: id.Cache().ToOID(table.OID.AsId()), name: table.Item.Name(), hasIndexes: hasIndexes, + hasTriggers: hasTriggers, kind: "r", schemaOid: schema.OID.AsId(), schemaOidNative: id.Cache().ToOID(schema.OID.AsId()), @@ -378,6 +416,7 @@ type pgClass struct { schemaOid id.Id schemaOidNative uint32 hasIndexes bool + hasTriggers bool kind string // r = ordinary table, i = index, S = sequence, t = TOAST table, v = view, m = materialized view, c = composite type, f = foreign table, p = partitioned table, I = partitioned index relType id.Id } @@ -425,39 +464,39 @@ func pgClassToRow(class *pgClass) sql.Row { // TODO: Fill in the rest of the pg_class columns return sql.Row{ - class.oid, // oid - class.name, // relname - class.schemaOid, // relnamespace - class.relType, // reltype - id.Null, // reloftype - id.Null, // relowner - relam, // relam - id.Null, // relfilenode - id.Null, // reltablespace - int32(0), // relpages - float32(0), // reltuples - int32(0), // relallvisible - id.Null, // reltoastrelid - class.hasIndexes, // relhasindex - false, // relisshared - "p", // relpersistence - class.kind, // relkind - int16(0), // relnatts - int16(0), // relchecks - false, // relhasrules - false, // relhastriggers - false, // relhassubclass - false, // relrowsecurity - false, // relforcerowsecurity - true, // relispopulated - "d", // relreplident - false, // relispartition - id.Null, // relrewrite - uint32(0), // relfrozenxid - uint32(0), // relminmxid - nil, // relacl - nil, // reloptions - nil, // relpartbound + class.oid, // oid + class.name, // relname + class.schemaOid, // relnamespace + class.relType, // reltype + id.Null, // reloftype + id.Null, // relowner + relam, // relam + id.Null, // relfilenode + id.Null, // reltablespace + int32(0), // relpages + float32(0), // reltuples + int32(0), // relallvisible + id.Null, // reltoastrelid + class.hasIndexes, // relhasindex + false, // relisshared + "p", // relpersistence + class.kind, // relkind + int16(0), // relnatts + int16(0), // relchecks + false, // relhasrules + class.hasTriggers, // relhastriggers + false, // relhassubclass + false, // relrowsecurity + false, // relforcerowsecurity + true, // relispopulated + "d", // relreplident + false, // relispartition + id.Null, // relrewrite + uint32(0), // relfrozenxid + uint32(0), // relminmxid + nil, // relacl + nil, // reloptions + nil, // relpartbound } } diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index 050b049da5..8e19288574 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -822,6 +822,29 @@ func TestFunctionsMath(t *testing.T) { func TestFunctionsOID(t *testing.T) { RunScripts(t, []ScriptTest{ + { + Name: "oid comparisons", + SetUpScript: []string{ + `CREATE TABLE testing (pk INT primary key, v1 INT);`, + }, + Assertions: []ScriptTestAssertion{ + { + // A raw OID of 0 given by a client and an internal null OID are the same value + Query: `SELECT 0::oid = 0, 0::oid <> 0, 0::oid < 1::oid, 845743985::oid = 845743985::oid, 845743985::oid = 845743986::oid;`, + Expected: []sql.Row{{"t", "f", "t", "t", "f"}}, + }, + { + // conparentid is a null OID internally, and clients compare it against a raw 0 + Query: `SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid = 'testing'::regclass AND conparentid = 0;`, + Expected: []sql.Row{{"testing_pkey"}}, + }, + { + // A relation's cached OID and the same OID given as a numeric literal are equal + Query: `SELECT ('testing'::regclass::oid)::text::oid = 'testing'::regclass::oid;`, + Expected: []sql.Row{{"t"}}, + }, + }, + }, { Name: "to_regclass", SetUpScript: []string{ @@ -2624,16 +2647,39 @@ ORDER BY i.indisprimary DESC, c2.relname;`, }, }, { - Name: "pg_get_triggerdef", - SetUpScript: []string{}, + Name: "pg_get_triggerdef", + SetUpScript: []string{ + "CREATE TABLE trig_test (pk INT PRIMARY KEY, v1 INT);", + "CREATE FUNCTION trig_fn() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;", + "CREATE TRIGGER trig_before BEFORE INSERT ON trig_test FOR EACH ROW EXECUTE FUNCTION trig_fn();", + }, Assertions: []ScriptTestAssertion{ { - // TODO: triggers are not supported yet + // not a trigger OID Query: `SELECT pg_get_triggerdef(22)`, Expected: []sql.Row{ {""}, }, }, + { + Query: `SELECT pg_get_triggerdef(t.oid) FROM pg_catalog.pg_trigger t WHERE t.tgname = 'trig_before';`, + Expected: []sql.Row{ + {"CREATE TRIGGER trig_before BEFORE INSERT ON trig_test FOR EACH ROW EXECUTE FUNCTION trig_fn()"}, + }, + }, + { + // The pretty flag only affects expression formatting, so both variants return the same text + Query: `SELECT pg_get_triggerdef(t.oid, true) FROM pg_catalog.pg_trigger t WHERE t.tgname = 'trig_before';`, + Expected: []sql.Row{ + {"CREATE TRIGGER trig_before BEFORE INSERT ON trig_test FOR EACH ROW EXECUTE FUNCTION trig_fn()"}, + }, + }, + { + Query: `SELECT pg_get_triggerdef(t.oid, false) FROM pg_catalog.pg_trigger t WHERE t.tgname = 'trig_before';`, + Expected: []sql.Row{ + {"CREATE TRIGGER trig_before BEFORE INSERT ON trig_test FOR EACH ROW EXECUTE FUNCTION trig_fn()"}, + }, + }, }, }, { @@ -4651,6 +4697,78 @@ func TestSetReturningFunctions(t *testing.T) { }, }, }, + { + Name: "table function WITH ORDINALITY", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT * FROM generate_series(2,4) WITH ORDINALITY`, + Expected: []sql.Row{{2, 1}, {3, 2}, {4, 3}}, + }, + { + Query: `SELECT * FROM generate_series(2,4) WITH ORDINALITY AS a(n, ord)`, + Expected: []sql.Row{{2, 1}, {3, 2}, {4, 3}}, + ExpectedColNames: []string{"n", "ord"}, + }, + { + Query: `SELECT ord, n FROM generate_series(2,4) WITH ORDINALITY AS a(n, ord) ORDER BY ord DESC`, + Expected: []sql.Row{{3, 4}, {2, 3}, {1, 2}}, + }, + { + Query: `SELECT n FROM generate_series(5,7) WITH ORDINALITY AS a(n, ord) WHERE ord = 2`, + Expected: []sql.Row{{6}}, + }, + }, + }, + { + Name: "pg_partition_ancestors", + SetUpScript: []string{ + "CREATE TABLE anc_test (pk INT PRIMARY KEY, v1 INT);", + "CREATE VIEW anc_view AS SELECT pk FROM anc_test;", + "CREATE TABLE anc_child (pk INT PRIMARY KEY, apk INT REFERENCES anc_test(pk));", + }, + Assertions: []ScriptTestAssertion{ + { + // Partitioning is not supported, so a relation's only ancestor is itself + Query: `SELECT * FROM pg_partition_ancestors('anc_test'::regclass);`, + Expected: []sql.Row{{"anc_test"}}, + ExpectedColNames: []string{"relid"}, + }, + { + Query: `SELECT * FROM pg_partition_ancestors('anc_test'::regclass) WITH ORDINALITY AS a(relid, depth);`, + Expected: []sql.Row{{"anc_test", 1}}, + }, + { + // Relations that aren't tables or indexes return an empty set + Query: `SELECT * FROM pg_partition_ancestors('anc_view'::regclass);`, + Expected: []sql.Row{}, + }, + { + // OID does not exist + Query: `SELECT * FROM pg_partition_ancestors(845743985);`, + Expected: []sql.Row{}, + }, + { + // The foreign-key listing query issued by psql's \d command + Query: `SELECT conrelid = 'anc_child'::pg_catalog.regclass AS sametable, + conname, pg_catalog.pg_get_constraintdef(oid, true) AS condef, conrelid::pg_catalog.regclass::text AS ontable +FROM pg_catalog.pg_constraint, pg_catalog.pg_partition_ancestors('anc_child'::regclass) +WHERE conrelid = relid AND contype = 'f' AND conparentid = 0 +ORDER BY sametable DESC, conname;`, + Expected: []sql.Row{{"t", "anc_child_apk_fkey", "FOREIGN KEY (apk) REFERENCES anc_test(pk)", "anc_child"}}, + }, + { + // The referenced-by listing query issued by psql's \d command + Query: `SELECT conname, conrelid::pg_catalog.regclass::text AS ontable, + pg_catalog.pg_get_constraintdef(oid, true) AS condef +FROM pg_catalog.pg_constraint c +WHERE confrelid IN (SELECT pg_catalog.pg_partition_ancestors('anc_test'::regclass) + UNION ALL VALUES ('anc_test'::pg_catalog.regclass)) + AND contype = 'f' AND conparentid = 0 +ORDER BY conname;`, + Expected: []sql.Row{{"anc_child_apk_fkey", "anc_child", "FOREIGN KEY (apk) REFERENCES anc_test(pk)"}}, + }, + }, + }, { Name: "set-returning function as join operand", SetUpScript: []string{ diff --git a/testing/go/pgcatalog_test.go b/testing/go/pgcatalog_test.go index 2bcb494ab8..9618bc8c0a 100644 --- a/testing/go/pgcatalog_test.go +++ b/testing/go/pgcatalog_test.go @@ -665,6 +665,30 @@ func TestPgClass(t *testing.T) { }, }, }, + { + Name: "pg_class relhastriggers", + SetUpScript: []string{ + `CREATE TABLE plain (pk INT primary key);`, + `CREATE TABLE fk_parent (pk INT primary key);`, + `CREATE TABLE fk_child (pk INT primary key, ppk INT REFERENCES fk_parent(pk));`, + `CREATE TABLE trig_table (pk INT primary key);`, + `CREATE FUNCTION trig_fn() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;`, + `CREATE TRIGGER trig BEFORE INSERT ON trig_table FOR EACH ROW EXECUTE FUNCTION trig_fn();`, + }, + Assertions: []ScriptTestAssertion{ + { + // Postgres enforces foreign keys with system triggers on both tables of the + // constraint, so tables on either side report relhastriggers + Query: `SELECT relname, relhastriggers FROM pg_catalog.pg_class WHERE relname IN ('plain', 'fk_parent', 'fk_child', 'trig_table') ORDER BY relname;`, + Expected: []sql.Row{ + {"fk_child", "t"}, + {"fk_parent", "t"}, + {"plain", "f"}, + {"trig_table", "t"}, + }, + }, + }, + }, { Name: "pg_class with regclass", SetUpScript: []string{ diff --git a/testing/go/psql_test.go b/testing/go/psql_test.go index 47a9700e9a..c6920d68a9 100755 --- a/testing/go/psql_test.go +++ b/testing/go/psql_test.go @@ -126,6 +126,34 @@ ORDER BY 1, 2, 4;`, }, }, }, + { + Name: `\d tablename triggers`, + SetUpScript: []string{ + "CREATE TABLE test_table (id INT PRIMARY KEY, name TEXT);", + "CREATE FUNCTION trig_fn() RETURNS trigger AS $$ BEGIN RETURN NEW; END; $$ LANGUAGE plpgsql;", + "CREATE TRIGGER test_trig BEFORE INSERT ON test_table FOR EACH ROW EXECUTE FUNCTION trig_fn();", + }, + Assertions: []ScriptTestAssertion{ + { + // The trigger listing query issued by psql's \d command + Query: `SELECT t.tgname, pg_catalog.pg_get_triggerdef(t.oid, true), t.tgenabled, t.tgisinternal, + CASE WHEN t.tgparentid != 0 THEN + (SELECT u.tgrelid::pg_catalog.regclass + FROM pg_catalog.pg_trigger AS u, + pg_catalog.pg_partition_ancestors(t.tgrelid) WITH ORDINALITY AS a(relid, depth) + WHERE u.tgname = t.tgname AND u.tgrelid = a.relid + AND u.tgparentid = 0 + ORDER BY a.depth LIMIT 1) + END AS parent +FROM pg_catalog.pg_trigger t +WHERE t.tgrelid = 'test_table'::regclass AND (NOT t.tgisinternal OR (t.tgisinternal AND t.tgenabled = 'D')) +ORDER BY 1;`, + Expected: []sql.Row{ + {"test_trig", "CREATE TRIGGER test_trig BEFORE INSERT ON test_table FOR EACH ROW EXECUTE FUNCTION trig_fn()", "O", "f", nil}, + }, + }, + }, + }, { Name: `\d tablename`, SetUpScript: []string{ From e9d49892f095060323a032f0c5de71c8c5523749 Mon Sep 17 00:00:00 2001 From: zachmu Date: Fri, 14 Aug 2026 00:24:24 +0000 Subject: [PATCH 3/4] [ga-format-pr] Run scripts/format_repo.sh --- server/ast/aliased_table_expr.go | 1 - 1 file changed, 1 deletion(-) diff --git a/server/ast/aliased_table_expr.go b/server/ast/aliased_table_expr.go index 91a4dec04f..47775fc9e0 100644 --- a/server/ast/aliased_table_expr.go +++ b/server/ast/aliased_table_expr.go @@ -18,7 +18,6 @@ import ( "strings" "github.com/cockroachdb/errors" - vitess "github.com/dolthub/vitess/go/vt/sqlparser" "github.com/dolthub/doltgresql/postgres/parser/sem/tree" From 7a59f8efd8f6986e4c8f5624abd1e3a4abb49a1b Mon Sep 17 00:00:00 2001 From: Zach Musgrave Date: Thu, 13 Aug 2026 17:31:08 -0700 Subject: [PATCH 4/4] skipped test --- testing/go/dolt_functions_test.go | 58 +++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/testing/go/dolt_functions_test.go b/testing/go/dolt_functions_test.go index fd94825680..9072410796 100644 --- a/testing/go/dolt_functions_test.go +++ b/testing/go/dolt_functions_test.go @@ -1321,6 +1321,64 @@ func TestDoltDiff(t *testing.T) { }, }, }, + { + // TODO: WITH ORDINALITY is currently only supported for set-returning functions, which are expanded in a + // select list. Dolt system table functions are GMS table functions that can only appear in a FROM + // clause, so these all error with "is a table function and must be used in a FROM clause". + Name: "dolt table functions WITH ORDINALITY", + Skip: true, + SetUpScript: []string{ + "CREATE TABLE t1 (pk INT4 PRIMARY KEY, v1 TEXT);", + "SELECT length(DOLT_COMMIT('-A', '-m', 'initial')::text) = 32;", + "INSERT INTO t1 VALUES (1, 'one'), (2, 'two');", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT diff_type, from_pk, to_pk, ordinality FROM dolt_diff('HEAD', 'WORKING', 't1') WITH ORDINALITY;", + Expected: []sql.Row{ + {"added", nil, 1, 1}, + {"added", nil, 2, 2}, + }, + }, + { + Query: "SELECT ord, to_pk1 FROM dolt_diff('HEAD', 'WORKING', 't1') WITH ORDINALITY AS a(to_pk1, to_v11, to_commit1, to_commit_date1, from_pk1, from_v11, from_commit1, from_commit_date1, diff_type1, ord);", + Expected: []sql.Row{ + {1, 1}, + {2, 2}, + }, + }, + { + Query: "SELECT message, ordinality FROM dolt_log() WITH ORDINALITY LIMIT 1;", + Expected: []sql.Row{ + {"initial", 1}, + }, + }, + { + Query: "SELECT table_name, ordinality FROM dolt_diff_stat('HEAD', 'WORKING') WITH ORDINALITY;", + Expected: []sql.Row{ + {"public.t1", 1}, + }, + }, + { + Query: "SELECT to_table_name, diff_type, ordinality FROM dolt_diff_summary('HEAD', 'WORKING') WITH ORDINALITY;", + Expected: []sql.Row{ + {"public.t1", "modified", 1}, + }, + }, + { + Query: "SELECT table_name, diff_type, ordinality FROM dolt_patch('HEAD', 'WORKING') WITH ORDINALITY;", + Expected: []sql.Row{ + {"public.t1", "data", 1}, + {"public.t1", "data", 2}, + }, + }, + { + // No schema changes in the working set, so ordinality over an empty result is empty + Query: "SELECT * FROM dolt_schema_diff('HEAD', 'WORKING') WITH ORDINALITY;", + Expected: []sql.Row{}, + }, + }, + }, }) }