ALTER TABLE ADD COLUMN, falls in SQLite nicht vorhanden

87

Vor kurzem mussten wir einigen unserer vorhandenen SQLite-Datenbanktabellen Spalten hinzufügen. Dies kann mit gemacht werden ALTER TABLE ADD COLUMN. Wenn die Tabelle bereits geändert wurde, möchten wir sie natürlich in Ruhe lassen. Leider unterstützt SQLite keine IF NOT EXISTSKlausel für ALTER TABLE.

Unsere aktuelle Problemumgehung besteht darin, die Anweisung ALTER TABLE auszuführen und alle Fehler "doppelter Spaltenname" zu ignorieren, genau wie in diesem Python-Beispiel (jedoch in C ++).

Unser üblicher Ansatz zum Einrichten von Datenbankschemata besteht jedoch darin, ein .sql-Skript mit CREATE TABLE IF NOT EXISTSund CREATE INDEX IF NOT EXISTSAnweisungen zu haben, die mit sqlite3_execoder über das sqlite3Befehlszeilentool ausgeführt werden können . Wir können ALTER TABLEdiese Skriptdateien nicht einfügen, da diese Anweisung nicht ausgeführt wird, wenn sie fehlschlägt.

Ich möchte die Tabellendefinitionen an einem Ort haben und nicht zwischen SQL- und CPP-Dateien aufteilen. Gibt es eine Möglichkeit, eine Problemumgehung ALTER TABLE ADD COLUMN IF NOT EXISTSin reinem SQLite SQL zu schreiben ?

dan04
quelle

Antworten:

63

Ich habe eine zu 99% reine SQL-Methode. Die Idee ist, Ihr Schema zu versionieren. Sie können dies auf zwei Arten tun:

  • Verwenden Sie den Pragma-Befehl 'user_version' ( PRAGMA user_version), um eine inkrementelle Nummer für Ihre Datenbankschemaversion zu speichern.

  • Speichern Sie Ihre Versionsnummer in Ihrer eigenen definierten Tabelle.

Auf diese Weise kann die Software beim Starten das Datenbankschema überprüfen und bei Bedarf Ihre ALTER TABLEAbfrage ausführen und dann die gespeicherte Version erhöhen. Dies ist bei weitem besser als der Versuch, verschiedene Updates "blind" zu machen, insbesondere wenn Ihre Datenbank im Laufe der Jahre einige Male wächst und sich ändert.

MPelletier
quelle
7
Was ist der Anfangswert von user_version? Ich gehe von Null aus, aber es wäre schön, wenn das dokumentiert wäre.
Craig McQueen
Kann dies auch in reinem SQL durchgeführt werden, da SQLite nicht unterstützt wird IFund das ALTER TABLEkeine Bedingung hat? Was meinst du mit "99% reines SQL"?
Craig McQueen
1
@CraigMcQueen Der Anfangswert von user_versionscheint 0 zu sein, aber es ist wirklich ein benutzerdefinierter Wert, sodass Sie Ihren eigenen Anfangswert erstellen können.
MPelletier
7
Die Frage zum user_versionAnfangswert ist relevant, wenn Sie über eine vorhandene Datenbank verfügen und diese noch nie verwendet user_versionhaben, diese jedoch verwenden möchten. Daher müssen Sie davon ausgehen, dass sqlite sie auf einen bestimmten Anfangswert festgelegt hat.
Craig McQueen
1
@CraigMcQueen Ich stimme zu, aber es scheint nicht dokumentiert zu sein.
MPelletier
30

Eine Problemumgehung besteht darin, nur die Spalten zu erstellen und die Ausnahme / den Fehler abzufangen, die auftreten, wenn die Spalte bereits vorhanden ist. Wenn Sie mehrere Spalten hinzufügen, fügen Sie diese in separaten ALTER TABLE-Anweisungen hinzu, damit ein Duplikat nicht verhindert, dass die anderen erstellt werden.

