ALTER TABLE ADD COLUMN IF NOT EXISTS in SQLite

92

Recentemente, tivemos a necessidade de adicionar colunas a algumas de nossas tabelas de banco de dados SQLite existentes. Isso pode ser feito com ALTER TABLE ADD COLUMN. Claro, se a tabela já foi alterada, queremos deixá-la como está. Infelizmente, o SQLite não oferece suporte a uma IF NOT EXISTScláusula sobre ALTER TABLE.

Nossa solução alternativa atual é executar a instrução ALTER TABLE e ignorar quaisquer erros de "nome de coluna duplicado", assim como este exemplo Python (mas em C ++).

No entanto, nossa abordagem usual para configurar esquemas de banco de dados é ter um script .sql contendo instruções CREATE TABLE IF NOT EXISTSe CREATE INDEX IF NOT EXISTS, que podem ser executados usando sqlite3_execou a sqlite3ferramenta de linha de comando. Não podemos inserir ALTER TABLEesses arquivos de script porque, se essa instrução falhar, tudo o que vier depois não será executado.

Quero ter as definições de tabela em um só lugar e não dividir entre arquivos .sql e .cpp. Existe uma maneira de escrever uma solução alternativa ALTER TABLE ADD COLUMN IF NOT EXISTSem SQLite SQL puro?

dan04
fonte

Respostas:

65

Eu tenho um método SQL 99% puro. A ideia é criar uma versão do seu esquema. Você pode fazer isso de duas maneiras:

  • Use o comando pragma 'user_version' ( PRAGMA user_version) para armazenar um número incremental para a versão do esquema do banco de dados.

  • Armazene seu número de versão em sua própria tabela definida.

Dessa forma, ao iniciar o software, ele pode verificar o esquema do banco de dados e, se necessário, executar sua ALTER TABLEconsulta e, em seguida, incrementar a versão armazenada. Isso é muito melhor do que tentar várias atualizações "às cegas", especialmente se seu banco de dados cresce e muda algumas vezes ao longo dos anos.

MPelletier
fonte
7
Qual é o valor inicial de user_version? Presumo que seja zero, mas seria bom ver isso documentado.
Craig McQueen
Mesmo assim, pode ser feito em SQL puro, já que sqlite não suporta IFe ALTER TABLEnão possui condicional? O que você quer dizer com "SQL 99% puro"?
Craig McQueen
1
@CraigMcQueen Quanto ao valor inicial de user_version, parece ser 0, mas é realmente um valor definido pelo usuário, então você pode fazer seu próprio valor inicial.
MPelletier
7
A questão sobre user_versiono valor inicial é relevante quando você tem um banco de dados existente e nunca o usou user_versionantes, mas quer começar a usá-lo, então você precisa assumir que o sqlite o definiu para um valor inicial específico.
Craig McQueen
1
@CraigMcQueen Concordo, mas não parece estar documentado.
MPelletier
31

Uma solução alternativa é apenas criar as colunas e capturar a exceção / erro que surge se a coluna já existir. Ao adicionar várias colunas, adicione-as em instruções ALTER TABLE separadas para que uma duplicata não evite que as outras sejam criadas.

Com sqlite-net , fizemos algo assim. Não é perfeito, pois não podemos distinguir erros de sqlite duplicados de outros erros de sqlite.

Dictionary<string, string> columnNameToAddColumnSql = new Dictionary<string, string>
{
    {
        "Column1",
        "ALTER TABLE MyTable ADD COLUMN Column1 INTEGER"
    },
    {
        "Column2",
        "ALTER TABLE MyTable ADD COLUMN Column2 TEXT"
    }
};

foreach (var pair in columnNameToAddColumnSql)
{
    string columnName = pair.Key;
    string sql = pair.Value;

    try
    {
        this.DB.ExecuteNonQuery(sql);
    }
    catch (System.Data.SQLite.SQLiteException e)
    {
        _log.Warn(e, string.Format("Failed to create column [{0}]. Most likely it already exists, which is fine.", columnName));
    }
}
angularsen
fonte
27

SQLite também suporta uma instrução pragma chamada "table_info" que retorna uma linha por coluna em uma tabela com o nome da coluna (e outras informações sobre a coluna). Você pode usar isso em uma consulta para verificar a coluna ausente e, se não houver, alterar a tabela.

