Skip to content

fix unknown type array input for any expr - #3031

Open
jennifersp wants to merge 8 commits into
mainfrom
jennifer/any
Open

fix unknown type array input for any expr#3031
jennifersp wants to merge 8 commits into
mainfrom
jennifer/any

Conversation

@jennifersp

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
Main PR
covering_index_scan_postgres 2017.07/s 1986.31/s -1.6%
groupby_scan_postgres 149.58/s 148.05/s -1.1%
index_join_postgres 671.21/s 670.69/s -0.1%
index_join_scan_postgres 849.83/s 844.15/s -0.7%
index_scan_postgres 31.73/s 31.48/s -0.8%
oltp_delete_insert_postgres 861.80/s 826.51/s -4.1%
oltp_insert 799.61/s ${\color{red}675.76/s}$ ${\color{red}-15.5\%}$
oltp_point_select 3602.94/s 3522.66/s -2.3%
oltp_read_only 3559.42/s 3470.81/s -2.5%
oltp_read_write 2760.57/s 2642.82/s -4.3%
oltp_update_index 832.45/s 785.71/s -5.7%
oltp_update_non_index 869.03/s 816.33/s -6.1%
oltp_write_only 1919.74/s 1897.05/s -1.2%
select_random_points 2205.06/s 2124.80/s -3.7%
select_random_ranges 1654.64/s 1536.60/s -7.2%
table_scan_postgres 32.05/s 30.85/s -3.8%
types_delete_insert_postgres 875.77/s 855.06/s -2.4%
types_table_scan_postgres 14.64/s 14.12/s -3.6%

@itoqa

itoqa Bot commented Aug 4, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: ff5b79d: 14 test cases ran, 6 failed ❌, 8 passed ✅.

Summary

The run exercises database querying across catalog filtering, recursive relationships, typed and untyped array comparisons, empty and NULL-containing arrays, invalid-input recovery, and repeatability. Several happy paths are healthy, but untyped array quantifiers and NULL-sensitive comparisons remain broadly unreliable.

Not safe to merge yet — the PR’s central array-coercion behavior fails across ordinary untyped comparisons, NULL-aware semantics, and valid retries, including in fresh sessions, so the changed functionality is not dependable. Other catalog, typed-array, and recovery flows pass, but they do not offset these concentrated, PR-attributable correctness failures.

Tests run by Ito

View full run

Result Severity Type Description
Medium severity Coercion The integer comparison does not return true. The query stops with an unknown-type error.
Medium severity Coercion The ANY query returned SQL NULL instead of true after finding the matching value 2. The ALL query failed with 'unknown type received int32' instead of returning SQL NULL for the preserved NULL element.
Medium severity Coercion The malformed value produced the expected conversion error, but the later valid query returned 'unknown type received int32' instead of true. The fresh-session query returned the same error, so the valid query is broken independently of any state left by the malformed query.
Medium severity Coercion The bad values are rejected as expected, but the following valid comparison does not run. It shows a type error instead of returning true.
Medium severity Quantifier The string-form array query returns an unknown type error instead of true, true, and false. The explicitly typed array control query returns true.
Medium severity Quantifier Both queries return the error unknown type received int32 result. The first query should return SQL NULL because it has a NULL comparison and no false result, while the second should return false because the value 3 is a later nonmatching element.
Catalog Filtering the namespace catalog with a text array returned exactly one row: public.
Catalog The recursive query completed, marked the repeated ID as a cycle, and kept the full path for each row.
Catalog The supported bind-parameter query accepted an integer array and returned the expected rows. The earlier error came from using SQL PREPARE and EXECUTE, which this server does not support.
Catalog The combined catalog filter returned only the public schema, which matched the intersection of the separate filters. Repeating the query returned the same result.
Catalog A malformed array value returned a clear SQL error, and the same connection continued to run valid typed, catalog, and repeated array checks successfully.
Coercion The text comparison with a string array returned true, so the fallback type handled both unknown operands correctly.
Quantifier An empty array returns false for ANY and SOME, and true for ALL, as expected.
Quantifier Each row was checked against its own array values. The results stayed the same when the query was repeated, run in reverse order, and compared with independent calculations.

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread server/expression/any.go
Comment thread server/expression/any.go
Comment thread server/expression/any.go
Comment thread server/expression/any.go
Comment thread server/expression/any.go
Comment thread server/expression/any.go
@itoqa

itoqa Bot commented Aug 4, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportff5b79dddc74f0: 21 test cases ran, 4 new failures ❌, 11 passing ✅, 6 additional findings ⚠️.

Diff Summary

The run covered catalog lookups and joins, hidden metadata fields, type behavior, ordering and filtering, error recovery, and NULL-sensitive edge cases. Basic projections and join flows work, but several changed hidden-field behaviors fail in more complex query contexts, including server crashes and incorrect NULL handling.

Not safe to merge yet — this PR introduces multiple medium-severity failures in its core hidden-field behavior, including query paths that can crash the server and return incorrect results for outer joins. Separate pre-existing array-coercion and quantifier failures are caveats rather than merge drivers.

Tests run by Ito

View full run

Result State Severity Type Description
❌ New Failure Medium severity Column Sorting a query by the hidden cmin or cmax name makes the server panic instead of returning the requested result.
❌ New Failure Medium severity Column Sorting or filtering rows by the hidden transaction column xmin does not work. One form crashes the server connection, and another returns a cast error instead of a result.
❌ New Failure Medium severity Column Ordering by tableoid caused a recovered server panic about an unhandled OID conversion, so the query returned no result set. Unqualified, aliased, and quoted ORDER BY forms showed the same failure, while schema-qualified and JOIN forms without that ordering completed with zero rows.
❌ New Failure Medium severity Type When a LEFT JOIN has no matching row, hidden columns from the empty side return zero-like values instead of NULL.
Passing Catalog The full catalog join completed successfully and returned the expected empty result set without a planner or analyzer error.
Passing Catalog Unmatched inner joins returned no rows, while left joins kept the left row and showed empty right-side values. A known catalog name also matched the expected row.
Passing Catalog A catalog lookup returned the expected value for one match, NULL for no matches, and a clear error for multiple matches. The next query still returned 42.
Passing Catalog Compound catalog joins and CASE expressions completed successfully for matching, empty, and reordered lookups. A deliberate multi-row lookup returned a controlled error, and the next query still worked.
Passing Column The aliased join returned the expected row, and the query plan kept hidden names and ordinary qualified columns separate.
Passing Column Invalid star and overlong names returned clear SQL errors, and the same session continued to run valid queries successfully.
Passing Column Direct projections returned the expected zero values, NULL table identifier, column labels, and PostgreSQL-compatible types. The ordinary table column also returned the expected row value.
Passing Type The database returned the expected zero, tuple, and NULL placeholder values with the correct PostgreSQL types. The query completed without conversion errors.
Passing Type Typed comparisons for hidden database columns returned the expected results, and unsupported casts returned clear SQL errors without crashing the server.
Passing Type An invalid hidden-column cast returned a SQL error, but later queries in the same session returned the same values and type information each time.
Passing Type OID and REGCLASS comparisons returned consistent results in filters and joins. Empty and multi-row scalar queries behaved as expected, and a later lookup succeeded after the expected multi-row error.
⏸️ Skipped Catalog Filtering the namespace catalog with a text array returned exactly one row: public.
⏸️ Skipped Catalog The recursive query completed, marked the repeated ID as a cycle, and kept the full path for each row.
⏸️ Skipped Catalog The supported bind-parameter query accepted an integer array and returned the expected rows. The earlier error came from using SQL PREPARE and EXECUTE, which this server does not support.
⏸️ Skipped Catalog The combined catalog filter returned only the public schema, which matched the intersection of the separate filters. Repeating the query returned the same result.
⏸️ Skipped Catalog A malformed array value returned a clear SQL error, and the same connection continued to run valid typed, catalog, and repeated array checks successfully.
⏸️ Skipped Coercion The text comparison with a string array returned true, so the fallback type handled both unknown operands correctly.
⏸️ Skipped Quantifier An empty array returns false for ANY and SOME, and true for ALL, as expected.
⏸️ Skipped Quantifier Each row was checked against its own array values. The results stayed the same when the query was repeated, run in reverse order, and compared with independent calculations.
⚠️ Additional Finding Medium severity Coercion The comparison returns a server error instead of the expected true value.
⚠️ Additional Finding Medium severity Coercion The ANY query returned NULL instead of true, and the ALL query failed with an unknown-type conversion error instead of returning NULL.
⚠️ Additional Finding Medium severity Coercion The malformed query reports an invalid value as expected. Every later valid query returns an error instead of the expected boolean result, and the fresh-session query fails in the same way.
⚠️ Additional Finding Medium severity Coercion After SELECT 2 = ANY('{1,abc,2}'), SELECT 2 = SOME('{1,abc,2}'), and SELECT 2 = ALL('{1,abc,2}') each returned the expected int4 conversion error, the valid expression shape using '{1,2,3}' failed in every form with 'unknown type received int32 result'. The query returned an error instead of a boolean and did not demonstrate a partial comparison result.
⚠️ Additional Finding Medium severity Quantifier The query returned an error and no result row. The expected values were true for ANY, true for SOME, and false for ALL.
⚠️ Additional Finding Medium severity Quantifier Both queries returned an error and no result row. The expected results were NULL when all non-NULL comparisons pass and false when one comparison fails.
Additional Findings Details

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

🟡 Integer array comparison returns a server error
  • Severity: Medium Medium severity
  • Description: The comparison returns a server error instead of the expected true value.
  • Impact: Queries that compare a number with a string-form array fail instead of returning the correct match. Applications using this valid query receive an error and no result.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as postgres.
    2. Run SELECT 2 = ANY('{1,2,3}');.
    3. Check the result. The expected value is true, but the server returns unknown type received int32 result.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The runtime result is a direct application error: SELECT 2 = ANY('{1,2,3}'); produced unknown type received int32 result. In postgres/parser/sem/tree/type_check.go:1766-1773, the non-tuple right-hand expression is type-checked as an array of the left operand's type (types.MakeArray(cmpTypeLeft)). For this unknown string-form array, that conversion produces a concrete integer value before the comparison reaches output handling. In server/functions/unknown.go:47-58, unknownout only accepts a Go string; lines 53-56 reject any other value and return unknown type received %T result, which is the observed int32 failure. The smallest practical fix is to make unknown-array conversion preserve the original string representation until unknownout consumes it, or to route the converted value through the correct typed array output path instead of invoking unknownout with an integer.
