ALTER TABLE AGGIUNGI COLONNA SE NON ESISTE in SQLite


92

Recentemente abbiamo avuto la necessità di aggiungere colonne ad alcune delle nostre tabelle di database SQLite esistenti. Questo può essere fatto con ALTER TABLE ADD COLUMN. Ovviamente, se la tabella è già stata modificata, vogliamo lasciarla sola. Sfortunatamente, SQLite non supporta una IF NOT EXISTSclausola su ALTER TABLE.

La nostra soluzione attuale è eseguire l'istruzione ALTER TABLE e ignorare qualsiasi errore di "nome colonna duplicato", proprio come questo esempio di Python (ma in C ++).

Tuttavia, il nostro approccio abituale alla configurazione degli schemi di database consiste nell'avere uno script .sql contenente istruzioni CREATE TABLE IF NOT EXISTSe CREATE INDEX IF NOT EXISTS, che può essere eseguito utilizzando sqlite3_execosqlite3 strumento della riga di comando. Non possiamo inserire ALTER TABLEquesti file di script perché se quell'istruzione fallisce, qualsiasi cosa dopo non verrà eseguita.

Voglio avere le definizioni delle tabelle in un unico posto e non dividerle tra i file .sql e .cpp. C'è un modo per scrivere una soluzione alternativa ALTER TABLE ADD COLUMN IF NOT EXISTSin puro SQLite SQL?

Risposte:


65

Ho un metodo SQL puro al 99%. L'idea è di rivedere il tuo schema. Puoi farlo in due modi:

  • Utilizzare il comando pragma "user_version" ( PRAGMA user_version) per memorizzare un numero incrementale per la versione dello schema del database.

  • Memorizza il tuo numero di versione nella tua tabella definita.

In questo modo, quando il software viene avviato, può controllare lo schema del database e, se necessario, eseguire la ALTER TABLEquery, quindi incrementare la versione memorizzata. Questo è di gran lunga migliore che tentare vari aggiornamenti "alla cieca", soprattutto se il database cresce e cambia alcune volte nel corso degli anni.


7
Qual è il valore iniziale di user_version? Presumo zero, ma sarebbe bello vederlo documentato.
Craig McQueen

Anche con questo, può essere fatto in SQL puro, poiché sqlite non supporta IFe ALTER TABLEnon ha un condizionale? Cosa intendi con "SQL puro al 99%"?
Craig McQueen

1
@CraigMcQueen Per quanto riguarda il valore iniziale di user_version, sembra essere 0, ma in realtà è un valore definito dall'utente, quindi puoi creare il tuo valore iniziale.
MPelletier

7
La domanda sul user_versionvalore iniziale è rilevante quando hai un database esistente e non l'hai mai usato user_versionprima, ma vuoi iniziare a usarlo, quindi devi presumere che sqlite lo abbia impostato su un particolare valore iniziale.
Craig McQueen

1
@ CraigMcQueen Sono d'accordo, ma non sembra essere documentato.
MPelletier

31

Una soluzione alternativa è creare semplicemente le colonne e rilevare l'eccezione / errore che si verifica se la colonna esiste già. Quando si aggiungono più colonne, aggiungerle in istruzioni ALTER TABLE separate in modo che un duplicato non impedisca la creazione degli altri.

Con sqlite-net , abbiamo fatto qualcosa di simile. Non è perfetto, poiché non possiamo distinguere gli errori sqlite duplicati da altri errori 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));
    }
}

27

SQLite supporta anche un'istruzione pragma chiamata "table_info" che restituisce una riga per colonna in una tabella con il nome della colonna (e altre informazioni sulla colonna). È possibile utilizzarlo in una query per verificare la colonna mancante e, se non presente, modificare la tabella.

PRAGMA table_info(foo_table_name)

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


32
La tua risposta sarebbe molto più eccellente se fornissi il codice con cui completare la ricerca invece di un semplice collegamento.
Michael Alan Huff

PRAGMA tabella_info (nome_tabella). Questo comando elencherà ogni colonna di table_name come una riga nel risultato. In base a questo risultato, è possibile determinare se la colonna esisteva o meno.
Hao Nguyen

2
C'è un modo per farlo combinando il pragma in una parte di un'istruzione SQL più grande in modo tale che la colonna venga aggiunta se non esiste ma altrimenti non lo è, in una sola query?
Michael

1
@Michael. Per quanto ne so, no non puoi. Il problema con il comando PRAGMA è che non puoi interrogarlo. il comando non presenta i dati al motore SQL, restituisce direttamente i risultati
Kowlown

1
Questo non crea una condizione di gara? Diciamo che controllo i nomi delle colonne, vedo che la mia colonna è mancante, ma nel frattempo un altro processo aggiunge la colonna. Quindi cercherò di aggiungere la colonna ma riceverò un errore perché esiste già. Immagino che dovrei prima bloccare il database o qualcosa del genere? Sono un noob di sqlite temo :).
Ben Farmer

26

Se lo stai facendo in un'istruzione di aggiornamento DB, forse il modo più semplice è quello di catturare l'eccezione generata se stai tentando di aggiungere un campo che potrebbe già esistere.

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}

3
Non mi piace la programmazione in stile eccezioni, ma è straordinariamente pulita. Forse mi hai influenzato un po '.
Stephen J

Non mi piace nemmeno, ma C ++ è il linguaggio di programmazione con lo stile più eccezionale di sempre. Quindi immagino che si possa ancora vederlo come "valido".
tmighty

