Skip to content

Enable functional index support#2642

Merged
fulghum merged 1 commit into
mainfrom
fulghum/function_expr_index
May 21, 2026
Merged

Enable functional index support#2642
fulghum merged 1 commit into
mainfrom
fulghum/function_expr_index

Conversation

@fulghum

@fulghum fulghum commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Minor changes to enable functional index support and run Dolt's functional index enginetests.

There are three regressions reported in this PR. One is a valid issue where CREATE TABLE ... INHERITS can't copy a table with a functional index. I'll tackle this one in a follow-up PR. There's another recovered panic that seems to have to do with polymorphic array handling and doesn't seem directly related to this PR. I tested the last regression listed (from vaccum) and it's an issue with returning elements from a wrapped function, which also doesn't appear directly related to this PR.

Related to #2298

@github-actions

github-actions Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor
Main PR
covering_index_scan_postgres 1290.33/s 1318.68/s +2.1%
index_join_postgres 198.32/s 197.77/s -0.3%
index_join_scan_postgres 208.13/s 208.87/s +0.3%
index_scan_postgres 12.34/s 12.32/s -0.2%
oltp_point_select 2246.23/s 2254.67/s +0.3%
oltp_read_only 1812.82/s 1822.16/s +0.5%
select_random_points 130.03/s 132.09/s +1.5%
select_random_ranges 865.25/s 866.99/s +0.2%
table_scan_postgres 11.97/s 12.01/s +0.3%
types_table_scan_postgres 5.51/s 5.47/s -0.8%

@github-actions

github-actions Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 18126 18157
Failures 23964 23933
Partial Successes1 5384 5386
Main PR
Successful 43.0649% 43.1385%
Failures 56.9351% 56.8615%

${\color{red}Regressions (3)}$

create_table_like

QUERY:          CREATE TABLE ctlt13_inh () INHERITS (ctlt1, ctlt3);
RECEIVED ERROR: invalid column name: !hidden!ctlt3_fnidx!0!0 (errno 1105) (sqlstate HY000)

polymorphism

QUERY:          create function first_el(anyarray) returns anyelement as
'select $1[1]' language sql strict immutable;
RECEIVED ERROR: receiveMessage recovered panic: unexpected type *types.deferredType for subscript: goroutine 367968 [running]:
runtime/debug.Stack()
	/opt/hostedtoolcache/go/1.26.2/x64/src/runtime/debug/stack.go:26 +0x5e
github.com/dolthub/doltgresql/server.(*ConnectionHandler).receiveMessage.func1()
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:351 +0x4c
panic({0x3c08560?, 0x17fee2e1e6b0?})
	/opt/hostedtoolcache/go/1.26.2/x64/src/runtime/panic.go:860 +0x13a
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).bindOnly.func1()
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/parse.go:107 +0x71
panic({0x3c08560?, 0x17fee2e1e6b0?})
	/opt/hostedtoolcache/go/1.26.2/x64/src/runtime/panic.go:860 +0x13a
github.com/dolthub/doltgresql/server/expression.Subscript.Type({{0x4f81ba0?, 0x17feedcce560?}, {0x4f81b00?, 0x17fee6cc50e0?}}, 0x17ff048e0320)
	/home/runner/work/doltgresql/doltgresql/server/expression/subscript.go:58 +0xcd