Evidence Package
🟡 NULL array comparisons return wrong results
  • Severity: Medium Medium severity
  • Description: The ANY query returned NULL instead of true, and the ALL query failed with an unknown-type conversion error instead of returning NULL.
  • Impact: Queries that compare a value with an array containing NULL can return the wrong answer or fail instead of following normal SQL rules. This can cause incorrect application decisions for users whose data includes NULL values.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as postgres.
    2. Run SELECT 2 = ANY('{NULL,2}'); and record the result.
    3. Run SELECT 2 = ALL('{NULL,2}'); and record the result.
    4. Compare the results with the expected PostgreSQL behavior: ANY is true and ALL is NULL.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The runtime evidence is consistent with a production-code defect. In server/functions/unknown.go:47-58, unknownout accepts only a Go string; lines 52-56 return unknown type received %T result for any other value, including the int32 result observed by the ALL query. The comparison type-check path in postgres/parser/sem/tree/type_check.go:1766-1773 type-checks a non-constructor right-hand expression as an array of the left operand's type, and lines 1776-1778 treat an unresolved unknown result as always NULL. That path does not establish a safe typed representation for NULL array elements before the unknown output function is reached, so the array comparison cannot preserve PostgreSQL's ANY/ALL three-valued logic. The smallest practical fix is to make unknown-array element conversion preserve NULL elements and pass the resulting typed array through the comparison evaluator, while ensuring unknownout is not called with a non-string comparison result; avoid changing the unrelated hidden-column resolution code unless a focused regression proves it is involved.
Evidence Package
🟡 Valid queries keep failing after a bad array query
  • Severity: Medium Medium severity
  • Description: The malformed query reports an invalid value as expected. Every later valid query returns an error instead of the expected boolean result, and the fresh-session query fails in the same way.
  • Impact: Valid SQL comparisons using this array form fail instead of returning true or false. Users can still run other queries, and there is no evidence of data loss or corruption.
  • Steps to Reproduce:
    1. Run SELECT 2 = ANY('{1,abc,3}'); and confirm that the invalid element returns a type-conversion error.
    2. In the same session, run SELECT 2 = ANY('{1,2,3}'); three times.
    3. Run the valid query once in a fresh session and compare its result with the previous attempts.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The comparison type-checking path in postgres/parser/sem/tree/type_check.go handles a non-constructor right-hand expression by first type-checking the left expression, then calling right.TypeCheck with types.MakeArray(cmpTypeLeft) at lines 1749-1773. This is the deferred unknown-array conversion path used by the recorded ANY expression. The resulting comparison proceeds to the unknown output function in server/functions/unknown.go:47-58. unknownout accepts the Unknown type but requires the runtime value to be a Go string at lines 52-55; an int32 result is rejected with the exact observed error unknown type received int32 result. Because this same failure occurs in a fresh session, it is not leftover session state from the malformed query. The smallest practical fix is to correct the unknown-array result conversion at this boundary, or ensure the deferred array type-check path produces the string representation expected by unknownout, while preserving the normal boolean comparison result. The PR diff in server/ast/expr.go:866-889 only maps hidden column names to zero values, and testing/go/regression_test.go:293-337 only adds a test; neither changed path can fix or cause this failure.
Evidence Package
🟡 Valid array comparisons fail after mixed-value errors
  • Severity: Medium Medium severity
  • Description: After SELECT 2 = ANY('{1,abc,2}'), SELECT 2 = SOME('{1,abc,2}'), and SELECT 2 = ALL('{1,abc,2}') each returned the expected int4 conversion error, the valid expression shape using '{1,2,3}' failed in every form with 'unknown type received int32 result'. The query returned an error instead of a boolean and did not demonstrate a partial comparison result.
  • Impact: Queries using unknown string arrays with ANY, SOME, or ALL fail instead of returning true or false. Users can avoid the issue by providing an explicit array type, but the affected comparisons do not work as written.
  • Steps to Reproduce:
    1. Run SELECT 2 = ANY('{1,abc,2}'), SELECT 2 = SOME('{1,abc,2}'), and SELECT 2 = ALL('{1,abc,2}'); each statement reports an invalid int4 value for abc.
    2. Run the same three statements with '{1,2,3}' instead of '{1,abc,2}'.
    3. Observe that all three valid statements fail with 'unknown type received int32 result' instead of returning boolean values.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The runtime result is consistent with the production conversion path. postgres/parser/sem/tree/parse_array.go:185-227 creates a parseState with a new DArray at lines 188-193, parses the first element at lines 202-203, and then parses later comma-separated elements at lines 206-211. Each parseElement call can append to parser.result before a later element returns an error, so '{1,abc,2}' can have already accumulated a prefix when conversion of abc fails; doParseDArrayFromString then returns nil/error to its caller at lines 209-211. For the valid unknown-array case, postgres/parser/sem/tree/type_check.go:1766-1773 asks the right expression to type-check as an array of the left expression's type. The resulting array is then used by the comparison machinery, but the value eventually reaches the pg_catalog unknown output function in server/functions/unknown.go:47-58. unknownout requires val to be a Go string at lines 52-55 and returns 'unknown type received %T result' for any other value, which matches the observed int32 error. The failure therefore comes from application handling of unknown-array values, not from the browser or from accepting a partial boolean result. The smallest practical fix is to keep failed array parsing from escaping as a usable value and to make the unknown-array comparison path resolve the array to the inferred scalar array type before output; at minimum, unknownout must not be used to serialize the already-typed int32 comparison value.
Evidence Package
🟡 Unknown arrays break quantifier comparisons
  • Severity: Medium Medium severity
  • Description: The query returned an error and no result row. The expected values were true for ANY, true for SOME, and false for ALL.
  • Impact: Queries that compare values with untyped arrays fail instead of returning the correct ANY, SOME, and ALL results. Users must rewrite valid SQL with explicit casts to get an answer.
  • Steps to Reproduce:
    1. Run the local Doltgres server and connect to the postgres database.
    2. Execute SELECT 2 = ANY('{1,2,3}'), 2 = SOME('{1,2,3}'), 2 = ALL('{1,2,3}');
    3. Observe the error instead of the expected row true|true|false.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/expression/any.go:414-432 detects an unknown right-hand array and selects an implicit cast to the left expression's array type. During evaluation, server/expression/any.go:220-224 applies that cast before the quantifier loop at lines 240-276. For the required string-form array, the conversion can produce int32 elements, which then reach the UNKNOWN output function. server/functions/unknown.go:46-58 implements unknownout and accepts only a string at lines 52-55; an int32 value therefore returns the exact error unknown type received int32 result. This happens before the ANY/SOME/ALL comparison logic can produce booleans. The PR context lists only server/ast/expr.go and testing/go/regression_test.go as changed files, so none of the defective conversion or evaluation lines were introduced by this PR. The smallest practical fix is to make unknown-array coercion preserve the intended element representation, or to make the unknown output conversion correctly handle the converted value before quantifier evaluation proceeds; the fix should be limited to that conversion boundary.
Evidence Package
🟡 Unknown arrays break NULL comparisons
  • Severity: Medium Medium severity
  • Description: Both queries returned an error and no result row. The expected results were NULL when all non-NULL comparisons pass and false when one comparison fails.
  • Impact: Queries that compare a value with an untyped array fail instead of returning the expected NULL or false result. Applications using this valid form must add an explicit type cast or cannot complete the comparison.
  • Steps to Reproduce:
    1. Run the local Doltgres server and connect to the postgres database.
    2. Execute SELECT 2 = ALL('{NULL,2}'); and SELECT 2 = ALL('{NULL,3}');.
    3. Observe an error instead of NULL for the first query and false for the second query.
    4. Run the typed controls SELECT 2 = ALL(ARRAY[NULL::int,2]); and SELECT 2 = ALL(ARRAY[NULL::int,3]); to see NULL and false results.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/expression/any.go:220-224 evaluates an implicit cast whenever the right-hand expression has the UNKNOWN type, before server/expression/any.go:240-259 enters the ALL three-valued-logic loop. The unknown-array cast can produce an int32 value. server/functions/unknown.go:46-58 defines unknownout, and its Callable at lines 52-55 accepts only a string; an int32 value therefore returns the exact error unknown type received int32 result. Because this error is returned during the cast, the loop's nil handling at lines 248-257 is never reached, even though that loop would correctly return NULL for {NULL,2} and false for {NULL,3}. The typed ARRAY controls confirm that the comparison and NULL logic work once the array type is explicit. The PR diff changes only server/ast/expr.go and testing/go/regression_test.go, so it does not introduce these defective lines. The smallest practical fix is to correct the unknown-array conversion/output boundary so converted elements retain a representation accepted by the unknown output function, then allow the existing ALL loop to evaluate them.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread server/ast/expr.go
Comment thread server/ast/expr.go
Comment thread server/ast/expr.go
Comment thread server/ast/expr.go
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 18918 18952
Failures 23172 23138
Partial Successes1 5325 5342
Main PR
Successful 44.9465% 45.0273%
Failures 55.0535% 54.9727%

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

arrays

QUERY: select 33 = any ('{1,2,3}');
QUERY: select 33 = any ('{1,2,33}');
QUERY: select 33 = all ('{1,2,33}');
QUERY: select 33 >= all ('{1,2,33}');
QUERY: select null::int >= all ('{1,2,33}');
QUERY: select null::int >= all ('{}');
QUERY: select null::int >= any ('{}');
QUERY: select null::int = any ('{1,2,3}');
QUERY: select 33 = any ('{1,null,3}');
QUERY: select null::int = all ('{1,2,3}');
QUERY: select 33 = all ('{1,null,3}');
QUERY: select 33 = all ('{33,null,33}');

constraints

QUERY: CREATE TABLE SYS_COL_CHECK_TBL (city text, state text, is_capital bool,
                  altitude int,
                  CHECK (NOT (is_capital AND tableoid::regclass::text = 'sys_col_check_tbl')));
