c++sqlite

sqlite3 returning strange characters when value is too long


Okay so I am using sqlite3 for an OOP assignment in C++. I have a Restaurant table which has some text fields. The problem is - if i insert a value in a text column which is kinda long(about like 20 or so characters) and then try to retrieve the data using SELECT statements, it returns some garbage strange characters. Weird part is it works fine with shorter values. But I don't think 20 characters or so is long enough to cause any kind of problems.

here's the code for my DB class:

#include "doof/DB.h"

DB::DB() {
  if (sqlite3_open("./db/doof.db", &db) != SQLITE_OK) {
    cerr << "Error opening database: " << sqlite3_errmsg(db) << "\n";
  }

  DB::execute("PRAGMA foreign_keys = ON;");
  DB::execute("PRAGMA encoding = 'UTF-8';");
}

DB::~DB() {
  sqlite3_close(db);
}

sqlite3*& DB::getDb() {
  return db;
}

bool DB::execute(const string& sql) const {
  char* errorMessage;
  if (sqlite3_exec(db, sql.c_str(), nullptr, nullptr, &errorMessage) != SQLITE_OK) {
    cerr << "SQL Error: " << errorMessage << "\n";
    sqlite3_free(errorMessage);
    return false;
  }

  return true;
}

void DB::createTables() {
  DB::execute(
    "CREATE TABLE IF NOT EXISTS Restaurant("
    "r_id INTEGER PRIMARY KEY AUTOINCREMENT,"
    "r_name TEXT NOT NULL,"
    "r_email TEXT UNIQUE NOT NULL,"
    "r_password TEXT NOT NULL,"
    "r_address TEXT,"
    "r_contact TEXT,"
    "r_type INTEGER NOT NULL CHECK (r_type in (0, 1, 2)));"
  );

  DB::execute(
    "CREATE TABLE IF NOT EXISTS Food("
    "f_id INTEGER PRIMARY KEY AUTOINCREMENT,"
    "f_name TEXT NOT NULL,"
    "f_price REAL NOT NULL,"
    "f_available INTEGER NOT NULL CHECK (f_available in (0, 1)),"
    "r_id INTEGER NOT NULL,"
    "FOREIGN KEY(r_id) REFERENCES Restaurant(r_id),"
    "UNIQUE(f_name, r_id));"
  );
}

bool DB::insertRestaurant(Restaurant& restaurant) const {
  bool success = true;

  sqlite3_stmt* stmt;
  const char* sql = "INSERT INTO Restaurant (r_name, r_email, r_password, r_address, r_contact, r_type) VALUES (?, ?, ?, ?, ?, ?);";

  if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
    cerr << "Error in preparing restaurant insert statement: " << sqlite3_errmsg(db) << "\n";
    success = false;
    sqlite3_finalize(stmt);

    return success;
  }

  sqlite3_bind_text(stmt, 1, restaurant.getName().c_str(), -1, SQLITE_STATIC);
  sqlite3_bind_text(stmt, 2, restaurant.getEmail().c_str(), -1, SQLITE_STATIC);
  sqlite3_bind_text(stmt, 3, restaurant.getPassword().c_str(), -1, SQLITE_STATIC);
  sqlite3_bind_text(stmt, 4, restaurant.getAddress().c_str(), -1, SQLITE_STATIC);
  sqlite3_bind_text(stmt, 5, restaurant.getContact().c_str(), -1, SQLITE_STATIC);
  sqlite3_bind_int(stmt, 6, static_cast<int>(restaurant.getType()));

  if (sqlite3_step(stmt) != SQLITE_DONE) {
    cerr << "Error executing restaurant insert statement: " << sqlite3_errmsg(db) << "\n";
    success = false;
    sqlite3_finalize(stmt);

    return success;
  }

  const int restaurant_id = static_cast<int>(sqlite3_last_insert_rowid(db));
  restaurant.setId(restaurant_id);

  cout << "Data inserted successfully! Last inserted ID: " << restaurant_id << "\n";

  sqlite3_finalize(stmt);

  return success;
}