github.com/dolthub/go-mysql-server/sql/expression.(*Alias).Type(...)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/expression/alias.go:122
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).analyzeSelectList(0x17fedff7c540, 0x17fee23fe700, 0x17fee23fe800, {0x17fee2e1e400, 0x1, 0x0?})
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/project.go:156 +0x597
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).analyzeProjectionList(...)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/project.go:29
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).buildSelect(0x17fedff7c540, 0x4479780?, 0x17fee239db30)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/select.go:78 +0x2a5
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).buildSelectStmt(0x17fedff7c540, 0x17feeb2bad00?, {0x5007038?, 0x17fee239db30})
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/select.go:36 +0xf7
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).buildSubquery(0x17fedff7c540, 0x17ff048e0320?, {0x4f3b008, 0x17fee239db30}, {0x17ff131d54f0, 0xc}, {0x17ff131d54f0?, 0xc?})
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/builder.go:234 +0x266c
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).build(...)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/builder.go:223
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).bindOnly(0x17fedff7c540, {0x4f3b008, 0x17fee239db30}, {0x17ff131d54f0, 0xc}, 0x0)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/parse.go:119 +0x165
github.com/dolthub/go-mysql-server/sql/planbuilder.(*Builder).BindOnly(0x17fedff7c540, {0x4f3b008?, 0x17fee239db30?}, {0x17ff131d54f0?, 0x0?}, 0x17ff11cac3c8?)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/planbuilder/parse.go:92 +0x27
github.com/dolthub/doltgresql/server/analyzer.ValidateCreateFunction(0x17ff048e0320, 0x17fee047c140, {0x4f7b940, 0x17ff06935380}, 0x4f7b940?, 0x17ff06935380?, 0x17fee2e1e460?)
	/home/runner/work/doltgresql/doltgresql/server/analyzer/create_function.go:49 +0x16f
github.com/dolthub/go-mysql-server/sql/analyzer.(*Batch).evalOnce(0x17feeb2bb1b8?, 0x17ff048e0320, 0x17fee047c140, {0x4f7b940?, 0x17ff06935380?}, 0x0, 0x4ed8d50, 0x17fee2e1e460)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/analyzer/batch.go:85 +0x263
github.com/dolthub/go-mysql-server/sql/analyzer.(*Batch).EvalWithSelector(0x4f5ccf0?, 0x4f7b940?, 0x17fee047c140, {0x4f7b940?, 0x17ff06935380?}, 0x0?, 0x6?, 0x4509369?)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/analyzer/batch.go:59 +0x151
github.com/dolthub/go-mysql-server/sql/analyzer.(*Batch).Eval(...)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/analyzer/batch.go:51
github.com/dolthub/go-mysql-server/sql/analyzer.(*Analyzer).analyzeWithSelector(0x17fee047c140, 0x4f81ce0?, {0x4f7b940, 0x17ff06935380}, 0x0, 0x4ed8d58, 0x4ed8d50, 0x17fee2e1e460)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/analyzer/analyzer.go:547 +0x4a5
github.com/dolthub/go-mysql-server/sql/analyzer.(*Analyzer).Analyze(0x2801c07?, 0x17fedff7c480?, {0x4f7b940?, 0x17ff06935380?}, 0x17fee8f5a540?, 0x17feeb2bb3a8?)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/sql/analyzer/analyzer.go:500 +0x139
github.com/dolthub/go-mysql-server.(*Engine).analyzeNode(0x17ff048e0320?, 0x17ff048e0320?, {0x17ff06935380?, 0x65?}, {0x4f7b940?, 0x17ff06935380?}, 0x0?)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/engine.go:559 +0x286
github.com/dolthub/go-mysql-server.(*Engine).QueryWithBindings(0x17fee08743c0, 0x17ff048e0320, {0x17fee8f5a540, 0x66}, {0x4f3a9b8, 0x17fee2497ab0}, 0x0, 0x0)
	/home/runner/go/pkg/mod/github.com/dolthub/go-mysql-server@v0.20.1-0.20260515230210-e6b6608edbd2/engine.go:365 +0x355
github.com/dolthub/doltgresql/server.(*DoltgresHandler).executeQuery(0x17fee6cc5220?, 0x17fee2e1e450?, {0x17fee8f5a540?, 0x2a8bd40?}, {0x4f3a9b8?, 0x17fee2497ab0?}, {0x2b49f7f?, 0x17feeb2bb638?})
	/home/runner/work/doltgresql/doltgresql/server/doltgres_handler.go:438 +0x2b
