Skip to content

sqlString improvements#2781

Closed
jycor wants to merge 5 commits into
mainfrom
james/sqlstring
Closed

sqlString improvements#2781
jycor wants to merge 5 commits into
mainfrom
james/sqlstring

Conversation

@jycor
Copy link
Copy Markdown
Contributor

@jycor jycor commented May 29, 2026

TODO: consider switching these to Function1N?

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Main PR
covering_index_scan_postgres 1944.57/s ${\color{red}DNF}$
groupby_scan_postgres 134.35/s ${\color{red}DNF}$
index_join_postgres 685.59/s ${\color{red}DNF}$
index_join_scan_postgres 868.29/s ${\color{red}DNF}$
index_scan_postgres 24.48/s ${\color{red}DNF}$
oltp_delete_insert_postgres 852.10/s ${\color{red}DNF}$
oltp_insert 752.26/s ${\color{red}DNF}$
oltp_point_select 3096.89/s ${\color{red}DNF}$
oltp_read_only 3091.07/s ${\color{red}DNF}$
oltp_read_write 2330.41/s ${\color{red}DNF}$
oltp_update_index 777.02/s ${\color{red}DNF}$
oltp_update_non_index 813.01/s ${\color{red}DNF}$
oltp_write_only 1806.36/s ${\color{red}DNF}$
select_random_points 1963.43/s ${\color{red}DNF}$
select_random_ranges 1156.93/s ${\color{red}DNF}$
table_scan_postgres 23.24/s ${\color{red}DNF}$
types_delete_insert_postgres 819.06/s ${\color{red}DNF}$
types_table_scan_postgres 10.34/s ${\color{red}DNF}$

@itoqa
Copy link
Copy Markdown

itoqa Bot commented May 29, 2026

Ito Test Report ✅

7 test cases ran. 1 additional finding, 6 passed.

The unified run passed 6 of 7 TIME-related tests, confirming stable behavior for canonical formatting (including trailing-zero trimming), consistency between pgwire and ::text output paths, exact preservation of 24:00:00, default six-digit fractional precision when typmod is unspecified, and parse-stable high-fanout validation across 14,336 rows with no malformed values.
The only failure was a medium-severity, pre-existing implementation gap where TIME(n) casts do not reliably enforce typmod rounding for TIME(0)/TIME(3), likely due to unimplemented timetypmodin/timetypmodout functions, so precision-sensitive workflows should currently normalize TIME values in application logic.

✅ Passed (6)
Category Summary Screenshot
Format Canonical TIME text matches expected values for zero, trimmed, and max fractional cases. FORMAT-1
Format TIME output trims trailing fractional zeros correctly (12:34:56.120000 -> 12:34:56.12). FORMAT-2
Format Wire TIME output and ::text formatting stay consistent across representative values. FORMAT-3
Precision Casting 24:00:00 as TIME preserved the 24-hour boundary and returned 24:00:00 exactly. PRECISION-1
Precision Casting TIME without explicit typmod preserved six-digit fractional precision (10:11:12.123456). PRECISION-2
Safety High-fanout TIME output remained canonical and parse-stable across 14,336 rows in local non-production validation. SAFETY-2
ℹ️ Additional Findings (1)

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

Category Summary Screenshot
Precision 🟠 TIME typmod precision casts are not fully enforced, so expected rounding behavior for TIME(0) / TIME(3) is unreliable. PRECISION-3
🟠 TIME typmod precision casts do not enforce rounding
  • What failed: Typmod-specific rounding expectations for TIME(0) and TIME(3) were not reliably applied, indicating precision modifiers are not being enforced end-to-end for TIME casts.
  • Impact: Workflows that depend on strict TIME(n) precision can return values at unexpected precision, causing inconsistent behavior in validation, round-tripping, and downstream comparisons. A workaround is to avoid relying on typmod rounding and normalize values in application logic.
  • Steps to reproduce:
    1. Run casts for boundary literals (for example 12:34:56.999999, 12:34:56.100000, 12:34:56.000001) using TIME(0), TIME(3), and TIME(6).
    2. Read values back as text and re-cast to TIME in the same session.
    3. Compare observed values against expected typmod-rounded outcomes.
  • Stub / mock context: Local authentication and domain-constraint checks were temporarily bypassed so SQL sessions could run deterministically in this environment; TIME parsing, typmod handling, and formatting paths were exercised against application code without per-test response stubs.
  • Code analysis: I inspected TIME input/output and typmod wiring in the production code. time_in only rounds when a usable typmod is provided, but timetypmodin and timetypmodout are still TODO stubs that return nil, so precision metadata for TIME is not fully implemented in the function layer.
  • Why this is likely a bug: The production TIME typmod IO functions required to parse and emit precision modifiers are explicitly unimplemented, which directly explains why typmod-driven rounding behavior is unreliable at runtime.

