Adição de Colunas em Bancos de Dados SQLite com Compatibilidade de Versões no Qt

Estrutura do Projeto

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlRecord>
#include <QSqlError>

class MainWindow : public QMainWindow {
    Q_OBJECT

public:
    explicit MainWindow(QWidget* parent = nullptr);
    ~MainWindow();

private:
    bool configurarBaseDados();

    QSqlDatabase m_dbConn;
};

#endif

Implementação com Qt SQL

A estratégia consiste em comparar a estrutura de tabelas esperada (definida como string SQL de criação) com a estrutura atual no banco de dados. Se houverr colunas extras na definição esperada, elas são adicionadas via ALTER TABLE.

#include "mainwindow.h"
#include <QApplication>
#include <QDir>
#include <QDebug>

// Definições esperadas das tabelas (versão atual)
const QStringList TABELAS_ESPERADAS = {
    "CREATE TABLE usuarios(id INTEGER, idade INTEGER, nome VARCHAR, cargo VARCHAR)",
    "CREATE TABLE produtos(id INTEGER, quantidade INTEGER, descricao VARCHAR, preco REAL)"
};

const QStringList NOMES_TABELAS = { "usuarios", "produtos" };

MainWindow::MainWindow(QWidget* parent)
    : QMainWindow(parent)
{
    configurarBaseDados();
}

MainWindow::~MainWindow() {}

bool MainWindow::configurarBaseDados()
{
    m_dbConn = QSqlDatabase::addDatabase("QSQLITE", "conexao_principal");

    QString dirDados = QApplication::applicationDirPath() + "/dados";
    QString caminhoBanco = dirDados + "/app.db";

    QDir dir(dirDados);
    if (!dir.exists()) {
        dir.mkpath(dirDados);
    }

    m_dbConn.setDatabaseName(caminhoBanco);

    if (!m_dbConn.open()) {
        qCritical() << "Falha ao abrir banco:" << m_dbConn.lastError().text();
        return false;
    }

    QStringList tabelasExistentes = m_dbConn.tables();
    QSqlQuery query(m_dbConn);

    for (int idx = 0; idx < NOMES_TABELAS.size(); ++idx) {
        QString nomeTabela = NOMES_TABELAS[idx];
        QString sqlCriacao = TABELAS_ESPERADAS[idx];

        if (tabelasExistentes.contains(nomeTabela)) {
            // Obter colunas atuais da tabela
            QString sqlSelect = QString("SELECT * FROM %1 LIMIT 1").arg(nomeTabela);
            if (!query.exec(sqlSelect)) {
                qWarning() << "Erro ao consultar:" << query.lastError().text();
                continue;
            }

            QStringList colunasAtuais;
            QSqlRecord registro = query.record();
            for (int c = 0; c < registro.count(); ++c) {
                colunasAtuais.append(registro.fieldName(c));
            }

            // Extrair definição de colunas do SQL esperado
            QString parteColunas = sqlCriacao.mid(
                sqlCriacao.indexOf("(") + 1,
                sqlCriacao.indexOf(")") - sqlCriacao.indexOf("(") - 1
            );
            parteColunas.remove("\n");
            QStringList definicaoColunas = parteColunas.split(",");

            // Verificar colunas que precisam ser adicionadas
            for (int k = colunasAtuais.size(); k < definicaoColunas.size(); ++k) {
                QString defColuna = definicaoColunas[k].trimmed();
                if (!defColuna.isEmpty()) {
                    QString nomeColuna = defColuna.split(" ").first();

                    if (!colunasAtuais.contains(nomeColuna)) {
                        QString sqlAlter = QString("ALTER TABLE %1 ADD COLUMN %2")
                            .arg(nomeTabela)
                            .arg(defColuna.trimmed());

                        qDebug() << "Executando:" << sqlAlter;
                        if (!query.exec(sqlAlter)) {
                            qWarning() << "Erro ao adicionar coluna:" << query.lastError().text();
                        }
                    }
                }
            }
        } else {
            // Criar tabela do zero
            if (!query.exec(sqlCriacao)) {
                qWarning() << "Erro ao criar tabela:" << query.lastError().text();
            }
        }
    }

    return true;
}