QUERY: DROP TABLE SYS_COL_CHECK_TBL;

generated

QUERY: CREATE TABLE gtest_tableoid (
  a int PRIMARY KEY,
  b bool GENERATED ALWAYS AS (tableoid = 'gtest_tableoid'::regclass) STORED
);

inherit

QUERY: SELECT relname, c.* FROM ONLY c, pg_class where c.tableoid = pg_class.oid;
QUERY: SELECT relname, a.* FROM a, pg_class where a.tableoid = pg_class.oid;
QUERY: SELECT relname, b.* FROM b, pg_class where b.tableoid = pg_class.oid;
QUERY: SELECT relname, c.* FROM c, pg_class where c.tableoid = pg_class.oid;
QUERY: SELECT relname, d.* FROM d, pg_class where d.tableoid = pg_class.oid;
QUERY: SELECT relname, a.* FROM ONLY a, pg_class where a.tableoid = pg_class.oid;
QUERY: SELECT relname, b.* FROM ONLY b, pg_class where b.tableoid = pg_class.oid;
QUERY: SELECT relname, c.* FROM ONLY c, pg_class where c.tableoid = pg_class.oid;
QUERY: SELECT relname, d.* FROM ONLY d, pg_class where d.tableoid = pg_class.oid;

opr_sanity

QUERY: SELECT p1.oid, p1.proname
FROM pg_proc as p1 LEFT JOIN pg_description as d
     ON p1.tableoid = d.classoid and p1.oid = d.objoid and d.objsubid = 0
WHERE d.classoid IS NULL AND p1.oid <= 9999;
QUERY: SELECT o1.oid, o1.oprname
FROM pg_operator as o1 LEFT JOIN pg_description as d
     ON o1.tableoid = d.classoid and o1.oid = d.objoid and d.objsubid = 0
WHERE d.classoid IS NULL AND o1.oid <= 9999;
QUERY: WITH funcdescs AS (
  SELECT p.oid as p_oid, proname, o.oid as o_oid,
    pd.description as prodesc,
    'implementation of ' || oprname || ' operator' as expecteddesc,
    od.description as oprdesc
  FROM pg_proc p JOIN pg_operator o ON oprcode = p.oid
       LEFT JOIN pg_description pd ON
         (pd.objoid = p.oid and pd.classoid = p.tableoid and pd.objsubid = 0)
       LEFT JOIN pg_description od ON
         (od.objoid = o.oid and od.classoid = o.tableoid and od.objsubid = 0)
  WHERE o.oid <= 9999
)
SELECT * FROM funcdescs
  WHERE prodesc IS DISTINCT FROM expecteddesc
    AND oprdesc NOT LIKE 'deprecated%'
    AND prodesc IS DISTINCT FROM oprdesc;

psql

QUERY: SELECT l.lanname AS "Name",
       pg_catalog.pg_get_userbyid(l.lanowner) as "Owner",
       l.lanpltrusted AS "Trusted",
       d.description AS "Description"
FROM pg_catalog.pg_language l
LEFT JOIN pg_catalog.pg_description d
  ON d.classoid = l.tableoid AND d.objoid = l.oid
  AND d.objsubid = 0
WHERE l.lanname OPERATOR(pg_catalog.~) E'^(no\\.such\\.language)$' COLLATE pg_catalog.default
ORDER BY 1;

strings

QUERY: SELECT encode('\x1234567890abcdef00', 'hex');

tidrangescan

QUERY: SELECT ctid FROM tidrangescan WHERE ctid < '(0,0)';
QUERY: SELECT ctid FROM tidrangescan WHERE ctid >= '(100,0)';
QUERY: SELECT ctid FROM tidrangescan WHERE ctid > '(0,65535)' AND ctid < '(1,0)' LIMIT 1;
QUERY: SELECT ctid FROM tidrangescan WHERE ctid < '(0,0)' LIMIT 1;
QUERY: SELECT ctid FROM tidrangescan WHERE ctid > '(4294967295,65535)';
QUERY: SELECT ctid FROM tidrangescan WHERE ctid < '(0,0)';
QUERY: SELECT ctid FROM tidrangescan WHERE ctid >= (SELECT NULL::tid);

transactions

QUERY: SELECT a.xmin = b.xmin FROM savepoints a, savepoints b WHERE a.a=6 AND b.a=8;

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.

@itoqa

itoqa Bot commented Aug 5, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportddc74f0f2d7e37: 24 test cases ran, 1 new failure ❌, 13 passing ✅, 10 additional findings ⚠️.

Diff Summary

The run covers core date and time calculations, binary encoding, function and catalog lookups, hidden-column handling, and SQL comparison behavior, including nulls, daylight-saving boundaries, malformed inputs, and recovery after errors. Most exercised paths behave correctly, but a newly affected date-boundary edge case produces an incorrectly signed duration.

Merge with caution — the PR introduces a medium-severity correctness defect in valid fractional timestamp calculations, so affected users may receive misleading duration results. Other medium failures in array comparisons and hidden-column handling are unrelated pre-existing findings and should be tracked separately rather than driving this merge decision.

Tests run by Ito

View full run

Result State Severity Type Description
❌ New Failure Medium severity Age Calculating the age between 2013-03-01 00:00:00.500 UTC and 2013-01-31 23:59:59.250 UTC returned 1 mon -1 days +00:00:01.25. The day component should not remain negative after the interval is normalized.
Passing Age Subtracting March 1 from July 1 returned the expected four-month interval without an overload or query error.
Passing Age The age function returned SQL NULL when either timestamp was NULL and for a NULL transaction ID. A valid call afterward returned 4 mons, so the earlier NULL calls did not affect the session.
Passing Age Timestamp and timestamptz age calls returned four months, the one-argument call returned the expected interval, and explicit NULL casts still returned SQL NULL.
Passing Age Age calculations stayed consistent across the America/New_York spring-forward change. Named time-zone values returned two hours in both directions, while matching fixed offsets returned one hour in both directions.
Passing Encode The database returned lowercase hexadecimal for both hex format spellings and returned an empty value for empty byte data.
Passing Encode Byte values are returned correctly in escape format: printable text stays readable, backslashes are doubled, and control or high-bit bytes use octal escapes.
Passing Function Each object ID returned the exact SQL body for its own function or procedure, even when the lookups were repeated in reverse order.
Passing Function An unknown function ID returned no definition, and the next valid lookup returned the correct SQL body twice.
Passing Function After an invalid OID cast failed, the same database session returned the correct function body twice.
Passing Literal Catalog ordering, grouping, and limit queries returned the expected rows without a planner error. Numeric OID matching and the corrected REGCLASS lookup also preserved the catalog identity for pg_class.
Passing Literal A table-value wrapper passed the catalog object's ID through a visibility check, which returned the expected catalog row.
Passing Literal A NULL object ID produced no catalog matches and stayed NULL. A valid catalog lookup immediately afterward still returned the expected pg_class row.
Passing Literal Catalog lookups returned the same table for direct filters, grouped queries, limits, offsets, and VALUES wrappers. Empty values stayed empty, and a later valid lookup still worked.
⏸️ Skipped Catalog The full catalog join completed successfully and returned the expected empty result set without a planner or analyzer error.
⏸️ Skipped Catalog Unmatched inner joins returned no rows, while left joins kept the left row and showed empty right-side values. A known catalog name also matched the expected row.
⏸️ Skipped Catalog A catalog lookup returned the expected value for one match, NULL for no matches, and a clear error for multiple matches. The next query still returned 42.
⏸️ Skipped Catalog Compound catalog joins and CASE expressions completed successfully for matching, empty, and reordered lookups. A deliberate multi-row lookup returned a controlled error, and the next query still worked.
⏸️ Skipped Column The aliased join returned the expected row, and the query plan kept hidden names and ordinary qualified columns separate.
⏸️ Skipped Column Invalid star and overlong names returned clear SQL errors, and the same session continued to run valid queries successfully.
⏸️ Skipped Column Direct projections returned the expected zero values, NULL table identifier, column labels, and PostgreSQL-compatible types. The ordinary table column also returned the expected row value.
⏸️ Skipped Type The database returned the expected zero, tuple, and NULL placeholder values with the correct PostgreSQL types. The query completed without conversion errors.
⏸️ Skipped Type Typed comparisons for hidden database columns returned the expected results, and unsupported casts returned clear SQL errors without crashing the server.
⏸️ Skipped Type An invalid hidden-column cast returned a SQL error, but later queries in the same session returned the same values and type information each time.
⏸️ Skipped Type OID and REGCLASS comparisons returned consistent results in filters and joins. Empty and multi-row scalar queries behaved as expected, and a later lookup succeeded after the expected multi-row error.
⚠️ Additional Finding Medium severity Coercion The comparison shows a server error instead of returning true for a valid integer array.
⚠️ Additional Finding Medium severity Coercion The ANY comparison did not return true after the later element matched 2. The ALL comparison failed with unknown type received int32 result instead of returning SQL NULL.
⚠️ Additional Finding Medium severity Coercion The malformed query returned the expected integer conversion error, but every later valid query returned unknown type received int32 result instead of true. The fresh-session check returned the same error, showing that the valid comparison is independently broken as well as unsuitable for retry after an error.
⚠️ Additional Finding Medium severity Coercion The malformed array was rejected, but the following valid array comparison also failed. It should have returned true and been independent of the earlier bad value.
⚠️ Additional Finding Medium severity Column Sorting by a hidden system column crashes the database query, so the expected rows are never returned.
⚠️ Additional Finding Medium severity Column The direct hidden-column projection returned placeholder values, but the matching filter failed with operator does not exist: cid = integer. The expected behavior is for equivalent uses of the same hidden column to accept the query and apply the documented placeholder semantics.
⚠️ Additional Finding Medium severity Column The same hidden columns do not behave consistently when their names are qualified or quoted. A query using a quoted xmin predicate reaches a planner error instead of completing, and the mixed join and ordering form returns malformed output.
⚠️ Additional Finding Medium severity Quantifier The untyped query does not return the expected ANY=true, SOME=true, and ALL=false values. It fails while the result is being written because the server receives an int32 result where the unknown output function expects a string.
⚠️ Additional Finding Medium severity Quantifier Both untyped queries return ERROR: unknown type received int32 result instead of returning SQL NULL for {NULL,2} and false for {NULL,3}.
⚠️ Additional Finding Medium severity Type The outer-join row showed cmin, tableoid, xmin, and xmax as numeric zero values and ctid as a valid zero block/offset value. These columns should all be SQL NULL when the catalog side of the left join is missing.
Additional Findings Details

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