Mit sqlite-net haben wir so etwas gemacht. Es ist nicht perfekt, da wir doppelte SQLite-Fehler nicht von anderen SQLite-Fehlern unterscheiden können.

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
quelle
27

SQLite unterstützt auch eine Pragma-Anweisung namens "table_info", die eine Zeile pro Spalte in einer Tabelle mit dem Namen der Spalte (und anderen Informationen zur Spalte) zurückgibt. Sie können dies in einer Abfrage verwenden, um nach der fehlenden Spalte zu suchen und, falls nicht vorhanden, die Tabelle zu ändern.

PRAGMA table_info(foo_table_name)

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

Robert Hawkey
quelle
29
Ihre Antwort wäre viel besser, wenn Sie den Code für die Durchführung dieser Suche anstelle eines Links angeben würden.
Michael Alan Huff
PRAGMA table_info (table_name). Dieser Befehl listet jede Spalte des Tabellennamens als Zeile im Ergebnis auf. Anhand dieses Ergebnisses können Sie feststellen, ob die Spalte vorhanden ist oder nicht.
Hao Nguyen
2
Gibt es eine Möglichkeit, dies zu tun, indem das Pragma in einem Teil einer größeren SQL-Anweisung so kombiniert wird, dass die Spalte hinzugefügt wird, wenn sie nicht vorhanden ist, ansonsten aber nicht, nur in einer einzigen Abfrage?
Michael
1
@ Michael. Soweit ich weiß, nein, das kannst du nicht. Das Problem mit dem PRAGMA-Befehl ist, dass Sie ihn nicht abfragen können. Der Befehl präsentiert keine Daten für die SQL-Engine, sondern gibt die Ergebnisse direkt zurück
Kowlown
1
Schafft dies nicht eine Rennbedingung? Angenommen, ich überprüfe die Spaltennamen und stelle fest, dass meine Spalte fehlt. In der Zwischenzeit fügt ein anderer Prozess die Spalte hinzu. Dann werde ich versuchen, die Spalte hinzuzufügen, aber es wird eine Fehlermeldung angezeigt, da sie bereits vorhanden ist. Ich denke, ich soll zuerst die Datenbank sperren oder so? Ich fürchte, ich bin ein Noob für SQLite :).
Ben Farmer
24

Wenn Sie dies in einer DB-Upgrade-Anweisung tun, ist es möglicherweise am einfachsten, die ausgelöste Ausnahme abzufangen, wenn Sie versuchen, ein Feld hinzuzufügen, das möglicherweise bereits vorhanden ist.

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
quelle
2
Ich mag keine Programmierung im Ausnahmestil, aber das ist erstaunlich sauber. Vielleicht hast du mich ein bisschen beeinflusst.
Stephen J
Ich mag es auch nicht, aber C ++ ist die außergewöhnlichste Programmiersprache aller Zeiten. Ich denke, man kann es immer noch als "gültig" ansehen.
mächtige
Mein Anwendungsfall für SQLite = Ich möchte nicht viel zusätzliche Codierung für etwas Dummes Einfaches / Einzeiler in anderen Sprachen (MSSQL) durchführen. Gute Antwort ... obwohl es sich um "Exception Style Programming" handelt, befindet es sich in einer Upgrade-Funktion / isoliert, also nehme ich an, dass es akzeptabel ist.
Maplemale
Während andere es nicht mögen, denke ich, dass dies die beste Lösung ist lol
Adam Varhegyi
13

Es gibt eine Methode von PRAGMA, die table_info (table_name) lautet und alle Informationen der Tabelle zurückgibt.

Hier ist eine Implementierung, wie es für die Prüfspalte verwendet werden kann oder nicht.

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

