properly handle NULL values with postgres result helpers

This commit is contained in:
fusion32 2025-10-18 03:40:40 -03:00
parent 601ce32ee8
commit ebf536a791
4 changed files with 52 additions and 14 deletions

View File

@ -7,5 +7,6 @@ files will be executed exactly ONCE. If multiple patches are pending at startup,
they're executed in alphabetical order. they're executed in alphabetical order.
As for statement restrictions, the only thing prohibited is the presence of As for statement restrictions, the only thing prohibited is the presence of
transaction statements "BEGIN", "ROLLBACK", and "COMMIT". This is because all transaction statements "BEGIN", "ROLLBACK", and "COMMIT". This is because all
patches will be bundled into the same transaction, to ensure atomicity. patches will be bundled into the same transaction, to ensure atomicity, and
there can't be nested transactions.

View File

@ -285,10 +285,20 @@ static int ResultAffectedRows(PGresult *Result){
return AffectedRows; return AffectedRows;
} }
// IMPORTANT(fusion): `GetResult*` helpers need to properly handle NULL values.
// In text format, they're represented as empty strings which could cause parsing
// errors. In binary format, they're represented as zero length blobs which could
// cause assertions to fire.
static bool GetResultBool(PGresult *Result, int Row, int Col){ static bool GetResultBool(PGresult *Result, int Row, int Col){
bool Value = false; bool Value = false;
if(PQgetisnull(Result, Row, Col)){
return Value;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1);
if(Format == 0){ // TEXT FORMAT if(Format == 0){ // TEXT FORMAT
if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){ if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){
LOG_ERR("Failed to properly parse column (%d) %s as BOOLEAN", LOG_ERR("Failed to properly parse column (%d) %s as BOOLEAN",
@ -341,6 +351,10 @@ static bool GetResultBool(PGresult *Result, int Row, int Col){
static int GetResultInt(PGresult *Result, int Row, int Col){ static int GetResultInt(PGresult *Result, int Row, int Col){
int Value = 0; int Value = 0;
if(PQgetisnull(Result, Row, Col)){
return Value;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
@ -402,6 +416,10 @@ static int GetResultInt(PGresult *Result, int Row, int Col){
static const char *GetResultText(PGresult *Result, int Row, int Col){ static const char *GetResultText(PGresult *Result, int Row, int Col){
const char *Text = ""; const char *Text = "";
if(PQgetisnull(Result, Row, Col)){
return Text;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
@ -433,6 +451,10 @@ static const char *GetResultText(PGresult *Result, int Row, int Col){
static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int BufferSize){ static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int BufferSize){
int Size = -1; int Size = -1;
if(PQgetisnull(Result, Row, Col)){
return Size;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
if(Format == 0){ // TEXT FORMAT if(Format == 0){ // TEXT FORMAT
@ -454,11 +476,15 @@ static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int
return Size; return Size;
} }
#if 0 #if 1
// NOTE(fusion): DO NOT REMOVE. It is currently not being used so it's switched // NOTE(fusion): DO NOT REMOVE. It is currently not being used so it's switched
// off to avoid compiler warnings. // off to avoid compiler warnings.
static int GetResultIPAddress(PGresult *Result, int Row, int Col){ static int GetResultIPAddress(PGresult *Result, int Row, int Col){
int IPAddress = 0; int IPAddress = 0;
if(PQgetisnull(Result, Row, Col)){
return IPAddress;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
@ -573,6 +599,10 @@ static bool ParseTimestamp(int *Dest, const char *String){
static int GetResultTimestamp(PGresult *Result, int Row, int Col){ static int GetResultTimestamp(PGresult *Result, int Row, int Col){
int Timestamp = 0; int Timestamp = 0;
if(PQgetisnull(Result, Row, Col)){
return Timestamp;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
@ -842,6 +872,10 @@ static bool ParseInterval(int *Dest, const char *String){
static int GetResultInterval(PGresult *Result, int Row, int Col){ static int GetResultInterval(PGresult *Result, int Row, int Col){
int Interval = 0; int Interval = 0;
if(PQgetisnull(Result, Row, Col)){
return Interval;
}
int Format = PQfformat(Result, Col); int Format = PQfformat(Result, Col);
Oid Type = PQftype(Result, Col); Oid Type = PQftype(Result, Col);
ASSERT(Format == 0 || Format == 1); ASSERT(Format == 0 || Format == 1);
@ -1242,22 +1276,26 @@ TDatabase *DatabaseOpen(void){
// TODO(fusion): REMOVE. This is for testing query input/output, to make sure // TODO(fusion): REMOVE. This is for testing query input/output, to make sure
// they're consitent across different formats (text/binary). // they're consitent across different formats (text/binary).
const char *Stmt = PrepareQuery(Database, const char *Stmt = PrepareQuery(Database,
"SELECT $1::INTERVAL, $2::INTERVAL"); "SELECT NULL::BOOLEAN, NULL::INTEGER, NULL::TEXT, NULL::BYTEA,"
" NULL::INET, NULL::TIMESTAMP, NULL::INTERVAL");
ASSERT(Stmt != NULL); ASSERT(Stmt != NULL);
for(int i = 0; i <= 1; i += 1) for(int i = 0; i <= 1; i += 1)
for(int j = 0; j <= 1; j += 1){ for(int j = 0; j <= 1; j += 1){
LOG("TEST (%d, %d)", i, j); LOG("TEST (%d, %d)", i, j);
ParamBuffer Params; ParamBuffer Params;
ParamBegin(&Params, 2, i); ParamBegin(&Params, 0, i);
ParamInterval(&Params, 86400 + 3600);
ParamInterval(&Params, - 86400 * 4 + 7 * 3600);
PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams,
Params.Values, Params.Lengths, Params.Formats, j); Params.Values, Params.Lengths, Params.Formats, j);
AutoResultClear ResultGuard(Result); AutoResultClear ResultGuard(Result);
if(PQresultStatus(Result) == PGRES_TUPLES_OK){ if(PQresultStatus(Result) == PGRES_TUPLES_OK){
LOG("0: %d", GetResultInterval(Result, 0, 0)); LOG("0: %d", GetResultBool(Result, 0, 0));
LOG("1: %d", GetResultInterval(Result, 0, 1)); LOG("1: %d", GetResultInt(Result, 0, 1));
LOG("2: %s", GetResultText(Result, 0, 2));
LOG("3: %d", GetResultByteA(Result, 0, 3, NULL, 0));
LOG("4: %d", GetResultIPAddress(Result, 0, 4));
LOG("5: %d", GetResultTimestamp(Result, 0, 5));
LOG("6: %d", GetResultInterval(Result, 0, 6));
}else{ }else{
LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle));
} }

View File

@ -412,9 +412,8 @@ static bool CheckDatabaseSchema(TDatabase *Database){
return false; return false;
} }
// IMPORTANT(fusion): After checking the application id, we want to apply // IMPORTANT(fusion): We want to apply patches before checking user version,
// patches before checking user version, just in case some migration needs // just in case some migration needs to take place.
// to take place.
if(!ApplyDatabasePatches(Database, "sqlite/patches")){ if(!ApplyDatabasePatches(Database, "sqlite/patches")){
LOG_ERR("Failed to apply database patches"); LOG_ERR("Failed to apply database patches");
return false; return false;

View File

@ -243,9 +243,9 @@ bool StringFormatTime(char *Dest, int DestCapacity, const char *Format, int Time
struct tm tm = GetLocalTime((int)Timestamp); struct tm tm = GetLocalTime((int)Timestamp);
int Result = (int)strftime(Dest, DestCapacity, Format, &tm); int Result = (int)strftime(Dest, DestCapacity, Format, &tm);
// NOTE(fusion): `strftime` will should return ZERO if it's unable to fit // NOTE(fusion): `strftime` will return ZERO if it's unable to fit the result
// the result in the supplied buffer, which is annoying because ZERO may // in the supplied buffer, which is annoying because ZERO may not represent a
// not represent a failure if the result is an empty string. // failure if the result is an empty string.
ASSERT(Result >= 0 && Result < DestCapacity); ASSERT(Result >= 0 && Result < DestCapacity);
if(Result == 0){ if(Result == 0){
memset(Dest, 0, DestCapacity); memset(Dest, 0, DestCapacity);