ALTER TABLE ADD COLUMN IF NOT EXISTS dans SQLite

89

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 sûr, si le tableau a déjà été modifié, nous voulons le laisser seul. Malheureusement, SQLite ne prend pas en charge une IF NOT EXISTSclause sur ALTER TABLE.

Notre solution de contournement actuelle consiste à exécuter l'instruction ALTER TABLE et à ignorer toutes les erreurs de "nom de colonne en double", tout comme cet exemple Python (mais en C ++).

Cependant, notre approche habituelle pour configurer les schémas de base de données consiste à avoir un script .sql contenant des instructions CREATE TABLE IF NOT EXISTSet CREATE INDEX IF NOT EXISTS, qui peuvent être exécutées en utilisant sqlite3_execou l' sqlite3outil de ligne de commande. Nous ne pouvons pas insérer ALTER TABLEces fichiers de script car si cette instruction échoue, tout ce qui suit ne sera pas exécuté.

Je veux avoir les définitions de table au même endroit et ne pas être réparties entre les fichiers .sql et .cpp. Existe-t-il un moyen d'écrire une solution de contournement ALTER TABLE ADD COLUMN IF NOT EXISTSen SQLite SQL pur?

dan04
la source

Réponses:

64

J'ai une méthode SQL pure à 99%. L'idée est de versionner votre schéma. Vous pouvez le faire de deux manières:

  • Utilisez la commande pragma 'user_version' ( PRAGMA user_version) pour stocker un numéro incrémentiel pour votre version de schéma de base de données.

  • Stockez votre numéro de version dans votre propre table définie.

De cette façon, au démarrage du logiciel, il peut vérifier le schéma de la base de données et, si nécessaire, exécuter votre ALTER TABLErequête, puis incrémenter la version stockée. C'est de loin mieux que de tenter diverses mises à jour «à l'aveugle», surtout si votre base de données s'agrandit et change plusieurs fois au fil des ans.

MPelletier
la source
7
Quelle est la valeur initiale de user_version? Je suppose zéro, mais ce serait bien de voir cela documenté.
Craig McQueen
Même avec cela, cela peut-il être fait en SQL pur, puisque sqlite ne prend pas en charge IFet ALTER TABLEn'a pas de conditionnel? Qu'entendez-vous par "SQL pur à 99%"?
Craig McQueen
1
@CraigMcQueen Quant à la valeur initiale de user_version, elle semble être 0, mais c'est vraiment une valeur définie par l'utilisateur, vous pouvez donc créer votre propre valeur initiale.
MPelletier
7
La question sur user_versionla valeur initiale est pertinente lorsque vous avez une base de données existante et que vous ne l'avez jamais utilisée user_versionauparavant, mais que vous souhaitez commencer à l'utiliser, vous devez donc supposer que sqlite l'a définie sur une valeur initiale particulière.
Craig McQueen
1
@CraigMcQueen Je suis d'accord, mais cela ne semble pas être documenté.
MPelletier
30

Une solution de contournement consiste simplement à créer les colonnes et à détecter l'exception / erreur qui se produit si la colonne existe déjà. Lors de l'ajout de plusieurs colonnes, ajoutez-les dans des instructions ALTER TABLE distinctes afin qu'un doublon n'empêche pas la création des autres.

Avec sqlite-net , nous avons fait quelque chose comme ça. Ce n'est pas parfait, car nous ne pouvons pas distinguer les erreurs sqlite dupliquées des autres erreurs 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
la source
28

