Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 19 additions & 28 deletions core/casts/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (pgc *Collection) GetExplicitCast(ctx *sql.Context, sourceType *pgtypes.Dol
return c, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(sourceType, targetType, CastType_Explicit); cast.ID.IsValid() {
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Explicit); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
Expand Down Expand Up @@ -134,8 +134,8 @@ func (pgc *Collection) GetExplicitCast(ctx *sql.Context, sourceType *pgtypes.Dol
// GetAssignmentCast returns the assignment type cast function that will cast the source type to the target type.
// Returns a Cast with an invalid ID if such a cast is not valid.
func (pgc *Collection) GetAssignmentCast(ctx *sql.Context, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType) (Cast, error) {
castID := id.NewCast(sourceType.ID, targetType.ID)
c, err := pgc.getCast(ctx, castID, sourceType, targetType, CastType_Assignment)
castID := id.NewCast(sourceType.ID, targetType.ID) // TODO: expensive
c, err := pgc.getCast(ctx, castID, sourceType, targetType, CastType_Assignment) // TODO: expensive
if err != nil {
return Cast{}, err
}
Expand All @@ -146,7 +146,7 @@ func (pgc *Collection) GetAssignmentCast(ctx *sql.Context, sourceType *pgtypes.D
return c, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(sourceType, targetType, CastType_Assignment); cast.ID.IsValid() {
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Assignment); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
Expand Down Expand Up @@ -193,7 +193,7 @@ func (pgc *Collection) GetImplicitCast(ctx *sql.Context, sourceType *pgtypes.Dol
return Cast{}, nil
}
// We check for the identity and sizing casts after checking the maps, as the identity may be overridden by a user.
if cast := pgc.getSizingOrIdentityCast(sourceType, targetType, CastType_Implicit); cast.ID.IsValid() {
if cast := pgc.getSizingOrIdentityCast(castID, sourceType, targetType, CastType_Implicit); cast.ID.IsValid() {
return cast, nil
}
// We then check for a record to composite cast
Expand Down Expand Up @@ -301,38 +301,29 @@ func (pgc *Collection) getCast(ctx context.Context, castID id.Cast, sourceType *
// only differ in their atttypmod values. Returns a Cast with an invalid ID if no cast is matched. This mirrors the
// behavior as described in:
// https://www.postgresql.org/docs/15/typeconv-query.html
func (pgc *Collection) getSizingOrIdentityCast(sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType, castType CastType) Cast {
func (pgc *Collection) getSizingOrIdentityCast(castID id.Cast, sourceType *pgtypes.DoltgresType, targetType *pgtypes.DoltgresType, castType CastType) Cast {
// If we receive different types, then we can return immediately
if sourceType.ID != targetType.ID {
return Cast{}
}
// TODO: We don't have any sizing cast functions implemented, so for now we'll approximate using output to input.
// We can use the query below to find all implemented sizing cast functions. It's also detailed in the link above.
// Lastly, not all sizing functions accept a boolean, but for those that do, we need to see whether true is
// used for explicit casts, or whether true is used for implicit casts.
// SELECT
// format_type(c.castsource, NULL) AS source,
// format_type(c.casttarget, NULL) AS target,
// p.oid::regprocedure AS func
// FROM pg_cast c JOIN pg_proc p ON p.oid = c.castfunc WHERE c.castsource = c.casttarget ORDER BY 1,2;
// If we have different atttypmod values, then we need to do a sizing cast only if one exists
if sourceType.GetAttTypMod() != targetType.GetAttTypMod() {
// TODO: We don't have any sizing cast functions implemented, so for now we'll approximate using output to input.
// We can use the query below to find all implemented sizing cast functions. It's also detailed in the link above.
// Lastly, not all sizing functions accept a boolean, but for those that do, we need to see whether true is
// used for explicit casts, or whether true is used for implicit casts.
// SELECT
// format_type(c.castsource, NULL) AS source,
// format_type(c.casttarget, NULL) AS target,
// p.oid::regprocedure AS func
// FROM pg_cast c JOIN pg_proc p ON p.oid = c.castfunc WHERE c.castsource = c.casttarget ORDER BY 1,2;
return Cast{
ID: id.NewCast(sourceType.ID, targetType.ID),
CastType: castType,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: true,
request: castType,
}
}
// If there is no sizing cast, then we simply use the identity cast
// Otherwise, then we simply use the identity cast
useInOut := sourceType.GetAttTypMod() != targetType.GetAttTypMod()
return Cast{
ID: id.NewCast(sourceType.ID, targetType.ID),
ID: castID,
CastType: castType,
Function: id.NullFunction,
BuiltIn: nil,
UseInOut: false,
UseInOut: useInOut,
request: castType,
}
}
Expand Down
13 changes: 12 additions & 1 deletion core/id/id_wrappers.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Medium severity Prolly map round-trip preserves bytes but loses semantic cast identity

What failed: The system preserves raw cast ID bytes in storage, but semantic decode uses wrapped uint8 segment lengths, so cast identity is corrupted and lookup paths such as CastIDToTableName cannot reliably resolve the original cast.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: Creating casts with very long type identifiers can fail and crash lookup logic, preventing that cast workflow from completing reliably.
  • Steps to Reproduce:
    1. Create cast IDs with source or target Type IDs longer than 255 bytes and persist them through the normal cast storage path.
    2. Read the same cast IDs back after the prolly map round-trip and inspect SourceType()/TargetType() lengths.
    3. Observe that byte payloads round-trip, but decoded source/target segments are truncated (for example 257-byte source decodes as length 1), causing downstream cast lookup mismatch/failure.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In core/id/id_wrappers.go, NewCast now manually encodes two segment lengths with uint8(len(sourceType)) and uint8(len(targetType)). For lengths above 255, those bytes wrap and the first-format decoder in Id.Segment reads the wrapped values, returning truncated SourceType()/TargetType(). core/casts/collection.go then builds table names from those truncated types in CastIDToTableName, which explains the persisted-cast lookup failure despite distinct stored bytes. The smallest practical fix is to restore NewCast to the prior NewId-based construction (or equivalent fallback to the second format when a segment exceeds 255) so encode/decode preserves full segment boundaries.
  • Why this is likely a bug: A valid identifier pair should round-trip through persistence without changing cast source/target semantics, but the current implementation decodes different identities from the same persisted bytes when lengths exceed 255. That deterministic mismatch in production code directly breaks cast usability for affected IDs.
Relevant code

core/id/id_wrappers.go:89-105

func NewCast(sourceType Type, targetType Type) Cast {
	if len(sourceType) == 0 && len(targetType) == 0 {
		return NullCast
	}

	return Cast(
		string(
			[]byte{
				uint8(Section_Cast),
				2,
				uint8(len(sourceType)),
				uint8(len(targetType)),
			}) +
			string(sourceType) +
			string(targetType),
	)
}

core/id/id.go:102-113

if len(data) > 255 {
	return newIdSecondFormat(section, data)
}
...
for _, segment := range data {
	segmentLength := len(segment)
	if segmentLength > 255 {
		return newIdSecondFormat(section, data)
	}
	buf.WriteByte(uint8(segmentLength))
}

core/casts/collection.go:700-706

func CastIDToTableName(castID id.Cast) doltdb.TableName {
	name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`,
		castID.SourceType().SchemaName(),
		castID.SourceType().TypeName(),
		castID.TargetType().SchemaName(),
		castID.TargetType().TypeName())
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Prolly map round-trip preserves bytes but loses semantic cast identity**

**What failed:** The system preserves raw cast ID bytes in storage, but semantic decode uses wrapped uint8 segment lengths, so cast identity is corrupted and lookup paths such as CastIDToTableName cannot reliably resolve the original cast.

- **Impact:** Creating casts with very long type identifiers can fail and crash lookup logic, preventing that cast workflow from completing reliably.
- **Steps to reproduce:**
  1. Create cast IDs with source or target Type IDs longer than 255 bytes and persist them through the normal cast storage path.
  2. Read the same cast IDs back after the prolly map round-trip and inspect SourceType()/TargetType() lengths.
  3. Observe that byte payloads round-trip, but decoded source/target segments are truncated (for example 257-byte source decodes as length 1), causing downstream cast lookup mismatch/failure.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** In core/id/id_wrappers.go, NewCast now manually encodes two segment lengths with uint8(len(sourceType)) and uint8(len(targetType)). For lengths above 255, those bytes wrap and the first-format decoder in Id.Segment reads the wrapped values, returning truncated SourceType()/TargetType(). core/casts/collection.go then builds table names from those truncated types in CastIDToTableName, which explains the persisted-cast lookup failure despite distinct stored bytes. The smallest practical fix is to restore NewCast to the prior NewId-based construction (or equivalent fallback to the second format when a segment exceeds 255) so encode/decode preserves full segment boundaries.
- **Why this is likely a bug:** A valid identifier pair should round-trip through persistence without changing cast source/target semantics, but the current implementation decodes different identities from the same persisted bytes when lengths exceed 255. That deterministic mismatch in production code directly breaks cast usability for affected IDs.

**Relevant code:**

`core/id/id_wrappers.go:89-105`

~~~go
func NewCast(sourceType Type, targetType Type) Cast {
	if len(sourceType) == 0 && len(targetType) == 0 {
		return NullCast
	}

	return Cast(
		string(
			[]byte{
				uint8(Section_Cast),
				2,
				uint8(len(sourceType)),
				uint8(len(targetType)),
			}) +
			string(sourceType) +
			string(targetType),
	)
}
~~~

`core/id/id.go:102-113`

~~~go
if len(data) > 255 {
	return newIdSecondFormat(section, data)
}
...
for _, segment := range data {
	segmentLength := len(segment)
	if segmentLength > 255 {
		return newIdSecondFormat(section, data)
	}
	buf.WriteByte(uint8(segmentLength))
}
~~~

`core/casts/collection.go:700-706`

~~~go
func CastIDToTableName(castID id.Cast) doltdb.TableName {
	name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`,
		castID.SourceType().SchemaName(),
		castID.SourceType().TypeName(),
		castID.TargetType().SchemaName(),
		castID.TargetType().TypeName())
~~~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Minor severity Very long type identifiers do not silently mis-key casts

What failed: The CREATE CAST path panics during cast ID decoding instead of returning a deterministic SQL error for oversized identifiers, producing asymmetric behavior versus DROP CAST.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Minor Minor severity
  • Impact: Creating a cast can fail with a runtime panic when type identifiers are extremely long, so affected teams cannot complete that schema change until identifiers are shortened.
  • Steps to Reproduce:
    1. Create a schema and source type whose schema-qualified type ID exceeds 255 bytes (for example schema length 193 and type length 164).
    2. Create a conversion function, then run CREATE CAST (long_type AS text) WITH FUNCTION ... in the same session.
    3. Observe runtime panic slice bounds out of range from core/id/id.go:211; running DROP CAST for the same pair returns a deterministic does-not-exist error.
  • Stub / mock content: The run used local QA setup with authentication disabled for scripted access, but no cast-logic stubs, mocks, or bypasses were applied to this test.
  • Code Analysis: The PR-changed implementation of NewCast now serializes cast IDs with fixed one-byte length headers (uint8(len(sourceType)), uint8(len(targetType))). For long type IDs, these headers truncate true lengths, so later decode via castID.SourceType().SchemaName() (in CastIDToTableName) passes malformed segment metadata into Id.Segment, which slices past available data and panics at core/id/id.go:211. Before this PR, NewCast delegated to NewId, which falls back to second-format encoding when any segment length exceeds 255. The smallest practical fix is to make NewCast use NewId(Section_Cast, ...) again, or add equivalent >255 guards and second-format fallback before writing first-format bytes.
  • Why this is likely a bug: A runtime panic from CREATE CAST is not an acceptable deterministic SQL outcome, and the panic trace matches a concrete unsafe slice caused by truncated cast-ID segment lengths. This is a production code defect in cast ID encoding/decoding rather than a test harness artifact.
Relevant code

core/id/id_wrappers.go:89-105

func NewCast(sourceType Type, targetType Type) Cast {
	if len(sourceType) == 0 && len(targetType) == 0 {
		return NullCast
	}

	return Cast(
		string(
			[]byte{
				uint8(Section_Cast),
				2,
				uint8(len(sourceType)),
				uint8(len(targetType)),
			}) +
			string(sourceType) +
			string(targetType),
	)
}

core/id/id.go:96-113

func NewId(section Section, data ...string) Id {
	if len(data) > 255 {
		return newIdSecondFormat(section, data)
	}
	...
	for _, segment := range data {
		segmentLength := len(segment)
		if segmentLength > 255 {
			return newIdSecondFormat(section, data)
		}
		buf.WriteByte(uint8(segmentLength))
	}
}

core/id/id.go:200-212

segmentCount := int(id[1])
data := id[2+segmentCount:]
...
for i := 0; i <= index; i++ {
	start += currentLength
	currentLength = int(id[2+i])
}
return string(data[start : start+currentLength])

core/casts/collection.go:701-706

func CastIDToTableName(castID id.Cast) doltdb.TableName {
	name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`,
		castID.SourceType().SchemaName(),
		castID.SourceType().TypeName(),
		castID.TargetType().SchemaName(),
		castID.TargetType().TypeName())
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Minor severity — Very long type identifiers do not silently mis-key casts**

**What failed:** The CREATE CAST path panics during cast ID decoding instead of returning a deterministic SQL error for oversized identifiers, producing asymmetric behavior versus DROP CAST.

- **Impact:** Creating a cast can fail with a runtime panic when type identifiers are extremely long, so affected teams cannot complete that schema change until identifiers are shortened.
- **Steps to reproduce:**
  1. Create a schema and source type whose schema-qualified type ID exceeds 255 bytes (for example schema length 193 and type length 164).
  2. Create a conversion function, then run CREATE CAST (long_type AS text) WITH FUNCTION ... in the same session.
  3. Observe runtime panic `slice bounds out of range` from `core/id/id.go:211`; running DROP CAST for the same pair returns a deterministic does-not-exist error.
- **Stub / mock content:** The run used local QA setup with authentication disabled for scripted access, but no cast-logic stubs, mocks, or bypasses were applied to this test.
- **Code analysis:** The PR-changed implementation of `NewCast` now serializes cast IDs with fixed one-byte length headers (`uint8(len(sourceType))`, `uint8(len(targetType))`). For long type IDs, these headers truncate true lengths, so later decode via `castID.SourceType().SchemaName()` (in `CastIDToTableName`) passes malformed segment metadata into `Id.Segment`, which slices past available data and panics at `core/id/id.go:211`. Before this PR, `NewCast` delegated to `NewId`, which falls back to second-format encoding when any segment length exceeds 255. The smallest practical fix is to make `NewCast` use `NewId(Section_Cast, ...)` again, or add equivalent >255 guards and second-format fallback before writing first-format bytes.
- **Why this is likely a bug:** A runtime panic from CREATE CAST is not an acceptable deterministic SQL outcome, and the panic trace matches a concrete unsafe slice caused by truncated cast-ID segment lengths. This is a production code defect in cast ID encoding/decoding rather than a test harness artifact.

**Relevant code:**

`core/id/id_wrappers.go:89-105`

~~~go
func NewCast(sourceType Type, targetType Type) Cast {
	if len(sourceType) == 0 && len(targetType) == 0 {
		return NullCast
	}

	return Cast(
		string(
			[]byte{
				uint8(Section_Cast),
				2,
				uint8(len(sourceType)),
				uint8(len(targetType)),
			}) +
			string(sourceType) +
			string(targetType),
	)
}
~~~

`core/id/id.go:96-113`

~~~go
func NewId(section Section, data ...string) Id {
	if len(data) > 255 {
		return newIdSecondFormat(section, data)
	}
	...
	for _, segment := range data {
		segmentLength := len(segment)
		if segmentLength > 255 {
			return newIdSecondFormat(section, data)
		}
		buf.WriteByte(uint8(segmentLength))
	}
}
~~~

`core/id/id.go:200-212`

~~~go
segmentCount := int(id[1])
data := id[2+segmentCount:]
...
for i := 0; i <= index; i++ {
	start += currentLength
	currentLength = int(id[2+i])
}
return string(data[start : start+currentLength])
~~~

`core/casts/collection.go:701-706`

~~~go
func CastIDToTableName(castID id.Cast) doltdb.TableName {
	name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`,
		castID.SourceType().SchemaName(),
		castID.SourceType().TypeName(),
		castID.TargetType().SchemaName(),
		castID.TargetType().TypeName())
~~~

Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,18 @@ func NewCast(sourceType Type, targetType Type) Cast {
if len(sourceType) == 0 && len(targetType) == 0 {
return NullCast
}
return Cast(NewId(Section_Cast, string(sourceType), string(targetType)))

return Cast(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Medium severity User-defined cast cannot be resolved after successful CREATE CAST

What failed: The cast registration write path acknowledges success, but subsequent cast retrieval paths do not resolve that same cast identity for execution/catalog access.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: Users cannot reliably use user-defined casts after creation because the cast is not discoverable by lookup or coercion paths. Workflows that depend on custom type coercion fail until the ID parity bug is fixed.
  • Steps to Reproduce:
    1. Create source and target user-defined types and a conversion function.
    2. Run CREATE CAST (source AS target) WITH FUNCTION ... AS ASSIGNMENT and confirm it returns success.
    3. Attempt coercion with SELECT ROW(42)::enc1_x::enc1_y; and observe cast does not exist.
    4. Query pg_catalog.pg_cast for the created cast and observe no matching discoverable cast entry for the new pair.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: NewCast now bypasses NewId and manually emits first-format bytes with uint8(len(...)) in core/id/id_wrappers.go lines 94-104. The cast system uses this encoded key for persistence and lookup (server/node/create_cast.go line 86 and core/casts/collection.go lines 422-427), while catalog/runtime consumers decode source/target via segment extraction (server/tables/pgcatalog/pg_cast.go lines 117-119 and core/id/id_wrappers.go lines 258-264). The changed encoding path is therefore a direct practical cause of the observed write/read mismatch for cast discoverability.
  • Why this is likely a bug: The same source/target pair succeeds during CREATE CAST but is immediately unresolved during coercion, which indicates an internal cast ID consistency defect rather than expected SQL behavior. The failure aligns with the PR's changed cast-ID encoding path and impacts core cast retrieval semantics.
Relevant code

core/id/id_wrappers.go:94-104

return Cast(
	string(
		[]byte{
			uint8(Section_Cast),
			2,
			uint8(len(sourceType)),
			uint8(len(targetType)),
		}) +
		string(sourceType) +
		string(targetType),
)

server/node/create_cast.go:85-87

newCast := casts.Cast{
	ID:       id.NewCast(c.Source.ID, c.Target.ID),
	CastType: c.Type,
}

server/tables/pgcatalog/pg_cast.go:117-119

return sql.Row{
	cast.ID.AsId(),
	cast.ID.SourceType().AsId(),
	cast.ID.TargetType().AsId(),
...
}
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — User-defined cast cannot be resolved after successful CREATE CAST**

**What failed:** The cast registration write path acknowledges success, but subsequent cast retrieval paths do not resolve that same cast identity for execution/catalog access.

- **Impact:** Users cannot reliably use user-defined casts after creation because the cast is not discoverable by lookup or coercion paths. Workflows that depend on custom type coercion fail until the ID parity bug is fixed.
- **Steps to reproduce:**
  1. Create source and target user-defined types and a conversion function.
  2. Run CREATE CAST (source AS target) WITH FUNCTION ... AS ASSIGNMENT and confirm it returns success.
  3. Attempt coercion with `SELECT ROW(42)::enc1_x::enc1_y;` and observe `cast does not exist`.
  4. Query pg_catalog.pg_cast for the created cast and observe no matching discoverable cast entry for the new pair.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** `NewCast` now bypasses `NewId` and manually emits first-format bytes with `uint8(len(...))` in `core/id/id_wrappers.go` lines 94-104. The cast system uses this encoded key for persistence and lookup (`server/node/create_cast.go` line 86 and `core/casts/collection.go` lines 422-427), while catalog/runtime consumers decode source/target via segment extraction (`server/tables/pgcatalog/pg_cast.go` lines 117-119 and `core/id/id_wrappers.go` lines 258-264). The changed encoding path is therefore a direct practical cause of the observed write/read mismatch for cast discoverability.
- **Why this is likely a bug:** The same source/target pair succeeds during CREATE CAST but is immediately unresolved during coercion, which indicates an internal cast ID consistency defect rather than expected SQL behavior. The failure aligns with the PR's changed cast-ID encoding path and impacts core cast retrieval semantics.

**Relevant code:**

`core/id/id_wrappers.go:94-104`

~~~go
return Cast(
	string(
		[]byte{
			uint8(Section_Cast),
			2,
			uint8(len(sourceType)),
			uint8(len(targetType)),
		}) +
		string(sourceType) +
		string(targetType),
)
~~~

`server/node/create_cast.go:85-87`

~~~go
newCast := casts.Cast{
	ID:       id.NewCast(c.Source.ID, c.Target.ID),
	CastType: c.Type,
}
~~~

`server/tables/pgcatalog/pg_cast.go:117-119`

~~~go
return sql.Row{
	cast.ID.AsId(),
	cast.ID.SourceType().AsId(),
	cast.ID.TargetType().AsId(),
...
}
~~~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Medium severity Cast ID encoding breaks long type identifier handling

What failed: Cast IDs are encoded with fixed one-byte length fields in NewCast, so long-segment IDs can overflow and break lookup/catalog identity consistency.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: Users cannot rely on custom cast definitions because CREATE CAST appears to succeed but casts are missing from the catalog and runtime conversions fail.
  • Steps to Reproduce:
    1. Create source and target types with long names so their internal type IDs can exceed one-byte segment limits.
    2. Create a cast between those types with CREATE CAST ... WITH FUNCTION ....
    3. Query pg_catalog.pg_cast and execute a coercion using that cast.
    4. Observe cast identity/lookup mismatch (missing catalog entry or cast-not-found behavior) when ID decoding does not round-trip.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In core/id/id_wrappers.go, NewCast now manually builds first-format ID bytes and stores segment lengths as uint8. In core/id/id.go, NewId explicitly switches to a second format when any segment length is greater than 255. The PR change bypasses that safeguard specifically for cast IDs, creating a direct defect path for long source/target type IDs.
  • Why this is likely a bug: The shared ID constructor already defines the required fallback for segments above 255 bytes, but NewCast no longer uses it and can emit truncated length metadata. The smallest practical fix is to restore NewCast to NewId(Section_Cast, string(sourceType), string(targetType)) or mirror the same second-format fallback before writing one-byte lengths.
Relevant code

core/id/id_wrappers.go:94-104

return Cast(
	string(
		[]byte{
			uint8(Section_Cast),
			2,
			uint8(len(sourceType)),
			uint8(len(targetType)),
		}) +
		string(sourceType) +
		string(targetType),
)

core/id/id.go:109-113

segmentLength := len(segment)
if segmentLength > 255 {
	return newIdSecondFormat(section, data)
}
buf.WriteByte(uint8(segmentLength))
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Cast ID encoding breaks long type identifier handling**

**What failed:** Cast IDs are encoded with fixed one-byte length fields in `NewCast`, so long-segment IDs can overflow and break lookup/catalog identity consistency.

- **Impact:** Users cannot rely on custom cast definitions because CREATE CAST appears to succeed but casts are missing from the catalog and runtime conversions fail.
- **Steps to reproduce:**
  1. Create source and target types with long names so their internal type IDs can exceed one-byte segment limits.
  2. Create a cast between those types with `CREATE CAST ... WITH FUNCTION ...`.
  3. Query `pg_catalog.pg_cast` and execute a coercion using that cast.
  4. Observe cast identity/lookup mismatch (missing catalog entry or cast-not-found behavior) when ID decoding does not round-trip.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** In `core/id/id_wrappers.go`, `NewCast` now manually builds first-format ID bytes and stores segment lengths as `uint8`. In `core/id/id.go`, `NewId` explicitly switches to a second format when any segment length is greater than 255. The PR change bypasses that safeguard specifically for cast IDs, creating a direct defect path for long source/target type IDs.
- **Why this is likely a bug:** The shared ID constructor already defines the required fallback for segments above 255 bytes, but `NewCast` no longer uses it and can emit truncated length metadata. The smallest practical fix is to restore `NewCast` to `NewId(Section_Cast, string(sourceType), string(targetType))` or mirror the same second-format fallback before writing one-byte lengths.

**Relevant code:**

`core/id/id_wrappers.go:94-104`

~~~go
return Cast(
	string(
		[]byte{
			uint8(Section_Cast),
			2,
			uint8(len(sourceType)),
			uint8(len(targetType)),
		}) +
		string(sourceType) +
		string(targetType),
)
~~~

`core/id/id.go:109-113`

~~~go
segmentLength := len(segment)
if segmentLength > 255 {
	return newIdSecondFormat(section, data)
}
buf.WriteByte(uint8(segmentLength))
~~~

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Minor severity Long type cast IDs break lookup

What failed: Expected long-identifier casts to be uniquely encoded and resolvable before and after restart, but casts with type IDs above 255 bytes fail at creation because decoded source/target identity is truncated.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Minor Minor severity
  • Impact: Users who define very long type identifiers (over 255 bytes) cannot create casts because lookup resolves to an invalid identifier. Typical schemas with shorter identifiers continue to work.
  • Steps to Reproduce:
    1. Create two types where one generated type ID exceeds 255 bytes and attempt CREATE CAST from int4 to that type.
    2. Observe CREATE CAST fails with root returned table (pg_catalog)|(int4)|()|() but it could not be found.
    3. Create a cast to a similar type whose ID is below 256 bytes and observe it succeeds, then restart and retry the long-ID cast to confirm the same failure persists.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: In core/id/id_wrappers.go, NewCast now manually writes segment lengths with uint8(len(sourceType)) and uint8(len(targetType)), which overflows for IDs >255 bytes. core/id/id.go shows NewId would otherwise switch to newIdSecondFormat when any segment exceeds 255, but NewCast bypasses that guard. core/casts/collection.go builds lookup table names from castID.SourceType()/TargetType(), so truncated decode yields the observed (pg_catalog)|(int4)|()|() lookup miss. The failing path is directly in PR-changed code.
  • Why this is likely a bug: Runtime evidence consistently fails only when IDs cross the 255-byte boundary, and source inspection shows deterministic uint8 truncation in the changed encoding path. A targeted fix is to route NewCast back through NewId (or equivalent overflow-safe encoding) so long segments use the second format.
Relevant code

core/id/id_wrappers.go:94-103

return Cast(string([]byte{uint8(Section_Cast), 2, uint8(len(sourceType)), uint8(len(targetType))}) + string(sourceType) + string(targetType))

core/id/id.go:102-113

if len(data) > 255 { return newIdSecondFormat(section, data) } ... if segmentLength > 255 { return newIdSecondFormat(section, data) }

core/casts/collection.go:701-706

name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`, castID.SourceType().SchemaName(), castID.SourceType().TypeName(), castID.TargetType().SchemaName(), castID.TargetType().TypeName())
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Minor severity — Long type cast IDs break lookup**

**What failed:** Expected long-identifier casts to be uniquely encoded and resolvable before and after restart, but casts with type IDs above 255 bytes fail at creation because decoded source/target identity is truncated.

- **Impact:** Users who define very long type identifiers (over 255 bytes) cannot create casts because lookup resolves to an invalid identifier. Typical schemas with shorter identifiers continue to work.
- **Steps to reproduce:**
  1. Create two types where one generated type ID exceeds 255 bytes and attempt CREATE CAST from int4 to that type.
  2. Observe CREATE CAST fails with `root returned table `(pg_catalog)|(int4)|()|()` but it could not be found`.
  3. Create a cast to a similar type whose ID is below 256 bytes and observe it succeeds, then restart and retry the long-ID cast to confirm the same failure persists.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** In `core/id/id_wrappers.go`, `NewCast` now manually writes segment lengths with `uint8(len(sourceType))` and `uint8(len(targetType))`, which overflows for IDs >255 bytes. `core/id/id.go` shows `NewId` would otherwise switch to `newIdSecondFormat` when any segment exceeds 255, but `NewCast` bypasses that guard. `core/casts/collection.go` builds lookup table names from `castID.SourceType()/TargetType()`, so truncated decode yields the observed `(pg_catalog)|(int4)|()|()` lookup miss. The failing path is directly in PR-changed code.
- **Why this is likely a bug:** Runtime evidence consistently fails only when IDs cross the 255-byte boundary, and source inspection shows deterministic uint8 truncation in the changed encoding path. A targeted fix is to route `NewCast` back through `NewId` (or equivalent overflow-safe encoding) so long segments use the second format.

**Relevant code:**

`core/id/id_wrappers.go:94-103`

~~~go
return Cast(string([]byte{uint8(Section_Cast), 2, uint8(len(sourceType)), uint8(len(targetType))}) + string(sourceType) + string(targetType))
~~~

`core/id/id.go:102-113`

~~~go
if len(data) > 255 { return newIdSecondFormat(section, data) } ... if segmentLength > 255 { return newIdSecondFormat(section, data) }
~~~

`core/casts/collection.go:701-706`

~~~go
name := fmt.Sprintf(`(%s)|(%s)|(%s)|(%s)`, castID.SourceType().SchemaName(), castID.SourceType().TypeName(), castID.TargetType().SchemaName(), castID.TargetType().TypeName())
~~~

string(
[]byte{
uint8(Section_Cast),
2,
uint8(len(sourceType)),
uint8(len(targetType)),
}) +
string(sourceType) +
string(targetType),
)
}

// NewCheck returns a new Check. This wrapper must not be returned to the client.
Expand Down
Loading