🟡 Integer array comparison returns a server error
  • Severity: Medium Medium severity
  • Description: The comparison shows a server error instead of returning true for a valid integer array.
  • Impact: Users running a valid SQL comparison against an untyped integer array receive a server error instead of the expected true or false result. This blocks that query pattern but does not indicate data loss or security exposure.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as the postgres user.
    2. Run SELECT 2 = ANY('{1,2,3}');.
    3. Check the result. The expected result is true, but the server returns unknown type received int32 result.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The runtime failure is reproduced against the local Doltgres service and matches the source path. In server/expression/any.go:220-225, expressionAnyExpr.eval evaluates the right expression and, when its type is Unknown, invokes a.arrCast.Eval to convert the literal to the inferred array type. The resulting value is then asserted as []any at lines 227-229. For the valid literal '{1,2,3}', server/functions/array.go:102-117 parses each element with baseType.IoInput and appends the converted value; the final element follows the same path at lines 133-148. In this execution path the converted scalar values are int32 values rather than strings. When the expression result is rendered, server/functions/unknown.go:46-58 implements unknownout with an Unknown parameter but accepts only a Go string at lines 52-55, returning errors.Errorf("unknown type received %T result", val) for the int32 value. That is the observed error and prevents the valid ANY comparison from producing its boolean result. The smallest practical fix is to keep unknown-array elements in the representation expected by the unknown output path, or to convert the value to its string form before unknownout is called; the fix should be limited to this unknown-array coercion/output boundary.
Evidence Package
🟡 NULL array comparisons return wrong results
  • Severity: Medium Medium severity
  • Description: The ANY comparison did not return true after the later element matched 2. The ALL comparison failed with unknown type received int32 result instead of returning SQL NULL.
  • Impact: Queries that compare a value with an array containing NULL may return the wrong result or fail with a type error. This affects users who rely on these SQL comparisons, but it is limited to this array and NULL combination.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as postgres.
    2. Run SELECT 2 = ANY('{NULL,2}');.
    3. Run SELECT 2 = ALL('{NULL,2}'); as a separate query.
    4. Compare the results with the expected values: true for ANY and SQL NULL for ALL.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The production code has two independent defects that explain the observed behavior. In server/expression/any.go:220-225, expressionAnyExpr.eval converts an unknown array through arrCast, then server/expression/any.go:227-229 requires the converted value to be []any. The conversion can instead produce typed scalar values, so the comparison reaches the unknown output function with an int32 value. server/functions/unknown.go:47-56 confirms that unknownout only accepts string and emits unknown type received int32 result for the typed value seen in the run. Separately, the ANY branch in server/expression/any.go:262-274 returns nil immediately at lines 268-269 when a comparison is NULL. For the input {NULL,2}, SQL ANY semantics require retaining that pending NULL, continuing to inspect the later element, and returning true when 2 matches. The current early return prevents that later match. A focused fix should preserve a foundNull flag in the ANY loop, continue after NULL results, return true on a later true result, and return NULL only if no element matches. The unknown-array conversion should also normalize the converted elements to the representation expected by the comparison path, or invoke the scalar comparison with the actual converted element type instead of sending a typed value through unknownout.
Evidence Package
🟡 Valid array retry still fails
  • Severity: Medium Medium severity
  • Description: The malformed query returned the expected integer conversion error, but every later valid query returned unknown type received int32 result instead of true. The fresh-session check returned the same error, showing that the valid comparison is independently broken as well as unsuitable for retry after an error.
  • Impact: Users cannot complete valid SQL comparisons that use an unknown array with ANY; the query returns an error instead of true. The same failure occurs in a fresh session, so retrying does not provide a workaround.
  • Steps to Reproduce:
    1. Run a quantifier query with an unknown array containing a value that cannot be converted to the inferred integer type, such as a malformed array with bad.
    2. In the same database session, run the valid comparison SELECT 2 = ANY('{1,2,3}'); several times.
    3. Run the same valid comparison in a fresh session and compare the result with the expected boolean value true.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The source supports two connected defects in the production path. In server/functions/array.go:102-117 and 133-148, array_in calls baseType.IoInput for each element, stores the first conversion error in err, and still appends the converted value to values; it returns the partially populated slice together with the error at lines 152. This means malformed input is not represented as a cleanly discarded array value. More importantly for the valid retry, server/expression/any.go:220-225 evaluates a cast for an unknown-typed array, then lines 227-229 require the result to be []any. The resulting path can pass typed int32 values into the unknown comparison machinery. server/functions/unknown.go:52-56 implements unknownout with a string assertion and returns unknown type received int32 result for those values, matching the persistent-session and fresh-session evidence. The PR context changes server/expression/literal.go, server/functions/age.go, server/functions/encode.go, server/functions/init.go, server/functions/pg_get_function_sqlbody.go, and test files; it does not change array.go, any.go, or unknown.go. The smallest practical fix is to make unknown-array coercion produce values of the comparison target type before evaluation and to stop malformed array conversion from propagating partial values; the relevant implementation should be corrected in those existing coercion paths rather than in the PR's unrelated files.
Evidence Package
🟡 A bad array value breaks the next valid query
  • Severity: Medium Medium severity
  • Description: The malformed array was rejected, but the following valid array comparison also failed. It should have returned true and been independent of the earlier bad value.
  • Impact: A valid array comparison can fail after a malformed mixed-value query, so users may not get the expected true or false result. The issue affects a specific query sequence and does not indicate data loss.
  • Steps to Reproduce:
    1. Run SELECT 2 = ANY('{1,bad,3}');, SELECT 2 = SOME('{1,bad,3}');, and SELECT 2 = ALL('{1,bad,3}'); in one local Doltgres session.
    2. Confirm that each malformed query reports an int4 conversion error for the value bad.
    3. Run SELECT 2 = ANY('{1,2,3}'); without restarting the session.
    4. Observe that the valid query returns unknown type received int32 result instead of true.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The failure is supported by both the recorded local SQL output and the production code. In server/functions/array.go:102-117, array_in parses each element and appends innerValue even when baseType.IoInput returns an error; it stores the first non-critical conversion error in err rather than discarding the partially built array. The final element path at server/functions/array.go:133-149 repeats the same behavior, and array_in returns values together with err at line 152. That allows a partially converted []any to escape the conversion boundary. In server/expression/any.go:220-225, an unknown array is evaluated through a cast and its result is then required to be []any at lines 227-229. The resulting values can include typed int32 elements, while server/functions/unknown.go:52-56 accepts only a string in unknownout and returns unknown type received int32 result for a typed value. This matches the prior local SQL sequence: three malformed statements returned clear int4 conversion errors, then the valid ANY statement returned the unknownout type error. The smallest practical fix is to make the array conversion boundary fail closed for element conversion errors, returning a nil array with the conversion error rather than appending or returning partial values; the unknown-array cast path should then ensure a successful conversion produces values compatible with the comparison path before evaluation.
Evidence Package
🟡 Sorting by hidden columns crashes the query
  • Severity: Medium Medium severity
  • Description: Sorting by a hidden system column crashes the database query, so the expected rows are never returned.
  • Impact: Queries that sort by hidden system columns can crash before returning any rows. Other queries are not shown to be affected, and no data is lost.
  • Steps to Reproduce:
    1. Start Doltgres locally and create a table with at least one row.
    2. Run a SELECT that references cmin or cmax in the filter or selected expressions and also sorts by that hidden column, such as SELECT cmin FROM pg_catalog.pg_class ORDER BY cmin.
    3. Observe that the server fails during ORDER BY planning instead of returning the query result.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/ast/expr.go:875-885 deliberately replaces hidden PostgreSQL names with typed zero literals. For cmin and cmax, nodeExpr returns pgexprs.NewUnsafeLiteral(uint32(0), pgtypes.Cid). In server/ast/order_by.go:64-75, an injected expression literal is passed to pgexprs.ToVitessLiteral because the downstream planner expects a Vitess SQLVal. server/expression/literal.go:164-194 handles Bool, Int32, Int64, Numeric, Text, Unknown, and Oid, but has no pgtypes.Cid case; the default branch at lines 193-194 panics with unhandled type in temporary literal conversion: cid. The recorded query failure matches this exact path. The smallest practical fix is to add a safe Cid conversion in ToVitessLiteral, or avoid forcing typed hidden-column literals through this temporary Vitess conversion boundary; the fix should preserve the zero Cid placeholder and must be covered for ORDER BY.
Evidence Package
🟡 Hidden column filters reject valid expressions
  • Severity: Medium Medium severity
  • Description: The direct hidden-column projection returned placeholder values, but the matching filter failed with operator does not exist: cid = integer. The expected behavior is for equivalent uses of the same hidden column to accept the query and apply the documented placeholder semantics.
  • Impact: Queries that use hidden columns in filters or equivalent expressions can fail with a type error instead of returning results. Some sorting or grouping queries using these columns may also crash, but the issue is limited to this specialized query feature and does not indicate data loss.
  • Steps to Reproduce:
    1. Start the local Doltgres server and create a table with one row.
    2. Run a query that selects cmin directly and also uses cmin in a WHERE or CASE expression, such as SELECT id, cmin, CASE WHEN cmin = 0 THEN 'zero' ELSE 'other' END FROM the_table WHERE cmin = 0.
    3. Compare it with a query that only projects cmin or moves the same expression into ORDER BY or GROUP BY.
    4. Observe that direct projection can return a placeholder, while the predicate fails with operator does not exist: cid = integer; ordering and grouping can also reach an unsupported literal conversion panic.
  • Stub / mock content: A local Doltgres instance and a small local fixture table were used; no stubs, mocks, or bypasses were applied to the SQL behavior under test.
  • Code Analysis: The failure is supported by two independent production paths. In server/ast/select.go:127-149, direct unresolved hidden names are replaced with typed injected literals: cmin/cmax become a uint32(0) with pgtypes.Cid, ctid becomes an empty pgtypes.TidValue with pgtypes.Tid, tableoid becomes id.Null with pgtypes.Oid, and xmin/xmax become uint32(0) with pgtypes.Xid. In server/ast/expr.go:875-885, unresolved hidden names in predicates and other expression contexts are replaced with the same typed literals. That makes the projection and predicate representations superficially consistent, but the predicate path then asks the operator resolver to compare a Cid literal with an integer constant; the recorded operator does not exist: cid = integer error is the resulting incompatible type boundary. The same injected literals are converted by server/ast/order_by.go:68-75 and server/ast/group_by.go:40-47 through server/expression.ToVitessLiteral. ToVitessLiteral handles Bool, Int32, Int64, Numeric, Text, Unknown, and Oid at server/expression/literal.go:166-192, but its default at lines 193-194 panics for Cid, Tid, and Xid. The smallest practical fix is to make hidden-column expression semantics type-compatible with their supported placeholder comparisons, and to add explicit Cid/Tid/Xid handling or avoid the Vitess literal conversion for those injected types before ORDER BY/GROUP BY can reach the panic. The PR diff does not modify any of these hidden-column or unsupported-type cases.
