Files
bun.sh/src/sql/postgres/AnyPostgresError.zig
robobun e1de7563e1 Fix PostgreSQL TIME and TIMETZ binary format handling (#22354)
## Summary
- Fixes binary format handling for PostgreSQL TIME and TIMETZ data types
- Resolves issue where time values were returned as garbled binary data
with null bytes

## Problem
When PostgreSQL returns TIME or TIMETZ columns in binary format, Bun.sql
was not properly converting them from their binary representation
(microseconds since midnight) to readable time strings. This resulted in
corrupted output like `\u0000\u0000\u0000\u0000\u0076` instead of proper
time values like `09:00:00`.

## Solution
Added proper binary format decoding for:
- **TIME (OID 1083)**: Converts 8 bytes of microseconds since midnight
to `HH:MM:SS.ffffff` format
- **TIMETZ (OID 1266)**: Converts 8 bytes of microseconds + 4 bytes of
timezone offset to `HH:MM:SS.ffffff±HH:MM` format

## Changes
- Added binary format handling in `src/sql/postgres/DataCell.zig` for
TIME and TIMETZ types
- Added `InvalidTimeFormat` error to `AnyPostgresError` error set
- Properly formats microseconds with trailing zero removal
- Handles timezone offsets correctly (PostgreSQL uses negative values
for positive UTC offsets)

## Test plan
Added comprehensive tests in `test/js/bun/sql/postgres-time.test.ts`:
- [x] TIME and TIMETZ column values with various formats
- [x] NULL handling
- [x] Array types (TIME[] and TIMETZ[])
- [x] JSONB structures containing time strings
- [x] Verification that no binary/null bytes appear in output

All tests pass locally with PostgreSQL.

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-03 15:43:04 -07:00

134 lines
5.6 KiB
Zig

pub const AnyPostgresError = error{
ConnectionClosed,
ExpectedRequest,
ExpectedStatement,
InvalidBackendKeyData,
InvalidBinaryData,
InvalidByteSequence,
InvalidByteSequenceForEncoding,
InvalidCharacter,
InvalidMessage,
InvalidMessageLength,
InvalidQueryBinding,
InvalidServerKey,
InvalidServerSignature,
InvalidTimeFormat,
JSError,
MultidimensionalArrayNotSupportedYet,
NullsInArrayNotSupportedYet,
OutOfMemory,
Overflow,
PBKDFD2,
SASL_SIGNATURE_MISMATCH,
SASL_SIGNATURE_INVALID_BASE64,
ShortRead,
TLSNotAvailable,
TLSUpgradeFailed,
UnexpectedMessage,
UNKNOWN_AUTHENTICATION_METHOD,
UNSUPPORTED_AUTHENTICATION_METHOD,
UnsupportedByteaFormat,
UnsupportedIntegerSize,
UnsupportedArrayFormat,
UnsupportedNumericFormat,
UnknownFormatCode,
};
/// Options for creating a PostgresError
pub const PostgresErrorOptions = struct {
code: []const u8,
errno: ?[]const u8 = null,
detail: ?[]const u8 = null,
hint: ?[]const u8 = null,
severity: ?[]const u8 = null,
position: ?[]const u8 = null,
internalPosition: ?[]const u8 = null,
internalQuery: ?[]const u8 = null,
where: ?[]const u8 = null,
schema: ?[]const u8 = null,
table: ?[]const u8 = null,
column: ?[]const u8 = null,
dataType: ?[]const u8 = null,
constraint: ?[]const u8 = null,
file: ?[]const u8 = null,
line: ?[]const u8 = null,
routine: ?[]const u8 = null,
};
pub fn createPostgresError(
globalObject: *jsc.JSGlobalObject,
message: []const u8,
options: PostgresErrorOptions,
) bun.JSError!JSValue {
const opts_obj = JSValue.createEmptyObject(globalObject, 18);
opts_obj.ensureStillAlive();
opts_obj.put(globalObject, jsc.ZigString.static("code"), try bun.String.createUTF8ForJS(globalObject, options.code));
inline for (std.meta.fields(PostgresErrorOptions)) |field| {
const FieldType = @typeInfo(@TypeOf(@field(options, field.name)));
if (FieldType == .optional) {
if (@field(options, field.name)) |value| {
opts_obj.put(globalObject, jsc.ZigString.static(field.name), try bun.String.createUTF8ForJS(globalObject, value));
}
}
}
opts_obj.put(globalObject, jsc.ZigString.static("message"), try bun.String.createUTF8ForJS(globalObject, message));
return opts_obj;
}
pub fn postgresErrorToJS(globalObject: *jsc.JSGlobalObject, message: ?[]const u8, err: AnyPostgresError) JSValue {
const code = switch (err) {
error.ConnectionClosed => "ERR_POSTGRES_CONNECTION_CLOSED",
error.ExpectedRequest => "ERR_POSTGRES_EXPECTED_REQUEST",
error.ExpectedStatement => "ERR_POSTGRES_EXPECTED_STATEMENT",
error.InvalidBackendKeyData => "ERR_POSTGRES_INVALID_BACKEND_KEY_DATA",
error.InvalidBinaryData => "ERR_POSTGRES_INVALID_BINARY_DATA",
error.InvalidByteSequence => "ERR_POSTGRES_INVALID_BYTE_SEQUENCE",
error.InvalidByteSequenceForEncoding => "ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING",
error.InvalidCharacter => "ERR_POSTGRES_INVALID_CHARACTER",
error.InvalidMessage => "ERR_POSTGRES_INVALID_MESSAGE",
error.InvalidMessageLength => "ERR_POSTGRES_INVALID_MESSAGE_LENGTH",
error.InvalidQueryBinding => "ERR_POSTGRES_INVALID_QUERY_BINDING",
error.InvalidServerKey => "ERR_POSTGRES_INVALID_SERVER_KEY",
error.InvalidServerSignature => "ERR_POSTGRES_INVALID_SERVER_SIGNATURE",
error.InvalidTimeFormat => "ERR_POSTGRES_INVALID_TIME_FORMAT",
error.MultidimensionalArrayNotSupportedYet => "ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET",
error.NullsInArrayNotSupportedYet => "ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET",
error.Overflow => "ERR_POSTGRES_OVERFLOW",
error.PBKDFD2 => "ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2",
error.SASL_SIGNATURE_MISMATCH => "ERR_POSTGRES_SASL_SIGNATURE_MISMATCH",
error.SASL_SIGNATURE_INVALID_BASE64 => "ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64",
error.TLSNotAvailable => "ERR_POSTGRES_TLS_NOT_AVAILABLE",
error.TLSUpgradeFailed => "ERR_POSTGRES_TLS_UPGRADE_FAILED",
error.UnexpectedMessage => "ERR_POSTGRES_UNEXPECTED_MESSAGE",
error.UNKNOWN_AUTHENTICATION_METHOD => "ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD",
error.UNSUPPORTED_AUTHENTICATION_METHOD => "ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD",
error.UnsupportedByteaFormat => "ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT",
error.UnsupportedArrayFormat => "ERR_POSTGRES_UNSUPPORTED_ARRAY_FORMAT",
error.UnsupportedIntegerSize => "ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE",
error.UnsupportedNumericFormat => "ERR_POSTGRES_UNSUPPORTED_NUMERIC_FORMAT",
error.UnknownFormatCode => "ERR_POSTGRES_UNKNOWN_FORMAT_CODE",
error.JSError => {
return globalObject.takeException(error.JSError);
},
error.OutOfMemory => {
// TODO: add binding for creating an out of memory error?
return globalObject.takeException(globalObject.throwOutOfMemory());
},
error.ShortRead => {
bun.unreachablePanic("Assertion failed: ShortRead should be handled by the caller in postgres", .{});
},
};
var buffer_message = [_]u8{0} ** 256;
const msg = message orelse std.fmt.bufPrint(buffer_message[0..], "Failed to bind query: {s}", .{@errorName(err)}) catch "Failed to bind query";
return createPostgresError(globalObject, msg, .{ .code = code }) catch |e| globalObject.takeError(e);
}
const bun = @import("bun");
const std = @import("std");
const jsc = bun.jsc;
const JSValue = jsc.JSValue;