ALTER TABLE AGREGAR COLUMNA SI NO EXISTE en SQLite

89

Recientemente hemos tenido la necesidad de agregar columnas a algunas de nuestras tablas de bases de datos SQLite existentes. Esto se puede hacer con ALTER TABLE ADD COLUMN. Por supuesto, si la tabla ya ha sido alterada, queremos dejarla sola. Desafortunadamente, SQLite no admite una IF NOT EXISTScláusula sobre ALTER TABLE.

Nuestra solución actual es ejecutar la instrucción ALTER TABLE e ignorar cualquier error de "nombre de columna duplicado", como este ejemplo de Python (pero en C ++).

Sin embargo, nuestro enfoque habitual para configurar esquemas de base de datos es tener un script .sql que contenga instrucciones CREATE TABLE IF NOT EXISTSy CREATE INDEX IF NOT EXISTS, que se pueden ejecutar utilizando sqlite3_execo la sqlite3herramienta de línea de comandos. No podemos poner ALTER TABLEestos archivos de script porque si esa declaración falla, no se ejecutará nada después.

Quiero tener las definiciones de la tabla en un solo lugar y no dividir entre archivos .sql y .cpp. ¿Hay alguna manera de escribir una solución ALTER TABLE ADD COLUMN IF NOT EXISTSen SQLite SQL puro?

dan04
fuente

Respuestas:

64

Tengo un método SQL 99% puro. La idea es versionar su esquema. Puede hacer esto de dos maneras:

  • Utilice el comando pragma 'user_version' ( PRAGMA user_version) para almacenar un número incremental para la versión del esquema de su base de datos.

  • Almacene su número de versión en su propia tabla definida.

De esta manera, cuando se inicia el software, puede verificar el esquema de la base de datos y, si es necesario, ejecutar su ALTER TABLEconsulta, luego incrementar la versión almacenada. Esto es mucho mejor que intentar varias actualizaciones "a ciegas", especialmente si su base de datos crece y cambia varias veces a lo largo de los años.

MPelletier
fuente
7
¿Cuál es el valor inicial de user_version? Supongo que es cero, pero sería bueno verlo documentado.
Craig McQueen
Incluso con esto, ¿se puede hacer en SQL puro, ya que sqlite no es compatible IFy ALTER TABLEno tiene un condicional? ¿Qué quiere decir con "SQL puro al 99%"?
Craig McQueen
1
@CraigMcQueen En cuanto al valor inicial de user_version, parece ser 0, pero en realidad es un valor definido por el usuario, por lo que puede crear su propio valor inicial.
MPelletier
7
La pregunta sobre user_versionel valor inicial es relevante cuando tiene una base de datos existente y nunca la ha usado user_versionantes, pero desea comenzar a usarla, por lo que debe asumir que sqlite la configuró en un valor inicial particular.
Craig McQueen
1
@CraigMcQueen Estoy de acuerdo, pero no parece estar documentado.
MPelletier
30

Una solución es simplemente crear las columnas y detectar la excepción / error que surja si la columna ya existe. Cuando agregue varias columnas, agréguelas en declaraciones ALTER TABLE separadas para que un duplicado no impida que se creen las otras.

Con sqlite-net , hicimos algo como esto. No es perfecto, ya que no podemos distinguir los errores de sqlite duplicados de otros errores 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));
    }
}
angularsen
fuente
28

SQLite también admite una declaración pragma llamada "table_info" que devuelve una fila por columna en una tabla con el nombre de la columna (y otra información sobre la columna). Puede usar esto en una consulta para verificar la columna que falta y, si no está presente, modificar la tabla.

PRAGMA table_info(foo_table_name)

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

Robert Hawkey
fuente
30
Su respuesta sería mucho mejor si proporcionara el código con el que completar esa búsqueda en lugar de solo un enlace.
Michael Alan Huff
PRAGMA table_info (nombre_tabla). Este comando enumerará cada columna de table_name como una fila en el resultado. En función de este resultado, puede determinar si la columna existía o no.
Hao Nguyen
2
¿Hay alguna forma de hacer esto combinando el pragma en parte de una declaración SQL más grande de modo que la columna se agregue si no existe pero de lo contrario no lo es, en una sola consulta?
Michael
1
@Miguel. Hasta donde yo sé, no, no puedes. El problema con el comando PRAGMA es que no puede consultarlo. el comando no presenta datos al motor SQL, devuelve resultados directamente
Kowlown
1
¿No crea esto una condición de carrera? Digamos que verifico los nombres de las columnas, veo que falta mi columna, pero mientras tanto, otro proceso agrega la columna. Luego intentaré agregar la columna, pero obtendré un error porque ya existe. Supongo que se supone que debo bloquear la base de datos primero o algo así. Soy un novato en sqlite, me temo :).
Ben Farmer
25