Evidence Package
🟡 Qualified hidden columns can crash query planning
  • Severity: Medium Medium severity
  • Description: The same hidden columns do not behave consistently when their names are qualified or quoted. A query using a quoted xmin predicate reaches a planner error instead of completing, and the mixed join and ordering form returns malformed output.
  • Impact: Users using qualified or quoted hidden-column names may receive malformed query results or have the server fail while planning a query. Other query forms are not shown to be affected, and no data loss or security exposure was found.
  • Steps to Reproduce:
    1. Start a local Doltgres server and create a table with one row.
    2. Select tableoid and xmin using unqualified, table-qualified, schema-qualified, and quoted forms, mixing the references into predicates and ordinary qualified columns.
    3. Add a join or ordering expression that uses one of the qualified or quoted hidden-column references.
    4. Observe that projection forms return placeholder values, while the mixed form can return malformed output or fail with an xid temporary-literal conversion panic.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In server/ast/expr.go, nodeExpr calls unresolvedNameToColName at lines 870-873. That helper preserves table and schema qualifiers in the returned ColName, but the hidden-column dispatch at lines 875-885 then compares only colName.Name.String(), so unqualified, table-qualified, schema-qualified, and quoted forms that normalize to names such as tableoid or xmin all enter the same typed-placeholder branches. Those branches create Cid and Tid literals for cmin/cmax and ctid at lines 878-881, an Oid literal backed by id.Null for tableoid at lines 882-883, and Xid literals for xmin/xmax at lines 884-885. In server/ast/order_by.go lines 64-75 and server/ast/group_by.go lines 35-47, an injected expression literal is passed to pgexprs.ToVitessLiteral. server/expression/literal.go handles Bool, integer, numeric, text, unknown, and Oid at lines 166-192, but Cid, Tid, and Xid fall through to the panic at lines 193-194. The observed quoted xmin failure is therefore explained by a production conversion gap, not by browser evidence or local setup. The smallest fix is to make every hidden literal type used by nodeExpr either convert safely at this boundary, including Cid/Tid/Xid and the id.Null Oid case, or avoid routing these typed placeholders through Vitess SQLVal conversion; the fix should cover ORDER BY and GROUP BY consistently.
Evidence Package
🟡 Untyped quantifier query returns an error
  • Severity: Medium Medium severity
  • Description: The untyped query does not return the expected ANY=true, SOME=true, and ALL=false values. It fails while the result is being written because the server receives an int32 result where the unknown output function expects a string.
  • Impact: Queries that use standard ANY, SOME, or ALL syntax with an untyped array fail instead of returning results. Users can work around this by adding an explicit integer-array cast, but that is not PostgreSQL-compatible behavior.
  • Steps to Reproduce:
    1. Connect to a local Doltgres server as postgres.
    2. Run SELECT 2 = ANY('{1,2,3}'), 2 = SOME('{1,2,3}'), 2 = ALL('{1,2,3}');.
    3. Observe that the server returns ERROR: unknown type received int32 result instead of three boolean columns.
    4. Run the same expressions with '{1,2,3}'::int[] and observe true, true, and false.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The parser-to-expression path in server/ast/expr.go:471-484 explicitly lowers tree.Any, tree.Some, and tree.All to pgexprs.NewAnyExpr, NewSomeExpr, and NewAllExpr, so all three operators are recognized and reach the dedicated evaluator. In server/expression/any.go:211-225, an unknown right-hand expression is implicitly cast to the inferred array type when an array cast is available; the explicitly typed diagnostic therefore reaches the evaluator and returns the expected booleans. The untyped expression retains the Unknown result type through output. server/functions/unknown.go:46-58 defines unknownout with a strict type assertion at lines 52-55: it accepts only string and returns errors.Errorf("unknown type received %T result", val) for any other value. The observed int32 error is the exact failure from that branch. The smallest practical fix is to ensure the untyped quantifier result is converted to the textual representation expected by unknownout, or to resolve the result type before output so a boolean is serialized through the normal boolean path; changing unknownout alone should preserve NULL handling and must not mask unrelated invalid unknown values.
Evidence Package
🟡 ALL comparisons fail on untyped arrays
  • Severity: Medium Medium severity
  • Description: Both untyped queries return ERROR: unknown type received int32 result instead of returning SQL NULL for {NULL,2} and false for {NULL,3}.
  • Impact: Queries that use an untyped array with ALL fail instead of returning the expected NULL or false result. Users can avoid the error by adding an explicit integer-array cast.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as the postgres user.
    2. Run SELECT 2 = ALL('{NULL,2}');.
    3. Run SELECT 2 = ALL('{NULL,3}');.
    4. Run the same queries with ::int[] added to the array literal and compare the results.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The failure is in production code, not in the test harness. In server/expression/any.go:240-259, the ALL evaluator correctly tracks NULL comparisons with foundNull, immediately returns false when a later comparison is false, and otherwise returns NULL at line 257. The typed diagnostic queries reach this code path and return NULL and false, confirming that the quantifier semantics are implemented. The untyped array path instead leaves the final comparison result associated with the Unknown type. server/functions/unknown.go:46-58 defines unknownout and accepts only a Go string at lines 52-57; when the ALL result is an int32, line 55 returns unknown type received int32 result. The smallest practical fix is to make the unknown-result output path serialize the scalar result produced by an untyped quantifier, or to ensure this quantifier result is converted to the expected unknown/string representation before unknownout is called, while preserving nil as SQL NULL and false as false.
Evidence Package
🟡 Outer catalog joins return zero values instead of NULL
  • Severity: Medium Medium severity
  • Description: The outer-join row showed cmin, tableoid, xmin, and xmax as numeric zero values and ctid as a valid zero block/offset value. These columns should all be SQL NULL when the catalog side of the left join is missing.
  • Impact: Clients using an outer catalog join may read zero-valued hidden columns as real values instead of NULL. This can make them treat a missing catalog row as present or otherwise return the wrong result.
  • Steps to Reproduce:
    1. Start the local Doltgres server and connect with a PostgreSQL client.
    2. Run SELECT c.cmin AS cmin_alias, c.ctid AS ctid_alias, c.tableoid AS tableoid_alias, c.xmin AS xmin_alias, c.xmax AS xmax_alias FROM (SELECT 1 AS k) s LEFT JOIN pg_catalog.pg_class c ON false.
    3. Inspect the returned row values and compare them with the expected SQL NULL value for every c column because the left join found no pg_class row.
    4. Compare the result with a matching catalog query and confirm that the outer-NULL row still contains zero-valued Cid, Tid, Oid, and Xid values instead of NULL markers.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The failure is implemented directly in two production AST conversion paths. In server/ast/expr.go:875-885, the hidden names cmin/cmax and xmin/xmax are replaced with NewUnsafeLiteral(uint32(0), ...) and ctid is replaced with NewUnsafeLiteral(pgtypes.TidValue{}, ...). tableoid is replaced with NewUnsafeLiteral(id.Null, pgtypes.Oid). server/ast/select.go:127-149 repeats the same substitutions for direct projection, so the behavior is not limited to one query context. These are non-NULL typed literals: the Cid serializer at server/types/cid.go:63-68 always serializes uint32 values into four bytes, and the Tid serializer at server/types/tid.go:69-76 serializes an empty TidValue into six zero bytes. core/id/id.go:92-95 identifies id.Id as an internal identifier type; using id.Null as an Oid literal does not create a SQL NULL expression. The captured outer-NULL query therefore returns the same zero encodings as the matching query, while metadata, aliases, empty-result row counts, and post-error recovery remain correct. The smallest practical fix is to preserve the hidden-column type while producing a nullable SQL expression for an outer-NULL input, rather than unconditionally injecting a non-NULL zero literal; the fix must be applied consistently in both expr.go and select.go and should be covered by a wire-level left-join test.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@itoqa

itoqa Bot commented Aug 5, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportf2d7e37c5e892d: 19 test cases ran, 7 fixed ✅, 7 passing ✅, 5 additional findings ⚠️.

Diff Summary

Coverage spans database behavior for date and time calculations, type inference and conversion, array comparisons, quantifier logic, catalog metadata, hidden columns, and recovery after invalid queries. It includes normal results, NULL and invalid-input edge cases, repeated-session behavior, and specialized sorting and lookup scenarios; the exercised PR-related behavior is broadly healthy, with a few unrelated pre-existing defects identified.

Safe to merge — no observed failures are attributable to this PR, including no regressions or previously flagged failures that remain unresolved. The unrelated NULL comparison and hidden-column sorting issues are medium-severity follow-up findings, not merge blockers for this change.

Tests run by Ito

View full run