Abordagem Alternativa com API nativa do SQLite3

Para casos onde se prefere ou necessita usar a API C do SQLite diretamente, o mesmo princípio de comparação de colunas pode ser aplicado usando sqlite3_prepare_v2 e sqlite3_step com a consulta pragma_table_info.

#include <sqlite3.h>

bool atualizarEsquemaComSqlite3()
{
    QString dirDados = QApplication::applicationDirPath() + "/dados";
    QString caminhoBanco = dirDados + "/app.db";

    sqlite3* handle = nullptr;
    if (sqlite3_open(caminhoBanco.toStdString().c_str(), &handle) != SQLITE_OK) {
        qCritical() << sqlite3_errmsg(handle);
        return false;
    }

    char* msgErro = nullptr;
    sqlite3_stmt* stmt = nullptr;

    for (int idx = 0; idx < NOMES_TABELAS.size(); ++idx) {
        QString nomeTabela = NOMES_TABELAS[idx];
        QString sqlCriacao = TABELAS_ESPERADAS[idx];

        // Consultar colunas existentes via pragma_table_info
        QString sqlInfo = QString("SELECT * FROM pragma_table_info('%1')").arg(nomeTabela);
        QStringList colunasExistentes;

        if (sqlite3_prepare_v2(handle, sqlInfo.toStdString().c_str(), -1, &stmt, nullptr) == SQLITE_OK) {
            while (sqlite3_step(stmt) == SQLITE_ROW) {
                const char* nomeCol = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
                colunasExistentes.append(QString::fromUtf8(nomeCol));
            }
        }
        sqlite3_finalize(stmt);
        stmt = nullptr;

        // Extrair colunas esperadas do SQL de criação
        QString trechoColunas = sqlCriacao.mid(
            sqlCriacao.indexOf("(") + 1,
            sqlCriacao.indexOf(")") - sqlCriacao.indexOf("(") - 1
        );
        trechoColunas.remove("\n");
        QStringList listaColunasEsperadas = trechoColunas.split(",");

        if (!colunasExistentes.isEmpty() && listaColunasEsperadas.size() != colunasExistentes.size()) {
            for (int k = colunasExistentes.size(); k < listaColunasEsperadas.size(); ++k) {
                QString defColuna = listaColunasEsperadas[k].trimmed();
                if (!defColuna.isEmpty()) {
                    QString nomeColuna = defColuna.split(" ").first();

                    if (!colunasExistentes.contains(nomeColuna)) {
                        QString sqlAlter = QString("ALTER TABLE %1 ADD COLUMN %2")
                            .arg(nomeTabela)
                            .arg(defColuna);

                        qDebug() << "Adicionando coluna:" << sqlAlter;
                        if (sqlite3_exec(handle, sqlAlter.toStdString().c_str(), nullptr, nullptr, &msgErro) != SQLITE_OK) {
                            qWarning() << "Erro:" << sqlite3_errmsg(handle);
                            sqlite3_free(msgErro);
                        }
                    }
                }
            }
        } else if (colunasExistentes.isEmpty()) {
            // Tabela não existe - criar
            if (sqlite3_exec(handle, sqlCriacao.toStdString().c_str(), nullptr, nullptr, &msgErro) != SQLITE_OK) {
                qWarning() << "Erro ao criar:" << sqlite3_errmsg(handle);
                sqlite3_free(msgErro);
            }
        }
    }

    sqlite3_close(handle);
    return true;
}

Em ambos os métodos, ao modificar as definições em TABELAS_ESPERADAS para incluir novas colunas, o mecanismo detecta automaticamente a diferença entre o esquema atual e o esperado, aplicando apenas as adições necessárias sem comprometer dados pré-existentes.

Tags: Qt sqlite QSqlDatabase QSqlQuery ALTER TABLE

Publicado em 9-23 01:04