#if DATABASE_POSTGRESQL #include "querymanager.hh" #include "libpq-fe.h" #include // IMPORTANT(fusion): With PostgreSQL being a distributed database, we cannot // rely on automatic schema upgrades like in the case of SQLite. It must be // managed manually and there must be an agreement on the current version // which is why there is a `SchemaInfo` table. #define POSTGRESQL_SCHEMA_VERSION 1 // IMPORTANT(fusion): PostgreSQL timestamps will count the number of microseconds // since 2000-01-01 00:00:00, with negative values for timestamps before it. To be // able to convert between PostgreSQL and UNIX timestamps we need the EPOCH of one // represented as a timestamp of the other, which is exactly what this is. This is // the PostgreSQL EPOCH represented as an UNIX timestamp. #define POSTGRESQL_EPOCH 946684800 // IMPORTANT(fusion): Address families used with INET and CIDR binary format. // They're taken from `utils/inet.h` which is not included with libpq but should // be stable across different systems, mostly because AF_INET should be stable. #define POSTGRESQL_AF_INET (AF_INET + 0) #define POSTGRESQL_AF_INET6 (AF_INET + 1) STATIC_ASSERT(POSTGRESQL_AF_INET == 2); STATIC_ASSERT(POSTGRESQL_AF_INET6 == 3); // IMPORTANT(fusion): These are the OIDs for a few of built-in data types in // PostgreSQL. They're taken from `catalog/pg_type_d.h` which is not included // with libpq but should be STABLE across different versions and are needed // for properly handling binary data from the server. #define BOOLOID 16 #define BYTEAOID 17 #define CHAROID 18 #define INT8OID 20 #define INT2OID 21 #define INT4OID 23 #define TEXTOID 25 #define FLOAT4OID 700 #define FLOAT8OID 701 #define CIDROID 650 #define INETOID 869 #define VARCHAROID 1043 #define DATEOID 1082 #define TIMEOID 1083 #define TIMESTAMPOID 1114 #define TIMESTAMPTZOID 1184 #define INTERVALOID 1186 #define TIMETZOID 1266 struct TCachedStatement{ char Name[16]; int LastUsed; uint32 Hash; char *Text; }; struct TDatabase{ PGconn *Handle; int MaxCachedStatements; TCachedStatement *CachedStatements; }; // Param Buffer //============================================================================== struct ParamBuffer{ const char **Values; int *Lengths; int *Formats; int NumParams; int MaxParams; int PreferredFormat; // NOTE(fusion): 8KB should be more than enough for all case scenarios. We'll // know if it's not. int ArenaPos; uint8 Arena[KB(8)]; }; static void *ParamAllocImpl(ParamBuffer *Params, int Size, int Alignment){ usize ArenaStart = (usize)Params->Arena; usize ArenaEnd = ArenaStart + sizeof(Params->Arena); usize ArenaPos = ArenaStart + Params->ArenaPos; usize AllocStart = AlignUp(ArenaPos, Alignment); usize AllocEnd = AllocStart + Size; if(AllocEnd > ArenaEnd){ PANIC("Param buffer is FULL"); } Params->ArenaPos = (int)(AllocEnd - ArenaStart); return (void*)AllocStart; } template static T *ParamAlloc(ParamBuffer *Params, int Count){ ASSERT(Count > 0); return (T*)ParamAllocImpl(Params, sizeof(T) * Count, alignof(T)); } static void ParamBegin(ParamBuffer *Params, int MaxParams, int PreferredFormat){ // NOTE(fusion): Reset arena. memset(Params->Arena, 0, sizeof(Params->Arena)); Params->ArenaPos = 0; // NOTE(fusion): Reset params. if(MaxParams > 0){ Params->Values = ParamAlloc(Params, MaxParams); Params->Lengths = ParamAlloc(Params, MaxParams); Params->Formats = ParamAlloc(Params, MaxParams); }else{ Params->Values = NULL; Params->Lengths = NULL; Params->Formats = NULL; } Params->NumParams = 0; Params->MaxParams = MaxParams; Params->PreferredFormat = PreferredFormat; } static void InsertParam(ParamBuffer *Params, const char *Param, int Length, int Format){ if(Params->NumParams >= Params->MaxParams){ PANIC("Too many parameters specified (%d/%d)", Params->NumParams + 1, Params->MaxParams); } Params->Values[Params->NumParams] = Param; Params->Lengths[Params->NumParams] = Length; Params->Formats[Params->NumParams] = Format; Params->NumParams += 1; } static void InsertTextParam(ParamBuffer *Params, const char *Text){ int TextLength = (int)strlen(Text); char *Copy = ParamAlloc(Params, TextLength + 1); memcpy(Copy, Text, TextLength + 1); InsertParam(Params, Copy, TextLength, 0); } static void InsertBinaryParam(ParamBuffer *Params, const uint8 *Data, int Length){ uint8 *Copy = ParamAlloc(Params, Length); memcpy(Copy, Data, Length); InsertParam(Params, (const char*)Copy, Length, 1); } static void ParamBool(ParamBuffer *Params, bool Value){ if(Params->PreferredFormat == 1){ // BINARY FORMAT uint8 Data = (Value ? 0x01 : 0x00); InsertBinaryParam(Params, &Data, 1); }else{ InsertTextParam(Params, (Value ? "TRUE" : "FALSE")); } } static void ParamInt(ParamBuffer *Params, int Value){ if(Params->PreferredFormat == 1){ // BINARY FORMAT uint8 Data[4]; BufferWrite32BE(Data, (uint32)Value); InsertBinaryParam(Params, Data, 4); }else{ char Text[16]; StringBufFormat(Text, "%d", Value); InsertTextParam(Params, Text); } } static void ParamText(ParamBuffer *Params, const char *Text){ // NOTE(fusion): Always use TEXT format. InsertTextParam(Params, Text); } static void ParamByteA(ParamBuffer *Params, const uint8 *Data, int Length){ // TODO(fusion): Always use BINARY format? InsertBinaryParam(Params, Data, Length); } static void ParamIPAddress(ParamBuffer *Params, int IPAddress){ if(Params->PreferredFormat == 1){ // BINARY FORMAT uint8 Data[8]; Data[0] = POSTGRESQL_AF_INET; // AddressFamily Data[1] = 32; // MaskBits Data[2] = 0; // IsCIDR Data[3] = 4; // AddressSize BufferWrite32BE(Data + 4, (uint32)IPAddress); InsertBinaryParam(Params, Data, 8); }else{ char Text[16]; StringBufFormat(Text, "%d.%d.%d.%d", ((IPAddress >> 24) & 0xFF), ((IPAddress >> 16) & 0xFF), ((IPAddress >> 8) & 0xFF), ((IPAddress >> 0) & 0xFF)); InsertTextParam(Params, Text); } } static void ParamTimestamp(ParamBuffer *Params, int Timestamp){ if(Params->PreferredFormat == 1){ // BINARY FORMAT // NOTE(fusion): See `POSTGRESQL_EPOCH`. uint8 Data[8]; int64 PGTimestamp = (int64)(Timestamp - POSTGRESQL_EPOCH) * 1000000; BufferWrite64BE(Data, (uint64)PGTimestamp); InsertBinaryParam(Params, Data, 8); }else{ char Text[32]; struct tm tm = GetGMTime((time_t)Timestamp); strftime(Text, sizeof(Text), "%Y-%m-%d %H:%M:%S+00", &tm); InsertTextParam(Params, Text); } } static void ParamInterval(ParamBuffer *Params, int Interval){ int OneDay = 60 * 60 * 24; int Years = (Interval / (OneDay * 365)); Interval -= Years * (OneDay * 365); int Months = (Interval / (OneDay * 30)); Interval -= Months * (OneDay * 30); int Days = (Interval / OneDay); Interval -= Days * OneDay; int Seconds = Interval; Interval -= Seconds; if(Params->PreferredFormat == 1){ // BINARY FORMAT uint8 Data[16]; int64 Microseconds = (int64)Seconds * 1000000; BufferWrite64BE(Data + 0, (uint64)Microseconds); BufferWrite32BE(Data + 8, (uint32)Days); BufferWrite32BE(Data + 12, (uint32)(Months + Years * 12)); InsertBinaryParam(Params, Data, 16); }else{ StringBuffer<256> Text; if(Years != 0){ if(!Text.Empty()) Text.Append(" "); Text.FormatAppend("%d years", Years); } if(Months != 0){ if(!Text.Empty()) Text.Append(" "); Text.FormatAppend("%d mons", Months); } if(Days != 0){ if(!Text.Empty()) Text.Append(" "); Text.FormatAppend("%d days", Days); } if(Seconds != 0){ bool Negative = false; if(Seconds < 0){ Negative = true; Seconds = -Seconds; } int OneMinute = 60; int Hours = (Seconds / (OneMinute * 60)); Seconds -= Hours * (OneMinute * 60); int Minutes = (Seconds / OneMinute); Seconds -= Minutes * OneMinute; if(!Text.Empty()) Text.Append(" "); if(Negative) Text.Append("-"); Text.FormatAppend("%02d:%02d:%02d", Hours, Minutes, Seconds); } ASSERT(!Text.Overflowed()); InsertTextParam(Params, Text.CString()); } } // Result Helpers //============================================================================== struct AutoResultClear{ private: PGresult *m_Result; public: AutoResultClear(PGresult *Result){ m_Result = Result; } ~AutoResultClear(void){ if(m_Result != NULL){ PQclear(m_Result); m_Result = NULL; } } }; static int ResultAffectedRows(PGresult *Result){ int AffectedRows = 0; ParseInteger(&AffectedRows, PQcmdTuples(Result)); return AffectedRows; } static bool GetResultBool(PGresult *Result, int Row, int Col){ bool Value = false; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); if(Format == 0){ // TEXT FORMAT if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to properly parse column (%d) %s as BOOLEAN", Col, PQfname(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT switch(Type){ case BOOLOID:{ ASSERT(PQgetlength(Result, Row, Col) == 1); Value = (BufferRead8((const uint8*)PQgetvalue(Result, Row, Col)) != 0); break; } case INT8OID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); Value = (BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)) != 0); break; } case INT2OID:{ ASSERT(PQgetlength(Result, Row, Col) == 2); Value = (BufferRead16BE((const uint8*)PQgetvalue(Result, Row, Col)) != 0); break; } case INT4OID:{ ASSERT(PQgetlength(Result, Row, Col) == 4); Value = (BufferRead32BE((const uint8*)PQgetvalue(Result, Row, Col)) != 0); break; } case TEXTOID: case VARCHAROID:{ if(!ParseBoolean(&Value, PQgetvalue(Result, Row, Col))){ LOG_WARN("Failed to properly convert column (%d) %s from TEXT to BOOLEAN", Col, PQfname(Result, Col)); } break; } default:{ LOG_ERR("Column (%d) %s has OID %d which is not convertible to BOOLEAN", Col, PQfname(Result, Col), Type); break; } } } return Value; } static int GetResultInt(PGresult *Result, int Row, int Col){ int Value = 0; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT if(!ParseInteger(&Value, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to properly parse column (%d) %s as INT4", Col, PQfname(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT switch(Type){ case BOOLOID:{ ASSERT(PQgetlength(Result, Row, Col) == 1); Value = BufferRead8((const uint8*)PQgetvalue(Result, Row, Col)); break; } case INT8OID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); int64 Value64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)); if(Value64 < INT_MIN || Value64 > INT_MAX){ LOG_WARN("Lossy conversion of column (%d) %s from INT8 to INT4", Col, PQfname(Result, Col)); } Value = (int)Value64; break; } case INT2OID:{ ASSERT(PQgetlength(Result, Row, Col) == 2); Value = (int16)BufferRead16BE((const uint8*)PQgetvalue(Result, Row, Col)); break; } case INT4OID:{ ASSERT(PQgetlength(Result, Row, Col) == 4); Value = (int)BufferRead32BE((const uint8*)PQgetvalue(Result, Row, Col)); break; } case TEXTOID: case VARCHAROID:{ if(!ParseInteger(&Value, PQgetvalue(Result, Row, Col))){ LOG_WARN("Failed to properly convert column (%d) %s from TEXT to INT4", Col, PQfname(Result, Col)); } break; } default:{ LOG_ERR("Column (%d) %s has OID %d which is not convertible to INT4", Col, PQfname(Result, Col), Type); break; } } } return Value; } static const char *GetResultText(PGresult *Result, int Row, int Col){ const char *Text = ""; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT Text = PQgetvalue(Result, Row, Col); }else if(Format == 1){ // BINARY FORMAT switch(Type){ case TEXTOID: case VARCHAROID:{ Text = PQgetvalue(Result, Row, Col); break; } default:{ // IMPORTANT(fusion): There is no trivial way to convert whatever // value we received back to string. We'd either need to allocate // or modify the prototype of this function to accept an output // buffer. // The fact is, we shouldn't expect implicit conversions to work // when using the binary format, PERIOD. LOG_ERR("Column (%d) %s has OID %d which is not trivially convertible to TEXT", Col, PQfname(Result, Col), Type); break; } } } return Text; } static int GetResultByteA(PGresult *Result, int Row, int Col, uint8 *Buffer, int BufferSize){ int Size = -1; int Format = PQfformat(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT const char *String = PQgetvalue(Result, Row, Col); if(String[0] == '\\' && String[1] == 'x'){ Size = ParseHexString(Buffer, BufferSize, String + 2); }else{ LOG_ERR("Column (%d) %s (OID %d) doesn't contain a valid BYTEA literal", Col, PQfname(Result, Col), PQftype(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT Size = PQgetlength(Result, Row, Col); if(Size > 0 && Size <= BufferSize){ memcpy(Buffer, PQgetvalue(Result, Row, Col), Size); } } ASSERT(Size <= BufferSize); return Size; } #if 0 // NOTE(fusion): DO NOT REMOVE. It is currently not being used so it's switched // off to avoid compiler warnings. static int GetResultIPAddress(PGresult *Result, int Row, int Col){ int IPAddress = 0; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT if(!ParseIPAddress(&IPAddress, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to parse column (%d) %s as IPV4", Col, PQfname(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT switch(Type){ case INT8OID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); int64 IPAddress64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)); if(IPAddress64 < INT_MIN || IPAddress64 > INT_MAX){ LOG_WARN("Lossy conversion of column (%d) %s from INT8 to IPV4", Col, PQfname(Result, Col)); } IPAddress = (int)IPAddress64; break; } case INT4OID:{ ASSERT(PQgetlength(Result, Row, Col) == 4); IPAddress = (int)BufferRead32BE((const uint8*)PQgetvalue(Result, Row, Col)); break; } case TEXTOID: case VARCHAROID:{ if(!ParseIPAddress(&IPAddress, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to convert column (%d) %s from TEXT to IPV4", Col, PQfname(Result, Col)); } break; } case CIDROID: case INETOID:{ int Size = PQgetlength(Result, Row, Col); const uint8 *Data = (const uint8*)PQgetvalue(Result, Row, Col); if(Size >= 4){ int AddressFamily = (int)Data[0]; // Data[1]; // MaskBits // Data[2]; // IsCIDR int AddressSize = (int)Data[3]; if(AddressFamily == POSTGRESQL_AF_INET && AddressSize == 4 && Size >= 8){ IPAddress = (int)BufferRead32BE(Data + 4); }else{ LOG_ERR("CIDR/INET column (%d) %s doesn't contain IPV4 address", Col, PQfname(Result, Col)); } }else{ LOG_ERR("CIDR/INET column (%d) %s has unexpected binary format", Col, PQfname(Result, Col)); } break; } default:{ LOG_ERR("Column (%d) %s has OID %d which is not convertible to IPV4", Col, PQfname(Result, Col), Type); break; } } } return IPAddress; } #endif static bool ParseTimestamp(int *Dest, const char *String){ ASSERT(Dest != NULL && String != NULL); // TODO(fusion): I don't think this function exists on Windows but neither // are we running on Windows so does it even matter? There might be better // ways to properly parse this timestamp format. struct tm tm = {}; const char *Rem = strptime(String, "%Y-%m-%d %H:%M:%S", &tm); if(Rem == NULL){ LOG_ERR("Invalid timestamp format \"%s\"", String); return false; } // NOTE(fusion): Skip optional milliseconds/microseconds. if(Rem[0] == '.'){ Rem += 1; while(isdigit(Rem[0])){ Rem += 1; } } // NOTE(fusion): Parse optional timezone. int GMTOffset = 0; if((Rem[0] == '-' || Rem[0] == '+') && isdigit(Rem[1]) && isdigit(Rem[2])){ // NOTE(fusion): Hours. GMTOffset += ((Rem[1] - '0') * 10 + (Rem[2] - '0')) * 3600; // NOTE(fusion): Optional minutes. if(isdigit(Rem[3]) && isdigit(Rem[4])){ GMTOffset += ((Rem[3] - '0') * 10 + (Rem[4] - '0')) * 60; } if(Rem[0] == '+'){ GMTOffset = -GMTOffset; } } *Dest = (int)timegm(&tm) + GMTOffset; return true; } static int GetResultTimestamp(PGresult *Result, int Row, int Col){ int Timestamp = 0; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT if(!ParseTimestamp(&Timestamp, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to parse column (%d) %s as TIMESTAMP", Col, PQfname(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT switch(Type){ case INT8OID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); int64 Timestamp64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)); if(Timestamp64 < INT_MIN || Timestamp64 > INT_MAX){ LOG_WARN("Lossy conversion of column (%d) %s from INT8 to TIMESTAMP", Col, PQfname(Result, Col)); } Timestamp = (int)Timestamp64; break; } case INT4OID:{ ASSERT(PQgetlength(Result, Row, Col) == 4); Timestamp = (int)BufferRead32BE((const uint8*)PQgetvalue(Result, Row, Col)); break; } case TEXTOID: case VARCHAROID:{ if(!ParseTimestamp(&Timestamp, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to convert column (%d) %s from TEXT to TIMESTAMP", Col, PQfname(Result, Col)); } break; } case TIMESTAMPOID: case TIMESTAMPTZOID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); // NOTE(fusion): See `POSTGRESQL_EPOCH`. int64 PGTimestamp = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)); int64 Timestamp64 = ((PGTimestamp / 1000000) + POSTGRESQL_EPOCH); if(Timestamp64 < INT_MIN){ Timestamp = INT_MIN; }else if(Timestamp64 > INT_MAX){ Timestamp = INT_MAX; }else{ Timestamp = (int)Timestamp64; } break; } default:{ LOG_ERR("Column (%d) %s has OID %d which is not convertible to TIMESTAMP", Col, PQfname(Result, Col), Type); break; } } } return Timestamp; } static void SkipWhitespace(int *Position, const char *String){ while(isspace(String[*Position])){ *Position += 1; } } static bool SkipNextChar(int Ch, int *Position, const char *String){ ASSERT(Ch != 0); if(String[*Position] != Ch){ return false; } *Position += 1; return true; } static bool ReadNextNumber(int *Dest, int *Position, const char *String){ SkipWhitespace(Position, String); bool Negative = false; if(!isdigit(String[*Position])){ if(String[*Position] != '+' && String[*Position] != '-'){ return false; } if(!isdigit(String[*Position + 1])){ return false; } if(String[*Position] == '-'){ Negative = true; } *Position += 1; } int Number = 0; while(isdigit(String[*Position])){ Number = (Number * 10) + (String[*Position] - '0'); *Position += 1; } if(Negative){ Number = -Number; } *Dest = Number; return true; } static bool ReadNextWord(char *Dest, int DestCapacity, int *Position, const char *String){ ASSERT(DestCapacity > 0); SkipWhitespace(Position, String); if(!isalpha(String[*Position])){ return false; } int WritePos = 0; while(isalpha(String[*Position])){ if(WritePos < DestCapacity){ Dest[WritePos] = String[*Position]; WritePos += 1; } *Position += 1; } bool Result = (WritePos < DestCapacity); if(Result){ Dest[WritePos] = 0; }else{ Dest[DestCapacity - 1] = 0; } return Result; } static bool ParseInterval(int *Dest, const char *String){ // TODO(fusion): This is rather rudimentary but should work at least with // the values that the server is currently returning. I also assume this // is stable enough or it would break other client libraries as well. ASSERT(Dest != NULL && String != NULL); int Interval = 0; int Position = 0; bool Negate = false; while(true){ SkipWhitespace(&Position, String); if(String[Position] == 0){ break; } int Number; if(!ReadNextNumber(&Number, &Position, String)){ // "Expected number" return false; } if(SkipNextChar(':', &Position, String)){ int Minutes, Seconds; if(!ReadNextNumber(&Minutes, &Position, String) || !SkipNextChar(':', &Position, String) || !ReadNextNumber(&Seconds, &Position, String)){ // "Expected HH:MM:SS.FFFFFF" return false; } if(Minutes < 0 || Minutes > 59){ // "Expected minutes to be within [0, 59]" return false; } if(Seconds < 0 || Seconds > 59){ // "Expected seconds to be within [0, 59]" return false; } // NOTE(fusion): Parse microseconds but ignore it. if(SkipNextChar('.', &Position, String)){ int Frac; int PrevPosition = Position; if(!ReadNextNumber(&Frac, &Position, String)){ // "Expected fractional part" return false; } int FracDigits = Position - PrevPosition; if(FracDigits > 6){ // "Too many fractional digits" return false; } } Interval += (Number * 3600 + Minutes * 60 + Seconds); }else{ char Unit[16]; if(!ReadNextWord(Unit, sizeof(Unit), &Position, String)){ // "Expected unit" return false; } // NOTE(fusion): Remove ending 's' to allow for plurals. This probably // doesn't work for "centuries" or "millennia" but we can adjust later. // Not to mention that either a single century or millennium will already // cause an integer overflow. int UnitLength = strlen(Unit); if(UnitLength > 1 && Unit[UnitLength - 1] == 's'){ Unit[UnitLength - 1] = 0; } if(StringStartsWithCI("microsecond", Unit)){ Interval += Number / 1000000; }else if(StringStartsWithCI("millisecond", Unit)){ Interval += Number / 1000; }else if(StringStartsWithCI("second", Unit)){ Interval += Number; }else if(StringStartsWithCI("minute", Unit)){ Interval += Number * 60; }else if(StringStartsWithCI("hour", Unit)){ Interval += Number * 60 * 60; }else if(StringStartsWithCI("day", Unit)){ Interval += Number * 60 * 60 * 24; }else if(StringStartsWithCI("week", Unit)){ Interval += Number * 60 * 60 * 24 * 7; }else if(StringStartsWithCI("month", Unit)){ Interval += Number * 60 * 60 * 24 * 30; }else if(StringStartsWithCI("year", Unit)){ Interval += Number * 60 * 60 * 24 * 365; }else if(StringStartsWithCI("decade", Unit)){ Interval += Number * 60 * 60 * 24 * 365 * 10; }else if(StringStartsWithCI("century", Unit)){ Interval += Number * 60 * 60 * 24 * 365 * 100; }else if(StringStartsWithCI("millennium", Unit)){ Interval += Number * 60 * 60 * 24 * 365 * 1000; }else{ // "Invalid unit" return false; } } char Direction[8]; if(ReadNextWord(Direction, sizeof(Direction), &Position, String)){ if(!StringEqCI(Direction, "ago")){ // "Invalid interval direction" return false; } SkipWhitespace(&Position, String); if(String[Position] != 0){ // "Interval direction is expected only at the very end" return false; } Negate = true; } } if(Negate){ Interval = -Interval; } *Dest = Interval; return true; } static int GetResultInterval(PGresult *Result, int Row, int Col){ int Interval = 0; int Format = PQfformat(Result, Col); Oid Type = PQftype(Result, Col); ASSERT(Format == 0 || Format == 1); if(Format == 0){ // TEXT FORMAT if(!ParseInterval(&Interval, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to parse column (%d) %s as INTERVAL", Col, PQfname(Result, Col)); } }else if(Format == 1){ // BINARY FORMAT switch(Type){ case INT8OID:{ ASSERT(PQgetlength(Result, Row, Col) == 8); int64 Interval64 = (int64)BufferRead64BE((const uint8*)PQgetvalue(Result, Row, Col)); if(Interval64 < INT_MIN || Interval64 > INT_MAX){ LOG_WARN("Lossy conversion of column (%d) %s from INT8 to INTERVAL", Col, PQfname(Result, Col)); } Interval = (int)Interval64; break; } case INT4OID:{ ASSERT(PQgetlength(Result, Row, Col) == 4); Interval = (int)BufferRead32BE((const uint8*)PQgetvalue(Result, Row, Col)); break; } case TEXTOID: case VARCHAROID:{ if(!ParseInterval(&Interval, PQgetvalue(Result, Row, Col))){ LOG_ERR("Failed to convert column (%d) %s from TEXT to INTERVAL", Col, PQfname(Result, Col)); } break; } case INTERVALOID:{ ASSERT(PQgetlength(Result, Row, Col) == 16); const uint8 *Data = (const uint8*)PQgetvalue(Result, Row, Col); int64 Microseconds = (int64)BufferRead64BE(Data + 0); int Days = (int)BufferRead32BE(Data + 8); int Months = (int)BufferRead32BE(Data + 12); // NOTE(fusion): Split months into years for better precision. Using 30 days // months will reduce the number of year days to 360. int Years = Months / 12; Months = Months % 12; // NOTE(fusion): We just can't support the whole range of intervals // with a simple integer but the vast majority won't need more than // that. Anything else we'll just call "undefined behaviour" lol. int64 Interval64 = (Microseconds / 1000000) + ((int64)Days * 60 * 60 * 24) + ((int64)Months * 60 * 60 * 24 * 30) + ((int64)Years * 60 * 60 * 24 * 365); if(Interval64 < INT_MIN){ Interval = INT_MIN; }else if(Interval64 > INT_MAX){ Interval = INT_MAX; }else{ Interval = (int)Interval64; } break; } default:{ LOG_ERR("Column (%d) %s has OID %d which is not convertible to INTERVAL", Col, PQfname(Result, Col), Type); break; } } } return Interval; } // Other Helpers //============================================================================== static bool ExecInternal(TDatabase *Database, const char *Format, ...) ATTR_PRINTF(2, 3); static bool ExecInternal(TDatabase *Database, const char *Format, ...){ va_list ap; va_start(ap, Format); char Text[1024]; int Written = vsnprintf(Text, sizeof(Text), Format, ap); va_end(ap); if(Written >= (int)sizeof(Text)){ LOG_ERR("Query is too long"); return false; } PGresult *Result = PQexec(Database->Handle, Text); AutoResultClear ResultGuard(Result); bool Status = PQresultStatus(Result) == PGRES_COMMAND_OK || PQresultStatus(Result) == PGRES_TUPLES_OK; if(!Status){ char Preview[30]; StringBufCopyEllipsis(Preview, Text); LOG_ERR("Failed to execute query \"%s\": %s", Preview, PQerrorMessage(Database->Handle)); } return Status; } static bool GetSchemaVersion(TDatabase *Database, int *Version){ PGresult *Result = PQexec(Database->Handle, "SELECT Value FROM SchemaInfo WHERE Key = 'VERSION'"); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } if(PQntuples(Result) == 0){ LOG_ERR("Query returned no rows"); return false; } *Version = GetResultInt(Result, 0, 0); return true; } // Statement Cache //============================================================================== // NOTE(fusion): Prepared statements are stored server-side and only referenced // by name. They're not shared between sessions and are automatically cleaned up // when the connection is CLOSED or RESET. void EnsureStatementCache(TDatabase *Database){ ASSERT(Database != NULL); if(Database->CachedStatements == NULL){ ASSERT(g_Config.PostgreSQL.MaxCachedStatements > 0); Database->MaxCachedStatements = g_Config.PostgreSQL.MaxCachedStatements; if(Database->MaxCachedStatements > 9999){ LOG_WARN("There is currently a hard limit of 9999 max cached statements" " for PostgreSQL but it should be way more than needed because" " there are ABSOLUTELY NOT 9999 different queries."); Database->MaxCachedStatements = 9999; } Database->CachedStatements = (TCachedStatement*)calloc( Database->MaxCachedStatements, sizeof(TCachedStatement)); for(int i = 0; i < Database->MaxCachedStatements; i += 1){ if(!StringBufFormat(Database->CachedStatements[i].Name, "STMT%d", i)){ PANIC("Failed to format statement cache entry name for STMT%d", i); } } } } void DeleteStatementCache(TDatabase *Database){ ASSERT(Database != NULL); if(Database->CachedStatements != NULL){ ASSERT(Database->MaxCachedStatements > 0); for(int i = 0; i < Database->MaxCachedStatements; i += 1){ TCachedStatement *Entry = &Database->CachedStatements[i]; if(Entry->Text != NULL){ free(Entry->Text); Entry->LastUsed = 0; Entry->Hash = 0; Entry->Text = NULL; } } // NOTE(fusion): This function would usually be called along with `PQreset` or // `PQfinish` but it's probably a good idea to close all prepared statements if // the connection is still going. There is no libpq wrapper but we can execute // `DEALLOCATE ALL`. if(PQstatus(Database->Handle) == CONNECTION_OK){ if(!ExecInternal(Database, "DEALLOCATE ALL")){ LOG_WARN("Failed to close all prepared statements"); } } free(Database->CachedStatements); Database->MaxCachedStatements = 0; Database->CachedStatements = NULL; } } // IMPORTANT(fusion): Even though it is possible to declare parameter types // with OIDs, it is simpler to use explicit casts such as `$1::INTEGER` to // enforce types. It also makes so all relevant information about the query // is packed into `Text` so we don't need to track anything else to ensure // queries with with different types are kept separate. // Keep in mind that using the same parameter multiple times with different // explicit type casts will make so only the first one is used when inferring // the actual parameter type. Others are considered casts from it. // For example, `SELECT $1::TIMESTAMP, $1::TIMESTAMPTZ` will make so $1 is // inferred as `TIMESTAMP`, so `$1::TIMESTAMPTZ` will actually be a cast from // `TIMESTAMP` into `TIMESTAMPTZ`, which will most likely yield unexpected // results. const char *PrepareQuery(TDatabase *Database, const char *Text){ ASSERT(Database != NULL); EnsureStatementCache(Database); TCachedStatement *Stmt = NULL; int LeastRecentlyUsed = 0; int LeastRecentlyUsedTime = Database->CachedStatements[0].LastUsed; uint32 Hash = HashString(Text); for(int i = 0; i < Database->MaxCachedStatements; i += 1){ TCachedStatement *Entry = &Database->CachedStatements[i]; if(Entry->LastUsed < LeastRecentlyUsedTime){ LeastRecentlyUsed = i; LeastRecentlyUsedTime = Entry->LastUsed; } if(Entry->Text != NULL && Entry->Hash == Hash){ if(StringEq(Entry->Text, Text)){ Stmt = Entry; Entry->LastUsed = GetMonotonicUptime(); break; } } } if(Stmt == NULL){ Stmt = &Database->CachedStatements[LeastRecentlyUsed]; if(Stmt->Text != NULL){ PGresult *Result = PQclosePrepared(Database->Handle, Stmt->Name); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ char OldPreview[30]; StringBufCopyEllipsis(OldPreview, Stmt->Text); LOG_ERR("Failed to close prepared query \"%s\": %s", OldPreview, PQerrorMessage(Database->Handle)); } free(Stmt->Text); } { PGresult *Result = PQprepare(Database->Handle, Stmt->Name, Text, 0, NULL); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ char NewPreview[30]; StringBufCopyEllipsis(NewPreview, Text); LOG_ERR("Failed to prepare query \"%s\": %s", NewPreview, PQerrorMessage(Database->Handle)); return NULL; } } Stmt->LastUsed = GetMonotonicUptime(); Stmt->Hash = Hash; Stmt->Text = strdup(Text); ASSERT(Stmt->Text != NULL); #if 1 // DEBUG_STATEMENT_CACHE { char Preview[30]; StringBufCopyEllipsis(Preview, Text); LOG("New statement cached: \"%s\"", Preview); PGresult *Result = PQdescribePrepared(Database->Handle, Stmt->Name); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) == PGRES_COMMAND_OK){ LOG(" PARAM OIDs:"); for(int i = 0; i < PQnparams(Result); i += 1){ LOG(" $%d: %d", i, PQparamtype(Result, i)); } LOG(" RESULT OIDs:"); for(int i = 0; i < PQnfields(Result); i += 1){ LOG(" (%d) %s: %d", i, PQfname(Result, i), PQftype(Result, i)); } } } #endif } return Stmt->Name; } // TransactionScope //============================================================================== TransactionScope::TransactionScope(const char *Context){ m_Context = (Context != NULL ? Context : "NOCONTEXT"); m_Database = NULL; } TransactionScope::~TransactionScope(void){ if(m_Database != NULL){ if(!ExecInternal(m_Database, "ROLLBACK")){ LOG_ERR("Failed to rollback transaction (%s)", m_Context); } m_Database = NULL; } } bool TransactionScope::Begin(TDatabase *Database){ if(m_Database != NULL){ LOG_ERR("Transaction (%s) already running", m_Context); return false; } if(!ExecInternal(Database, "BEGIN")){ LOG_ERR("Failed to begin transaction (%s)", m_Context); return false; } m_Database = Database; return true; } bool TransactionScope::Commit(void){ if(m_Database == NULL){ LOG_ERR("Transaction (%s) not running", m_Context); return false; } if(!ExecInternal(m_Database, "COMMIT")){ LOG_ERR("Failed to commit transaction (%s)", m_Context); return false; } m_Database = NULL; return true; } // Database Management //============================================================================== void DatabaseClose(TDatabase *Database){ if(Database != NULL){ if(Database->Handle != NULL){ PQfinish(Database->Handle); Database->Handle = NULL; } free(Database); } } TDatabase *DatabaseOpen(void){ const char *Keys[] = { "host", "port", "dbname", "user", "password", "connect_timeout", "application_name", "sslmode", "sslrootcert", "client_encoding", NULL, // sentinel }; const char *Values[] = { g_Config.PostgreSQL.Host, g_Config.PostgreSQL.Port, g_Config.PostgreSQL.DBName, g_Config.PostgreSQL.User, g_Config.PostgreSQL.Password, g_Config.PostgreSQL.ConnectTimeout, g_Config.PostgreSQL.ApplicationName, g_Config.PostgreSQL.SSLMode, g_Config.PostgreSQL.SSLRootCert, "UTF8", NULL, // sentinel }; TDatabase *Database = (TDatabase*)calloc(1, sizeof(TDatabase)); Database->Handle = PQconnectdbParams(Keys, Values, 0); if(Database->Handle == NULL){ LOG_ERR("Failed to allocate database connection"); DatabaseClose(Database); return NULL; } if(PQstatus(Database->Handle) != CONNECTION_OK){ LOG_ERR("Failed to establish connection: %s", PQerrorMessage(Database->Handle)); DatabaseClose(Database); return NULL; } int SchemaVersion; if(!GetSchemaVersion(Database, &SchemaVersion)){ LOG_ERR("Failed to retrieve schema version..." " Database schema may not have been initialized"); DatabaseClose(Database); return NULL; } if(SchemaVersion != POSTGRESQL_SCHEMA_VERSION){ LOG_ERR("Schema version MISMATCH (expected %d, got %d)", POSTGRESQL_SCHEMA_VERSION, SchemaVersion); DatabaseClose(Database); return NULL; } #if 0 { // TODO(fusion): REMOVE. This is for testing query input/output, to make sure // they're consitent across different formats (text/binary). const char *Stmt = PrepareQuery(Database, "SELECT $1::INTERVAL, $2::INTERVAL"); ASSERT(Stmt != NULL); for(int i = 0; i <= 1; i += 1) for(int j = 0; j <= 1; j += 1){ LOG("TEST (%d, %d)", i, j); ParamBuffer Params; ParamBegin(&Params, 2, i); ParamInterval(&Params, 86400 + 3600); ParamInterval(&Params, - 86400 * 4 + 7 * 3600); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, j); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) == PGRES_TUPLES_OK){ LOG("0: %d", GetResultInterval(Result, 0, 0)); LOG("1: %d", GetResultInterval(Result, 0, 1)); }else{ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); } } DatabaseClose(Database); return NULL; } #endif return Database; } bool DatabaseCheckpoint(TDatabase *Database){ ASSERT(Database != NULL); bool Result = true; if(PQstatus(Database->Handle) != CONNECTION_OK){ DeleteStatementCache(Database); PQreset(Database->Handle); Result = (PQstatus(Database->Handle) == CONNECTION_OK); } return Result; } int DatabaseMaxConcurrency(void){ return INT_MAX; } // Primary Tables //============================================================================== bool GetWorldID(TDatabase *Database, const char *World, int *WorldID){ ASSERT(Database != NULL && World != NULL && WorldID != NULL); const char *Stmt = PrepareQuery(Database, "SELECT WorldID FROM Worlds WHERE Name = $1::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } // TODO(fusion): In this specific case it doesn't make a difference but // we'll probably need some helper struct to organize query parameters. // It would have an internal buffer which would be used as an arena. //ParamBuffer Params = {}; //ParamBegin(&Params, 1, 1); //ParamText(&Params, World); //PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, // Params.Values, Params.Lengths, Params.Formats, 1); const char *ParamValues[] = { World }; PGresult *Result = PQexecPrepared(Database->Handle, Stmt, 1, ParamValues, NULL, NULL, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *WorldID = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool GetWorlds(TDatabase *Database, DynamicArray *Worlds){ ASSERT(Database != NULL && Worlds != NULL); const char *Stmt = PrepareQuery(Database, "WITH N (WorldID, NumPlayers) AS (" "SELECT WorldID, COUNT(*) FROM OnlineCharacters GROUP BY WorldID" ")" " SELECT W.Name, W.Type, COALESCE(N.NumPlayers, 0), W.MaxPlayers," " W.OnlineRecord, W.OnlineRecordTimestamp" " FROM Worlds AS W" " LEFT JOIN N ON W.WorldID = N.WorldID"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } PGresult *Result = PQexecPrepared(Database->Handle, Stmt, 0, NULL, NULL, NULL, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TWorld World = {}; StringBufCopy(World.Name, GetResultText(Result, Row, 0)); World.Type = GetResultInt(Result, Row, 1); World.NumPlayers = GetResultInt(Result, Row, 2); World.MaxPlayers = GetResultInt(Result, Row, 3); World.OnlineRecord = GetResultInt(Result, Row, 4); World.OnlineRecordTimestamp = GetResultTimestamp(Result, Row, 5); Worlds->Push(World); } return true; } bool GetWorldConfig(TDatabase *Database, int WorldID, TWorldConfig *WorldConfig){ ASSERT(Database != NULL && WorldConfig != NULL); const char *Stmt = PrepareQuery(Database, "SELECT WorldID, Type, RebootTime, Host, Port, MaxPlayers," " PremiumPlayerBuffer, MaxNewbies, PremiumNewbieBuffer" " FROM Worlds WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } // TODO(fusion): We probably need a way to differentiate a failure from an // empty result set. memset(WorldConfig, 0, sizeof(TWorldConfig)); if(PQntuples(Result) > 0){ WorldConfig->WorldID = GetResultInt(Result, 0, 0); WorldConfig->Type = GetResultInt(Result, 0, 1); WorldConfig->RebootTime = GetResultInt(Result, 0, 2); StringBufCopy(WorldConfig->HostName, GetResultText(Result, 0, 3)); WorldConfig->Port = GetResultInt(Result, 0, 4); WorldConfig->MaxPlayers = GetResultInt(Result, 0, 5); WorldConfig->PremiumPlayerBuffer = GetResultInt(Result, 0, 6); WorldConfig->MaxNewbies = GetResultInt(Result, 0, 7); WorldConfig->PremiumNewbieBuffer = GetResultInt(Result, 0, 8); } return true; } bool AccountExists(TDatabase *Database, int AccountID, const char *Email, bool *Exists){ ASSERT(Database != NULL && Email != NULL && Exists != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts" " WHERE AccountID = $1::INTEGER OR Email = $2::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, AccountID); ParamText(&Params, Email); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Exists = (PQntuples(Result) > 0); return true; } bool AccountNumberExists(TDatabase *Database, int AccountID, bool *Exists){ ASSERT(Database != NULL && Exists != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts WHERE AccountID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Exists = (PQntuples(Result) > 0); return true; } bool AccountEmailExists(TDatabase *Database, const char *Email, bool *Exists){ ASSERT(Database != NULL && Email != NULL && Exists != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Accounts WHERE Email = $1::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamText(&Params, Email); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Exists = (PQntuples(Result) > 0); return true; } bool CreateAccount(TDatabase *Database, int AccountID, const char *Email, const uint8 *Auth, int AuthSize){ ASSERT(Database != NULL && Email != NULL && Auth != NULL && AuthSize > 0); const char *Stmt = PrepareQuery(Database, "INSERT INTO Accounts (AccountID, Email, Auth)" " VALUES ($1::INTEGER, $2::TEXT, $3::BYTEA)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, AccountID); ParamText(&Params, Email); ParamByteA(&Params, Auth, AuthSize); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetAccountData(TDatabase *Database, int AccountID, TAccount *Account){ ASSERT(Database != NULL && Account != NULL); const char *Stmt = PrepareQuery(Database, "SELECT AccountID, Email, Auth," " GREATEST(PremiumEnd - CURRENT_TIMESTAMP, '0')," " PendingPremiumDays, Deleted" " FROM Accounts WHERE AccountID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } memset(Account, 0, sizeof(TAccount)); if(PQntuples(Result) > 0){ uint8 Auth[sizeof(Account->Auth)]; Account->AccountID = GetResultInt(Result, 0, 0); StringBufCopy(Account->Email, GetResultText(Result, 0, 1)); if(GetResultByteA(Result, 0, 2, Auth, sizeof(Auth)) == sizeof(Auth)){ memcpy(Account->Auth, Auth, sizeof(Auth)); } Account->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 3)); Account->PendingPremiumDays = GetResultInt(Result, 0, 4); Account->Deleted = GetResultBool(Result, 0, 5); } return true; } bool GetAccountOnlineCharacters(TDatabase *Database, int AccountID, int *OnlineCharacters){ ASSERT(Database != NULL && OnlineCharacters != NULL); const char *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM Characters" " WHERE AccountID = $1::INTEGER AND IsOnline != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *OnlineCharacters = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool IsCharacterOnline(TDatabase *Database, int CharacterID, bool *Online){ ASSERT(Database != NULL && Online != NULL); const char *Stmt = PrepareQuery(Database, "SELECT IsOnline FROM Characters WHERE CharacterID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Online = (PQntuples(Result) > 0 && GetResultInt(Result, 0, 0) != 0); return true; } bool ActivatePendingPremiumDays(TDatabase *Database, int AccountID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "UPDATE Accounts" " SET PremiumEnd = GREATEST(PremiumEnd, CURRENT_TIMESTAMP)" " + MAKE_INTERVAL(days => PendingPremiumDays)," " PendingPremiumDays = 0" " WHERE AccountID = $1::INTEGER AND PendingPremiumDays > 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetCharacterEndpoints(TDatabase *Database, int AccountID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); const char *Stmt = PrepareQuery(Database, "SELECT C.Name, W.Name, W.Host, W.Port" " FROM Characters AS C" " INNER JOIN Worlds AS W ON W.WorldID = C.WorldID" " WHERE C.AccountID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TCharacterEndpoint Character = {}; StringBufCopy(Character.Name, GetResultText(Result, Row, 0)); StringBufCopy(Character.WorldName, GetResultText(Result, Row, 1)); StringBufCopy(Character.WorldHost, GetResultText(Result, Row, 2)); Character.WorldPort = GetResultInt(Result, Row, 3); Characters->Push(Character); } return true; } bool GetCharacterSummaries(TDatabase *Database, int AccountID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); const char *Stmt = PrepareQuery(Database, "SELECT C.Name, W.Name, C.Level, C.Profession, C.IsOnline, C.Deleted" " FROM Characters AS C" " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID" " WHERE C.AccountID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TCharacterSummary Character = {}; StringBufCopy(Character.Name, GetResultText(Result, Row, 0)); StringBufCopy(Character.World, GetResultText(Result, Row, 1)); Character.Level = GetResultInt(Result, Row, 2); StringBufCopy(Character.Profession, GetResultText(Result, Row, 3)); Character.Online = GetResultBool(Result, Row, 4); Character.Deleted = GetResultBool(Result, Row, 5);; Characters->Push(Character); } return true; } bool CharacterNameExists(TDatabase *Database, const char *Name, bool *Exists){ ASSERT(Database != NULL && Exists != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Characters WHERE Name = $1::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamText(&Params, Name); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Exists = (PQntuples(Result) > 0); return true; } bool CreateCharacter(TDatabase *Database, int WorldID, int AccountID, const char *Name, int Sex){ ASSERT(Database != NULL && Name != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Characters (WorldID, AccountID, Name, Sex)" " VALUES ($1::INTEGER, $2::INTEGER, $3::TEXT, $4::INTEGER)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamInt(&Params, AccountID); ParamText(&Params, Name); ParamInt(&Params, Sex); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetCharacterID(TDatabase *Database, int WorldID, const char *CharacterName, int *CharacterID){ ASSERT(Database != NULL && CharacterName != NULL && CharacterID != NULL); const char *Stmt = PrepareQuery(Database, "SELECT CharacterID FROM Characters" " WHERE WorldID = $1::INTEGER AND Name = $2::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamText(&Params, CharacterName); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *CharacterID = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool GetCharacterLoginData(TDatabase *Database, const char *CharacterName, TCharacterLoginData *Character){ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL); const char *Stmt = PrepareQuery(Database, "SELECT WorldID, CharacterID, AccountID, Name," " Sex, Guild, Rank, Title, Deleted" " FROM Characters WHERE Name = $1::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamText(&Params, CharacterName); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } memset(Character, 0, sizeof(TCharacterLoginData)); if(PQntuples(Result) > 0){ Character->WorldID = GetResultInt(Result, 0, 0); Character->CharacterID = GetResultInt(Result, 0, 1); Character->AccountID = GetResultInt(Result, 0, 2); StringBufCopy(Character->Name, GetResultText(Result, 0, 3)); Character->Sex = GetResultInt(Result, 0, 4); StringBufCopy(Character->Guild, GetResultText(Result, 0, 5)); StringBufCopy(Character->Rank, GetResultText(Result, 0, 6)); StringBufCopy(Character->Title, GetResultText(Result, 0, 7)); Character->Deleted = GetResultBool(Result, 0, 8); } return true; } bool GetCharacterProfile(TDatabase *Database, const char *CharacterName, TCharacterProfile *Character){ ASSERT(Database != NULL && CharacterName != NULL && Character != NULL); const char *Stmt = PrepareQuery(Database, "SELECT C.Name, W.Name, C.Sex, C.Guild, C.Rank, C.Title, C.Level," " C.Profession, C.Residence, C.LastLoginTime, C.IsOnline," " C.Deleted, GREATEST(A.PremiumEnd - CURRENT_TIMESTAMP, '0')" " FROM Characters AS C" " LEFT JOIN Worlds AS W ON W.WorldID = C.WorldID" " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID" " LEFT JOIN CharacterRights AS R" " ON R.CharacterID = C.CharacterID" " AND R.Name = 'NO_STATISTICS'" " WHERE C.Name = $1::TEXT AND R.Name IS NULL"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamText(&Params, CharacterName); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } memset(Character, 0, sizeof(TCharacterProfile)); if(PQntuples(Result) > 0){ StringBufCopy(Character->Name, GetResultText(Result, 0, 0)); StringBufCopy(Character->World, GetResultText(Result, 0, 1)); Character->Sex = GetResultInt(Result, 0, 2); StringBufCopy(Character->Guild, GetResultText(Result, 0, 3)); StringBufCopy(Character->Rank, GetResultText(Result, 0, 4)); StringBufCopy(Character->Title, GetResultText(Result, 0, 5)); Character->Level = GetResultInt(Result, 0, 6); StringBufCopy(Character->Profession, GetResultText(Result, 0, 7)); StringBufCopy(Character->Residence, GetResultText(Result, 0, 8)); Character->LastLogin = GetResultTimestamp(Result, 0, 9); Character->Online = GetResultBool(Result, 0, 10); Character->Deleted = GetResultBool(Result, 0, 11); Character->PremiumDays = RoundSecondsToDays(GetResultInterval(Result, 0, 12)); } return true; } bool GetCharacterRight(TDatabase *Database, int CharacterID, const char *Right, bool *HasRight){ ASSERT(Database != NULL && Right != NULL && HasRight != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM CharacterRights" " WHERE CharacterID = $1::INTEGER AND Name = $2::TEXT"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, CharacterID); ParamText(&Params, Right); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *HasRight = (PQntuples(Result) > 0); return true; } bool GetCharacterRights(TDatabase *Database, int CharacterID, DynamicArray *Rights){ ASSERT(Database != NULL && Rights != NULL); const char *Stmt = PrepareQuery(Database, "SELECT Name FROM CharacterRights WHERE CharacterID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TCharacterRight Right = {}; StringBufCopy(Right.Name, GetResultText(Result, Row, 0)); Rights->Push(Right); } return true; } bool GetGuildLeaderStatus(TDatabase *Database, int WorldID, int CharacterID, bool *GuildLeader){ ASSERT(Database != NULL && GuildLeader != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. const char *Stmt = PrepareQuery(Database, "SELECT Guild, Rank FROM Characters" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *GuildLeader = false; if(PQntuples(Result) > 0){ const char *Guild = GetResultText(Result, 0, 0); const char *Rank = GetResultText(Result, 0, 1); if(Guild != NULL && !StringEmpty(Guild) && Rank != NULL && StringEqCI(Rank, "Leader")){ *GuildLeader = true; } } return false; } bool IncrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. const char *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = IsOnline + 1" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool DecrementIsOnline(TDatabase *Database, int WorldID, int CharacterID){ ASSERT(Database != NULL); // NOTE(fusion): A character is uniquely identified by its id. The world id // check is purely to avoid a world from modifying a character from another // world. const char *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = IsOnline - 1" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool ClearIsOnline(TDatabase *Database, int WorldID, int *NumAffectedCharacters){ ASSERT(Database != NULL && NumAffectedCharacters != NULL); const char *Stmt = PrepareQuery(Database, "UPDATE Characters SET IsOnline = 0" " WHERE WorldID = $1::INTEGER AND IsOnline != 0"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *NumAffectedCharacters = ResultAffectedRows(Result); return true; } bool LogoutCharacter(TDatabase *Database, int WorldID, int CharacterID, int Level, const char *Profession, const char *Residence, int LastLoginTime, int TutorActivities){ ASSERT(Database != NULL && Profession != NULL && Residence != NULL); const char *Stmt = PrepareQuery(Database, "UPDATE Characters" " SET Level = $3::INTEGER," " Profession = $4::TEXT," " Residence = $5::TEXT," " LastLoginTime = $6::TIMESTAMPTZ," " TutorActivities = $7::INTEGER," " IsOnline = IsOnline - 1" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 7, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); ParamInt(&Params, Level); ParamText(&Params, Profession); ParamText(&Params, Residence); ParamTimestamp(&Params, LastLoginTime); ParamInt(&Params, TutorActivities); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetCharacterIndexEntries(TDatabase *Database, int WorldID, int MinimumCharacterID, int MaxEntries, int *NumEntries, TCharacterIndexEntry *Entries){ ASSERT(Database != NULL && MaxEntries > 0 && NumEntries != NULL && Entries != NULL); const char *Stmt = PrepareQuery(Database, "SELECT CharacterID, Name FROM Characters" " WHERE WorldID = $1::INTEGER AND CharacterID >= $2::INTEGER" " ORDER BY CharacterID ASC LIMIT $3::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, WorldID); ParamInt(&Params, MinimumCharacterID); ParamInt(&Params, MaxEntries); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); if(NumRows > MaxEntries){ LOG_WARN("Query returned too many rows (expected %d, got %d)", MaxEntries, NumRows); NumRows = MaxEntries; } for(int Row = 0; Row < NumRows; Row += 1){ Entries[Row].CharacterID = GetResultInt(Result, Row, 0); StringBufCopy(Entries[Row].Name, GetResultText(Result, Row, 1)); } *NumEntries = NumRows; return true; } bool InsertCharacterDeath(TDatabase *Database, int WorldID, int CharacterID, int Level, int OffenderID, const char *Remark, bool Unjustified, int Timestamp){ ASSERT(Database != NULL && Remark != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. const char *Stmt = PrepareQuery(Database, "INSERT INTO CharacterDeaths (CharacterID, Level," " OffenderID, Remark, Unjustified, Timestamp)" " SELECT $2::INTEGER, $3::INTEGER, $4::INTEGER," "$5::TEXT, $6::BOOLEAN, $7::TIMESTAMPTZ" " FROM Characters" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 7, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); ParamInt(&Params, Level); ParamInt(&Params, OffenderID); ParamText(&Params, Remark); ParamBool(&Params, Unjustified); ParamTimestamp(&Params, Timestamp); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool InsertBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. // NOTE(fusion): Use the `DO NOTHING` conflict resolution to make duplicate // row errors appear as successful insertions. const char *Stmt = PrepareQuery(Database, "INSERT INTO Buddies (WorldID, AccountID, BuddyID)" " SELECT $1::INTEGER, $2::INTEGER, $3::INTEGER FROM Characters" " WHERE WorldID = $1::INTEGER AND CharacterID = $3::INTEGER" " ON CONFLICT DO NOTHING"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, WorldID); ParamInt(&Params, AccountID); ParamInt(&Params, BuddyID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool DeleteBuddy(TDatabase *Database, int WorldID, int AccountID, int BuddyID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "DELETE FROM Buddies" " WHERE WorldID = $1::INTEGER" " AND AccountID = $2::INTEGER" " AND BuddyID = $3::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, WorldID); ParamInt(&Params, AccountID); ParamInt(&Params, BuddyID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetBuddies(TDatabase *Database, int WorldID, int AccountID, DynamicArray *Buddies){ ASSERT(Database != NULL && Buddies != NULL); const char *Stmt = PrepareQuery(Database, "SELECT B.BuddyID, C.Name" " FROM Buddies AS B" " INNER JOIN Characters AS C" " ON C.WorldID = B.WorldID AND C.CharacterID = B.BuddyID" " WHERE B.WorldID = $1::INTEGER AND B.AccountID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TAccountBuddy Buddy = {}; Buddy.CharacterID = GetResultInt(Result, Row, 0); StringBufCopy(Buddy.Name, GetResultText(Result, Row, 1)); Buddies->Push(Buddy); } return true; } bool GetWorldInvitation(TDatabase *Database, int WorldID, int CharacterID, bool *Invited){ ASSERT(Database != NULL && Invited != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM WorldInvitations" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Invited = (PQntuples(Result) > 0); return true; } bool InsertLoginAttempt(TDatabase *Database, int AccountID, int IPAddress, bool Failed){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO LoginAttempts (AccountID, IPAddress, Timestamp, Failed)" " VALUES ($1::INTEGER, $2::INET, CURRENT_TIMESTAMP, $3::BOOLEAN)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, AccountID); ParamIPAddress(&Params, IPAddress); ParamBool(&Params, Failed); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetAccountFailedLoginAttempts(TDatabase *Database, int AccountID, int TimeWindow, int *FailedAttempts){ ASSERT(Database != NULL && FailedAttempts != NULL); const char *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM LoginAttempts" " WHERE AccountID = $1::INTEGER" " AND (CURRENT_TIMESTAMP - Timestamp) <= $2::INTERVAL" " AND Failed"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, AccountID); ParamInterval(&Params, TimeWindow); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *FailedAttempts = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool GetIPAddressFailedLoginAttempts(TDatabase *Database, int IPAddress, int TimeWindow, int *FailedAttempts){ ASSERT(Database != NULL && FailedAttempts != NULL); const char *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM LoginAttempts" " WHERE IPAddress = $1::INET" " AND (CURRENT_TIMESTAMP - Timestamp) <= $2::INTERVAL" " AND Failed"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamIPAddress(&Params, IPAddress); ParamInterval(&Params, TimeWindow); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *FailedAttempts = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } // House Tables //============================================================================== bool FinishHouseAuctions(TDatabase *Database, int WorldID, DynamicArray *Auctions){ ASSERT(Database != NULL && Auctions != NULL); // TODO(fusion): If the application crashes while processing finished auctions, // non processed auctions will be lost but with no other side-effects. It could // be an inconvenience but it's not a big problem. const char *Stmt = PrepareQuery(Database, "DELETE FROM HouseAuctions" " WHERE WorldID = $1::INTEGER" " AND FinishTime IS NOT NULL" " AND FinishTime <= CURRENT_TIMESTAMP" " RETURNING HouseID, BidderID, BidAmount, FinishTime," " (SELECT Name FROM Characters WHERE CharacterID = BidderID)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ THouseAuction Auction = {}; Auction.HouseID = GetResultInt(Result, Row, 0); Auction.BidderID = GetResultInt(Result, Row, 1); Auction.BidAmount = GetResultInt(Result, Row, 2); Auction.FinishTime = GetResultTimestamp(Result, Row, 3); StringBufCopy(Auction.BidderName, GetResultText(Result, Row, 4)); Auctions->Push(Auction); } return true; } bool FinishHouseTransfers(TDatabase *Database, int WorldID, DynamicArray *Transfers){ ASSERT(Database != NULL && Transfers != NULL); // TODO(fusion): Same as `FinishHouseAuctions` but with house transfers. const char *Stmt = PrepareQuery(Database, "DELETE FROM HouseTransfers" " WHERE WorldID = $1::INTEGER" " RETURNING HouseID, NewOwnerID, Price," " (SELECT Name FROM Characters WHERE CharacterID = NewOwnerID)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ THouseTransfer Transfer = {}; Transfer.HouseID = GetResultInt(Result, Row, 0); Transfer.NewOwnerID = GetResultInt(Result, Row, 1); Transfer.Price = GetResultInt(Result, Row, 2); StringBufCopy(Transfer.NewOwnerName, GetResultText(Result, Row, 4)); Transfers->Push(Transfer); } return true; } bool GetFreeAccountEvictions(TDatabase *Database, int WorldID, DynamicArray *Evictions){ ASSERT(Database != NULL && Evictions != NULL); const char *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " LEFT JOIN Accounts AS A ON A.AccountID = C.AccountID" " WHERE O.WorldID = $1::INTEGER" " AND (A.PremiumEnd IS NULL OR A.PremiumEnd < CURRENT_TIMESTAMP)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ THouseEviction Eviction = {}; Eviction.HouseID = GetResultInt(Result, Row, 0); Eviction.OwnerID = GetResultInt(Result, Row, 1); Evictions->Push(Eviction); } return true; } bool GetDeletedCharacterEvictions(TDatabase *Database, int WorldID, DynamicArray *Evictions){ ASSERT(Database != NULL && Evictions != NULL); const char *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " WHERE O.WorldID = $1::INTEGER" " AND (C.CharacterID IS NULL OR C.Deleted)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ THouseEviction Eviction = {}; Eviction.HouseID = GetResultInt(Result, Row, 0); Eviction.OwnerID = GetResultInt(Result, Row, 1); Evictions->Push(Eviction); } return true; } bool InsertHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO HouseOwners (WorldID, HouseID, OwnerID, PaidUntil)" " VALUES ($1::INTEGER, $2::INTEGER, $3::INTEGER, $4::TIMESTAMPTZ)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamInt(&Params, HouseID); ParamInt(&Params, OwnerID); ParamTimestamp(&Params, PaidUntil); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool UpdateHouseOwner(TDatabase *Database, int WorldID, int HouseID, int OwnerID, int PaidUntil){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "UPDATE HouseOwners" " SET OwnerID = $3::INTEGER, PaidUntil = $4::TIMESTAMPTZ" " WHERE WorldID = $1::INTEGER AND HouseID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamInt(&Params, HouseID); ParamInt(&Params, OwnerID); ParamTimestamp(&Params, PaidUntil); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool DeleteHouseOwner(TDatabase *Database, int WorldID, int HouseID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "DELETE FROM HouseOwners" " WHERE WorldID = $1::INTEGER AND HouseID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, HouseID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool GetHouseOwners(TDatabase *Database, int WorldID, DynamicArray *Owners){ ASSERT(Database != NULL && Owners != NULL); const char *Stmt = PrepareQuery(Database, "SELECT O.HouseID, O.OwnerID, C.Name, O.PaidUntil" " FROM HouseOwners AS O" " LEFT JOIN Characters AS C ON C.CharacterID = O.OwnerID" " WHERE O.WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ THouseOwner Owner = {}; Owner.HouseID = GetResultInt(Result, Row, 0); Owner.OwnerID = GetResultInt(Result, Row, 1); StringBufCopy(Owner.OwnerName, GetResultText(Result, Row, 2)); Owner.PaidUntil = GetResultTimestamp(Result, Row, 3); Owners->Push(Owner); } return true; } bool GetHouseAuctions(TDatabase *Database, int WorldID, DynamicArray *Auctions){ ASSERT(Database != NULL && Auctions != NULL); const char *Stmt = PrepareQuery(Database, "SELECT HouseID FROM HouseAuctions WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ Auctions->Push(GetResultInt(Result, Row, 0)); } return true; } bool StartHouseAuction(TDatabase *Database, int WorldID, int HouseID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO HouseAuctions (WorldID, HouseID)" " VALUES ($1::INTEGER, $2::INTEGER)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, HouseID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool DeleteHouses(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "DELETE FROM Houses WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool InsertHouses(TDatabase *Database, int WorldID, int NumHouses, THouse *Houses){ ASSERT(Database != NULL && NumHouses > 0 && Houses != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Houses (WorldID, HouseID, Name, Rent, Description," " Size, PositionX, PositionY, PositionZ, Town, GuildHouse)" " VALUES ($1::INTEGER, $2::INTEGER, $3::TEXT, $4::INTEGER, $5::TEXT," " $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, $10::TEXT," " $11::BOOLEAN)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } // TODO(fusion): We could use pipelining to speed up multiple inserts but I'm // not sure the complexity increase would warrant any speed-ups. ParamBuffer Params = {}; for(int i = 0; i < NumHouses; i += 1){ ParamBegin(&Params, 11, 1); ParamInt(&Params, WorldID); ParamInt(&Params, Houses[i].HouseID); ParamText(&Params, Houses[i].Name); ParamInt(&Params, Houses[i].Rent); ParamText(&Params, Houses[i].Description); ParamInt(&Params, Houses[i].Size); ParamInt(&Params, Houses[i].PositionX); ParamInt(&Params, Houses[i].PositionY); ParamInt(&Params, Houses[i].PositionZ); ParamText(&Params, Houses[i].Town); ParamBool(&Params, Houses[i].GuildHouse); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to insert house %d: %s", Houses[i].HouseID, PQerrorMessage(Database->Handle)); return false; } } return true; } bool ExcludeFromAuctions(TDatabase *Database, int WorldID, int CharacterID, int Duration, int BanishmentID){ ASSERT(Database != NULL); // NOTE(fusion): Same as `DecrementIsOnline`. const char *Stmt = PrepareQuery(Database, "INSERT INTO HouseAuctionExclusions (CharacterID, Issued, Until, BanishmentID)" " SELECT $2::INTEGER, CURRENT_TIMESTAMP, (CURRENT_TIMESTAMP + $3::INTERVAL), $4::INTEGER" " FROM Characters" " WHERE WorldID = $1::INTEGER AND CharacterID = $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamInt(&Params, CharacterID); ParamInterval(&Params, Duration); ParamInt(&Params, BanishmentID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } // Banishment Tables //============================================================================== bool IsCharacterNamelocked(TDatabase *Database, int CharacterID, bool *Namelocked){ ASSERT(Database != NULL && Namelocked != NULL); TNamelockStatus Status; if(!GetNamelockStatus(Database, CharacterID, &Status)){ return false; } *Namelocked = Status.Namelocked && !Status.Approved; return true; } bool GetNamelockStatus(TDatabase *Database, int CharacterID, TNamelockStatus *Status){ ASSERT(Database != NULL && Status != NULL); const char *Stmt = PrepareQuery(Database, "SELECT Approved FROM Namelocks WHERE CharacterID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } memset(Status, 0, sizeof(TNamelockStatus)); Status->Namelocked = (PQntuples(Result) > 0); if(Status->Namelocked){ Status->Approved = GetResultBool(Result, 0, 0); } return true; } bool InsertNamelock(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Namelocks (CharacterID, IPAddress, GamemasterID, Reason, Comment)" " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 5, 1); ParamInt(&Params, CharacterID); ParamIPAddress(&Params, IPAddress); ParamInt(&Params, GamemasterID); ParamText(&Params, Reason); ParamText(&Params, Comment); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool IsAccountBanished(TDatabase *Database, int AccountID, bool *Banished){ ASSERT(Database != NULL && Banished != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Banishments" " WHERE AccountID = $1::INTEGER" " AND (Until = Issued OR Until > CURRENT_TIMESTAMP)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, AccountID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Banished = (PQntuples(Result) > 0); return true; } bool GetBanishmentStatus(TDatabase *Database, int CharacterID, TBanishmentStatus *Status){ ASSERT(Database != NULL && Status != NULL); const char *Stmt = PrepareQuery(Database, "SELECT B.FinalWarning, (B.Until = B.Issued OR B.Until > CURRENT_TIMESTAMP)" " FROM Banishments AS B" " LEFT JOIN Characters AS C ON C.AccountID = B.AccountID" " WHERE C.CharacterID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = 0; memset(Status, 0, sizeof(TBanishmentStatus)); for(int Row = 0; Row < NumRows; Row += 1){ Status->TimesBanished += 1; if(GetResultBool(Result, Row, 0)){ Status->FinalWarning = true; } if(GetResultBool(Result, Row, 1)){ Status->Banished = true; } } return true; } bool InsertBanishment(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment, bool FinalWarning, int Duration, int *BanishmentID){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL && BanishmentID != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Banishments (AccountID, IPAddress, GamemasterID," " Reason, Comment, FinalWarning, Issued, Until)" " SELECT AccountID, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT," " $6::BOOLEAN, CURRENT_TIMESTAMP, (CURRENT_TIMESTAMP + $7::INTERVAL)" " FROM Characters WHERE CharacterID = $1::INTEGER" " RETURNING BanishmentID"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 7, 1); ParamInt(&Params, CharacterID); ParamIPAddress(&Params, IPAddress); ParamInt(&Params, GamemasterID); ParamText(&Params, Reason); ParamText(&Params, Comment); ParamBool(&Params, FinalWarning); ParamInterval(&Params, Duration); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *BanishmentID = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool GetNotationCount(TDatabase *Database, int CharacterID, int *Notations){ ASSERT(Database != NULL && Notations != NULL); const char *Stmt = PrepareQuery(Database, "SELECT COUNT(*) FROM Notations WHERE CharacterID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, CharacterID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Notations = (PQntuples(Result) > 0 ? GetResultInt(Result, 0, 0) : 0); return true; } bool InsertNotation(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Notations (CharacterID, IPAddress, GamemasterID, Reason, Comment)" " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT, $5::TEXT)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 5, 1); ParamInt(&Params, CharacterID); ParamIPAddress(&Params, IPAddress); ParamInt(&Params, GamemasterID); ParamText(&Params, Reason); ParamText(&Params, Comment); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool IsIPBanished(TDatabase *Database, int IPAddress, bool *Banished){ ASSERT(Database != NULL && Banished != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM IPBanishments" " WHERE IPAddress = $1::INET" " AND (Until = Issued OR Until > CURRENT_TIMESTAMP)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamIPAddress(&Params, IPAddress); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Banished = (PQntuples(Result) > 0); return true; } bool InsertIPBanishment(TDatabase *Database, int CharacterID, int IPAddress, int GamemasterID, const char *Reason, const char *Comment, int Duration){ ASSERT(Database != NULL && Reason != NULL && Comment != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO IPBanishments (CharacterID, IPAddress," " GamemasterID, Reason, Comment, Issued, Until)" " VALUES ($1::INTEGER, $2::INET, $3::INTEGER, $4::TEXT," " $5::TEXT, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + $6::INTERVAL)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 6, 1); ParamInt(&Params, CharacterID); ParamIPAddress(&Params, IPAddress); ParamInt(&Params, GamemasterID); ParamText(&Params, Reason); ParamText(&Params, Comment); ParamInterval(&Params, Duration); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool IsStatementReported(TDatabase *Database, int WorldID, TStatement *Statement, bool *Reported){ ASSERT(Database != NULL && Statement != NULL && Reported != NULL); const char *Stmt = PrepareQuery(Database, "SELECT 1 FROM Statements" " WHERE WorldID = $1::INTEGER" " AND Timestamp = $2::TIMESTAMPTZ" " AND StatementID = $3::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 3, 1); ParamInt(&Params, WorldID); ParamTimestamp(&Params, Statement->Timestamp); ParamInt(&Params, Statement->StatementID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *Reported = (PQntuples(Result) > 0); return true; } bool InsertStatements(TDatabase *Database, int WorldID, int NumStatements, TStatement *Statements){ // NOTE(fusion): Use the `DO NOTHING` conflict resolution because different // reports may include the same statements for context and I assume it's not // uncommon to see overlaps. ASSERT(Database != NULL && NumStatements > 0 && Statements != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO Statements (WorldID, Timestamp, StatementID, CharacterID, Channel, Text)" " VALUES ($1::INTEGER, $2::TIMESTAMPTZ, $3::INTEGER, $4::INTEGER, $5::TEXT, $6::TEXT)" " ON CONFLICT DO NOTHING"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; for(int i = 0; i < NumStatements; i += 1){ if(Statements[i].StatementID == 0){ LOG_WARN("Skipping statement without id"); continue; } ParamBegin(&Params, 6, 1); ParamInt(&Params, WorldID); ParamTimestamp(&Params, Statements[i].Timestamp); ParamInt(&Params, Statements[i].StatementID); ParamInt(&Params, Statements[i].CharacterID); ParamText(&Params, Statements[i].Channel); ParamText(&Params, Statements[i].Text); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to insert statement %d: %s", Statements[i].StatementID, PQerrorMessage(Database->Handle)); return false; } } return true; } bool InsertReportedStatement(TDatabase *Database, int WorldID, TStatement *Statement, int BanishmentID, int ReporterID, const char *Reason, const char *Comment){ ASSERT(Database != NULL && Statement != NULL && Reason != NULL && Comment != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO ReportedStatements (WorldID, Timestamp, StatementID," " CharacterID, BanishmentID, ReporterID, Reason, Comment)" " VALUES ($1::INTEGER, $2::TIMESTAMPTZ, $3::INTEGER, $4::INTEGER," " $5::INTEGER, $6::INTEGER, $7::TEXT, $8::TEXT)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 8, 1); ParamInt(&Params, WorldID); ParamTimestamp(&Params, Statement->Timestamp); ParamInt(&Params, Statement->StatementID); ParamInt(&Params, Statement->CharacterID); ParamInt(&Params, BanishmentID); ParamInt(&Params, ReporterID); ParamText(&Params, Reason); ParamText(&Params, Comment); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } // Info Tables //============================================================================== bool GetKillStatistics(TDatabase *Database, int WorldID, DynamicArray *Stats){ ASSERT(Database != NULL && Stats != NULL); const char *Stmt = PrepareQuery(Database, "SELECT RaceName, TimesKilled, PlayersKilled" " FROM KillStatistics WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TKillStatistics Entry = {}; StringBufCopy(Entry.RaceName, GetResultText(Result, Row, 0)); Entry.TimesKilled = GetResultInt(Result, Row, 1); Entry.PlayersKilled = GetResultInt(Result, Row, 2); Stats->Push(Entry); } return true; } bool MergeKillStatistics(TDatabase *Database, int WorldID, int NumStats, TKillStatistics *Stats){ ASSERT(Database != NULL && NumStats > 0 && Stats != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO KillStatistics (WorldID, RaceName, TimesKilled, PlayersKilled)" " VALUES ($1::INTEGER, $2::TEXT, $3::INTEGER, $4::INTEGER)" " ON CONFLICT (WorldID, RaceName)" " DO UPDATE SET TimesKilled = KillStatistics.TimesKilled + EXCLUDED.TimesKilled," " PlayersKilled = KillStatistics.PlayersKilled + EXCLUDED.PlayersKilled"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; for(int i = 0; i < NumStats; i += 1){ ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamText(&Params, Stats[i].RaceName); ParamInt(&Params, Stats[i].TimesKilled); ParamInt(&Params, Stats[i].PlayersKilled); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to merge \"%s\" stats: %s", Stats[i].RaceName, PQerrorMessage(Database->Handle)); return false; } } return true; } bool GetOnlineCharacters(TDatabase *Database, int WorldID, DynamicArray *Characters){ ASSERT(Database != NULL && Characters != NULL); const char *Stmt = PrepareQuery(Database, "SELECT Name, Level, Profession" " FROM OnlineCharacters WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_TUPLES_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } int NumRows = PQntuples(Result); for(int Row = 0; Row < NumRows; Row += 1){ TOnlineCharacter Character = {}; StringBufCopy(Character.Name, GetResultText(Result, Row, 0)); Character.Level = GetResultInt(Result, Row, 1); StringBufCopy(Character.Profession, GetResultText(Result, Row, 2)); Characters->Push(Character); } return true; } bool DeleteOnlineCharacters(TDatabase *Database, int WorldID){ ASSERT(Database != NULL); const char *Stmt = PrepareQuery(Database, "DELETE FROM OnlineCharacters WHERE WorldID = $1::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 1, 1); ParamInt(&Params, WorldID); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } return true; } bool InsertOnlineCharacters(TDatabase *Database, int WorldID, int NumCharacters, TOnlineCharacter *Characters){ ASSERT(Database != NULL && Characters != NULL); const char *Stmt = PrepareQuery(Database, "INSERT INTO OnlineCharacters (WorldID, Name, Level, Profession)" " VALUES ($1::INTEGER, $2::TEXT, $3::INTEGER, $4::TEXT)"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; for(int i = 0; i < NumCharacters; i += 1){ ParamBegin(&Params, 4, 1); ParamInt(&Params, WorldID); ParamText(&Params, Characters[i].Name); ParamInt(&Params, Characters[i].Level); ParamText(&Params, Characters[i].Profession); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to insert character \"%s\": %s", Characters[i].Name, PQerrorMessage(Database->Handle)); return false; } } return true; } bool CheckOnlineRecord(TDatabase *Database, int WorldID, int NumCharacters, bool *NewRecord){ ASSERT(Database != NULL && NewRecord != NULL); const char *Stmt = PrepareQuery(Database, "UPDATE Worlds SET OnlineRecord = $2::INTEGER," " OnlineRecordTimestamp = CURRENT_TIMESTAMP" " WHERE WorldID = $1::INTEGER AND OnlineRecord < $2::INTEGER"); if(Stmt == NULL){ LOG_ERR("Failed to prepare query"); return false; } ParamBuffer Params = {}; ParamBegin(&Params, 2, 1); ParamInt(&Params, WorldID); ParamInt(&Params, NumCharacters); PGresult *Result = PQexecPrepared(Database->Handle, Stmt, Params.NumParams, Params.Values, Params.Lengths, Params.Formats, 1); AutoResultClear ResultGuard(Result); if(PQresultStatus(Result) != PGRES_COMMAND_OK){ LOG_ERR("Failed to execute query: %s", PQerrorMessage(Database->Handle)); return false; } *NewRecord = (ResultAffectedRows(Result) > 0); return true; } #endif //DATABASE_POSTGRESQL