PRAGMA table_info(foo_table_name)

http://www.sqlite.org/pragma.html#pragma_table_info

Robert Hawkey
fonte
32
Sua resposta seria muito mais excelente se você fornecesse o código para concluir a pesquisa em vez de apenas um link.
Michael Alan Huff
PRAGMA table_info (table_name). Este comando listará cada coluna de table_name como uma linha no resultado. Com base neste resultado, você pode determinar se a coluna existia ou não.
Hao Nguyen de
2
Existe alguma maneira de fazer isso combinando o pragma em parte de uma instrução SQL maior, de forma que a coluna seja adicionada se não existir, mas de outra forma não for, em apenas uma única consulta?
Michael
1
@Michael. Pelo que eu sei, não, você não pode. O problema com o comando PRAGMA é que você não pode consultá-lo. o comando não apresenta dados ao mecanismo SQL, ele retorna os resultados diretamente
Kowlown
1
Isso não cria uma condição de corrida? Digamos que eu verifique os nomes das colunas, vejo que minha coluna está faltando, mas enquanto isso outro processo adiciona a coluna. Em seguida, tentarei adicionar a coluna, mas obterei um erro porque ela já existe. Acho que devo bloquear o banco de dados primeiro ou algo assim? Eu sou um novato em sqlite, estou com medo :).
Ben Farmer
26

Se você estiver fazendo isso em uma instrução de atualização de banco de dados, talvez a maneira mais simples seja apenas capturar a exceção lançada se você estiver tentando adicionar um campo que já exista.

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}
user7896780
fonte
3
Não gosto de programação estilo exceção, mas é incrivelmente limpa. Talvez você tenha me influenciado um pouco.
Stephen J
Eu também não gosto, mas C ++ é a linguagem de programação de estilo mais excepcional de todos os tempos. Então, eu acho que ainda pode ser visto como "válido".
poderoso
Meu caso de uso para SQLite = Não quero fazer uma tonelada de codificação extra para algo estúpido simples / um liner em outras linguagens (MSSQL). Boa resposta ... embora seja "programação de estilo de exceção", está em uma função de atualização / isolada, então suponho que seja aceitável.
maplemale,
Enquanto outros não gostam, acho que esta é a melhor solução lol
Adam Varhegyi
13

Outro é um método de PRAGMA é table_info (table_name), ele retorna todas as informações da tabela.

Aqui está a implementação de como usá-lo para verificar se a coluna existe ou não,

    public boolean isColumnExists (String table, String column) {
         boolean isExists = false
         Cursor cursor;
         try {           
            cursor = db.rawQuery("PRAGMA table_info("+ table +")", null);
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    String name = cursor.getString(cursor.getColumnIndex("name"));
                    if (column.equalsIgnoreCase(name)) {
                        isExists = true;
                        break;
                    }
                }
            }

         } finally {
            if (cursor != null && !cursor.isClose()) 
               cursor.close();
         }
         return isExists;
    }

Você também pode usar esta consulta sem usar loop,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);
Krunal Shah
fonte
Cursor cursor = db.rawQuery ("select * from tableName", null); colunas = cursor.getColumnNames ();
Vahe Gharibyan
1
Acho que você esqueceu de fechar o cursor :-)
Pecana
@VaheGharibyan, então você simplesmente selecionará tudo em seu banco de dados apenas para obter os nomes das colunas ?! O que você está simplesmente dizendo é we give no shit about performance:)).
Farid
Observe que a última consulta está incorreta. A consulta adequada é: SELECT * FROM pragma_table_info(...)(observe o SELECT e o sublinhado entre o pragma e as informações da tabela). Não tenho certeza de qual versão eles realmente adicionaram, ele não funcionou no 3.16.0, mas funciona no 3.22.0.
PressingOnAlways
3

Para aqueles que desejam usar pragma table_info()o resultado de como parte de um SQL maior.

select count(*) from
pragma_table_info('<table_name>')
where name='<column_name>';

A parte principal é usar em pragma_table_info('<table_name>')vez de pragma table_info('<table_name>').


Esta resposta foi inspirada na resposta de @Robert Hawkey. A razão de eu postar como uma nova resposta é que não tenho reputação suficiente para postar como comentário.

Sol
fonte
1

Eu vim com esta pergunta

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • A consulta interna retornará 0 ou 1 se a coluna existir.
  • Com base no resultado, altere a coluna
