When building Discord bots, you often need to persist data across bot restarts. This is where databases come in handy. SQLite3 is an excellent choice for Discord bots because:
- Self-contained: The entire database is stored in a single file
- Zero configuration: No separate server process to install or manage
- Cross-platform: Works on all major operating systems
- Reliability: ACID-compliant with excellent crash recovery
- Performance: Fast for most operations typical in Discord bots
- Lightweight: Small memory and disk footprint
This guide will walk you through integrating SQLite3 with your Concord bot, covering everything from basic setup to a complete working example.
Before starting, ensure you have the SQLite3 development library installed on your system:
# Debian/Ubuntu
sudo apt-get install libsqlite3-dev
# Fedora/RHEL
sudo dnf install sqlite-devel
# macOS (Homebrew)
brew install sqlite3When compiling your bot, you'll need to link against the SQLite3 library:
gcc mybot.c -o mybot -ldiscord -lcurl -lsqlite3The first step when working with SQLite3 is to open a connection to your database file. If the file doesn't exist, SQLite will create it automatically.
#include <sqlite3.h> // Include the SQLite3 header
// Open a database connection
sqlite3 *db;
int rc = sqlite3_open("bot_data.db", &db);
if (rc != SQLITE_OK) {
// Handle opening error
logmod_log(ERROR, NULL, "SQLite error: %s", sqlite3_errmsg(db));
// Always close the database handle, even on error
sqlite3_close(db);
return;
}
// Use the database connection...
// Close the database when done
sqlite3_close(db);Important: Always close your database connection with
sqlite3_close()when you're done using it, regardless of whether an error occurred during opening. Failing to do so will leak resources.
Before storing data, you need to create a table. Each table has a schema defining the columns and their data types.
// Error message holder
char *err_msg = NULL;
// Format the SQL query (using sqlite3_mprintf for proper escaping)
char *query = sqlite3_mprintf(
"CREATE TABLE IF NOT EXISTS users("
"user_id INTEGER PRIMARY KEY," // Discord user ID
"guild_id INTEGER NOT NULL," // Discord guild ID
"points INTEGER DEFAULT 0," // Example user data
"last_active INTEGER" // Timestamp
");"
);
// Execute the query
rc = sqlite3_exec(db, query, NULL, NULL, &err_msg);
// Free the query string (important to avoid memory leaks)
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "SQLite error: %s", err_msg);
sqlite3_free(err_msg); // Free the error message
sqlite3_close(db);
return;
}SQLite3 supports these basic data types:
INTEGER: Whole numbers (use this for Discord IDs)REAL: Floating-point numbersTEXT: String dataBLOB: Binary dataNULL: Null values
Discord IDs should be stored as INTEGER since they're 64-bit numbers.
To store data in your table:
// Insert a new user or update if already exists
char *query = sqlite3_mprintf(
"INSERT OR REPLACE INTO users(user_id, guild_id, last_active) "
"VALUES(%"PRIu64", %"PRIu64", %lld);",
msg->author->id, // Discord user ID
msg->guild_id, // Discord guild ID
(long long)time(NULL) // Current timestamp
);
rc = sqlite3_exec(db, query, NULL, NULL, &err_msg);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Insert failed: %s", err_msg);
sqlite3_free(err_msg);
return;
}The %"PRIu64" format specifier is used for 64-bit unsigned integers like Discord IDs. It's defined in <inttypes.h>, which you should include.
Querying data is slightly more complex because you need to prepare a statement and fetch results:
// Prepare a statement
sqlite3_stmt *stmt = NULL;
char *query = sqlite3_mprintf(
"SELECT points, last_active FROM users WHERE user_id = %"PRIu64" AND guild_id = %"PRIu64";",
target_user_id, guild_id
);
rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Failed to prepare query: %s", sqlite3_errmsg(db));
return;
}
// Execute the query and fetch results
if (sqlite3_step(stmt) == SQLITE_ROW) {
// Data found - access columns (0-indexed)
int points = sqlite3_column_int(stmt, 0);
long long last_active = sqlite3_column_int64(stmt, 1);
// Use the retrieved data
char response[256];
snprintf(response, sizeof(response),
"User has %d points, last active: %lld",
points, last_active);
struct discord_create_message params = { .content = response };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
} else {
// No data found
struct discord_create_message params = { .content = "No data found for this user." };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
}
// Always finalize your statements
sqlite3_finalize(stmt);SQLite3 provides different functions to retrieve column values based on their type:
sqlite3_column_int(stmt, col)- For 32-bit integerssqlite3_column_int64(stmt, col)- For 64-bit integers (use for Discord IDs)sqlite3_column_double(stmt, col)- For floating-point numberssqlite3_column_text(stmt, col)- For text (returnsunsigned char*)sqlite3_column_blob(stmt, col)- For binary data
Updating existing records uses similar syntax to inserting:
char *query = sqlite3_mprintf(
"UPDATE users SET points = points + 1 WHERE user_id = %"PRIu64" AND guild_id = %"PRIu64";",
user_id, guild_id
);
rc = sqlite3_exec(db, query, NULL, NULL, &err_msg);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Update failed: %s", err_msg);
sqlite3_free(err_msg);
}To remove records from your database:
char *query = sqlite3_mprintf(
"DELETE FROM users WHERE user_id = %"PRIu64" AND guild_id = %"PRIu64";",
user_id, guild_id
);
rc = sqlite3_exec(db, query, NULL, NULL, &err_msg);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Delete failed: %s", err_msg);
sqlite3_free(err_msg);
}For queries that are executed frequently, use prepared statements instead of sqlite3_exec(). They're more efficient and provide better protection against SQL injection:
// Prepare once
sqlite3_stmt *stmt = NULL;
rc = sqlite3_prepare_v2(db,
"INSERT INTO users(user_id, guild_id, points) VALUES(?, ?, ?);",
-1, &stmt, NULL);
// Execute many times with different values
sqlite3_bind_int64(stmt, 1, user_id); // Bind first parameter
sqlite3_bind_int64(stmt, 2, guild_id); // Bind second parameter
sqlite3_bind_int(stmt, 3, 100); // Bind third parameter
rc = sqlite3_step(stmt); // Execute
sqlite3_reset(stmt); // Reset for next execution
// Finalize when done
sqlite3_finalize(stmt);When performing multiple database operations, wrap them in a transaction for better performance:
// Begin transaction
sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
// Perform multiple operations...
// Commit transaction (or ROLLBACK if errors occurred)
sqlite3_exec(db, "COMMIT;", NULL, NULL, NULL);SQLite can handle concurrent reads, but writers block all other operations. For bots handling many guilds, consider:
- Using separate database files per guild
- Implementing a connection pool
- Using a more robust database system like PostgreSQL for high-traffic bots
Always check return codes and implement proper error handling:
if (rc != SQLITE_OK) {
// Log detailed error information
logmod_log(ERROR, NULL,
"SQLite error (%d): %s - %s",
rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
// Take appropriate recovery action
if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
// Database is locked, wait and retry
sleep(1);
// retry operation...
}
}This example bot demonstrates a simple points system using SQLite3:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <inttypes.h>
#include <concord/discord.h>
#include <concord/logmod.h>
#include <sqlite3.h>
// Initialize database and create tables if needed
sqlite3* init_database() {
sqlite3 *db;
int rc = sqlite3_open("bot_data.db", &db);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Cannot open database: %s", sqlite3_errmsg(db));
sqlite3_close(db);
return NULL;
}
char *err_msg = NULL;
const char *sql =
"CREATE TABLE IF NOT EXISTS users ("
" user_id INTEGER NOT NULL,"
" guild_id INTEGER NOT NULL,"
" points INTEGER DEFAULT 0,"
" last_active INTEGER,"
" PRIMARY KEY (user_id, guild_id)"
");";
rc = sqlite3_exec(db, sql, NULL, NULL, &err_msg);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "SQL error: %s", err_msg);
sqlite3_free(err_msg);
sqlite3_close(db);
return NULL;
}
return db;
}
// Global database connection
sqlite3 *db = NULL;
void on_ready(struct discord *client, const struct discord_ready *event) {
logmod_log(INFO, NULL, "Logged in as %s!", event->user->username);
}
// Add points to a user
void on_points_add(struct discord *client, const struct discord_message *msg) {
if (!msg->guild_id) return; // Ignore DMs
// Parse target user from message
if (!msg->mentions || !msg->mentions->size) {
struct discord_create_message params = { .content = "Please mention a user to add points to." };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
return;
}
u64snowflake target_id = msg->mentions->array[0]->id;
int points_to_add = 1; // Default 1 point
// Parse command for custom points amount
char *args = strstr(msg->content, " ");
if (args) {
char *end_of_mention = strstr(args + 1, ">");
if (end_of_mention && *(end_of_mention + 1)) {
int parsed = atoi(end_of_mention + 1);
if (parsed > 0) points_to_add = parsed;
}
}
char *query = sqlite3_mprintf(
"INSERT INTO users(user_id, guild_id, points, last_active) "
"VALUES(%"PRIu64", %"PRIu64", %d, %lld) "
"ON CONFLICT(user_id, guild_id) DO "
"UPDATE SET points = points + %d, last_active = %lld;",
target_id, msg->guild_id, points_to_add, (long long)time(NULL),
points_to_add, (long long)time(NULL)
);
char *err_msg = NULL;
int rc = sqlite3_exec(db, query, NULL, NULL, &err_msg);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Failed to add points: %s", err_msg);
sqlite3_free(err_msg);
struct discord_create_message params = { .content = "Failed to add points. Database error." };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
return;
}
char response[256];
snprintf(response, sizeof(response), "<@%"PRIu64"> received %d points!", target_id, points_to_add);
struct discord_create_message params = { .content = response };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
}
// Check points for a user
void on_points_check(struct discord *client, const struct discord_message *msg) {
if (!msg->guild_id) return; // Ignore DMs
u64snowflake target_id = msg->author->id; // Default to message author
// Check if a user was mentioned
if (msg->mentions && msg->mentions->size) {
target_id = msg->mentions->array[0]->id;
}
sqlite3_stmt *stmt = NULL;
char *query = sqlite3_mprintf(
"SELECT points, last_active FROM users "
"WHERE user_id = %"PRIu64" AND guild_id = %"PRIu64";",
target_id, msg->guild_id
);
int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Failed to prepare query: %s", sqlite3_errmsg(db));
struct discord_create_message params = { .content = "Database error occurred." };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
return;
}
char response[256];
if (sqlite3_step(stmt) == SQLITE_ROW) {
int points = sqlite3_column_int(stmt, 0);
long long last_active = sqlite3_column_int64(stmt, 1);
time_t active_time = (time_t)last_active;
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", localtime(&active_time));
snprintf(response, sizeof(response),
"<@%"PRIu64"> has %d points. Last active: %s",
target_id, points, time_str);
} else {
snprintf(response, sizeof(response),
"<@%"PRIu64"> has no points yet.", target_id);
}
sqlite3_finalize(stmt);
struct discord_create_message params = { .content = response };
discord_create_message(client, msg->channel_id, ¶ms, NULL);
}
// Show top 5 users by points
void on_points_leaderboard(struct discord *client, const struct discord_message *msg) {
if (!msg->guild_id) return; // Ignore DMs
sqlite3_stmt *stmt = NULL;
char *query = sqlite3_mprintf(
"SELECT user_id, points FROM users "
"WHERE guild_id = %"PRIu64" "
"ORDER BY points DESC LIMIT 5;",
msg->guild_id
);
int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
sqlite3_free(query);
if (rc != SQLITE_OK) {
logmod_log(ERROR, NULL, "Failed to prepare leaderboard query: %s", sqlite3_errmsg(db));
return;
}
struct discord_embed embed = {
.title = "Points Leaderboard",
.color = 0x3498DB,
.timestamp = discord_timestamp(client)
};
char description[512] = "Top 5 users by points:\n\n";
int position = 1;
int has_entries = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
has_entries = 1;
u64snowflake user_id = sqlite3_column_int64(stmt, 0);
int points = sqlite3_column_int(stmt, 1);
char line[128];
snprintf(line, sizeof(line), "%d. <@%"PRIu64"> - %d points\n",
position++, user_id, points);
strcat(description, line);
}
sqlite3_finalize(stmt);
if (!has_entries) {
strcpy(description, "No points have been awarded yet.");
}
embed.description = description;
struct discord_create_message params = {
.embeds = &(struct discord_embeds){
.size = 1,
.array = &embed
}
};
discord_create_message(client, msg->channel_id, ¶ms, NULL);
}
int main(void) {
// Initialize database
db = init_database();
if (!db) {
logmod_log(FATAL, NULL, "Failed to initialize database. Exiting.");
return 1;
}
// Initialize Discord bot
struct discord *client = discord_from_json("config.json");
discord_add_intents(client, DISCORD_GATEWAY_MESSAGE_CONTENT);
// Set up event handlers
discord_set_on_ready(client, &on_ready);
discord_set_on_command(client, ".points add", &on_points_add);
discord_set_on_command(client, ".points check", &on_points_check);
discord_set_on_command(client, ".points top", &on_points_leaderboard);
// Run the bot
discord_run(client);
// Clean up
sqlite3_close(db);
return 0;
}| Error | Description | Solution |
|---|---|---|
SQLITE_BUSY |
Database file is locked | Use transactions, implement retry logic |
SQLITE_CONSTRAINT |
Constraint violation (e.g., unique key) | Use INSERT OR REPLACE or proper error handling |
SQLITE_NOMEM |
Out of memory | Check system resources, optimize queries |
SQLITE_READONLY |
Database is read-only | Check file permissions |
SQLITE_IOERR |
Disk I/O error | Check disk space and health |
If your database becomes corrupted (rare with proper usage):
- Always maintain backups
- Use
PRAGMA integrity_check;to verify database integrity - Use SQLite's recovery tools like
.recoverin the SQLite CLI
SQLite3 is an excellent choice for Discord bots due to its simplicity, reliability, and zero-configuration nature. By following this guide, you should now be able to effectively store and retrieve persistent data in your Concord bot.
Remember these key points:
- Always check return codes and handle errors
- Free allocated resources (statements, error messages, query strings)
- Use transactions for multiple operations
- Consider database design carefully to match your bot's needs