mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-20 19:11:39 -06:00
This improves commit c20c58595a to
definitely make sure the primary key values are stored and handled
correctly by simply using the same data type as SQLite itself does.
1177 lines
38 KiB
C++
1177 lines
38 KiB
C++
#include "sqlitedb.h"
|
|
#include "sqlitetablemodel.h"
|
|
#include "CipherDialog.h"
|
|
|
|
#include <QFile>
|
|
#include <QMessageBox>
|
|
#include <QProgressDialog>
|
|
#include <QApplication>
|
|
#include <QSettings>
|
|
#include <QDebug>
|
|
#include <QInputDialog>
|
|
|
|
// collation callbacks
|
|
int collCompare(void* /*pArg*/, int /*eTextRepA*/, const void* sA, int /*eTextRepB*/, const void* sB)
|
|
{
|
|
size_t sizeA = strlen((const char*)sA);
|
|
size_t sizeB = strlen((const char*)sB);
|
|
|
|
if(sizeA == sizeB)
|
|
return memcmp(sA, sB, sizeA);
|
|
return sizeA - sizeB;
|
|
}
|
|
|
|
void collation_needed(void* /*pData*/, sqlite3* db, int eTextRep, const char* sCollationName)
|
|
{
|
|
QMessageBox::StandardButton reply = QMessageBox::question(
|
|
0,
|
|
QObject::tr("Collation needed! Proceed?"),
|
|
QObject::tr("A table in this database requires a special collation function '%1' "
|
|
"that this application can't provide without further knowledge.\n"
|
|
"If you choose to proceed, be aware bad things can happen to your database.\n"
|
|
"Create a backup!").arg(sCollationName), QMessageBox::Yes | QMessageBox::No);
|
|
if(reply == QMessageBox::Yes)
|
|
sqlite3_create_collation(db, sCollationName, eTextRep, NULL, collCompare);
|
|
}
|
|
|
|
bool DBBrowserDB::isOpen ( ) const
|
|
{
|
|
return _db!=0;
|
|
}
|
|
|
|
bool DBBrowserDB::getDirty() const
|
|
{
|
|
return !savepointList.empty();
|
|
}
|
|
|
|
bool DBBrowserDB::open(const QString& db)
|
|
{
|
|
if (isOpen()) close();
|
|
|
|
lastErrorMessage = tr("no error");
|
|
|
|
isEncrypted = false;
|
|
|
|
// Get encryption settings for database file
|
|
CipherDialog* cipher = 0;
|
|
if(tryEncryptionSettings(db, &isEncrypted, cipher) == false)
|
|
{
|
|
lastErrorMessage = QString::fromUtf8((const char*)sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
|
|
// Open database file
|
|
if(sqlite3_open_v2(db.toUtf8(), &_db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
|
|
{
|
|
lastErrorMessage = QString::fromUtf8((const char*)sqlite3_errmsg(_db));
|
|
return false;
|
|
}
|
|
|
|
// Set encryption details if database is encrypted
|
|
#ifdef ENABLE_SQLCIPHER
|
|
if(isEncrypted && cipher)
|
|
{
|
|
sqlite3_key(_db, cipher->password().toUtf8(), cipher->password().toUtf8().length());
|
|
if(cipher->pageSize() != 1024)
|
|
sqlite3_exec(_db, QString("PRAGMA cipher_page_size = %1;").arg(cipher->pageSize()).toUtf8(), NULL, NULL, NULL);
|
|
}
|
|
#endif
|
|
delete cipher;
|
|
|
|
if (_db)
|
|
{
|
|
// register collation callback
|
|
sqlite3_collation_needed(_db, NULL, collation_needed);
|
|
|
|
// set preference defaults
|
|
QSettings settings(QApplication::organizationName(), QApplication::organizationName());
|
|
settings.sync();
|
|
bool foreignkeys = settings.value( "/db/foreignkeys", false ).toBool();
|
|
setPragma("foreign_keys", foreignkeys ? "1" : "0");
|
|
|
|
// Enable extension loading
|
|
sqlite3_enable_load_extension(_db, 1);
|
|
|
|
curDBFilename = db;
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool DBBrowserDB::attach(const QString& filename, QString attach_as)
|
|
{
|
|
// Ask for name to be given to the attached database if none was provided
|
|
if(attach_as.isEmpty())
|
|
attach_as = QInputDialog::getText(0,
|
|
qApp->applicationName(),
|
|
tr("Please specify the database name under which you want to access the attached database")
|
|
).trimmed();
|
|
if(attach_as.isEmpty())
|
|
return false;
|
|
|
|
#ifdef ENABLE_SQLCIPHER
|
|
// Try encryption settings
|
|
CipherDialog* cipher = 0;
|
|
bool is_encrypted;
|
|
if(tryEncryptionSettings(filename, &is_encrypted, cipher) == false)
|
|
return false;
|
|
|
|
// Attach database
|
|
QString key;
|
|
if(cipher) key = cipher->password();
|
|
if(!executeSQL(QString("ATTACH '%1' AS `%2` KEY '%3'").arg(filename).arg(attach_as).arg(key), false))
|
|
{
|
|
QMessageBox::warning(0, qApp->applicationName(), lastErrorMessage);
|
|
return false;
|
|
}
|
|
if(cipher && cipher->pageSize() != 1024)
|
|
{
|
|
if(!executeSQL(QString("PRAGMA `%1`.cipher_page_size = %2").arg(attach_as).arg(cipher->pageSize()), false))
|
|
{
|
|
QMessageBox::warning(0, qApp->applicationName(), lastErrorMessage);
|
|
return false;
|
|
}
|
|
}
|
|
#else
|
|
// Attach database
|
|
if(!executeSQL(QString("ATTACH '%1' AS `%2`").arg(filename).arg(attach_as), false))
|
|
{
|
|
QMessageBox::warning(0, qApp->applicationName(), lastErrorMessage);
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::tryEncryptionSettings(const QString& filename, bool* encrypted, CipherDialog*& cipherSettings)
|
|
{
|
|
// Open database file
|
|
sqlite3* dbHandle;
|
|
if(sqlite3_open_v2(filename.toUtf8(), &dbHandle, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
|
|
return false;
|
|
|
|
// Try reading from database
|
|
*encrypted = false;
|
|
cipherSettings = 0;
|
|
while(true)
|
|
{
|
|
QString statement = "SELECT COUNT(*) FROM sqlite_master;";
|
|
QByteArray utf8Statement = statement.toUtf8();
|
|
sqlite3_stmt* vm;
|
|
const char* tail;
|
|
sqlite3_prepare_v2(dbHandle, utf8Statement, utf8Statement.length(), &vm, &tail);
|
|
if(sqlite3_step(vm) != SQLITE_ROW)
|
|
{
|
|
sqlite3_finalize(vm);
|
|
#ifdef ENABLE_SQLCIPHER
|
|
delete cipherSettings;
|
|
cipherSettings = new CipherDialog(0, false);
|
|
if(cipherSettings->exec())
|
|
{
|
|
// Close and reopen database first to be in a clean state after the failed read attempt from above
|
|
sqlite3_close(dbHandle);
|
|
if(sqlite3_open_v2(filename.toUtf8(), &dbHandle, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK)
|
|
{
|
|
delete cipherSettings;
|
|
cipherSettings = 0;
|
|
return false;
|
|
}
|
|
|
|
// Set key and, if it differs from the default value, the page size
|
|
sqlite3_key(dbHandle, cipherSettings->password().toUtf8(), cipherSettings->password().toUtf8().length());
|
|
if(cipherSettings->pageSize() != 1024)
|
|
sqlite3_exec(dbHandle, QString("PRAGMA cipher_page_size = %1;").arg(cipherSettings->pageSize()).toUtf8(), NULL, NULL, NULL);
|
|
|
|
*encrypted = true;
|
|
} else {
|
|
sqlite3_close(dbHandle);
|
|
*encrypted = false;
|
|
delete cipherSettings;
|
|
cipherSettings = 0;
|
|
return false;
|
|
}
|
|
#else
|
|
sqlite3_close(dbHandle);
|
|
return false;
|
|
#endif
|
|
} else {
|
|
sqlite3_finalize(vm);
|
|
sqlite3_close(dbHandle);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DBBrowserDB::setRestorePoint(const QString& pointname)
|
|
{
|
|
if(!isOpen())
|
|
return false;
|
|
if(savepointList.contains(pointname))
|
|
return true;
|
|
|
|
if(_db)
|
|
{
|
|
QString query = QString("SAVEPOINT %1;").arg(pointname);
|
|
sqlite3_exec(_db, query.toUtf8(), NULL, NULL, NULL);
|
|
savepointList.append(pointname);
|
|
emit dbChanged(getDirty());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::save(const QString& pointname)
|
|
{
|
|
if(!isOpen() || savepointList.contains(pointname) == false)
|
|
return false;
|
|
|
|
if(_db)
|
|
{
|
|
QString query = QString("RELEASE %1;").arg(pointname);
|
|
sqlite3_exec(_db, query.toUtf8(), NULL,NULL,NULL);
|
|
savepointList.removeAll(pointname);
|
|
emit dbChanged(getDirty());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::revert(const QString& pointname)
|
|
{
|
|
if(!isOpen() || savepointList.contains(pointname) == false)
|
|
return false;
|
|
|
|
if(_db)
|
|
{
|
|
QString query = QString("ROLLBACK TO SAVEPOINT %1;").arg(pointname);
|
|
sqlite3_exec(_db, query.toUtf8(), NULL, NULL, NULL);
|
|
query = QString("RELEASE %1;").arg(pointname);
|
|
sqlite3_exec(_db, query.toUtf8(), NULL, NULL, NULL);
|
|
savepointList.removeAll(pointname);
|
|
emit dbChanged(getDirty());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::saveAll()
|
|
{
|
|
foreach(const QString& point, savepointList)
|
|
{
|
|
if(!save(point))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::revertAll()
|
|
{
|
|
foreach(const QString& point, savepointList)
|
|
{
|
|
if(!revert(point))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::create ( const QString & db)
|
|
{
|
|
bool ok=false;
|
|
|
|
if (isOpen()) close();
|
|
|
|
lastErrorMessage = tr("no error");
|
|
|
|
// read encoding from settings and open with sqlite3_open for utf8
|
|
// and sqlite3_open16 for utf16
|
|
QSettings settings(QApplication::organizationName(), QApplication::organizationName());
|
|
QString sEncoding = settings.value("/db/defaultencoding", "UTF-8").toString();
|
|
|
|
int openresult = SQLITE_OK;
|
|
|
|
if(sEncoding == "UTF-8" || sEncoding == "UTF8" || sEncoding == "Latin1")
|
|
openresult = sqlite3_open(db.toUtf8(), &_db);
|
|
else
|
|
openresult = sqlite3_open16(db.utf16(), &_db);
|
|
|
|
if( openresult != SQLITE_OK ){
|
|
lastErrorMessage = QString::fromUtf8((const char*)sqlite3_errmsg(_db));
|
|
sqlite3_close(_db);
|
|
_db = 0;
|
|
return false;
|
|
}
|
|
|
|
if (_db)
|
|
{
|
|
// Enable extension loading
|
|
sqlite3_enable_load_extension(_db, 1);
|
|
|
|
// force sqlite3 do write proper file header
|
|
// if we don't create and drop the table we might end up
|
|
// with a 0 byte file, if the user cancels the create table dialog
|
|
sqlite3_exec(_db, "CREATE TABLE notempty (id integer primary key);", NULL, NULL, NULL);
|
|
sqlite3_exec(_db, "DROP TABLE notempty", NULL, NULL, NULL);
|
|
sqlite3_exec(_db, "COMMIT;", NULL, NULL, NULL);
|
|
|
|
ok = true;
|
|
curDBFilename = db;
|
|
}
|
|
|
|
return ok;
|
|
}
|
|
|
|
|
|
bool DBBrowserDB::close()
|
|
{
|
|
if(_db)
|
|
{
|
|
if (getDirty())
|
|
{
|
|
QMessageBox::StandardButton reply = QMessageBox::question(0,
|
|
QApplication::applicationName(),
|
|
tr("Do you want to save the changes "
|
|
"made to the database file %1?").arg(curDBFilename),
|
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
|
|
|
|
// If the user clicked the cancel button stop here and return false
|
|
if(reply == QMessageBox::Cancel)
|
|
return false;
|
|
|
|
// If he didn't it was either yes or no
|
|
if(reply == QMessageBox::Yes)
|
|
saveAll();
|
|
else
|
|
revertAll(); //not really necessary, I think... but will not hurt.
|
|
}
|
|
sqlite3_close(_db);
|
|
}
|
|
_db = 0;
|
|
objMap.clear();
|
|
savepointList.clear();
|
|
emit dbChanged(getDirty());
|
|
|
|
// Return true to tell the calling function that the closing wasn't cancelled by the user
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::dump(const QString& filename)
|
|
{
|
|
// Open file
|
|
QFile file(filename);
|
|
if(file.open(QIODevice::WriteOnly))
|
|
{
|
|
QApplication::setOverrideCursor(Qt::WaitCursor);
|
|
|
|
size_t numRecordsTotal = 0, numRecordsCurrent = 0;
|
|
QList<DBBrowserObject> tables = objMap.values("table");
|
|
QMutableListIterator<DBBrowserObject> it(tables);
|
|
while(it.hasNext())
|
|
{
|
|
it.next();
|
|
|
|
// Remove the sqlite_stat1 table if there is one
|
|
if(it.value().getname() == "sqlite_stat1" || it.value().getname() == "sqlite_sequence")
|
|
{
|
|
it.remove();
|
|
} else {
|
|
// Otherwise get the number of records in this table
|
|
SqliteTableModel tableModel(0, this);
|
|
tableModel.setTable(it.value().getname());
|
|
numRecordsTotal += tableModel.totalRowCount();
|
|
}
|
|
}
|
|
|
|
QProgressDialog progress(tr("Exporting database to SQL file..."),
|
|
tr("Cancel"), 0, numRecordsTotal);
|
|
progress.setWindowModality(Qt::ApplicationModal);
|
|
progress.show();
|
|
qApp->processEvents();
|
|
|
|
// Open text stream to the file
|
|
QTextStream stream(&file);
|
|
|
|
// Put the SQL commands in a transaction block
|
|
stream << "BEGIN TRANSACTION;\n";
|
|
|
|
// Loop through all tables first as they are required to generate views, indices etc. later
|
|
for(QList<DBBrowserObject>::ConstIterator it=tables.begin();it!=tables.end();++it)
|
|
{
|
|
// Write the SQL string used to create this table to the output file
|
|
stream << it->getsql() << ";\n";
|
|
|
|
QString sQuery = QString("SELECT * FROM `%1`;").arg(it->getTableName());
|
|
QByteArray utf8Query = sQuery.toUtf8();
|
|
sqlite3_stmt *stmt;
|
|
|
|
int status = sqlite3_prepare_v2(this->_db, utf8Query.data(), utf8Query.size(), &stmt, NULL);
|
|
if(SQLITE_OK == status)
|
|
{
|
|
int columns = sqlite3_column_count(stmt);
|
|
size_t counter = 0;
|
|
qApp->processEvents();
|
|
while(sqlite3_step(stmt) == SQLITE_ROW)
|
|
{
|
|
stream << "INSERT INTO `" << it->getTableName() << "` VALUES (";
|
|
for (int i = 0; i < columns; ++i)
|
|
{
|
|
int fieldsize = sqlite3_column_bytes(stmt, i);
|
|
int fieldtype = sqlite3_column_type(stmt, i);
|
|
QByteArray bcontent(
|
|
(const char*)sqlite3_column_blob(stmt, i),
|
|
fieldsize);
|
|
|
|
if(bcontent.left(2048).contains('\0')) // binary check
|
|
{
|
|
stream << QString("X'%1'").arg(QString(bcontent.toHex()));
|
|
}
|
|
else
|
|
{
|
|
switch(fieldtype)
|
|
{
|
|
case SQLITE_TEXT:
|
|
case SQLITE_BLOB:
|
|
stream << "'" << bcontent.replace("'", "''") << "'";
|
|
break;
|
|
case SQLITE_NULL:
|
|
stream << "NULL";
|
|
break;
|
|
default:
|
|
stream << bcontent;
|
|
}
|
|
}
|
|
if(i != columns - 1)
|
|
stream << ',';
|
|
}
|
|
stream << ");\n";
|
|
|
|
progress.setValue(++numRecordsCurrent);
|
|
if(counter % 5000 == 0)
|
|
qApp->processEvents();
|
|
counter++;
|
|
|
|
if(progress.wasCanceled())
|
|
{
|
|
sqlite3_finalize(stmt);
|
|
file.close();
|
|
file.remove();
|
|
QApplication::restoreOverrideCursor();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
}
|
|
|
|
// Now dump all the other objects
|
|
for(objectMap::ConstIterator it=objMap.begin();it!=objMap.end();++it)
|
|
{
|
|
// Make sure it's not a table again
|
|
if(it.value().gettype() == "table")
|
|
continue;
|
|
|
|
// Write the SQL string used to create this object to the output file
|
|
if(!it->getsql().isEmpty())
|
|
{
|
|
stream << it->getsql() << ";\n";
|
|
}
|
|
}
|
|
|
|
// Done
|
|
stream << "COMMIT;\n";
|
|
file.close();
|
|
|
|
QApplication::restoreOverrideCursor();
|
|
qApp->processEvents();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DBBrowserDB::executeSQL ( const QString & statement, bool dirtyDB, bool logsql)
|
|
{
|
|
char *errmsg;
|
|
bool ok = false;
|
|
|
|
if (!isOpen()) return false;
|
|
|
|
if (_db){
|
|
if (logsql) logSQL(statement, kLogMsg_App);
|
|
if (dirtyDB) setRestorePoint();
|
|
if (SQLITE_OK==sqlite3_exec(_db,statement.toUtf8(),
|
|
NULL,NULL,&errmsg)){
|
|
ok=true;
|
|
}
|
|
}
|
|
|
|
if (!ok){
|
|
lastErrorMessage = QString::fromUtf8(errmsg);
|
|
qWarning() << "executeSQL: " << statement << "->" << lastErrorMessage;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::executeMultiSQL(const QString& statement, bool dirty, bool log)
|
|
{
|
|
// First check if a DB is opened
|
|
if(!isOpen())
|
|
return false;
|
|
|
|
// Log the statement if needed
|
|
if(log)
|
|
logSQL(statement, kLogMsg_App);
|
|
|
|
// Set DB to dirty/create restore point if necessary
|
|
if(dirty)
|
|
setRestorePoint();
|
|
|
|
// Show progress dialog
|
|
int statement_size = statement.size();
|
|
QProgressDialog progress(tr("Executing SQL..."),
|
|
tr("Cancel"), 0, statement_size);
|
|
progress.setWindowModality(Qt::ApplicationModal);
|
|
progress.show();
|
|
|
|
// Execute the statement by looping until SQLite stops giving back a tail string
|
|
sqlite3_stmt* vm;
|
|
QByteArray utf8Query = statement.toUtf8();
|
|
const char *tail = utf8Query.data();
|
|
int res = 0;
|
|
unsigned int line = 0;
|
|
do
|
|
{
|
|
line++;
|
|
size_t tail_length = strlen(tail);
|
|
|
|
// Update progress dialog, keep UI responsive
|
|
progress.setValue(statement_size - tail_length);
|
|
qApp->processEvents();
|
|
if(progress.wasCanceled())
|
|
{
|
|
lastErrorMessage = tr("Action cancelled.");
|
|
return false;
|
|
}
|
|
|
|
// Execute next statement
|
|
res = sqlite3_prepare_v2(_db, tail, tail_length, &vm, &tail);
|
|
if(res == SQLITE_OK)
|
|
{
|
|
if(sqlite3_step(vm) == SQLITE_ERROR)
|
|
{
|
|
sqlite3_finalize(vm);
|
|
lastErrorMessage = tr("Error in statement #%1: %2.\n"
|
|
"Aborting execution.").arg(line).arg(sqlite3_errmsg(_db));
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
} else {
|
|
sqlite3_finalize(vm);
|
|
}
|
|
} else {
|
|
lastErrorMessage = tr("Error in statement #%1: %2.\n"
|
|
"Aborting execution.").arg(line).arg(sqlite3_errmsg(_db));
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
}
|
|
} while(tail && *tail != 0 && (res == SQLITE_OK || res == SQLITE_DONE));
|
|
|
|
// Exit
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::getRow(const QString& sTableName, sqlite3_int64 rowid, QList<QByteArray>& rowdata)
|
|
{
|
|
QString sQuery = QString("SELECT * FROM `%1` WHERE `%2`=%3;").arg(sTableName).arg(getObjectByName(sTableName).table.rowidColumn()).arg(rowid);
|
|
QByteArray utf8Query = sQuery.toUtf8();
|
|
sqlite3_stmt *stmt;
|
|
bool ret = false;
|
|
|
|
int status = sqlite3_prepare_v2(_db, utf8Query, utf8Query.size(), &stmt, NULL);
|
|
if(SQLITE_OK == status)
|
|
{
|
|
// even this is a while loop, the statement should always only return 1 row
|
|
while(sqlite3_step(stmt) == SQLITE_ROW)
|
|
{
|
|
for (int i = 0; i < sqlite3_column_count(stmt); ++i)
|
|
rowdata.append(QByteArray(static_cast<const char*>(sqlite3_column_blob(stmt, i)), sqlite3_column_bytes(stmt, i)));
|
|
ret = true;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int64_t DBBrowserDB::max(const sqlb::Table& t, sqlb::FieldPtr field) const
|
|
{
|
|
QString sQuery = QString("SELECT MAX(`%2`) from `%1`;").arg(t.name()).arg(field->name());
|
|
QByteArray utf8Query = sQuery.toUtf8();
|
|
sqlite3_stmt *stmt;
|
|
int64_t ret = 0;
|
|
|
|
int status = sqlite3_prepare_v2(_db, utf8Query, utf8Query.size(), &stmt, NULL);
|
|
if(SQLITE_OK == status)
|
|
{
|
|
// even this is a while loop, the statement should always only return 1 row
|
|
while(sqlite3_step(stmt) == SQLITE_ROW)
|
|
{
|
|
if(sqlite3_column_count(stmt) == 1)
|
|
{
|
|
ret = sqlite3_column_int64(stmt, 0);
|
|
}
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
return ret;
|
|
}
|
|
|
|
QString DBBrowserDB::emptyInsertStmt(const sqlb::Table& t, sqlite3_int64 pk_value) const
|
|
{
|
|
QString stmt = QString("INSERT INTO `%1`").arg(t.name());
|
|
|
|
QStringList vals;
|
|
QStringList fields;
|
|
foreach(sqlb::FieldPtr f, t.fields()) {
|
|
if(f->primaryKey()) {
|
|
if(f->isInteger())
|
|
{
|
|
fields << f->name();
|
|
|
|
if(pk_value != -1)
|
|
vals << QString::number(pk_value);
|
|
else
|
|
{
|
|
if(f->notnull())
|
|
{
|
|
int64_t maxval = this->max(t, f);
|
|
vals << QString::number(maxval + 1);
|
|
}
|
|
else
|
|
{
|
|
vals << "NULL";
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
fields << f->name();
|
|
|
|
vals << "''";
|
|
}
|
|
} else if(f->notnull() && f->defaultValue().length() == 0) {
|
|
fields << f->name();
|
|
|
|
if(f->isInteger())
|
|
vals << "0";
|
|
else
|
|
vals << "''";
|
|
} else {
|
|
// don't insert into fields with a default value
|
|
// or we will never see it.
|
|
if(f->defaultValue().length() == 0)
|
|
{
|
|
fields << f->name();
|
|
vals << "NULL";
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!fields.empty())
|
|
{
|
|
stmt.append("(`");
|
|
stmt.append(fields.join("`,`"));
|
|
stmt.append("`)");
|
|
}
|
|
stmt.append(" VALUES (");
|
|
stmt.append(vals.join(","));
|
|
stmt.append(");");
|
|
|
|
return stmt;
|
|
}
|
|
|
|
long DBBrowserDB::addRecord(const QString& sTableName)
|
|
{
|
|
char *errmsg;
|
|
if (!isOpen()) return false;
|
|
|
|
sqlb::Table table = getObjectByName(sTableName).table;
|
|
|
|
// For tables without rowid we have to set the primary key by ourselves. We do so by querying for the largest value in the PK column
|
|
// and adding one to it.
|
|
QString sInsertstmt;
|
|
long pk_value;
|
|
if(table.isWithoutRowidTable())
|
|
{
|
|
SqliteTableModel m(this, this);
|
|
m.setQuery(QString("SELECT MAX(`%1`) FROM `%2`;").arg(table.rowidColumn()).arg(sTableName));
|
|
pk_value = m.data(m.index(0, 0)).toLongLong() + 1;
|
|
sInsertstmt = emptyInsertStmt(table, pk_value);
|
|
} else {
|
|
sInsertstmt = emptyInsertStmt(table);
|
|
}
|
|
|
|
lastErrorMessage = "";
|
|
logSQL(sInsertstmt, kLogMsg_App);
|
|
setRestorePoint();
|
|
|
|
if (SQLITE_OK != sqlite3_exec(_db, sInsertstmt.toUtf8(), NULL, NULL, &errmsg))
|
|
{
|
|
lastErrorMessage = QString::fromUtf8(errmsg);
|
|
qWarning() << "addRecord: " << lastErrorMessage;
|
|
return -1;
|
|
} else {
|
|
if(table.isWithoutRowidTable())
|
|
return pk_value;
|
|
else
|
|
return sqlite3_last_insert_rowid(_db);
|
|
}
|
|
}
|
|
|
|
bool DBBrowserDB::deleteRecord(const QString& table, sqlite3_int64 rowid)
|
|
{
|
|
char * errmsg;
|
|
if (!isOpen()) return false;
|
|
bool ok = false;
|
|
lastErrorMessage = QString("no error");
|
|
|
|
QString statement = QString("DELETE FROM `%1` WHERE `%2`=%3;").arg(table).arg(getObjectByName(table).table.rowidColumn()).arg(rowid);
|
|
|
|
if (_db){
|
|
logSQL(statement, kLogMsg_App);
|
|
setRestorePoint();
|
|
if (SQLITE_OK==sqlite3_exec(_db,statement.toUtf8(),
|
|
NULL,NULL,&errmsg)){
|
|
ok=true;
|
|
} else {
|
|
lastErrorMessage = QString::fromUtf8(errmsg);
|
|
qWarning() << "deleteRecord: " << lastErrorMessage;
|
|
}
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
bool DBBrowserDB::updateRecord(const QString& table, const QString& column, sqlite3_int64 row, const QByteArray& value)
|
|
{
|
|
if (!isOpen()) return false;
|
|
|
|
lastErrorMessage = QString("no error");
|
|
|
|
QString sql = QString("UPDATE `%1` SET `%2`=? WHERE `%3`=%4;").arg(table).arg(column).arg(getObjectByName(table).table.rowidColumn()).arg(row);
|
|
|
|
logSQL(sql, kLogMsg_App);
|
|
setRestorePoint();
|
|
|
|
sqlite3_stmt* stmt;
|
|
int success = 1;
|
|
if(sqlite3_prepare_v2(_db, sql.toUtf8(), -1, &stmt, 0) != SQLITE_OK)
|
|
success = 0;
|
|
if(success == 1 && sqlite3_bind_text(stmt, 1, value.constData(), value.length(), SQLITE_STATIC) != SQLITE_OK)
|
|
success = -1;
|
|
if(success == 1 && sqlite3_step(stmt) != SQLITE_DONE)
|
|
success = -1;
|
|
if(success != 0 && sqlite3_finalize(stmt) != SQLITE_OK)
|
|
success = -1;
|
|
|
|
if(success == 1)
|
|
{
|
|
return true;
|
|
} else {
|
|
lastErrorMessage = sqlite3_errmsg(_db);
|
|
qWarning() << "updateRecord: " << lastErrorMessage;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool DBBrowserDB::createTable(const QString& name, const sqlb::FieldVector& structure)
|
|
{
|
|
// Build SQL statement
|
|
sqlb::Table table(name);
|
|
for(int i=0;i<structure.size();i++)
|
|
table.addField(structure.at(i));
|
|
|
|
// Execute it and update the schema
|
|
bool result = executeSQL(table.sql());
|
|
updateSchema();
|
|
return result;
|
|
}
|
|
|
|
bool DBBrowserDB::addColumn(const QString& tablename, const sqlb::FieldPtr& field)
|
|
{
|
|
QString sql = QString("ALTER TABLE `%1` ADD COLUMN %2").arg(tablename).arg(field->toString());
|
|
|
|
// Execute it and update the schema
|
|
bool result = executeSQL(sql);
|
|
updateSchema();
|
|
return result;
|
|
}
|
|
|
|
bool DBBrowserDB::renameColumn(const QString& tablename, const QString& name, sqlb::FieldPtr to, int move)
|
|
{
|
|
// NOTE: This function is working around the incomplete ALTER TABLE command in SQLite.
|
|
// If SQLite should fully support this command one day, this entire
|
|
// function can be changed to executing something like this:
|
|
//QString sql;
|
|
//if(to.isNull())
|
|
// sql = QString("ALTER TABLE `%1` DROP COLUMN `%2`;").arg(table).arg(column);
|
|
//else
|
|
// sql = QString("ALTER TABLE `%1` MODIFY `%2` %3").arg(tablename).arg(to).arg(type); // This is wrong...
|
|
//return executeSQL(sql);
|
|
|
|
// Collect information on the current DB layout
|
|
QString tableSql = getObjectByName(tablename).getsql();
|
|
if(tableSql.isEmpty())
|
|
{
|
|
lastErrorMessage = tr("renameColumn: cannot find table %1.").arg(tablename);
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
}
|
|
|
|
// Create table schema
|
|
sqlb::Table oldSchema = sqlb::Table::parseSQL(tableSql).first;
|
|
|
|
// Check if field actually exists
|
|
if(oldSchema.findField(name) == -1)
|
|
{
|
|
lastErrorMessage = tr("renameColumn: cannot find column %1.").arg(name);
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
}
|
|
|
|
// Create savepoint to be able to go back to it in case of any error
|
|
if(!executeSQL("SAVEPOINT sqlitebrowser_rename_column"))
|
|
{
|
|
lastErrorMessage = tr("renameColumn: creating savepoint failed. DB says: %1").arg(lastErrorMessage);
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
}
|
|
|
|
// Create a new table with a name that hopefully doesn't exist yet.
|
|
// Its layout is exactly the same as the one of the table to change - except for the column to change
|
|
// of course
|
|
sqlb::Table newSchema = oldSchema;
|
|
newSchema.setName("sqlitebrowser_rename_column_new_table");
|
|
QString select_cols;
|
|
if(to.isNull())
|
|
{
|
|
// We want drop the column - so just remove the field
|
|
newSchema.removeField(name);
|
|
|
|
for(int i=0;i<newSchema.fields().count();++i)
|
|
select_cols.append(QString("`%1`,").arg(newSchema.fields().at(i)->name()));
|
|
select_cols.chop(1); // remove last comma
|
|
} else {
|
|
// We want to modify it
|
|
|
|
// Move field
|
|
int index = newSchema.findField(name);
|
|
sqlb::FieldPtr temp = newSchema.fields().at(index);
|
|
newSchema.setField(index, newSchema.fields().at(index + move));
|
|
newSchema.setField(index + move, temp);
|
|
|
|
// Get names of fields to select from old table now - after the field has been moved and before it might be renamed
|
|
for(int i=0;i<newSchema.fields().count();++i)
|
|
select_cols.append(QString("`%1`,").arg(newSchema.fields().at(i)->name()));
|
|
select_cols.chop(1); // remove last comma
|
|
|
|
// Modify field
|
|
newSchema.setField(index + move, to);
|
|
}
|
|
|
|
// Create the new table
|
|
if(!executeSQL(newSchema.sql()))
|
|
{
|
|
lastErrorMessage = tr("renameColumn: creating new table failed. DB says: %1").arg(lastErrorMessage);
|
|
qWarning() << lastErrorMessage;
|
|
executeSQL("ROLLBACK TO SAVEPOINT sqlitebrowser_rename_column;");
|
|
return false;
|
|
}
|
|
|
|
// Copy the data from the old table to the new one
|
|
if(!executeSQL(QString("INSERT INTO sqlitebrowser_rename_column_new_table SELECT %1 FROM `%2`;").arg(select_cols).arg(tablename)))
|
|
{
|
|
lastErrorMessage = tr("renameColumn: copying data to new table failed. DB says:\n%1").arg(lastErrorMessage);
|
|
qWarning() << lastErrorMessage;
|
|
executeSQL("ROLLBACK TO SAVEPOINT sqlitebrowser_rename_column;");
|
|
return false;
|
|
}
|
|
|
|
// Save all indices, triggers and views associated with this table because SQLite deletes them when we drop the table in the next step
|
|
QString otherObjectsSql;
|
|
for(objectMap::ConstIterator it=objMap.begin();it!=objMap.end();++it)
|
|
{
|
|
// If this object references the table and it's not the table itself save it's SQL string
|
|
if((*it).getTableName() == tablename && (*it).gettype() != "table")
|
|
otherObjectsSql += (*it).getsql() + "\n";
|
|
}
|
|
|
|
// Delete the old table
|
|
if(!executeSQL(QString("DROP TABLE `%1`;").arg(tablename)))
|
|
{
|
|
lastErrorMessage = tr("renameColumn: deleting old table failed. DB says: %1").arg(lastErrorMessage);
|
|
qWarning() << lastErrorMessage;
|
|
executeSQL("ROLLBACK TO SAVEPOINT sqlitebrowser_rename_column;");
|
|
return false;
|
|
}
|
|
|
|
// Rename the temporary table
|
|
if(!renameTable("sqlitebrowser_rename_column_new_table", tablename))
|
|
{
|
|
executeSQL("ROLLBACK TO SAVEPOINT sqlitebrowser_rename_column;");
|
|
return false;
|
|
}
|
|
|
|
// Restore the saved triggers, views and indices
|
|
if(!executeMultiSQL(otherObjectsSql, true, true))
|
|
{
|
|
QMessageBox::information(0, qApp->applicationName(), tr("Restoring some of the objects associated with this table failed. "
|
|
"This is most likely because some column names changed. "
|
|
"Here's the SQL statement which you might want to fix and execute manually:\n\n")
|
|
+ otherObjectsSql);
|
|
}
|
|
|
|
// Release the savepoint - everything went fine
|
|
if(!executeSQL("RELEASE SAVEPOINT sqlitebrowser_rename_column;"))
|
|
{
|
|
lastErrorMessage = tr("renameColumn: releasing savepoint failed. DB says: %1").arg(lastErrorMessage);
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
}
|
|
|
|
// Success, update the DB schema before returning
|
|
updateSchema();
|
|
return true;
|
|
}
|
|
|
|
bool DBBrowserDB::renameTable(const QString& from_table, const QString& to_table)
|
|
{
|
|
QString sql = QString("ALTER TABLE `%1` RENAME TO `%2`").arg(from_table, to_table);
|
|
if(!executeSQL(sql))
|
|
{
|
|
QString error = tr("Error renaming table '%1' to '%2'."
|
|
"Message from database engine:\n%3").arg(from_table).arg(to_table).arg(lastErrorMessage);
|
|
lastErrorMessage = error;
|
|
qWarning() << lastErrorMessage;
|
|
return false;
|
|
} else {
|
|
updateSchema();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
QStringList DBBrowserDB::getBrowsableObjectNames() const
|
|
{
|
|
objectMap::ConstIterator it;
|
|
QStringList res;
|
|
|
|
for(it=objMap.begin();it!=objMap.end();++it)
|
|
{
|
|
if(it.key() == "table" || it.key() == "view")
|
|
res.append(it.value().getname());
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
objectMap DBBrowserDB::getBrowsableObjects() const
|
|
{
|
|
objectMap::ConstIterator it;
|
|
objectMap res;
|
|
|
|
for(it=objMap.begin();it!=objMap.end();++it)
|
|
{
|
|
if(it.key() == "table" || it.key() == "view")
|
|
res.insert(it.key(), it.value());
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
DBBrowserObject DBBrowserDB::getObjectByName(const QString& name) const
|
|
{
|
|
for (objectMap::ConstIterator it = objMap.begin(); it != objMap.end(); ++it )
|
|
{
|
|
if((*it).getname() == name)
|
|
return *it;
|
|
}
|
|
return DBBrowserObject();
|
|
}
|
|
|
|
void DBBrowserDB::logSQL(QString statement, int msgtype)
|
|
{
|
|
// Replace binary log messages by a placeholder text instead of printing gibberish
|
|
for(int i=0;i<statement.size();i++)
|
|
{
|
|
if(statement.at(i) < 32 && statement.at(i) != '\n')
|
|
{
|
|
statement.truncate(32);
|
|
statement.append(tr("... <string can not be logged, contains binary data> ..."));
|
|
|
|
// early exit if we detect a binary character,
|
|
// to prevent checking all characters in a potential big string
|
|
break;
|
|
}
|
|
}
|
|
|
|
emit sqlExecuted(statement, msgtype);
|
|
}
|
|
|
|
void DBBrowserDB::updateSchema( )
|
|
{
|
|
sqlite3_stmt *vm;
|
|
const char *tail;
|
|
int err=0;
|
|
lastErrorMessage = tr("no error");
|
|
|
|
objMap.clear();
|
|
|
|
// Exit here is no DB is opened
|
|
if(!isOpen())
|
|
return;
|
|
|
|
QString statement = "SELECT type,name,sql,tbl_name FROM sqlite_master UNION SELECT type,name,sql,tbl_name FROM sqlite_temp_master;";
|
|
|
|
QByteArray utf8Statement = statement.toUtf8();
|
|
err=sqlite3_prepare_v2(_db, utf8Statement, utf8Statement.length(),
|
|
&vm, &tail);
|
|
if (err == SQLITE_OK){
|
|
logSQL(statement, kLogMsg_App);
|
|
while ( sqlite3_step(vm) == SQLITE_ROW ){
|
|
QString val1 = QString::fromUtf8((const char*)sqlite3_column_text(vm, 0));
|
|
QString val2 = QString::fromUtf8((const char*)sqlite3_column_text(vm, 1));
|
|
QString val3 = QString::fromUtf8((const char*)sqlite3_column_text(vm, 2));
|
|
QString val4 = QString::fromUtf8((const char*)sqlite3_column_text(vm, 3));
|
|
val3.replace("\r", "");
|
|
|
|
if(val1 == "table" || val1 == "index" || val1 == "view" || val1 == "trigger")
|
|
objMap.insert(val1, DBBrowserObject(val2, val3, val1, val4));
|
|
else
|
|
qWarning() << tr("unknown object type %1").arg(val1);
|
|
}
|
|
sqlite3_finalize(vm);
|
|
}else{
|
|
qWarning() << tr("could not get list of db objects: %1, %2").arg(err).arg(sqlite3_errmsg(_db));
|
|
}
|
|
|
|
//now get the field list for each table
|
|
objectMap::Iterator it;
|
|
for ( it = objMap.begin(); it != objMap.end(); ++it )
|
|
{
|
|
// Use our SQL parser to generate the field list for tables. For views we currently have to fall back to the
|
|
// pragma SQLite offers.
|
|
if((*it).gettype() == "table")
|
|
{
|
|
(*it).table = sqlb::Table::parseSQL((*it).getsql()).first;
|
|
} else if((*it).gettype() == "view") {
|
|
statement = QString("PRAGMA TABLE_INFO(`%1`);").arg((*it).getname());
|
|
logSQL(statement, kLogMsg_App);
|
|
err=sqlite3_prepare_v2(_db,statement.toUtf8(),statement.length(),
|
|
&vm, &tail);
|
|
if (err == SQLITE_OK){
|
|
while ( sqlite3_step(vm) == SQLITE_ROW ){
|
|
if (sqlite3_column_count(vm)==6)
|
|
{
|
|
QString val_name = QString::fromUtf8((const char *)sqlite3_column_text(vm, 1));
|
|
QString val_type = QString::fromUtf8((const char *)sqlite3_column_text(vm, 2));
|
|
|
|
sqlb::FieldPtr f(new sqlb::Field(val_name, val_type));
|
|
(*it).table.addField(f);
|
|
}
|
|
}
|
|
sqlite3_finalize(vm);
|
|
} else{
|
|
lastErrorMessage = tr("could not get types");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QString DBBrowserDB::getPragma(const QString& pragma)
|
|
{
|
|
if(!isOpen())
|
|
return "";
|
|
|
|
QString sql = QString("PRAGMA %1").arg(pragma);
|
|
sqlite3_stmt* vm;
|
|
const char* tail;
|
|
QString retval = "";
|
|
|
|
// Get value from DB
|
|
int err = sqlite3_prepare_v2(_db, sql.toUtf8(), sql.toUtf8().length(), &vm, &tail);
|
|
if(err == SQLITE_OK){
|
|
logSQL(sql, kLogMsg_App);
|
|
if(sqlite3_step(vm) == SQLITE_ROW)
|
|
retval = QString::fromUtf8((const char *) sqlite3_column_text(vm, 0));
|
|
else
|
|
qWarning() << tr("didn't receive any output from pragma %1").arg(pragma);
|
|
|
|
sqlite3_finalize(vm);
|
|
} else {
|
|
qWarning() << tr("could not execute pragma command: %1, %2").arg(err).arg(sqlite3_errmsg(_db));
|
|
}
|
|
|
|
// Return it
|
|
return retval;
|
|
}
|
|
|
|
bool DBBrowserDB::setPragma(const QString& pragma, const QString& value)
|
|
{
|
|
// Set the pragma value
|
|
QString sql = QString("PRAGMA %1 = \"%2\";").arg(pragma).arg(value);
|
|
|
|
save();
|
|
bool res = executeSQL(sql, false, true); // PRAGMA statements are usually not transaction bound, so we can't revert
|
|
if( !res )
|
|
qWarning() << tr("Error setting pragma %1 to %2: %3").arg(pragma).arg(value).arg(lastErrorMessage);
|
|
return res;
|
|
}
|
|
|
|
bool DBBrowserDB::setPragma(const QString& pragma, const QString& value, QString& originalvalue)
|
|
{
|
|
if( originalvalue != value )
|
|
{
|
|
if( setPragma(pragma, value))
|
|
{
|
|
originalvalue = value;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DBBrowserDB::setPragma(const QString& pragma, int value, int& originalvalue)
|
|
{
|
|
if( originalvalue != value )
|
|
{
|
|
QString val = QString::number(value);
|
|
QString origval = QString::number(originalvalue);
|
|
if( setPragma(pragma, val, origval))
|
|
{
|
|
originalvalue = value;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool DBBrowserDB::loadExtension(const QString& filename)
|
|
{
|
|
if(!isOpen())
|
|
return false;
|
|
|
|
// Check if file exists
|
|
if(!QFile::exists(filename))
|
|
{
|
|
lastErrorMessage = tr("File not found.");
|
|
return false;
|
|
}
|
|
|
|
// Try to load extension
|
|
char* error;
|
|
if(sqlite3_load_extension(_db, filename.toUtf8(), 0, &error) == SQLITE_OK)
|
|
{
|
|
return true;
|
|
} else {
|
|
lastErrorMessage = QString::fromUtf8(error);
|
|
sqlite3_free(error);
|
|
return false;
|
|
}
|
|
}
|