Result State Severity Type Description
❌->✅ Fixed Age The database returned the expected forward interval, exact reverse negation, and zero for equal timestamps. Fractional seconds and month-end values were normalized consistently.
❌->✅ Fixed Coercion The comparison completed successfully and returned true, so an integer can be matched against an untyped string array.
❌->✅ Fixed Coercion A malformed array value returned a clear conversion error, and three valid retries in the same session all returned true, matching a fresh session.
❌->✅ Fixed Coercion ANY, SOME, and ALL each rejected the invalid later array value with a clear conversion error. A valid query afterward returned true, so no partial array or stale state leaked into the next query.
❌->✅ Fixed Quantifier The query returned true for ANY, true for SOME, and false for ALL, matching PostgreSQL behavior.
❌->✅ Fixed Quantifier The first comparison returned SQL NULL, and the second returned false when a later value did not match. This matches PostgreSQL three-valued logic for ALL.
❌->✅ Fixed Type Catalog queries returned the expected hidden-column types and values. Aliases, empty results, NULL-extended rows, and results after an error all stayed correct.
Passing Inference Both text array comparisons completed successfully and returned true.
Passing Inference The integer comparison returned true after the local database was started correctly, with no type-resolution error.
Passing Inference An invalid integer array value returned a clear cast error, and the next valid integer-array comparison returned true without a crash.
Passing Inference The integer subquery comparison returned true without an array-cast or type-resolution error.
Passing Retention The integer array comparison returned true for ANY and false for ALL, with no cast or stale-state error.
Passing Retention Unknown text arrays returned the expected results in SELECT and WHERE expressions. The prepared-statement check returned a clear unsupported-feature error instead of a cast panic or unresolved-expression failure.
Passing Semantics The catalog query comparing table IDs with index class metadata completed successfully. It returned no matches in the filtered query and 14 false results in the broader query, with the same results after reconnecting.
⏸️ Skipped Age Subtracting March 1 from July 1 returned the expected four-month interval without an overload or query error.
⏸️ Skipped Age The age function returned SQL NULL when either timestamp was NULL and for a NULL transaction ID. A valid call afterward returned 4 mons, so the earlier NULL calls did not affect the session.
⏸️ Skipped Age Timestamp and timestamptz age calls returned four months, the one-argument call returned the expected interval, and explicit NULL casts still returned SQL NULL.
⏸️ Skipped Age Age calculations stayed consistent across the America/New_York spring-forward change. Named time-zone values returned two hours in both directions, while matching fixed offsets returned one hour in both directions.
⏸️ Skipped Encode The database returned lowercase hexadecimal for both hex format spellings and returned an empty value for empty byte data.
⏸️ Skipped Encode Byte values are returned correctly in escape format: printable text stays readable, backslashes are doubled, and control or high-bit bytes use octal escapes.
⏸️ Skipped Function Each object ID returned the exact SQL body for its own function or procedure, even when the lookups were repeated in reverse order.
⏸️ Skipped Function An unknown function ID returned no definition, and the next valid lookup returned the correct SQL body twice.
⏸️ Skipped Function After an invalid OID cast failed, the same database session returned the correct function body twice.
⏸️ Skipped Literal Catalog ordering, grouping, and limit queries returned the expected rows without a planner error. Numeric OID matching and the corrected REGCLASS lookup also preserved the catalog identity for pg_class.
⏸️ Skipped Literal A table-value wrapper passed the catalog object's ID through a visibility check, which returned the expected catalog row.
⏸️ Skipped Literal A NULL object ID produced no catalog matches and stayed NULL. A valid catalog lookup immediately afterward still returned the expected pg_class row.
⏸️ Skipped Literal Catalog lookups returned the same table for direct filters, grouped queries, limits, offsets, and VALUES wrappers. Empty values stayed empty, and a later valid lookup still worked.
⚠️ Additional Finding Medium severity Coercion The ANY query returned NULL, even though the second array element is 2 and matches the left side. The ALL query returned NULL as expected.
⚠️ Additional Finding Medium severity Column The query crashes while it is being prepared, so it does not return the placeholder values or the row requested by the user.
⚠️ Additional Finding Medium severity Column The query should return the row and keep the same hidden-column value across the selected column, filter, and CASE expression. Instead, adding the sort causes the query to stop with an error before any rows are returned.
⚠️ Additional Finding Medium severity Column A query using qualified and quoted hidden column names failed before returning rows. The server reported an internal panic while sorting the result.
⚠️ Additional Finding Medium severity Retention The row with {NULL,2} returned NULL for ANY, but it should return true because the array contains a matching 2 after the NULL value.
Additional Findings Details

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

🟡 ANY stops at NULL before finding a match
  • Severity: Medium Medium severity
  • Description: The ANY query returned NULL, even though the second array element is 2 and matches the left side. The ALL query returned NULL as expected.
  • Impact: Queries that use ANY with a NULL value before a matching value return NULL instead of true. Applications may miss valid matches and make incorrect decisions based on the query result.
  • Steps to Reproduce:
    1. Connect to the local Doltgres SQL server.
    2. Run SELECT 2 = ANY('{NULL,2}'); and display SQL NULL values explicitly.
    3. Compare the result with SELECT 2 = ALL('{NULL,2}'); and the expected three-valued SQL results.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run. The query ran against a locally rebuilt Doltgres server.
  • Code Analysis: The source-level and rebuilt local-server checks agree. In server/expression/array.go:73-82, Array.Eval preserves a NULL child by storing nil and continuing, so the NULL element is intentionally passed into comparison evaluation. In server/expression/any.go:240-259, the ALL branch tracks a NULL comparison in foundNull, continues evaluating later elements, and returns NULL only if no false comparison is found. The ANY branch at server/expression/any.go:262-274 instead returns nil immediately at lines 268-270 when the first comparison is NULL. For {NULL, 2}, that prevents evaluation of the later matching 2 and produces NULL instead of true. The smallest fix is to add a foundNull flag to the ANY branch, replace the immediate NULL return with foundNull = true and continue, and return NULL after the loop only when no true comparison was found. The PR diff in server/expression/any.go changes arrCastToType scoping, arrType assignment, BaseType selection, and the stored arrType around lines 412-453; it does not change this evaluation loop, so the PR cannot be credited as the cause.
Evidence Package
🟡 Hidden-column sorting crashes the query
  • Severity: Medium Medium severity
  • Description: The query crashes while it is being prepared, so it does not return the placeholder values or the row requested by the user.
  • Impact: Queries that filter or sort by PostgreSQL hidden columns fail before returning any rows. Users cannot complete this specialized query, but there is no evidence of data loss or incorrect data being saved.
  • Steps to Reproduce:
    1. Start the local Doltgres server and connect with a PostgreSQL client.
    2. Create a table with one row in the public schema.
    3. Run a SELECT that uses cmin, cmax, ctid, tableoid, xmin, and xmax in filters or CASE expressions and sorts by all six names.
    4. Observe that the query returns an error before producing any rows.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In server/ast/expr.go:875-885, nodeExpr replaces cmin and cmax with an UnsafeLiteral of pgtypes.Cid, ctid with pgtypes.Tid, tableoid with pgtypes.Oid, and xmin/xmax with pgtypes.Xid. The same lowering is used when these names appear in filter, CASE, and ORDER BY expression trees. In server/ast/order_by.go:64-75, nodeOrderBy calls nodeExpr and then converts any injected expression.Literal through pgexprs.ToVitessLiteral because the downstream GMS ORDER BY path requires a Vitess SQLVal. server/expression/literal.go:164-195 handles Bool, Int32, Int64, Numeric, Text, Unknown, and Oid, but has no Cid, Tid, or Xid cases; those values reach the default panic at line 194. The recorded SQL error, unhandled type in temporary literal conversion: cid, matches this exact path. The smallest practical fix is to make this temporary ORDER BY conversion support the hidden literal types, or avoid converting these injected typed placeholders through ToVitessLiteral; it should not leave Cid, Tid, and Xid to the panic default.
Evidence Package
🟡 Hidden columns crash sorted queries
  • Severity: Medium Medium severity
  • Description: The query should return the row and keep the same hidden-column value across the selected column, filter, and CASE expression. Instead, adding the sort causes the query to stop with an error before any rows are returned.
  • Impact: Queries that sort by hidden columns such as xmin or xmax can fail before returning any rows. Users can still run other query shapes, and there is no evidence of data loss or corrupted data.
  • Steps to Reproduce:
    1. Create a table with one row in the local database and query its hidden xmin column.
    2. Use xmin in the selected columns and in a filter or CASE expression.
    3. Add ORDER BY xmin and run the query.
    4. Observe that the server returns a recovered panic instead of the query rows.
  • Stub / mock content: A temporary local table with one fixture row was used to exercise the query; no application mocks, route interception, or bypasses were applied.
  • Code Analysis: The direct projection path in server/ast/select.go:127-149 replaces xmin and xmax with an injected uint32(0) literal typed as pgtypes.Xid. General expression handling in server/ast/expr.go:875-885 performs the same replacement when hidden names occur in predicates, CASE expressions, or other expression contexts, so the projection and predicate contexts agree on the placeholder type. However, server/ast/order_by.go:64-75 calls nodeExpr for ORDER BY xmin, detects the injected expression, and sends its *expression.Literal to server/expression.ToVitessLiteral. In server/expression/literal.go:164-195, ToVitessLiteral handles Bool, Int32, Int64, Numeric, Text, Unknown, and Oid, but has no Xid, Cid, or Tid case; its default branch panics with the literal type. The smallest practical fix is to prevent hidden Xid/Cid/Tid literals from entering this unsupported ORDER BY conversion, either by adding correct conversions for these types or by returning a controlled unsupported-expression error at that boundary rather than panicking. To preserve the tested behavior, the preferred targeted fix is to add explicit Xid/Cid/Tid conversion compatible with the expected Vitess SQL literal representation and cover hidden-column ORDER BY queries with regression tests.