Il mio caso d'uso per SQLite = non voglio fare un sacco di codice extra per qualcosa di stupido semplice / una riga in altri linguaggi (MSSQL). Buona risposta ... sebbene sia "programmazione in stile eccezione" è in una funzione di aggiornamento / isolata quindi suppongo sia accettabile.
maplemale

Mentre ad altri non piace, penso che questa sia la soluzione migliore lol
Adam Varhegyi,

13

threre è un metodo di PRAGMA è table_info (table_name), restituisce tutte le informazioni di table.

Ecco l'implementazione come usarlo per controllare che la colonna esista o meno,

    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;
    }

Puoi anche usare questa query senza usare il loop,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);

Cursore cursore = db.rawQuery ("seleziona * da tableName", null); colonne = cursor.getColumnNames ();
Vahe Gharibyan

1
Immagino ti sia dimenticato di chiudere il cursore :-)
Pecana

@VaheGharibyan, quindi dovrai semplicemente selezionare tutto nel tuo DB solo per ottenere i nomi delle colonne ?! Quello che stai semplicemente dicendo è we give no shit about performance:)).
Farid

Nota, l'ultima query non è corretta. La query corretta è: SELECT * FROM pragma_table_info(...)(nota SELEZIONA e il carattere di sottolineatura tra pragma e informazioni sulla tabella). Non sono sicuro in quale versione l'hanno effettivamente aggiunto, non ha funzionato su 3.16.0 ma funziona su 3.22.0.
PressingOnAlways

3

Per coloro che desiderano utilizzare pragma table_info()il risultato di come parte di un SQL più ampio.

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

La parte fondamentale è usare pragma_table_info('<table_name>')invece di pragma table_info('<table_name>').


Questa risposta è ispirata dalla risposta di @Robert Hawkey. Il motivo per cui lo pubblico come nuova risposta è che non ho abbastanza reputazione per pubblicarlo come commento.


1

Mi viene in mente questa domanda

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • La query interna restituirà 0 o 1 se la colonna esiste.
  • In base al risultato, modifica la colonna

code = Errore (1), messaggio = System.Data.SQLite.SQLiteException (0x800007BF): errore logico SQL vicino a "ALTER": errore di sintassi in System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ

Hai un errore di battitura con le 2 semplici virgolette attorno alle stringhe (product e purchaseCopy) ma non riesco a farlo funzionare a causa di "THEN ALTER TABLE". Sei sicuro che sia possibile? Se funziona, dovrebbe essere la risposta accettata.
Neekobus


0

Ho preso la risposta sopra in C # /. Net e l'ho riscritta per Qt / C ++, non molto cambiata, ma volevo lasciarla qui per chiunque in futuro cerchi una risposta "ish" in 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;
}

0

In alternativa, puoi utilizzare l'istruzione CASE-WHEN TSQL in combinazione con pragma_table_info per sapere se esiste una colonna:

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') 

ecco come si altera la tavola? quando c'è la corrispondenza del nome della colonna?
user2700767

0

Ecco la mia soluzione, ma in python (ho provato e non sono riuscito a trovare alcun post sull'argomento relativo 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)

Ho usato PRAGMA per ottenere le informazioni sulla tabella. Restituisce un array multidimensionale pieno di informazioni sulle colonne: un array per colonna. Conto il numero di array per ottenere il numero di colonne. Se non ci sono abbastanza colonne, aggiungo le colonne usando il comando ALTER TABLE.


0

Tutte queste risposte vanno bene se esegui una riga alla volta. Tuttavia, la domanda originale era quella di inserire uno script sql che sarebbe stato eseguito da un singolo db eseguito e tutte le soluzioni (come il controllo per vedere se la colonna è presente in anticipo) richiederebbero che il programma in esecuzione conoscesse quali tabelle e le colonne vengono modificate / aggiunte o eseguire la pre-elaborazione e l'analisi dello script di input per determinare queste informazioni. In genere non lo eseguirai in tempo reale o spesso. Quindi l'idea di catturare un'eccezione è accettabile e poi andare avanti. Qui sta il problema ... come andare avanti. Fortunatamente il messaggio di errore ci fornisce tutte le informazioni di cui abbiamo bisogno per farlo. L'idea è di eseguire sql se fa eccezione su una chiamata di alter table possiamo trovare la riga di alter table in sql e restituire le righe rimanenti ed eseguire fino a quando non riesce o non è possibile trovare più righe di alter table corrispondenti. Ecco un po 'di codice di esempio in cui abbiamo script sql in un array. Si itera l'array eseguendo ogni script. Lo chiamiamo due volte per far fallire il comando alter table ma il programma riesce perché rimuoviamo il comando alter table da sql e rieseguiamo il codice aggiornato.

#!/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

output previsto

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 ) ) 
        )
-------------

0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

Logica: la colonna sql in sqlite_master contiene la definizione della tabella, quindi contiene certamente una stringa con il nome della colonna.

Poiché stai cercando una sottostringa, ha i suoi ovvi limiti. Quindi suggerirei di utilizzare una sottostringa ancora più restrittiva in ColumnName, ad esempio qualcosa di simile (soggetto a test poiché il carattere '``' non è sempre presente):

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

0

Lo risolvo in 2 domande. Questo è il mio script Unity3D che utilizza 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; }
            }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.