Si está haciendo esto en una declaración de actualización de base de datos, tal vez la forma más sencilla sea simplemente detectar la excepción lanzada si está intentando agregar un campo que ya puede existir.

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}
usuario7896780
fuente
2
No me gusta la programación al estilo de las excepciones, pero es increíblemente limpia. Quizás me hayas influido un poco.
Stephen J
Tampoco me gusta, pero C ++ es el lenguaje de programación de estilo más excepcional de todos los tiempos. Así que supongo que todavía se puede ver como "válido".
tmighty
Mi caso de uso para SQLite = No quiero hacer un montón de codificación adicional para algo estúpido simple / una línea en otros idiomas (MSSQL). Buena respuesta ... aunque es "programación de estilo de excepción", está en una función de actualización / aislada, así que supongo que es aceptable.
maplemale
Mientras que a otros no les gusta, creo que esta es la mejor solución jajaja
Adam Varhegyi
13

Threre es un método de PRAGMA es table_info (table_name), devuelve toda la información de la tabla.

Aquí está la implementación de cómo usarlo para comprobar si la columna existe o no,

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

También puede usar esta consulta sin usar loop,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);
Krunal Shah
fuente
Cursor cursor = db.rawQuery ("seleccionar * de tableName", nulo); columnas = cursor.getColumnNames ();
Vahe Gharibyan
1
Supongo que olvidaste cerrar el cursor :-)
Pecana
@VaheGharibyan, ¿simplemente seleccionará todo en su base de datos solo para obtener los nombres de las columnas? Lo que simplemente estás diciendo es we give no shit about performance:)).
Farid
Tenga en cuenta que la última consulta es incorrecta. La consulta adecuada es: SELECT * FROM pragma_table_info(...)(observe SELECT y el subrayado entre pragma e información de la tabla). No estoy seguro de en qué versión lo agregaron realmente, no funcionó en 3.16.0 pero funciona en 3.22.0.
PressingOnAlways
3

Para aquellos que quieran usar pragma table_info()el resultado como parte de un SQL más grande.

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

La parte clave es usar en pragma_table_info('<table_name>')lugar de pragma table_info('<table_name>').


Esta respuesta está inspirada en la respuesta de @Robert Hawkey. La razón por la que lo publico como una nueva respuesta es que no tengo la reputación suficiente para publicarlo como comentario.

Dom
fuente
1

Se me ocurre esta consulta

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 consulta interna devolverá 0 o 1 si la columna existe.
  • Según el resultado, modifique la columna.
Aravin
fuente
código = Error (1), mensaje = System.Data.SQLite.SQLiteException (0x800007BF): error de lógica SQL cerca de "ALTER": error de sintaxis en System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ
0

Tomé la respuesta anterior en C # /. Net y la reescribí para Qt / C ++, sin muchos cambios, pero quería dejarla aquí para cualquiera en el futuro que busque una respuesta 'ish' de 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;
}
Kevin B. Burns
fuente
0

Alternativamente, puede usar la instrucción CASE-WHEN TSQL en combinación con pragma_table_info para saber si existe una columna:

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
fuente
aquí, ¿cómo cambiamos la mesa? cuando hay una coincidencia de nombre de columna?
user2700767
0

Aquí está mi solución, pero en Python (intenté y no pude encontrar ninguna publicación sobre el tema relacionado con 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)

Usé PRAGMA para obtener la información de la mesa. Devuelve una matriz multidimensional llena de información sobre columnas: una matriz por columna. Cuento el número de matrices para obtener el número de columnas. Si no hay suficientes columnas, agrego las columnas usando el comando ALTER TABLE.

Thomas Weeks
fuente
0

Todas estas respuestas están bien si ejecuta una línea a la vez. Sin embargo, la pregunta original era ingresar un script sql que sería ejecutado por una sola ejecución de db y todas las soluciones (como verificar si la columna está allí antes de tiempo) requerirían que el programa en ejecución tenga conocimiento de qué tablas y las columnas se modifican / agregan o realizan un preprocesamiento y análisis del script de entrada para determinar esta información. Por lo general, no ejecutará esto en tiempo real ni con frecuencia. Entonces, la idea de detectar una excepción es aceptable y luego seguir adelante. Ahí radica el problema ... cómo seguir adelante. Afortunadamente, el mensaje de error nos da toda la información que necesitamos para hacer esto. La idea es ejecutar el sql si se hace una excepción en una llamada de tabla alter, podemos encontrar la línea de la tabla alter en el sql y devolver las líneas restantes y ejecutar hasta que tenga éxito o no se puedan encontrar más líneas de tabla alter coincidentes. Aquí hay un código de ejemplo donde tenemos scripts SQL en una matriz. Iteramos la matriz ejecutando cada script. Lo llamamos dos veces para que el comando alter table falle, pero el programa tiene éxito porque eliminamos el comando alter table del sql y volvemos a ejecutar el código actualizado.

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

Rendimiento esperado

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

Lógica: la columna sql en sqlite_master contiene la definición de la tabla, por lo que ciertamente contiene una cadena con el nombre de la columna.

Al buscar una subcadena, tiene sus limitaciones obvias. Por lo tanto, sugeriría usar una subcadena aún más restrictiva en ColumnName, por ejemplo, algo como esto (sujeto a pruebas, ya que el carácter '`' no siempre está allí):

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

Lo soluciono en 2 consultas. Este es mi script de 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; }
            }
イ ン コ グ ニ ト ア レ ク セ イ
fuente