github.com/dolthub/doltgresql/server.(*DoltgresHandler).doQuery(0x17fee2af5c80, {0x4f5ca88?, 0x980fa60?}, 0x17fedf399808?, {0x17fee8f5a540, 0x66}, {0x4f3a9b8, 0x17fee2497ab0}, {0x0, 0x0}, ...)
	/home/runner/work/doltgresql/doltgresql/server/doltgres_handler.go:376 +0x613
github.com/dolthub/doltgresql/server.(*DoltgresHandler).ComQuery(0x17fee2af5c80, {0x4f5ca88, 0x980fa60}, 0x17ff0bb02b40, {0x17fee8f5a540, 0x66}, {0x4f3a9b8, 0x17fee2497ab0}, 0x17fef7742d80)
	/home/runner/work/doltgresql/doltgresql/server/doltgres_handler.go:231 +0x125
github.com/dolthub/doltgresql/server.(*ConnectionHandler).query(0x17fee2af5d80, {{0x17fee8f5a540, 0x66}, {0x4f3a9b8, 0x17fee2497ab0}, {0x4511ca7, 0xf}})
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:996 +0x235
github.com/dolthub/doltgresql/server.(*ConnectionHandler).handleQuery(0x17fee2af5d80, 0x17ff0bb14380)
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:466 +0x22a
github.com/dolthub/doltgresql/server.(*ConnectionHandler).handleMessage(0x17ff0bb141e0?, {0x4f38d38?, 0x17ff0bb14380?})
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:408 +0x7d
github.com/dolthub/doltgresql/server.(*ConnectionHandler).receiveMessage(0x17fee2af5d80)
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:381 +0x2c5
github.com/dolthub/doltgresql/server.(*ConnectionHandler).HandleConnection(0x17fee2af5d80)
	/home/runner/work/doltgresql/doltgresql/server/connection_handler.go:186 +0x150
created by github.com/dolthub/doltgresql/server.(*Listener).Accept in goroutine 114
	/home/runner/work/doltgresql/doltgresql/server/listener.go:89 +0x11f

vacuum

QUERY:          INSERT INTO vaccluster VALUES (1), (2);
RECEIVED ERROR: expression does not result in a single value (errno 1105) (sqlstate HY000)

${\color{lightgreen}Progressions (35)}$

alter_table

QUERY: ALTER TABLE tt9 ADD CHECK(c > 2);

collate.icu.utf8

QUERY: CREATE INDEX collate_test1_idx3 ON collate_test1 ((b COLLATE "C"));
QUERY: CREATE INDEX collate_test1_idx4 ON collate_test1 (((b||'foo') COLLATE "POSIX"));

compression

QUERY: CREATE UNIQUE INDEX idx1 ON cmdata2 ((f1 || f2));

create_aggregate

QUERY: create function least_accum(int8, int8) returns int8 language sql as
  'select least($1, $2)';
QUERY: drop function least_accum(int8, int8);

create_index

QUERY: CREATE UNIQUE INDEX func_index_index on func_index_heap (textcat(f1,f2));
QUERY: INSERT INTO func_index_heap VALUES('ABCD', 'EF');
QUERY: CREATE UNIQUE INDEX func_index_index on func_index_heap ((f1 || f2) text_ops);
QUERY: INSERT INTO func_index_heap VALUES('ABCD', 'EF');
QUERY: CREATE UNIQUE INDEX concur_reindex_ind3 ON concur_reindex_tab(abs(c1));
QUERY: CREATE UNIQUE INDEX concur_exprs_index_expr
  ON concur_exprs_tab ((c1::text COLLATE "C"));

create_table

QUERY: create index part_column_drop_b_expr on part_column_drop((b = 1));
QUERY: create index part_column_drop_d_expr on part_column_drop((d = 2));

create_table_like

QUERY: CREATE INDEX ctlt1_fnidx ON ctlt1 ((a || b));
QUERY: CREATE INDEX ctlt3_fnidx ON ctlt3 ((a || c));
QUERY: CREATE TABLE inh_error1 () INHERITS (ctlt1, ctlt4);