Aravin
fonte
code = Error (1), message = System.Data.SQLite.SQLiteException (0x800007BF): erro lógico SQL próximo a "ALTER": erro de sintaxe em System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ
Você tem um erro de digitação com as 2 aspas simples ao redor das strings (product e purchaseCopy), mas não consigo fazer isso funcionar por causa do "THEN ALTER TABLE". Tem certeza que é possível? Se funcionar, deve ser a resposta aceita.
Neekobus
0

Peguei a resposta acima em C # / .Net e a reescrevi para Qt / C ++, não muito mudado, mas eu queria deixá-la aqui para qualquer pessoa no futuro procurando por uma resposta 'ish' em C ++.

    bool MainWindow::isColumnExisting(QString &table, QString &columnName){

    QSqlQuery q;

    try {
        if(q.exec("PRAGMA table_info("+ table +")"))
            while (q.next()) {
                QString name = q.value("name").toString();     
                if (columnName.toLower() == name.toLower())
                    return true;
            }

    } catch(exception){
        return false;
    }
    return false;
}
Kevin B Burns
fonte
0

Como alternativa, você pode usar a instrução CASE-WHEN TSQL em combinação com pragma_table_info para saber se existe uma coluna:

select case(CNT) 
    WHEN 0 then printf('not found')
    WHEN 1 then printf('found')
    END
FROM (SELECT COUNT(*) AS CNT FROM pragma_table_info('myTableName') WHERE name='columnToCheck') 
kevinH
fonte
aqui, como alteramos a mesa? quando há correspondência de nome de coluna?
user2700767
0

Aqui está minha solução, mas em python (tentei e não consegui encontrar nenhuma postagem sobre o tópico relacionado a python):

# modify table for legacy version which did not have leave type and leave time columns of rings3 table.
sql = 'PRAGMA table_info(rings3)' # get table info. returns an array of columns.
result = inquire (sql) # call homemade function to execute the inquiry
if len(result)<= 6: # if there are not enough columns add the leave type and leave time columns
    sql = 'ALTER table rings3 ADD COLUMN leave_type varchar'
    commit(sql) # call homemade function to execute sql
    sql = 'ALTER table rings3 ADD COLUMN leave_time varchar'
    commit(sql)

Usei o PRAGMA para obter as informações da tabela. Ele retorna uma matriz multidimensional cheia de informações sobre colunas - uma matriz por coluna. Eu conto o número de matrizes para obter o número de colunas. Se não houver colunas suficientes, adiciono as colunas usando o comando ALTER TABLE.

Thomas Weeks
fonte
0

Todas essas respostas estão bem se você executar uma linha de cada vez. No entanto, a questão original era inserir um script sql que seria executado por um único db execute e todas as soluções (como verificar se a coluna está lá antes do tempo) exigiriam que o programa em execução tivesse conhecimento de quais tabelas e colunas estão sendo alteradas / adicionadas ou fazem pré-processamento e análise do script de entrada para determinar essas informações. Normalmente, você não vai executar isso em tempo real ou com frequência. Portanto, a ideia de detectar uma exceção é aceitável e depois seguir em frente. É aí que reside o problema ... como seguir em frente. Felizmente, a mensagem de erro nos fornece todas as informações de que precisamos para fazer isso. A idéia é executar o sql se houver exceções em uma chamada alter table, podemos encontrar a linha alter table no sql e retornar as linhas restantes e executar até que tenha sucesso ou nenhuma linha de alter table correspondente possa ser encontrada. Aqui está um exemplo de código onde temos scripts sql em uma matriz. Nós iteramos o array executando cada script. Nós o chamamos duas vezes para fazer com que o comando alter table falhe, mas o programa é bem-sucedido porque removemos o comando alter table do sql e reexecutamos o código atualizado.

#!/bin/sh
# the next line restarts using wish \