void DB::getRestaurants() const {
  sqlite3_stmt* stmt;
  const char* sql = "SELECT r_id, r_name, r_email, r_password, r_address, r_contact, r_type FROM Restaurant;";

  if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
    cerr << "Error preparing SELECT Restaurant statement: " << sqlite3_errmsg(db) << "\n";
    sqlite3_finalize(stmt);
    return;
  }

  while (sqlite3_step(stmt) == SQLITE_ROW) {
    int id = sqlite3_column_int(stmt, 0);
    string name = (char*)sqlite3_column_text(stmt, 1);
    string email = (char*)sqlite3_column_text(stmt, 2);
    string password = (char*)sqlite3_column_text(stmt, 3);
    string address = (char*)sqlite3_column_text(stmt, 4);
    string contact = (char*)sqlite3_column_text(stmt, 5);
    RestaurantType type = static_cast<RestaurantType>(sqlite3_column_int(stmt, 6));

    cout << "ID: " << id << "\n";
    cout << "Name: " << name << "\n";
    cout << "Email: " << email << "\n";
    cout << "Password: " << password << "\n";
  }
}

and i'll show you two variants of my main.cpp along with their corresponding outputs

First:

#include "doof/Restaurant.h"
#include "doof/DB.h"

using namespace std;

int main() {
  DB db;
  db.execute("DROP TABLE Restaurant;");
  db.createTables();

  Restaurant res("Big Big Big Big Name", "bigbigemail@gmail.com", "password", "5121435", "Address", RESTAURANT_TYPE_FAST_FOOD);

  db.insertRestaurant(res);
  db.getRestaurants();
}

Output:

Data inserted successfully! Last inserted ID: 1
ID: 1
Name: p@▒┴☺
Email: p@▒┴☺
Password: password

Second:

#include <iostream>
#include "doof/Restaurant.h"
#include "doof/DB.h"

using namespace std;

int main() {
  DB db;
  db.execute("DROP TABLE Restaurant;");
  db.createTables();

  Restaurant res("Small Name", "small@gmail.com", "password", "5121435", "Address", RESTAURANT_TYPE_FAST_FOOD);

  db.insertRestaurant(res);
  db.getRestaurants();
}

Output:

Data inserted successfully! Last inserted ID: 1
ID: 1
Name: Small Name
Email: small@gmail.com
Password: password

Edit:

Restaurant Class:

#include "doof/Restaurant.h"

Restaurant::Restaurant() {}

Restaurant::Restaurant(const string& _name, const string& _email, const string& _pass, const string& _contact, const string& _addr, RestaurantType _type) : User(_name, _email, _pass, _contact, _addr) {
  type = _type;
}

Restaurant::~Restaurant() {
  for (int i = 0; i < menu.size(); i++) {
    delete menu[i];
  }
}

// getters
RestaurantType Restaurant::getType() const {
  return type;
}

// setters
void Restaurant::setType(const RestaurantType& _type) {
  type = _type;
}

User class:

#include "doof/User.h"

User::User() {}

User::User(const string& _name, const string& _email, const string& _pass, const string& _contact, const string& _addr) {
  name = _name;
  email = _email;
  password = _pass;
  contact = _contact;
  address = _addr;
}

// getters
int User::getId() const {
  return id;
}

string User::getName() const {
  return name;
}

string User::getEmail() const {
  return email;
}

string User::getPassword() const {
  return password;
}

string User::getAddress() const {
  return address;
}

string User::getContact() const {
  return contact;
}

// setters
void User::setId(const int _id) {
  id = _id;
}

void User::setName(const string& _name) {
  name = _name;
}

void User::setEmail(const string& _email) {
  email = _email;
}

void User::setPassword(const string& _password) {
  password = _password;
}

void User::setAddress(const string& _addr) {
  address = _addr;
}

void User::setContact(const string& _contact) {
  contact = _contact;
}

Solution

  • see When to use SQLITE_TRANSIENT vs SQLITE_STATIC?

    In short, you are returning a temporary copy of the string, and promising SQLITE that this temporary is static (relative to the length of the transaction).

    sqlite3_bind_text(stmt, 1, restaurant.getName().c_str(), -1, SQLITE_STATIC);
                                          ^^^^^^^^ returns a temporary string
    

    the proper solution is to change your return type to const string&

    const string& User::getName() const {
      return name;
    }
    

    so that now you are returning a reference to the object in your class instead of returning a temporary.

    the other solution is to not promise SQLITE that those strings are static and use SQLITE_TRANSIENT instead ... which will waste some performance in your case.