equivclass

QUERY: create unique index ec1_expr1 on ec1((ff + 1));
QUERY: create unique index ec1_expr2 on ec1((ff + 2 + 1));
QUERY: create unique index ec1_expr3 on ec1((ff + 3 + 1));
QUERY: create unique index ec1_expr4 on ec1((ff + 4));

generated

QUERY: CREATE INDEX gtest22c_expr_idx ON gtest22c ((b * 3));

hash_index

QUERY: create unique index hash_f8_index_1 on hash_f8_heap(abs(random));

indexing

QUERY: create index on idxpart1 ((a + 0));
QUERY: create index on idxpart ((a + b));
QUERY: create index on idxpart (abs(b));
QUERY: create index parted_isvalid_idx on parted_isvalid_tab ((a/b));

inherit

QUERY: alter table p2 add check (f2>0);
QUERY: create index matest0i on matest0 ((1-id));
QUERY: create index matest1i on matest1 ((1-id));
QUERY: create index matest3i on matest3 ((1-id));
QUERY: create index on permtest_parent (left(c, 3));

insert_conflict

QUERY: create unique index expr_key_index on insertconflicttest(lower(fruit));
QUERY: create unique index insertconflicti1 on insertconflict(coalesce(a, 0));

vacuum

QUERY: CREATE INDEX ON vaccluster(wrap_do_analyze(i));

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.

@fulghum fulghum force-pushed the fulghum/function_expr_index branch from df4a5d9 to f389aca Compare April 30, 2026 18:59
@fulghum fulghum force-pushed the fulghum/function_expr_index branch 3 times, most recently from 3a3e1cd to cf03e18 Compare May 18, 2026 16:42
@fulghum fulghum force-pushed the fulghum/function_expr_index branch from 4f849f0 to 1b44049 Compare May 19, 2026 22:19
@fulghum fulghum force-pushed the fulghum/function_expr_index branch from 1b44049 to 7c0ccb2 Compare May 20, 2026 19:11
@fulghum fulghum marked this pull request as ready for review May 20, 2026 20:11
@itoqa

itoqa Bot commented May 20, 2026

Copy link
Copy Markdown

Ito Test Report ❌

13 test cases ran. 3 failed, 1 additional finding, 9 passed.

Overall, the run showed good baseline stability and partial feature health—several expression-index and deep-expression scenarios completed successfully without server crashes, information_schema.columns correctly excluded hidden system columns, and enginetest CREATE/DROP index command-tag handling passed—but the aggregate result is not clean due to confirmed product bugs. The most important findings are high-severity expression-index contract mismatches where parser/analyzer acceptance still ends in execution-time “unsupported expression” failures (including mixed named+expression cases that suppress expected missing-column validation), plus inconsistent metadata exposure in pg_catalog.pg_indexes.indexdef leaking hidden internal backing names and a medium-severity SQL formatting bug where parenthesized cast expressions miss the intended implicit alias.