exec /opt/usr8.6.3/bin/tclsh8.6  "$0" ${1+"$@"}
foreach pkg {sqlite3 } {
    if { [ catch {package require {*}$pkg } err ] != 0 } {
    puts stderr "Unable to find package $pkg\n$err\n ... adjust your auto_path!";
    }
}
array set sqlArray {
    1 {
    CREATE TABLE IF NOT EXISTS Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      );
    CREATE TABLE IF NOT EXISTS Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        );
    INSERT INTO Version(version) values('1.0');
    }
    2 {
    CREATE TABLE IF NOT EXISTS Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        );
    ALTER TABLE Notes ADD COLUMN dump text;
    INSERT INTO Version(version) values('2.0');
    }
    3 {
    ALTER TABLE Version ADD COLUMN sql text;
    INSERT INTO Version(version) values('3.0');
    }
}

# create db command , use in memory database for demonstration purposes
sqlite3 db :memory:

proc createSchema { sqlArray } {
    upvar $sqlArray sql
    # execute each sql script in order 
    foreach version [lsort -integer [array names sql ] ] {
    set cmd $sql($version)
    set ok 0
    while { !$ok && [string length $cmd ] } {  
        try {
        db eval $cmd
        set ok 1  ;   # it succeeded if we get here
        } on error { err backtrace } {
        if { [regexp {duplicate column name: ([a-zA-Z0-9])} [string trim $err ] match columnname ] } {
            puts "Error:  $err ... trying again" 
            set cmd [removeAlterTable $cmd $columnname ]
        } else {
            throw DBERROR "$err\n$backtrace"
        }
        }
    }
    }
}
# return sqltext with alter table command with column name removed
# if no matching alter table line found or result is no lines then
# returns ""
proc removeAlterTable { sqltext columnname } {
    set mode skip
    set result [list]
    foreach line [split $sqltext \n ] {
    if { [string first "alter table" [string tolower [string trim $line] ] ] >= 0 } {
        if { [string first $columnname $line ] } {
        set mode add
        continue;
        }
    }
    if { $mode eq "add" } {
        lappend result $line
    }
    }
    if { $mode eq "skip" } {
    puts stderr "Unable to find matching alter table line"
    return ""
    } elseif { [llength $result ] }  { 
    return [ join $result \n ]
    } else {
    return ""
    }
}
               
proc printSchema { } {
    db eval { select * from sqlite_master } x {
    puts "Table: $x(tbl_name)"
    puts "$x(sql)"
    puts "-------------"
    }
}
createSchema sqlArray
printSchema
# run again to see if we get alter table errors 
createSchema sqlArray
printSchema

saída esperada

Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------
Error:  duplicate column name: dump ... trying again
Error:  duplicate column name: sql ... trying again
Table: Notes
CREATE TABLE Notes (
                      id INTEGER PRIMARY KEY AUTOINCREMENT,
                      name text,
                      note text,
                      createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                      updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
                      , dump text)
-------------
Table: sqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
-------------
Table: Version
CREATE TABLE Version (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        version text,
                        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
                        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) )
                        , sql text)
-------------
Table: Tags
CREATE TABLE Tags (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name text,
        tag text,
        createdDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) ,
        updatedDate integer(4) DEFAULT ( cast( strftime('%s', 'now') as int ) ) 
        )
-------------
Cjolly
fonte
0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

Lógica: a coluna sql em sqlite_master contém a definição da tabela, portanto, certamente contém a string com o nome da coluna.

Como você está procurando por uma string secundária, ela tem suas limitações óbvias. Portanto, eu sugeriria usar uma subcadeia de caracteres ainda mais restritiva em ColumnName, por exemplo, algo assim (sujeito a testes, pois o caractere '`' nem sempre está lá):

select * from sqlite_master where type = 'table' and tbl_name = 'MyTable' and sql like '%`MyColumn` TEXT%'
Jaro B
fonte
0

Resolvi em 2 consultas. Este é meu script Unity3D usando System.Data.SQLite.

IDbCommand command = dbConnection.CreateCommand();
            command.CommandText = @"SELECT count(*) FROM pragma_table_info('Candidat') c WHERE c.name = 'BirthPlace'";
            IDataReader reader = command.ExecuteReader();
            while (reader.Read())
            {
                try
                {
                    if (int.TryParse(reader[0].ToString(), out int result))
                    {
                        if (result == 0)
                        {
                            command = dbConnection.CreateCommand();
                            command.CommandText = @"ALTER TABLE Candidat ADD COLUMN BirthPlace VARCHAR";
                            command.ExecuteNonQuery();
                            command.Dispose();
                        }
                    }
                }
                catch { throw; }
            }
イ ン コ グ ニ ト ア レ ク セ イ
fonte