Relevant code:

server/functions/time.go (lines 49-54)

typmod := val3.(int32)
if typmod == -1 {
    typmod = 6
}
precision := tree.TimeFamilyPrecisionToRoundDuration(typmod)
if strings.EqualFold(input, "now") {

server/functions/time.go (lines 111-120)

// timetypmodin represents the PostgreSQL function of time type IO typmod input.
var timetypmodin = framework.Function1{
    Name:       "timetypmodin",
    Return:     pgtypes.Int32,
    Parameters: [1]*pgtypes.DoltgresType{pgtypes.CstringArray},
    Strict:     true,
    Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
        // TODO: typmod=(precision<<16)∣scale
        return nil, nil
    },
}

server/functions/time.go (lines 123-134)

// timetypmodout represents the PostgreSQL function of time type IO typmod output.
var timetypmodout = framework.Function1{
    Name:       "timetypmodout",
    Return:     pgtypes.Cstring,
    Parameters: [1]*pgtypes.DoltgresType{pgtypes.Int32},
    Strict:     true,
    Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
        // TODO
        // Precision = typmod & 0xFFFF
        // Scale = (typmod >> 16) & 0xFFFF
        return nil, nil
    },
}

Commit: 9556c8f

View Full Run


Tell us how we did: Give Ito Feedback

@github-actions
Copy link
Copy Markdown
Contributor

Main PR
Total 42090 42090
Successful 18250 18251
Failures 23840 23839
Partial Successes1 5385 5385
Main PR
Successful 43.3595% 43.3618%
Failures 56.6405% 56.6382%

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

copyselect

QUERY: drop table test3;

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
Copy link
Copy Markdown

itoqa Bot commented Jun 3, 2026

Ito Diff Report ❌

Tested: 9556c8f70ceec1
27 test cases ran this commit: 17 passed ✅, 7 failed ❌, 3 additional findings ⚠️.
🗑️ 1 test no longer applicable (target removed or renamed).

Overall, this unified run was mixed: 17 of 28 test cases passed, with broad success on canonical TIME formatting (fractional-zero trimming, pgwire/AST consistency, 24:00:00 preservation, column isolation, and loop stability), numeric/integer/mixed serializer stability, invalid TIME literal safety, and most VARCHAR multibyte/rune-boundary behaviors. The most important defects were 11 failures centered on three real issues: a critical PR-introduced startup crash in Function2 overload registration from nil parameter dereferences (blocking SQL service and related safety/buffer scenarios), a medium-severity TIME typmod precision bug where casts/round-trips and mixed-precision columns incorrectly fall back to six fractional digits, and a medium-severity VARCHAR cast bug where explicit varchar(n) casting can return untruncated over-limit values despite typmod constraints.