❌ Failed (3)
Category Summary Screenshot
Validation ⚠️ Re-execution confirms a real app defect: expression-index DDL does not complete successfully. Although analyzer logic explicitly skips name-based checks for expression entries (server/analyzer/validate_create_table.go:127-133), runtime behavior still rejects expression index attributes (also reflected by the existing skipped coverage note in testing/go/index_test.go:1379), and the index is not created. VALIDATION-1
Validation ⚠️ Re-execution confirms a real app defect: mixed index definitions do not produce the expected differentiated outcomes. Both the valid mixed case and the missing-column case fail with the same expression-index unsupported error, instead of surfacing missing-column validation for the bad statement. VALIDATION-2
Visibility 🟠 pg_catalog.pg_indexes.indexdef exposes hidden internal expression-index backing columns, creating contradictory schema introspection output. VISIBILITY-2
⚠️ Expression index creation fails after analyzer acceptance
  • What failed: The expression-index statement is accepted through conversion/validation stages but fails with an unsupported-expression error before index creation; expected behavior is successful creation once expression entries are accepted.
  • Impact: Teams cannot use functional indexes in this path, so a core indexing workflow in the feature is unusable. This blocks performance and query-plan use cases that depend on expression indexes.
  • Steps to reproduce:
    1. Create table t_val_expr with at least one text column.
    2. Run ALTER TABLE t_val_expr ADD INDEX idx_val_expr ((concat(v1, v1))).
    3. Retry with CREATE INDEX using the same expression entry.
    4. Check pg_indexes and verify idx_val_expr is not present after the unsupported-expression error.
  • Stub / mock context: Authentication was temporarily disabled in server/authentication_scram.go for deterministic local access while running SQL validation; no index-path mocks or fake SQL responses were used.
  • Code analysis: The parser conversion now materializes expression payloads (nodeIndexElemList), and analyzer validation explicitly skips name checks for expression entries (validateIndex), but repository tests still mark expression index attributes as unsupported in execution coverage.
  • Why this is likely a bug: Production code accepts expression-index definitions in parser and analyzer layers but does not provide a compatible execution path, creating a clear contract mismatch in shipped behavior.

Relevant code:

server/ast/index_elem.go (lines 63-76)

var expr vitess.Expr
if inputColumn.Expr != nil {
    var err error
    expr, err = nodeExpr(ctx, inputColumn.Expr)
    if err != nil {
        return nil, err
    }
}

vitessIndexColumns = append(vitessIndexColumns, &vitess.IndexField{
    Column:     vitess.NewColIdent(string(inputColumn.Column)),
    Order:      vitess.AscScr,
    Expression: expr,
})

server/analyzer/validate_create_table.go (lines 124-133)