SQLite prend également en charge une instruction pragma appelée "table_info" qui renvoie une ligne par colonne dans une table avec le nom de la colonne (et d'autres informations sur la colonne). Vous pouvez l'utiliser dans une requête pour rechercher la colonne manquante et, si elle n'est pas présente, modifier la table.

PRAGMA table_info(foo_table_name)

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

Robert Hawkey
la source
30
Votre réponse serait beaucoup plus excellente si vous fournissiez le code avec lequel effectuer cette recherche au lieu d'un simple lien.
Michael Alan Huff
PRAGMA table_info (nom_table). Cette commande listera chaque colonne de table_name sous forme de ligne dans le résultat. Sur la base de ce résultat, vous pouvez déterminer si la colonne existait ou non.
Hao Nguyen
2
Existe-t-il un moyen de le faire en combinant le pragma dans une partie d'une instruction SQL plus grande de sorte que la colonne soit ajoutée si elle n'existe pas mais ne l'est pas autrement, en une seule requête?
Michael
1
@Michael. Autant que je sache, non, vous ne pouvez pas. Le problème avec la commande PRAGMA est que vous ne pouvez pas interroger dessus. la commande ne présente pas de données au moteur SQL, elle renvoie directement les résultats
Kowlown
1
Cela ne crée-t-il pas une condition de concurrence? Disons que je vérifie les noms de colonne, que ma colonne est manquante, mais en attendant, un autre processus ajoute la colonne. Ensuite, j'essaierai d'ajouter la colonne mais j'obtiendrai une erreur car elle existe déjà. Je suppose que je dois d'abord verrouiller la base de données ou quelque chose? Je suis un noob de sqlite j'ai peur :).
Ben Farmer
25

Si vous faites cela dans une instruction de mise à niveau de la base de données, le moyen le plus simple est peut-être de simplement intercepter l'exception levée si vous essayez d'ajouter un champ qui peut déjà exister.

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
la source
2
Je n'aime pas la programmation de type exception, mais c'est incroyablement propre. Peut-être que vous m'avez un peu influencé.
Stephen J
Je n'aime pas ça non plus, mais C ++ est le langage de programmation de style le plus exceptionnel jamais créé. Donc je suppose que l'on peut encore le voir comme "valide".
tmighty
Mon cas d'utilisation pour SQLite = je ne veux pas faire une tonne de codage supplémentaire pour quelque chose de simple stupide / une ligne dans d'autres langues (MSSQL). Bonne réponse ... bien que ce soit une "programmation de style d'exception", c'est dans une fonction de mise à jour / isolée donc je suppose que c'est acceptable.
maplemale
Alors que d'autres ne l'aiment pas, je pense que c'est la meilleure solution lol
Adam Varhegyi
13

threre est une méthode de PRAGMA est table_info (table_name), elle retourne toutes les informations de table.

Voici la mise en œuvre comment l'utiliser pour vérifier la colonne existe ou non,

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

Vous pouvez également utiliser cette requête sans utiliser de boucle,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);
Krunal Shah
la source
Cursor cursor = db.rawQuery ("select * from tableName", null); colonnes = cursor.getColumnNames ();
Vahe Gharibyan
1
Je suppose que vous avez oublié de fermer le curseur :-)
Pecana
@VaheGharibyan, donc vous sélectionnerez simplement tout dans votre base de données juste pour obtenir les noms de colonnes?! Ce que vous dites simplement, c'est we give no shit about performance:)).
Farid
Notez que la dernière requête est incorrecte. La requête appropriée est: SELECT * FROM pragma_table_info(...)(notez le SELECT et le trait de soulignement entre le pragma et les informations de table). Je ne sais pas dans quelle version ils l'ont réellement ajouté, cela n'a pas fonctionné sur 3.16.0 mais cela fonctionne sur 3.22.0.
PressingOnAlways
3

Pour ceux qui veulent utiliser pragma table_info()le résultat dans le cadre d'un SQL plus large.

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

L'essentiel est d'utiliser pragma_table_info('<table_name>')au lieu de pragma table_info('<table_name>').


Cette réponse est inspirée de la réponse de @Robert Hawkey. La raison pour laquelle je la poste comme nouvelle réponse est que je n'ai pas assez de réputation pour la publier en commentaire.

Soleil
la source
1

Je viens avec cette requête

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 renverra 0 ou 1 si la colonne existe.
  • En fonction du résultat, modifiez la colonne
Aravin
la source
code = Error (1), message = System.Data.SQLite.SQLiteException (0x800007BF): erreur de logique SQL près de "ALTER": erreur de syntaxe à System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ
0

J'ai pris la réponse ci-dessus en C # /. Net, et je l'ai réécrite pour Qt / C ++, pas trop changé, mais je voulais la laisser ici pour quiconque à l'avenir à la recherche d'une réponse C ++ 'ish'.

    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
la source
0

Vous pouvez également utiliser l'instruction CASE-WHEN TSQL en combinaison avec pragma_table_info pour savoir si une colonne existe:

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
la source
ici comment modifier la table? quand il y a une correspondance de nom de colonne?
user2700767
0

Voici ma solution, mais en python (j'ai essayé et je n'ai trouvé aucun article sur le sujet lié à 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 de la table. Il renvoie un tableau multidimensionnel plein d'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, j'ajoute les colonnes à l'aide de la commande ALTER TABLE.

Thomas Weeks
la source
0

Toutes ces réponses conviennent si vous exécutez une ligne à la fois. Cependant, la question initiale était de saisir un script sql qui serait exécuté par une seule exécution de base de données et toutes les solutions (comme vérifier si la colonne est là à l'avance) nécessiteraient que le programme en cours d'exécution sache quelles tables et des colonnes sont modifiées / ajoutées ou effectuent un prétraitement et une analyse du script d'entrée pour déterminer ces informations. En règle générale, vous n'allez pas l'exécuter en temps réel ou souvent. L'idée d'attraper une exception est donc acceptable, puis de passer à autre chose. C'est là que réside le problème ... comment avancer. Heureusement, le message d'erreur nous donne toutes les informations dont nous avons besoin pour ce faire. L'idée est d'exécuter le sql s'il fait exception sur un appel alter table, nous pouvons trouver la ligne alter table dans le sql et renvoyer les lignes restantes et l'exécuter jusqu'à ce qu'il réussisse ou qu'aucune autre ligne alter table correspondante ne puisse être trouvée. Voici un exemple de code où nous avons des scripts SQL dans un tableau. Nous itérons le tableau exécutant chaque script. Nous l'appelons deux fois pour que la commande alter table échoue, mais le programme réussit car 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

production attendue

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
la source
0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

Logique: la colonne sql dans sqlite_master contient la définition de la table, elle contient donc certainement une chaîne avec le nom de la colonne.

Lorsque vous recherchez une sous-chaîne, elle a ses limites évidentes. Je suggérerais donc d'utiliser une sous-chaîne encore plus restrictive dans ColumnName, par exemple quelque chose comme ça (sous réserve de tests car le caractère `` 'n'est pas toujours là):

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

Je le résous en 2 requêtes. Ceci est mon script Unity3D 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; }
            }
イ ン コ グ ニ ト ア レ ク セ イ
la source