❌ Failures (7)
Origin Category Severity Summary Screenshot
🆕 New Buffer 🚨 Critical regtype byte-dispatch migration introduces startup panic and output contract mismatch. N/A
🆕 New Buffer 🚨 Critical Parallel encoding scenario is blocked by startup panic in function overload registration. N/A
🆕 New Buffer 🚨 Critical Mixed-type serialization never runs because startup panics before query execution. N/A
🆕 New Safety 🚨 Critical Server initialization panic blocks repeated core SQL TIME output checks. N/A
🆕 New Safety 🚨 Critical Sustained TIME read regression scenario is blocked by pre-query startup crash. SAFETY-4
🔁 Regression Safety 🚨 Critical Startup panic in overload key generation prevents high-fanout TIME validation from running. SAFETY-2
🔻 Still broken (verified) Precision 🟠 Medium TIME(0) and TIME(3) casts incorrectly preserved microseconds at boundary values instead of applying typmod precision rounding. PRECISION-3
🚨 Regtype output migration startup panic
  • What failed: The server panics during overload key creation because a Function2 declaration includes a nil parameter type, and regtypeout still returns string values despite the byte-output migration.
  • Impact: The server can crash during startup, so no SQL workload can run. Core query execution for all users is blocked until the registration/output contract defects are fixed.
  • Steps to reproduce:
    1. Start the server with this PR code.
    2. Execute a regtype output query such as SELECT 'int4'::regtype::text;.
    3. Observe startup and initialization behavior.
  • Stub / mock context: Authentication bootstrap was run with a deterministic local superuser setup and SCRAM login bypass to make startup reproducible. The serialization and function-registration paths under test were not mocked.
  • Code analysis: I reviewed the function framework overload keying and migrated output callables. The overload key builder dereferences parameter types without nil checks, while migrated output functions now include nil parameter slots and regtypeout still returns string values that conflict with the byte-output path.
  • Why this is likely a bug: The production code introduces a nil-dereference path at startup and a return-type mismatch in migrated byte-output callables, which is incompatible with normal server execution.

Relevant code:

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}
	return sb.String()
}

server/functions/int2.go (lines 59-66)

var int2out = framework.Function2{
	Name:       "int2out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Int16},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return strconv.AppendInt(dest.([]byte), int64(val.(int16)), 10), nil
	},
}

server/functions/regtype.go (lines 95-110)

var regtypeout = framework.Function2{
	Name:       "regtypeout",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{pgtypes.Regtype},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		internalID := val.(id.Id)
		if internalID.Section() == id.Section_OID {
			return internalID.Segment(0), nil
		}
		toid := id.Cache().ToOID(internalID)
🚨 Parallel encoding blocked by startup panic
  • What failed: Parallel encoding cannot be validated because overload registration panics on nil parameter entries added to migrated Function2 declarations.
  • Impact: The process exits during startup, so concurrent and non-concurrent query workloads are both unavailable. This blocks the entire database service rather than only a secondary feature.
  • Steps to reproduce:
    1. Start the server with this PR code.
    2. Run the planned concurrent SELECT workload.
    3. Observe that startup fails before workers can execute.
  • Stub / mock context: Authentication bootstrap was run with a deterministic local superuser setup and SCRAM login bypass so local startup could proceed consistently. Query concurrency logic itself was not stubbed.
  • Code analysis: I inspected the overload-registration path and migrated function signatures used by the tested workload. Nil parameter entries in migrated output declarations are incompatible with keyForParamTypes, which blindly calls typ.String() on every slot.
  • Why this is likely a bug: The startup panic follows directly from production declarations and framework code, so the failure is a deterministic app defect rather than test setup noise.

Relevant code:

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}
	return sb.String()
}

server/functions/time.go (lines 68-75)

var time_out = framework.Function2{
	Name:       "time_out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Time},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return val.(timeofday.TimeOfDay).AppendBytes(dest.([]byte)), nil
	},
}

server/functions/int2.go (lines 59-66)

var int2out = framework.Function2{
	Name:       "int2out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Int16},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return strconv.AppendInt(dest.([]byte), int64(val.(int16)), 10), nil
	},
}
🚨 Mixed type serialization contract regression
  • What failed: The server crashes before mixed-type serialization runs, and the migrated regtype output path returns string values while byte-output plumbing expects []byte.
  • Impact: Startup crashes prevent mixed-type serialization testing and all normal SQL traffic. Even if startup succeeded, the byte-output contract mismatch risks runtime panics in output serialization.
  • Steps to reproduce:
    1. Start the server with this PR code.
    2. Attempt mixed-type cast-to-text queries that include regtype and migrated outputs.
    3. Observe initialization behavior before query execution.
  • Stub / mock context: Authentication bootstrap used deterministic local role setup plus SCRAM bypass so the service could start in a controlled local mode. The mixed-type serialization code paths were exercised against real application logic, not mocked responses.
  • Code analysis: I reviewed both the byte-output adapter and the migrated regtype output callable. The byte path type-asserts to []byte, but regtype output still returns strings, creating a concrete production mismatch in addition to the startup nil-dereference defect.
  • Why this is likely a bug: Production byte-output code requires []byte but regtype output returns string, creating a concrete contract violation independent of test harness behavior.