func validateIndex(ctx *sql.Context, colMap map[string]*sql.Column, idxDef *sql.IndexDef) error {
    seenCols := make(map[string]struct{})
    for _, idxCol := range idxDef.Columns {
        if idxCol.Expression != nil {
            continue
        }

        schCol, exists := colMap[strings.ToLower(idxCol.Name)]
        if !exists {
            return sql.ErrKeyColumnDoesNotExist.New(idxCol.Name)
        }

testing/go/index_test.go (lines 1377-1380)

{ // https://github.com/dolthub/doltgresql/issues/2206
    Name: "Index attributes",
    Skip: true, // We were getting a syntax error previously, which is fixed, however we don't yet support expression index attributes
    SetUpScript: []string{
⚠️ Mixed index validation loses named-column error specificity
  • What failed: Both statements fail with the same expression-unsupported error, so the invalid named-column case does not surface the expected missing-column-specific validation signal.
  • Impact: Mixed index definitions cannot distinguish valid vs invalid named-column behavior when an expression entry is present, breaking predictable DDL validation outcomes. This makes index rollout and debugging unreliable for users mixing standard and functional index parts.
  • Steps to reproduce:
    1. Create table t_mixed with one valid named column and no missing_col column.
    2. Run CREATE INDEX idx_mixed_ok ON t_mixed (v1, (concat(v1, v1))).
    3. Run CREATE INDEX idx_mixed_bad ON t_mixed (missing_col, (concat(v1, v1))).
    4. Compare returned errors and confirm both fail with expression unsupported instead of surfacing missing-column validation for the bad case.
  • Stub / mock context: A local fallback container was used to execute SQL checks, and authentication was disabled in server/authentication_scram.go to keep access deterministic; no mocked query results were injected for index behavior.
  • Code analysis: validateIndex intentionally bypasses expression entries and validates only named columns, so the expected split outcome depends on downstream expression execution support; execution still lacks that support, collapsing both cases into the same error.
  • Why this is likely a bug: The code path explicitly aims to validate named columns independently in mixed definitions, but runtime behavior cannot realize that contract because expression elements are still unsupported later in production execution.

Relevant code:

server/analyzer/validate_create_table.go (lines 124-138)

func validateIndex(ctx *sql.Context, colMap map[string]*sql.Column, idxDef *sql.IndexDef) error {
    seenCols := make(map[string]struct{})
    for _, idxCol := range idxDef.Columns {
        if idxCol.Expression != nil {
            continue
        }

        schCol, exists := colMap[strings.ToLower(idxCol.Name)]
        if !exists {
            return sql.ErrKeyColumnDoesNotExist.New(idxCol.Name)
        }
        if _, ok := seenCols[schCol.Name]; ok {
            return sql.ErrDuplicateColumn.New(schCol.Name)
        }

server/ast/index_elem.go (lines 64-76)

if inputColumn.Expr != nil {
    var err error
    expr, err = nodeExpr(ctx, inputColumn.Expr)
    if err != nil {
        return nil, err
    }
}

vitessIndexColumns = append(vitessIndexColumns, &vitess.IndexField{
    Column:     vitess.NewColIdent(string(inputColumn.Column)),
    Order:      vitess.AscScr,
    Expression: expr,
})

testing/go/index_test.go (lines 1377-1380)

{ // https://github.com/dolthub/doltgresql/issues/2206
    Name: "Index attributes",
    Skip: true, // We were getting a syntax error previously, which is fixed, however we don't yet support expression index attributes
    SetUpScript: []string{
🟠 Expression index metadata exposes hidden internal columns
  • What failed: information_schema.columns hides internal system columns as expected, but pg_indexes.indexdef still emits hidden backing names like !hidden!idx_vis_expr!0!0 instead of a coherent user-facing expression definition.
  • Impact: Schema tooling and users relying on catalog introspection can receive contradictory metadata for the same index. This can break index discovery or downstream migration/inspection workflows unless clients add custom handling for internal names.
  • Steps to reproduce:
    1. Create a table and add an expression index on it.
    2. Query information_schema.columns to list visible columns for that table.
    3. Query pg_catalog.pg_indexes for the same table and inspect indexdef output.
    4. Compare outputs and observe hidden internal names in indexdef that are absent from information_schema.columns.
  • Stub / mock context: Authentication checks were temporarily bypassed by disabling the SCRAM gate in server/authentication_scram.go so index metadata queries could run in this environment; no route-level network mocking was used for this validation.
  • Code analysis: I reviewed the metadata generation paths in server/tables/pgcatalog/pg_indexes.go and server/tables/information_schema/columns_table.go. information_schema.columns now filters hidden system columns, but pg_indexes serializes raw index.Expressions() values, so hidden backing identifiers still leak into indexdef and drift from the filtered schema view.
  • Why this is likely a bug: The production code applies hidden-column filtering in one catalog surface but not the index definition surface, so the same schema state is reported inconsistently by design.

Relevant code:

server/tables/pgcatalog/pg_indexes.go (lines 119-130)

cols := make([]string, len(index.Expressions()))
for i, expr := range index.Expressions() {
    split := strings.Split(expr, ".")
    if len(split) > 1 {
        cols[i] = split[1]
    } else {
        cols[i] = expr
    }
}
colsStr := strings.Join(cols, ", ")

server/tables/information_schema/columns_table.go (lines 206-209)

for i, col := range information_schema.SchemaForTable(t, db.Database, allColsWithDefaultValue) {
    if col.HiddenSystem {
        continue
    }
    r := getRowFromColumn(ctx, i, col, db.CatalogName, db.SchemaName, tblName)
✅ Passed (9)
Category Summary Screenshot
Formatting No real bug reproduced. Reserved-word-like/case-sensitive identifiers remained unambiguous in output and executed successfully. N/A
Formatting Cast-expression auto aliases remained stable across quoted/case-sensitive identifier scenarios; equivalent forms produced consistent output and no semantic drift was observed in query execution. N/A
Harness Infrastructure blockage was resolved and TestIndexedExpressions passed, including CREATE/DROP lifecycle checks consistent with command-tag mapping support. HARNESS-1
Harness Indexed-expression harness signal check passed; skipped queries were unsupported syntax only, while relevant lifecycle and behavioral checks executed successfully. HARNESS-2
Index CREATE INDEX on concat(v1,v1) executed successfully and idx_expr_ok was present in pg_indexes metadata. INDEX-1
Index A deep concat expression index was created successfully and subsequent SELECT/pg_isready checks confirmed the server stayed responsive. INDEX-2
Index A valid expression index traversed layers and executed successfully, while an invalid expression was rejected with a consistent analyzer-level column error; server remained responsive. INDEX-3
Validation Syntactically valid expression-index DDL executed to backend and was rejected with clear deterministic error ('expression index attribute is not yet supported'); post-failure metadata remained clean with no index side effects. VALIDATION-3
Visibility Confirmed information_schema.columns only returns user-visible columns and excludes hidden system columns. VISIBILITY-1
ℹ️ Additional Findings (1)

These findings are unrelated to the current changes but were observed during testing.

Category Summary Screenshot
Formatting 🟠 Real bug confirmed: parenthesized cast expressions do not receive the intended implicit alias, and output falls back to full expression text. N/A
🟠 Parenthesized cast expression misses implicit alias
  • What failed: The result label is emitted as the full expression text ("CaseSensitive"::TEXT) instead of deriving the implicit alias from the cast input expression.
  • Impact: SQL clients and tooling that rely on stable implicit column labels receive inconsistent headers for equivalent cast expressions. This can break downstream query parsing or metadata matching workflows that assume PostgreSQL-like alias behavior.
  • Steps to reproduce:
    1. Create a table with a quoted identifier column such as "CaseSensitive".
    2. Run SELECT ("CaseSensitive"::TEXT) FROM t_fmt_alias LIMIT 1 without an AS alias.
    3. Inspect the returned column header in the query output.
    4. Compare the label against the expected implicit alias derived from the cast input expression.
  • Stub / mock context: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code analysis: I inspected cast alias derivation in server/ast/select.go and expression conversion in server/ast/expr.go. The implicit alias branch runs only when expr is directly a *tree.CastExpr; parenthesized casts are represented as *tree.ParenExpr, so alias derivation is skipped and the fallback label remains the full expression text.
  • Why this is likely a bug: The code path for implicit cast aliasing does not handle parenthesized casts, creating inconsistent labeling for semantically equivalent expressions in production SQL behavior.

Relevant code:

server/ast/select.go (lines 132-160)

vitessExpr, err := nodeExpr(ctx, expr)
if err != nil {
    return nil, err
}

if ce, ok := expr.(*tree.CastExpr); ok && node.As == "" {
    hasConst := false
    _, _ = tree.SimpleVisit(expr, func(visitingExpr tree.Expr) (recurse bool, newExpr tree.Expr, err error) {
        switch visitingExpr.(type) {
        case tree.Constant:
            hasConst = true
            return false, visitingExpr, nil
        }
        return true, visitingExpr, nil
    })

server/ast/select.go (lines 156-165)

// cast type is not part of column name
// e.g. `id::INT2` should create column name as `id`.
node.As = tree.UnrestrictedName(tree.AsString(ce.Expr))
}

return &vitess.AliasedExpr{
    Expr:            vitessExpr,
    As:              vitess.NewColIdent(string(node.As)),
    InputExpression: inputExpressionForSelectExpr(node),
}, nil

server/ast/expr.go (lines 735-742)

case *tree.ParenExpr:
    expr, err := nodeExpr(ctx, node.Expr)
    if err != nil {
        return nil, err
    }
    return &vitess.ParenExpr{
        Expr: expr,
    }, nil

Commit: 7c0ccb2

View Full Run


Tell us how we did: Give Ito Feedback

@fulghum fulghum requested a review from zachmu May 21, 2026 15:59

@zachmu zachmu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@fulghum fulghum merged commit d2e9a53 into main May 21, 2026
22 checks passed
@fulghum fulghum deleted the fulghum/function_expr_index branch May 21, 2026 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants