125 votes

ALTER TABLE ADD COLUMN IF NOT EXISTS dans SQLite

Nous avons récemment eu besoin d'ajouter des colonnes à quelques-unes de nos tables de base de données SQLite existantes. Cela peut être fait avec ALTER TABLE ADD COLUMN . Bien entendu, si la table a déjà été modifiée, nous voulons la laisser telle quelle. Malheureusement, SQLite ne prend pas en charge la fonction IF NOT EXISTS clause sur ALTER TABLE .

Notre solution actuelle consiste à exécuter l'instruction ALTER TABLE et à ignorer les erreurs de type "nom de colonne en double", de la manière suivante cet exemple Python (mais en C++).

Cependant, notre approche habituelle de la mise en place de schémas de base de données consiste à disposer d'un script .sql contenant les éléments suivants CREATE TABLE IF NOT EXISTS y CREATE INDEX IF NOT EXISTS qui peuvent être exécutées à l'aide de sqlite3_exec ou le sqlite3 outil de ligne de commande. Nous ne pouvons pas mettre ALTER TABLE dans ces fichiers script car si cette instruction échoue, tout ce qui suit ne sera pas exécuté.

Je souhaite que les définitions des tables soient regroupées en un seul endroit et ne soient pas réparties entre les fichiers .sql et .cpp. Existe-t-il un moyen d'écrire une solution de contournement pour ALTER TABLE ADD COLUMN IF NOT EXISTS en SQLite pur ?

-1voto

Thomas Weeks Points 45

Voici ma solution, mais en python (j'ai essayé et n'ai pas trouvé de post sur le sujet en rapport avec 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)

J'ai utilisé PRAGMA pour obtenir les informations du tableau. Il renvoie un tableau multidimensionnel contenant des informations sur les colonnes - un tableau par colonne. Je compte le nombre de tableaux pour obtenir le nombre de colonnes. S'il n'y a pas assez de colonnes, je les ajoute à l'aide de la commande ALTER TABLE.

-1voto

Cjolly Points 401

Toutes ces réponses conviennent si vous exécutez une ligne à la fois. Cependant, la question originale était d'entrer un sql script qui serait exécuté par une seule db execute et toutes les solutions ( comme vérifier si la colonne est là à l'avance ) nécessiteraient que le programme exécutant ait connaissance des tables et des colonnes qui sont modifiées/ajoutées ou qu'il fasse un pré-traitement et un parsing de l'entrée script pour déterminer cette information. Typiquement, vous n'allez pas exécuter cela en temps réel ou souvent. L'idée d'attraper une exception est donc acceptable et de passer à autre chose. C'est là que réside le problème... comment passer à autre chose. Heureusement, le message d'erreur nous donne toutes les informations dont nous avons besoin pour le faire. L'idée est d'exécuter le sql s'il y a des exceptions sur un appel de modification de table, nous pouvons trouver la ligne de modification de table dans le sql et renvoyer les lignes restantes et exécuter jusqu'à ce qu'il réussisse ou qu'aucune autre ligne de modification de table correspondante ne puisse être trouvée. Voici un exemple de code où nous avons des sql script dans un tableau. Nous itérons le tableau en exécutant chaque script. Nous l'appelons deux fois pour faire échouer la commande alter table mais le programme réussit parce que nous supprimons la commande alter table du sql et ré-exécutons le code mis à jour.

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

résultat attendu

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

-2voto

Aravin Points 2180

J'en arrive à la question suivante

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 requête interne renvoie 0 ou 1 si la colonne existe.
  • En fonction du résultat, modifiez la colonne

-2voto

Je résous le problème en deux questions. Voici mon script utilisant 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; }
            }

-3voto

Richard Points 1202

Apparemment... dans SQLite... l'instruction "alter table" ne génère pas d'exceptions si la colonne existe déjà.

Trouvé cette dans le forum d'assistance et l'a testé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X