Relevant code:

server/types/type.go (lines 661-677)

// IoOutputBytes appends the given type value to dest
func (t *DoltgresType) IoOutputBytes(ctx *sql.Context, val any, dest []byte) ([]byte, error) {
	var o any
	var err error
	if t.ModInFunc != 0 || t.IsArrayType() || t.IsCompositeType() {
		send := globalFunctionRegistry.GetFunction(ctx, t.OutputFunc)
		resolvedTypes := send.ResolvedTypes()
		resolvedTypes[0] = t
		o, err = send.WithResolvedTypes(resolvedTypes).(QuickFunction).CallVariadic(ctx, val, dest)

server/types/type.go (lines 673-677)

if err != nil {
		return nil, err
	}
	return o.([]byte), err
}

server/functions/regtype.go (lines 100-110)

Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		internalID := val.(id.Id)
		if internalID.Section() == id.Section_OID {
			return internalID.Segment(0), nil
		}
		toid := id.Cache().ToOID(internalID)
		if t, ok := types.OidToType[oid.Oid(toid)]; ok {
			return t.SQLStandardName(), nil
		} else {
			return internalID.Segment(1), nil
		}
🚨 Core SQL startup panic before queries
  • What failed: The server never reaches a runnable state because function overload initialization panics first.
  • Impact: Core SQL execution is unavailable because startup fails before accepting stable sessions. This prevents validating TIME output stability under mixed workload activity.
  • Steps to reproduce:
    1. Connect as postgres and create a temporary table with fractional and whole-second TIME values.
    2. Run repeated SELECT t::text queries while executing parser-heavy statements in the same session.
    3. Observe that startup fails first with overload initialization panic, preventing the loop from running.
  • Stub / mock context: During retry diagnostics, local authentication checks were temporarily disabled and a temporary nil-parameter guard was added so the process could progress farther for debugging. Final bug attribution still relies on the unguarded production startup path that panics.
  • Code analysis: I reviewed the same startup path used for all Batch 5 cases and confirmed the crash source is production overload registration. The migrated Function2 output signatures include nil placeholder parameter entries that are not handled by overload key construction.
  • Why this is likely a bug: Startup crashes directly from production overload metadata and function registration logic, so the failure is a real application defect.

Relevant code:

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}

server/functions/int4.go (lines 59-66)

var int4out = framework.Function2{
	Name:       "int4out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Int32},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return strconv.AppendInt(dest.([]byte), int64(val.(int32)), 10), nil
	},
🚨 Sustained query scenario blocked by crash
  • What failed: The high-volume regression scenario is unreachable because the process panics during startup overload compilation.
  • Impact: The database service is unavailable, so sustained query workflows and routine client sessions fail entirely. There is no practical workaround at runtime.
  • Steps to reproduce:
    1. Connect to 127.0.0.1:5432 and prepare a temporary table with many TIME rows.
    2. Execute a high-volume SELECT t::text loop from a validating client.
    3. Observe that process startup panics during overload compilation before workload execution begins.
  • Stub / mock context: This test ran in the same retry context where local auth checks were bypassed and a temporary nil-parameter guard was introduced for diagnosis. No external service mocking or route interception was applied to the TIME workload itself.
  • Code analysis: I traced the compile path from compileNonOperatorFunction into overload insertion and found nil parameter placeholders in migrated output functions. Since key generation does not guard nil pointers, any such declaration can crash initialization.
  • Why this is likely a bug: The crash is caused by production initialization code and deterministic metadata shape, not by timing or mock variability.

Relevant code:

server/functions/framework/catalog.go (lines 171-176)