Evidence Package
🟡 Qualified hidden names crash the query
  • Severity: Medium Medium severity
  • Description: A query using qualified and quoted hidden column names failed before returning rows. The server reported an internal panic while sorting the result.
  • Impact: Queries that sort by qualified or quoted hidden column names fail before returning rows. Users can still run other queries, and there is no evidence of data loss or corruption.
  • Steps to Reproduce:
    1. Start the local Doltgres server and create a table with one row.
    2. Run a SELECT that uses table-qualified or quoted tableoid and xmin in filters or joins and sorts by xmin, such as ORDER BY q.xmin or ORDER BY q."xmin".
    3. Observe that the query fails with an unhandled temporary literal conversion error instead of returning the matching row set.
  • Stub / mock content: A temporary local table and one fixture row were used to exercise the SQL query; no application mocks, route stubs, or bypasses were applied.
  • Code Analysis: In server/ast/expr.go:870-885, nodeExpr first converts an unresolved identifier with unresolvedNameToColName and then dispatches on colName.Name.String(). The dispatch intentionally substitutes cmin/cmax with an unsafe literal of pgtypes.Cid, ctid with pgtypes.Tid, tableoid with pgtypes.Oid, and xmin/xmax with pgtypes.Xid. This means the qualifier is removed for hidden-name recognition, so table-qualified, schema-qualified, and quoted spellings can all enter the same injected-literal path. In server/ast/order_by.go:64-75, an injected expression whose expression is *expression.Literal is unconditionally passed to pgexprs.ToVitessLiteral. server/expression/literal.go:164-195 handles Bool, Int32, Int64, Numeric, Text, Unknown, and Oid, but has no Cid, Tid, or Xid cases; the default branch at lines 193-195 panics with unhandled type in temporary literal conversion. The recorded query reaches this path with an Xid literal and produces that exact recovered panic. The smallest practical fix is to make the ORDER BY bridge support the hidden literal types, or to avoid converting those typed hidden literals through ToVitessLiteral and return a controlled unsupported-expression error instead of panicking; the preferred targeted fix is the former so supported hidden-column ordering can execute.
Evidence Package
🟡 ANY stops before finding a later match
  • Severity: Medium Medium severity
  • Description: The row with {NULL,2} returned NULL for ANY, but it should return true because the array contains a matching 2 after the NULL value.
  • Impact: Queries using ANY with a NULL value before a matching value can return NULL instead of true. This can make affected filters and decisions produce the wrong result, but the issue is limited to this array pattern.
  • Steps to Reproduce:
    1. Create rows with integer arrays {1,2}, {3,4}, and {NULL,2}.
    2. Run 2 = ANY(vals) for every row and repeat the same query in the session.
    3. Compare the row containing {NULL,2} with PostgreSQL behavior; it should return true because the later value 2 matches.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In /tmp/output-agent-workspace/repo/server/expression/any.go, expressionAnyExpr.eval iterates over rightValues at lines 262-274. At lines 268-270, a nil comparison result immediately returns nil. SQL ANY must remember that a NULL comparison occurred, continue evaluating the remaining elements, and return true if a later comparison is true; it should return NULL only when no comparison is true and at least one comparison is NULL. This explains both the observed {NULL,2} result and the stable repeated result. The PR diff for server/expression/any.go only modifies anyExpressionWithChildren around lines 412-453: it scopes arrCastToType, assigns arrType, changes BaseType selection, and stores the resolved array type. It does not modify the faulty loop, so the smallest fix is to retain a found-null flag in the non-ALL branch, continue after nil results, and return NULL after the loop only when that flag is set.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

@jennifersp
jennifersp requested a review from Hydrocharged August 5, 2026 21:39
@itoqa

itoqa Bot commented Aug 6, 2026

Copy link
Copy Markdown

Ito QA test results
Ito Diff Reportc5e892d47e370b: 24 test cases ran, 18 passing ✅, 6 additional findings ⚠️.

Diff Summary

Coverage exercised core database behavior across persistence, concurrent updates, reloads, object and extension lifecycle, name resolution, type handling, and recovery from invalid inputs. It included normal workflows plus edge cases involving errors, ambiguous names, malformed data, hidden fields, and three-valued query logic.

Safe to merge — no observed failure is attributable to this PR, so there is no regression or merge-blocking issue identified. Several unrelated pre-existing findings, including medium-severity query failures and a potential high-severity malformed-data crash, should be flagged for later remediation.

Tests run by Ito

View full run

Result State Severity Type Description
Passing Cache Creating a sequence and reading it again in the same session worked before and after commit. The first failure came from the local target being stopped, and the flow passed after the target was started.
Passing Cache Two separate writers added objects and committed them. A later session found both objects, so the newer writer's data was not lost.
Passing Cache The remaining database still returned its saved row after a missing-table error and a separate database was dropped.
Passing Cache Two separate sessions added different objects, and the final list showed both objects after they committed.
Passing Catalog The installed extension kept its metadata and UUID function after native backup and restore and after push and clone.
Passing Catalog The available extension list returned the uuid-ossp entry in a stable order before and after installation. Its nullable fields stayed empty, and the installed version changed from empty to 1.1 as expected.
Passing Catalog After opening a fresh session, uuid-ossp still showed version 1.1 and its UUID generator returned a valid value. The installed extension details matched the available-extension metadata.
Passing Extension The uuid-ossp extension installed successfully, reported version 1.1, and made all expected UUID functions available. The deterministic UUID result matched the known value, and the random UUID functions returned the expected version and variant.
Passing Extension Valid UUID namespace and text inputs returned the expected version 3 and version 5 values. Integer inputs were rejected with clear type errors, and the server stayed usable.
Passing Extension The extension list showed uuid-ossp version 1.1 as installed. Reinstalling it, using the wrong case, or requesting postgis returned clear errors without claiming that an unsupported extension was installed.
Passing Extension After a transaction was rolled back, the extension metadata and UUID functions were gone. Installing it again restored version 1.1 and working UUID calls in a fresh session.
Passing Object A root-backed object was created, committed, and found again after the database was reopened. The object stayed available without changing the surrounding tables or schemas.
Passing Object Dropping the only root-backed object leaves the collection empty after commit and keeps the other tables and catalog entries unchanged.
Passing Object A new root-backed object was added successfully, and existing table and schema data stayed available after the database was reopened.
Passing Object An initial lookup returned a controlled missing-object error. After another session created the objects, a later lookup found them and they remained available after persistence.
Passing Resolution The function was found by its full name and by its name through the active schema search path. A missing name returned no rows, and the function list contained one copy without duplicates.
Passing Resolution The database reports two matches for an unqualified duplicate name, selects the correct object when each schema is named, and ignores an unsupported identifier without deleting either object.
Passing Resolution After commit and a fresh-session reload, the function and type were visible and the ordinary table still had its row. After dropping the function and committing again, the function disappeared while the type and table remained.
⏸️ Skipped Age The database returned the expected forward interval, exact reverse negation, and zero for equal timestamps. Fractional seconds and month-end values were normalized consistently.
⏸️ Skipped Coercion The comparison completed successfully and returned true, so an integer can be matched against an untyped string array.
⏸️ Skipped Coercion A malformed array value returned a clear conversion error, and three valid retries in the same session all returned true, matching a fresh session.
⏸️ Skipped Coercion ANY, SOME, and ALL each rejected the invalid later array value with a clear conversion error. A valid query afterward returned true, so no partial array or stale state leaked into the next query.
⏸️ Skipped Inference Both text array comparisons completed successfully and returned true.
⏸️ Skipped Inference The integer comparison returned true after the local database was started correctly, with no type-resolution error.
⏸️ Skipped Inference An invalid integer array value returned a clear cast error, and the next valid integer-array comparison returned true without a crash.
⏸️ Skipped Inference The integer subquery comparison returned true without an array-cast or type-resolution error.
⏸️ Skipped Quantifier The query returned true for ANY, true for SOME, and false for ALL, matching PostgreSQL behavior.
⏸️ Skipped Quantifier The first comparison returned SQL NULL, and the second returned false when a later value did not match. This matches PostgreSQL three-valued logic for ALL.
⏸️ Skipped Retention The integer array comparison returned true for ANY and false for ALL, with no cast or stale-state error.
⏸️ Skipped Retention Unknown text arrays returned the expected results in SELECT and WHERE expressions. The prepared-statement check returned a clear unsupported-feature error instead of a cast panic or unresolved-expression failure.
⏸️ Skipped Semantics The catalog query comparing table IDs with index class metadata completed successfully. It returned no matches in the filtered query and 14 false results in the broader query, with the same results after reconnecting.
⏸️ Skipped Type Catalog queries returned the expected hidden-column types and values. Aliases, empty results, NULL-extended rows, and results after an error all stayed correct.
⚠️ Additional Finding High severity Object The collection did not safely reject malformed saved data. Loading a root-object field containing a value of the wrong type can crash the server instead of showing a controlled error.
⚠️ Additional Finding Medium severity Coercion The query returned NULL instead of TRUE. The first array element is NULL, but the later element is 2 and should make ANY return TRUE.
⚠️ Additional Finding Medium severity Column The query should accept all six hidden names in filter, expression, and ordering contexts and return the configured placeholder values. Instead, ordering by cmin causes the server to recover a panic for an unsupported cid literal, so the query does not complete.
⚠️ Additional Finding Medium severity Column The same hidden-column expression did not keep compatible behavior when moved between query positions. Direct projection, null-safe CASE logic, and a qualified join control returned zero-valued placeholders, but the required equality forms failed with operator does not exist: cid = integer.
⚠️ Additional Finding Medium severity Column The compound query did not complete. Both the original and repeated query reached the sort step and returned a server error instead of the expected result set.
⚠️ Additional Finding Medium severity Retention The database returned NULL for ANY and SOME when the array was {NULL,2}. The second item is a match, so the result should be true.
Additional Findings Details

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

🟠 Malformed saved data can crash collection loading
  • Severity: High High severity
  • Description: The collection did not safely reject malformed saved data. Loading a root-object field containing a value of the wrong type can crash the server instead of showing a controlled error.
  • Impact: A malformed saved value can crash the server when a collection loads, preventing users from accessing that collection until the data is repaired or the service restarts.
  • Steps to Reproduce:
    1. Create a controlled local database fixture whose root-object field points to a stored value that is not a serialized tree node.
    2. Open the affected root-object collection so the server loads that field.
    3. Observe that loading reaches a panic instead of returning a controlled decode error, then check whether the server and an unrelated table query remain available.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: core/rootobject/objinterface/serializer.go:51-72 implements RootObjectSerializer.LoadProllyMap. It reads the root hash at line 52, reads the referenced value at line 56, and handles read errors and nil values. At line 63 it performs val.(types.SerialMessage) without checking the assertion result. If RootValue.VRW().ReadValue returns a valid stored scalar or another value that does not implement types.SerialMessage, Go panics before tree.NodeFromBytes can return its normal decode error. The subsequent error handling at lines 64-69 only covers NodeFromBytes errors and an unexpected address-map file identifier, so those checks cannot protect the assertion. NewRootObjectMap in core/rootobject/objinterface/root_object_map.go:35-46 directly propagates LoadProllyMap while constructing every collection, making this a shared root-object loading failure path. The PR diff for serializer.go shows that the assertion was already present in the previous GetProllyMap implementation and was carried into the new loader, so this is a pre-existing defect rather than a behavior introduced by the PR. The smallest practical fix is to use an ok assertion, for example serialMessage, ok := val.(types.SerialMessage), and return a descriptive decode error when ok is false; add a focused malformed-value test around LoadProllyMap to ensure no panic occurs.
