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 EXISTS
clá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 EXISTS
e CREATE INDEX IF NOT EXISTS
, que podem ser executados usando sqlite3_exec
ou a sqlite3
ferramenta de linha de comando. Não podemos inserir ALTER TABLE
esses 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 EXISTS
em SQLite SQL puro?
fonte
user_version
? Presumo que seja zero, mas seria bom ver isso documentado.IF
eALTER TABLE
não possui condicional? O que você quer dizer com "SQL 99% puro"?user_version
, parece ser 0, mas é realmente um valor definido pelo usuário, então você pode fazer seu próprio valor inicial.user_version
o valor inicial é relevante quando você tem um banco de dados existente e nunca o usouuser_version
antes, mas quer começar a usá-lo, então você precisa assumir que o sqlite o definiu para um valor inicial específico.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)); } }
fonte
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
fonte
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()); }
fonte
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);
fonte
we give no shit about performance
:)).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.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 depragma 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.
fonte
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
fonte
Caso você esteja tendo esse problema no flex / adobe air e se encontre aqui primeiro, encontrei uma solução, e postei sobre uma questão relacionada: ADICIONE COLUNA ao sqlite db SE NÃO EXISTE - flex / air sqlite?
Meu comentário aqui: https://stackoverflow.com/a/24928437/2678219
fonte
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; }
fonte
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')
fonte
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.
fonte
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 ) ) ) -------------
fonte
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%'
fonte
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