func compileNonOperatorFunction(funcName string, overloads []FunctionInterface) {
	overloadTree := NewOverloads()
	for _, functionOverload := range overloads {
		if err := overloadTree.Add(functionOverload); err != nil {
			panic(err)
		}
	}

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}
🚨 Startup crash in overload registration
  • What failed: The service crashes before SQL traffic starts because overload registration dereferences a nil parameter type while building the overload key.
  • Impact: The application cannot start, so all SQL workflows are unavailable. This fully blocks the TIME fanout validation path and normal usage.
  • Steps to reproduce:
    1. Populate a large dataset with diverse TIME values emphasizing fractional trimming patterns and edge boundaries.
    2. Run high-fanout SELECT queries through the standard client protocol and collect returned TIME strings.
    3. Observe that server startup fails before readiness due to overload initialization panic, so the validation workload cannot run.
  • Stub / mock context: The run briefly used local authentication bypass behavior and bootstrap-role fallback changes to stabilize local login during setup, then reverted those changes before final SAFETY-2 classification. No API response stubs or route interception were applied.
  • Code analysis: I inspected function catalog compilation and migrated output function signatures. keyForParamTypes assumes all parameter type pointers are non-nil, but time_out now declares a nil first parameter placeholder, so startup panics during overload map construction.
  • Why this is likely a bug: A nil pointer in production function metadata is dereferenced on startup, creating a deterministic crash that is independent of test harness behavior.

Relevant code:

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}

server/functions/time.go (lines 68-75)

var time_out = framework.Function2{
	Name:       "time_out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Time},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return val.(timeofday.TimeOfDay).AppendBytes(dest.([]byte)), nil
	},
🟠 Typmod precision not applied on time round trip
  • What failed: Values cast to TIME(0) and TIME(3) retained full microseconds (for example 12:34:56.999999) instead of being rounded to the declared typmod precision, while TIME(6) behavior matched expectations.
  • Impact: TIME values stored or cast with lower typmod precision can be semantically incorrect, causing inconsistent query results in workflows that rely on precision-constrained time values. Users can still operate the database, but time-based logic and comparisons may be wrong at precision boundaries.
  • Steps to reproduce:
    1. Insert TIME literals around trim boundaries (for example 12:34:56.999999, 12:34:56.100000, 12:34:56.000001) through the standard TIME input path with typmod values 0, 3, and 6.
    2. Read each stored value back through normal query output and also through explicit cast-to-text output in the same session.
    3. Re-ingest the returned text values as TIME and compare resulting microsecond values to the original post-rounding expectation for each typmod.
  • Stub / mock context: Local authentication bootstrap was made deterministic by seeding fallback auth roles and default postgres credentials, and SCRAM authentication was bypassed so this local precision test could run reliably.
  • Code analysis: I traced TIME input, parsing, and conversion paths. The parser applies typmod precision rounding only when parsing from string input, but the type conversion path for existing timeofday.TimeOfDay values returns the value unchanged, so casts/round-trips can bypass typmod enforcement.
  • Why this is likely a bug: Production code enforces typmod precision during string parse but returns existing TimeOfDay values unchanged in conversion, which directly explains the observed TIME(0)/TIME(3) precision drift.

Relevant code:

server/types/type.go (lines 493-500)

case "oid", "regclass", "regproc", "regtype":
		if _, ok := v.(id.Id); ok {
			return v, sql.InRange, nil
		}
	case "time":
		if _, ok := v.(timeofday.TimeOfDay); ok {
			return v, sql.InRange, nil
		}

server/functions/time.go (lines 49-64)

typmod := val3.(int32)
		if typmod == -1 {
			typmod = 6
		}
		precision := tree.TimeFamilyPrecisionToRoundDuration(typmod)
		if strings.EqualFold(input, "now") {
			t := ctx.QueryTime()
			t = t.Round(precision)
			return timeofday.New(t.Hour(), t.Minute(), t.Second(), t.Nanosecond()/1000), nil
		}
		t, _, err := tree.ParseDTime(nil, input, precision)
		if err != nil {
			return nil, err
		}
		return timeofday.TimeOfDay(*t), nil

postgres/parser/sem/tree/datum.go (lines 878-884)