Sie können diese Abfrage auch ohne Schleife verwenden.

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);
Krunal Shah
quelle
Cursor cursor = db.rawQuery ("select * from tableName", null); column = cursor.getColumnNames ();
Vahe Gharibyan
1
Ich denke du hast vergessen den Cursor zu schließen :-)
Pecana
@ VaheGharibyan, also wählen Sie einfach alles in Ihrer Datenbank aus, um Spaltennamen zu erhalten?! Was du einfach sagst ist we give no shit about performance:)).
Farid
Beachten Sie, dass die letzte Abfrage falsch ist. Die richtige Abfrage lautet: SELECT * FROM pragma_table_info(...)(Beachten Sie SELECT und den Unterstrich zwischen Pragma und Tabelleninformationen). Nicht sicher, in welcher Version sie es tatsächlich hinzugefügt haben, es funktionierte nicht unter 3.16.0, aber es funktioniert unter 3.22.0.
Drücken Sie immer
1

Für diejenigen, die pragma table_info()das Ergebnis als Teil eines größeren SQL verwenden möchten .

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

Der Schlüsselteil ist, pragma_table_info('<table_name>')anstelle von zu verwenden pragma table_info('<table_name>').


Diese Antwort ist von der Antwort von @Robert Hawkey inspiriert. Der Grund, warum ich es als neue Antwort poste, ist, dass ich nicht genug Ruf habe, um es als Kommentar zu posten.

Sonne
quelle
0

Ich habe die obige Antwort in C # /. Net genommen und sie für Qt / C ++ umgeschrieben, nicht zu viel geändert, aber ich wollte sie hier für alle hinterlassen, die in Zukunft nach einer C ++ - Antwort suchen.

    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
quelle
0

Alternativ können Sie die TSQL-Anweisung CASE-WHEN in Kombination mit pragma_table_info verwenden, um festzustellen, ob eine Spalte vorhanden ist:

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
quelle
0

Hier ist meine Lösung, aber in Python (ich habe versucht und konnte keinen Beitrag zum Thema Python finden):

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

Ich habe PRAGMA verwendet, um die Tabelleninformationen zu erhalten. Es gibt ein mehrdimensionales Array mit Informationen zu Spalten zurück - ein Array pro Spalte. Ich zähle die Anzahl der Arrays, um die Anzahl der Spalten zu erhalten. Wenn nicht genügend Spalten vorhanden sind, füge ich die Spalten mit dem Befehl ALTER TABLE hinzu.

Thomas Weeks
quelle
0

Alle diese Antworten sind in Ordnung, wenn Sie jeweils eine Zeile ausführen. Die ursprüngliche Frage war jedoch, ein SQL-Skript einzugeben, das von einer einzelnen Datenbank ausgeführt werden würde, und für alle Lösungen (z. B. die Überprüfung, ob die Spalte im Voraus vorhanden ist) muss das ausführende Programm entweder wissen, welche Tabellen und welche Spalten werden geändert / hinzugefügt oder das Eingabeskript wird vorverarbeitet und analysiert, um diese Informationen zu ermitteln. Normalerweise werden Sie dies nicht in Echtzeit oder häufig ausführen. Die Idee, eine Ausnahme abzufangen, ist also akzeptabel und geht dann weiter. Darin liegt das Problem ... wie man weitergeht. Glücklicherweise gibt uns die Fehlermeldung alle Informationen, die wir dazu benötigen. Die Idee ist, die SQL auszuführen, wenn es Ausnahmen bei einem Aufruf der Änderungstabelle gibt. Wir können die Zeile der Änderungstabelle in der SQL finden und die verbleibenden Zeilen zurückgeben und ausführen, bis sie entweder erfolgreich ist oder keine übereinstimmenden Zeilen der Änderungstabelle mehr gefunden werden können. Hier ist ein Beispielcode, in dem wir SQL-Skripte in einem Array haben. Wir iterieren das Array, das jedes Skript ausführt. Wir rufen es zweimal auf, damit der Befehl alter table fehlschlägt, aber das Programm ist erfolgreich, da wir den Befehl alter table aus der SQL entfernen und den aktualisierten Code erneut ausführen.

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

erwartete Ausgabe

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
quelle