Evidence Package
🟡 ANY returns NULL before finding a match
  • Severity: Medium Medium severity
  • Description: The query returned NULL instead of TRUE. The first array element is NULL, but the later element is 2 and should make ANY return TRUE.
  • Impact: Queries that use ANY with a NULL value before a matching value can return NULL instead of TRUE, causing filters or decisions based on the query to miss valid matches.
  • Steps to Reproduce:
    1. Run SELECT 2 = ANY('{NULL,2}'); in the local Doltgres server.
    2. Check the returned value.
    3. Compare it with the expected SQL result TRUE, because the second array element matches 2.
  • Stub / mock content: Local authentication was disabled to allow the test client to connect to the nested Doltgres server. No query results, array values, or expression logic were mocked or intercepted.
  • Code Analysis: The production path in server/expression/any.go:262-276 evaluates each array element through a compiled comparison. At lines 268-269, the ANY branch returns NULL immediately when the comparison result is NULL. For the input '{NULL,2}', that exits before the loop evaluates 2 at lines 271-272, so the later TRUE result is never observed. The ALL branch immediately above, at lines 240-259, records NULL in foundNull and continues through the remaining elements, which demonstrates the intended control flow for NULL handling. The PR context for pull request 3031 does not contain a change to server/expression/any.go or this evaluator. Its changed files focus on array type input and unrelated extension/root-object work, so the defect is an unchanged pre-existing path and cannot be attributed directly to this PR. The smallest fix is to replace the ANY branch's immediate NULL return with a foundNull flag, continue evaluating the remaining elements, return TRUE on a later true comparison, and return NULL only after the loop if no true comparison was found but a NULL was seen.
Evidence Package
🟡 Ordering by hidden columns crashes queries
  • Severity: Medium Medium severity
  • Description: The query should accept all six hidden names in filter, expression, and ordering contexts and return the configured placeholder values. Instead, ordering by cmin causes the server to recover a panic for an unsupported cid literal, so the query does not complete.
  • Impact: Queries that sort or filter by hidden columns fail instead of returning results. Other database queries remain available, and users can avoid the failure by not using those hidden-column expressions.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server and create or use a table with one row.
    2. Run a SELECT that uses cmin, cmax, ctid, tableoid, xmin, and xmax in filters or expressions and sorts by those hidden columns.
    3. Observe that the query fails with an unhandled temporary literal conversion error instead of returning the row.
  • Stub / mock content: A local non-production fixture table with one seeded row was used to exercise the SQL query. No stubs, mocks, or bypasses were applied to the query or server logic.
  • Code Analysis: The unresolved-name branch in server/ast/expr.go:865-888 maps cmin and cmax to an injected Cid literal, ctid to Tid, tableoid to Oid, and xmin/xmax to Xid. In server/ast/order_by.go:64-80, nodeOrderBy passes every injected expression whose payload is an expression.Literal to expression.ToVitessLiteral. server/expression/literal.go:161-195 only converts Bool, Int32, Int64, Numeric, Text, Unknown, and Oid; the default branch at lines 193-194 panics for Cid, Tid, and Xid. The captured SQL stack reaches ToVitessLiteral at literal.go:194 from nodeOrderBy at order_by.go:74, confirming this is the production path. The smallest practical fix is to handle the hidden literal types before the default panic, or avoid routing these typed placeholders through the Vitess-literal conversion used by ORDER BY.
Evidence Package
🟡 Hidden-column rewrites fail in predicates
  • Severity: Medium Medium severity
  • Description: The same hidden-column expression did not keep compatible behavior when moved between query positions. Direct projection, null-safe CASE logic, and a qualified join control returned zero-valued placeholders, but the required equality forms failed with operator does not exist: cid = integer.
  • Impact: Queries that compare or sort by hidden columns can fail, and some sorting queries may crash the server request. Users cannot reliably use these expressions across normal SQL contexts.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server and use the hidden-column fixture table.
    2. Run a query that selects cmin directly and also compares it in a WHERE or CASE expression.
    3. Move the comparison into a qualified JOIN condition and keep the direct projection and ordering portions equivalent.
    4. Compare the result and error for each rewrite. The equality rewrites fail with operator does not exist: cid = integer, while the direct and null-safe controls return a row with value 0.
  • Stub / mock content: A local fixture table and one seeded row were used for the SQL checks. No application stubs, mocks, route interceptions, or bypasses were applied.
  • Code Analysis: The production code represents hidden names as typed zero-value literals in two separate conversion paths. server/ast/select.go:127-149 handles direct projections and injects Cid, Tid, Oid, or Xid literals with aliases. server/ast/expr.go:865-888 handles expressions used by predicates, CASE, JOIN, and ORDER BY and injects the same typed literal kinds at lines 875-885. Those paths are not compatible with ordinary integer comparisons: a cmin expression becomes a Cid literal, but the comparison value 0 is an integer, producing the observed cid = integer operator error. There is a second failure path for ordering: server/ast/order_by.go:64-75 calls nodeExpr, then unconditionally passes an injected expression literal to pgexprs.ToVitessLiteral. server/expression/literal.go:164-195 only converts Bool, Int32, Int64, Numeric, Text, Unknown, and Oid; its default branch at line 194 panics for Cid, Xid, and Tid. The smallest practical fix is to make hidden-column expression values use comparison-compatible representations in nodeExpr, or add explicit Cid/Xid/Tid conversions at the boundary before ToVitessLiteral, while preserving direct projection aliases. The PR context lists root-object persistence and extension changes and does not show these hidden-column conversion paths, so there is no evidence that this PR introduced the defect.
Evidence Package
🟡 Qualified hidden names crash ordered queries
  • Severity: Medium Medium severity
  • Description: The compound query did not complete. Both the original and repeated query reached the sort step and returned a server error instead of the expected result set.
  • Impact: Queries that sort qualified or quoted hidden columns fail during planning and return no rows. Users can avoid the failure only by changing the query shape or not using these hidden-column forms.
  • Steps to Reproduce:
    1. Start the local Doltgres server and connect to the qa_hidden_columns database.
    2. Use the public.hidden_column_fixture table and run one compound query that references tableoid and xmin in unqualified, table-qualified, schema-qualified, and quoted forms across WHERE, JOIN ON, and ORDER BY.
    3. Run the same hidden-column query again in the same session.
    4. Observe that both statements fail during planning with an unhandled xid temporary-literal conversion panic instead of returning rows.
  • Stub / mock content: The test used a local qa_hidden_columns database with one fixture table and row. No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In server/ast/expr.go:870-885, unresolved names are normalized by unresolvedNameToColName and then matched by colName.Name.String(). This means qualification is removed for hidden-name dispatch, so xmin and xmax become injected literals with pgtypes.Xid, while cmin/cmax, ctid, and tableoid become Cid, Tid, and Oid literals. server/ast/order_by.go:64-75 calls nodeExpr and then unconditionally passes any injected expression literal to pgexprs.ToVitessLiteral. server/expression/literal.go:164-195 supports Bool, Int32, Int64, Numeric, Text, Unknown, and Oid only; its default branch at line 194 panics for Xid, Cid, and Tid. The captured stack confirms this exact path: server/ast/nodeOrderBy -> server/expression.ToVitessLiteral -> server/expression/literal.go:194, with the recovered panic reporting xid. The smallest practical fix is to make ORDER BY handle these typed hidden literals without sending unsupported types through ToVitessLiteral, or to add correct Xid/Cid/Tid conversions there while preserving the existing Oid behavior.
Evidence Package
🟡 Array matching stops at unknown values
  • Severity: Medium Medium severity
  • Description: The database returned NULL for ANY and SOME when the array was {NULL,2}. The second item is a match, so the result should be true.
  • Impact: Queries using ANY or SOME can return the wrong result when an array contains NULL before a matching value. This can silently exclude valid rows from application results.
  • Steps to Reproduce:
    1. Create a local table with an integer array value containing NULL followed by 2, such as {NULL,2}.
    2. Run SELECT 2 = ANY('{NULL,2}'::integer[]); and SELECT 2 = SOME('{NULL,2}'::integer[]);.
    3. Compare the results with PostgreSQL semantics: both expressions should return true because the second element matches.
    4. Repeat the query across rows and in a second execution to confirm the incorrect NULL result is stable.
  • Stub / mock content: Local SQL authentication was disabled to allow the isolated test server to run, and the test used a fresh local database with test rows. No application query behavior was mocked or intercepted.
  • Code Analysis: In /tmp/output-agent-workspace/repo/server/expression/any.go, expressionAnyExpr.eval evaluates an array expression one element at a time. The ANY/SOME loop at lines 262-274 assigns each element to arrayLiteral and evaluates compFunc. At lines 268-270 it immediately returns nil, nil when a comparison returns NULL. That makes the first NULL element terminal, so {NULL,2} returns NULL without evaluating 2. The ALL branch immediately above, at lines 240-259, uses foundNull to remember NULL and continues evaluating later elements; ANY/SOME needs the analogous deferred-NULL behavior, returning true as soon as any later comparison is true and returning NULL only if no comparison is true but at least one comparison was NULL. The unknown-array cast resolution at lines 414-433 converts the input to the left operand's array type, so it does not remove the NULL element or explain the wrong result. The available PR file diff lists no change to server/expression/any.go, and the only recorded test setup change was disabling local authentication in server/authentication_scram.go, which is unrelated to expression evaluation. The smallest fix is to add a foundNull flag in the ANY/SOME loop, continue after a nil comparison, return true on a later true comparison, and return nil after the loop only when foundNull is set.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

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.

1 participant