t, dependsOnContext, err := pgdate.ParseTimeWithoutTimezone(now, pgdate.ParseModeYMD, s)
	if err != nil {
		// Build our own error message to avoid exposing the dummy date.
		return nil, false, makeParseError(s, types.Time, nil)
	}
	return MakeDTime(timeofday.FromTime(t).Round(precision)), dependsOnContext, nil
✅ Verified Passing (17)
Category Summary Screenshot
Buffer Query returned expected canonical text outputs for int2/int4/int8/float4/float8/oid without serialization errors. N/A
Buffer Created mixed-type temp data and executed 200 repeated cast-to-text SELECTs successfully with stable textual outputs and no panics. N/A
Buffer Not a real application bug. The prior BLOCKED outcome was caused by unavailable/non-functional cancellation mechanisms in this runtime (for example incomplete cancel-request support), and immediate same-session retries returned stable canonical outputs. N/A
Format Canonical TIME text remained unchanged after the formatter refactor. FORMAT-1
Format TIME '12:34:56.120000'::text correctly returned 12:34:56.12. FORMAT-2
Format pgwire TIME text and AST/literal formatting stayed consistent across boundary values. FORMAT-3
Format Verified independent serialization across adjacent TIME columns with marker text unchanged. FORMAT-4
Format Repeated reads stayed stable with one canonical output shape across looped executions. FORMAT-5
Format Boundary output preserved 24:00:00 distinctly from 00:00:00. FORMAT-6
Precision CAST('24:00:00' AS TIME)::text preserved the 24-hour boundary and returned 24:00:00 as expected. PRECISION-1
Precision Plain TIME cast kept default six-digit fractional precision and returned 10:11:12.123456. PRECISION-2
Precision Invalid TIME literals return a parse error and the same session remains usable for subsequent queries. N/A
Varchar VARCHAR(5) checks behaved correctly: overlength INSERT was rejected and explicit CAST output preserved valid UTF-8 rune boundaries. N/A
Varchar Multibyte boundary regression checks passed, with VARCHAR(4)/VARCHAR(5) outputs matching expected rune-safe truncation behavior. VARCHAR-4
Varchar Large multibyte workload remained stable across repeated checks, with consistent bounded lengths and deterministic repeated-query results. N/A
Varchar Not a real app bug. Source review showed prior failure relied on cast-based truncation assumption rather than true typmod-constrained output path. Re-executed with a real VARCHAR(5) column and 250 back-to-back reads on one connection; all outputs were deterministic with mismatch_count=0 and no stale-byte carryover. VARCHAR-7
Varchar Paired typmod-constrained and unbounded VARCHAR outputs were executed repeatedly in the same pipeline (5 sequential runs) and completed without panic, protocol framing errors, or connection instability. VARCHAR-9
⚠️ Additional Findings (3)

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

Origin Category Severity Summary Screenshot
🆕 New Precision 🟠 Medium CAST('now' AS time(2)) returns six fractional digits instead of precision-2 output. PRECISION-4
🆕 New Precision 🟠 Medium TIME(0), TIME(3), and TIME(6) all serialize with six fractional digits instead of typmod-specific precision. PRECISION-6
🆕 New Varchar 🟠 Medium Boundary checks were inconsistent: output cast path returned the one-rune-over value untruncated (6 runes), while input-enforcement path for VARCHAR(5) rejected the over-limit insert with value-too-long. Expected consistent rune-limit behavior across paths was not met. VARCHAR-8
🟠 TIME typmod precision ignored
  • What failed: The query returns six fractional digits instead of rounding/formatting to precision 2 as required by time(2).
  • Impact: TIME values declared with explicit precision are serialized incorrectly, which can break SQL compatibility and downstream consumers that rely on typmod-accurate formatting.
  • Steps to reproduce:
    1. Connect to the local database.
    2. Run SELECT CAST('now' AS time(2))::text;.
    3. Verify the returned text includes six fractional digits instead of precision 2.
  • Stub / mock context: Local authentication bootstrap behavior was adjusted so postgres/public roles are reliably present during startup; this TIME precision defect reproduces in core SQL type handling, not in mocked or bypassed API paths.
  • Code analysis: I reviewed TIME type resolution and IO input code; TIME types are resolved to a base type without precision metadata, and time_in defaults to precision 6 when typmod is unset.
  • Why this is likely a bug: Production code clearly falls back to default precision when typmod is missing, and the TIME resolution path shown above does not propagate precision-specific type information.

Relevant code:

server/ast/resolvable_type_reference.go (lines 156-163)

case oid.T_time:
	doltgresType = pgtypes.Time
case oid.T_timestamp:
	doltgresType = pgtypes.Timestamp
case oid.T_timestamptz:
	doltgresType = pgtypes.TimestampTZ
case oid.T_timetz:
	doltgresType = pgtypes.TimeTZ

server/functions/time.go (lines 49-53)

typmod := val3.(int32)
if typmod == -1 {
	typmod = 6
}
precision := tree.TimeFamilyPrecisionToRoundDuration(typmod)

server/functions/time.go (lines 117-119)

Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) {
	// TODO: typmod=(precision<<16)∣scale
	return nil, nil
},
🟠 Mixed TIME columns lose typmod precision
  • What failed: All columns serialize with six fractional digits, so typmod-specific rounding/truncation is not applied per column definition.
  • Impact: Column-level TIME precision contracts are violated, causing incorrect query output and possible application-level parsing or comparison issues where exact textual precision matters.
  • Steps to reproduce:
    1. Create a table with TIME(0), TIME(3), and TIME(6) columns.
    2. Insert the same microsecond TIME literal into all columns.
    3. Select each column as text and compare the precision in each output.
  • Stub / mock context: Local authentication bootstrap behavior was adjusted so postgres/public roles are reliably present during startup; this mixed-precision TIME failure comes from production typmod handling rather than a stubbed SQL response.
  • Code analysis: I inspected the same TIME typmod path and found no precision-specific type propagation before time_in executes; with typmod unset, each branch resolves to default precision 6 regardless of declared column typmod.
  • Why this is likely a bug: A single production-code path forces default precision when typmod is absent, which directly explains why all declared TIME precisions collapse to six fractional digits.

Relevant code:

server/ast/resolvable_type_reference.go (lines 156-163)

case oid.T_time:
	doltgresType = pgtypes.Time
case oid.T_timestamp:
	doltgresType = pgtypes.Timestamp
case oid.T_timestamptz:
	doltgresType = pgtypes.TimestampTZ
case oid.T_timetz:
	doltgresType = pgtypes.TimeTZ

server/functions/time.go (lines 49-53)

typmod := val3.(int32)
if typmod == -1 {
	typmod = 6
}
precision := tree.TimeFamilyPrecisionToRoundDuration(typmod)
🟠 Explicit varchar cast skips typmod truncation
  • What failed: The explicit cast path returned the over-limit value unchanged instead of enforcing typmod truncation semantics expected by the cast flow.
  • Impact: Queries that rely on explicit length-restricted VARCHAR casts can return values that violate the requested typmod, causing incorrect SQL behavior and compatibility drift. Users can hit inconsistent results between cast-based transformations and column-enforced input paths.
  • Steps to reproduce:
    1. Create an unbounded VARCHAR value with six runes, such as A😀BC😀D.
    2. Execute SELECT (v::varchar(5))::text for that value.
    3. Compare with inserting the same value into a VARCHAR(5) column, which enforces the limit with a value-too-long error.
  • Stub / mock context: A local deterministic auth bootstrap and SCRAM bypass were applied so SQL statements could run consistently; no mock or bypass changed VARCHAR cast logic in the code paths analyzed for this failure.
  • Code analysis: I inspected cast execution and string-cast helpers in server/expression/explicit_cast.go, core/casts/collection.go, and server/cast/utils.go. The sizing-cast path intentionally uses in/out conversion for same-type typmod changes, and explicit string casts intentionally ignore out-of-range errors, but handleStringCast returns the original untruncated value when it raises ErrCastOutOfRange, which directly causes the observed untruncated cast result.
  • Why this is likely a bug: The explicit cast logic depends on receiving a truncated string even when overflow is reported, but the VARCHAR cast helper returns the original untruncated input on overflow, producing the exact inconsistent behavior seen at runtime.

Relevant code:

server/expression/explicit_cast.go (lines 116-129)

castResult, err := cast.Eval(ctx, val, sourceType, c.castToType)
if err != nil {
	// For string types and string array types, we intentionally ignore the error as using a length-restricted cast
	// is a way to intentionally truncate the data. All string types will always return the truncated result, even
	// during an error, so it's safe to use.
	castToType := c.castToType
	if c.castToType.IsArrayType() {
		castToType = c.castToType.ArrayBaseTypeCtx(ctx)
	}
	if castToType.TypCategory != pgtypes.TypeCategory_StringTypes || castResult == nil {
		return nil, err
	}
}

core/casts/collection.go (lines 287-303)

if sourceType.GetAttTypMod() != targetType.GetAttTypMod() {
	return Cast{
		ID:       id.NewCast(sourceType.ID, targetType.ID),
		CastType: castType,
		Function: id.NullFunction,
		UseInOut: true,
	}
}

server/cast/utils.go (lines 60-69)

case pgtypes.VarChar.ID:
	if tm == -1 {
		return input, nil
	}
	length := uint32(pgtypes.GetCharLengthFromTypmod(tm))
	str, runeLength := truncateString(input, length)
	if runeLength > length {
		return input, cerrors.Wrap(pgtypes.ErrCastOutOfRange, fmt.Sprintf("value too long for type %s", targetType.String()))
	} else {
		return str, nil
	}
🗑️ No Longer Applicable (1)

Prior tests whose target code has been removed or renamed. Retired, not failing.

Category Summary Screenshot
Safety 🚨 Repeated fractional TIME output verification is blocked by startup nil dereference. SAFETY-1
🚨 Fractional TIME loop blocked at startup
  • What failed: The repeated fractional TIME check cannot execute because startup panics first on nil parameter dereference in overload key generation.
  • Impact: Users cannot run even basic TIME text queries because the server fails before accepting stable query traffic. This blocks both regression validation and normal database use.
  • Steps to reproduce:
    1. Connect to Doltgres on localhost:5432.
    2. Execute repeated query loops selecting a fractional TIME value such as SELECT TIME '16:17:18.123400'::text.
    3. Observe startup panic in overload initialization before repeated result comparisons can proceed.
  • Stub / mock context: The retry flow kept local authentication bypassed and included a temporary nil-parameter guard during environment troubleshooting. Even with that setup, the underlying startup and output-path defects remained reproducible and blocked the intended TIME verification loop.
  • Code analysis: I inspected the migrated time_out signature and overload key generation path. The first parameter is intentionally nil in Function2 declarations, but overload key generation still assumes every pointer is non-nil.
  • Why this is likely a bug: The failure is a deterministic startup panic in production code, so it is a true application defect rather than a flaky test condition.

Relevant code:

server/functions/time.go (lines 68-75)

var time_out = framework.Function2{
	Name:       "time_out",
	Return:     pgtypes.Cstring,
	Parameters: [2]*pgtypes.DoltgresType{nil, pgtypes.Time},
	Strict:     true,
	Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val, dest any) (any, error) {
		return val.(timeofday.TimeOfDay).AppendBytes(dest.([]byte)), nil
	},

server/functions/framework/overloads.go (lines 63-70)

func keyForParamTypes(types []*pgtypes.DoltgresType) string {
	sb := strings.Builder{}
	for i, typ := range types {
		if i > 0 {
			sb.WriteByte(',')
		}
		sb.WriteString(typ.String())
	}

View Full Run


Tell us how we did: Give Ito Feedback

@jycor
Copy link
Copy Markdown
Contributor Author

jycor commented Jun 4, 2026

Apparently these functions HAVE to be defined the way they are. A possible solution would be duplicating the output functions specifically for outputting to wire format. However, that is difficult to maintain so these will be as is until we can come up with a better solution.

@jycor jycor closed this Jun 4, 2026
@jycor jycor deleted the james/sqlstring branch June 4, 2026 05:13
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