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 EXISTS
clá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 EXISTS
y CREATE INDEX IF NOT EXISTS
, que se pueden ejecutar utilizando sqlite3_exec
o la sqlite3
herramienta de línea de comandos. No podemos poner ALTER TABLE
estos 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 EXISTS
en SQLite SQL puro?
fuente
user_version
? Supongo que es cero, pero sería bueno verlo documentado.IF
yALTER TABLE
no tiene un condicional? ¿Qué quiere decir con "SQL puro al 99%"?user_version
, parece ser 0, pero en realidad es un valor definido por el usuario, por lo que puede crear su propio valor inicial.user_version
el valor inicial es relevante cuando tiene una base de datos existente y nunca la ha usadouser_version
antes, pero desea comenzar a usarla, por lo que debe asumir que sqlite la configuró en un valor inicial particular.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)); } }
fuente
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
fuente
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()); }
fuente
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);
fuente
we give no shit about performance
:)).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.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 depragma 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.
fuente
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
fuente
En caso de que tenga este problema en flex / adobe air y se encuentre aquí primero, he encontrado una solución y la he publicado en una pregunta relacionada: AGREGAR COLUMNA a sqlite db SI NO EXISTE - ¿flex / air sqlite?
Mi comentario aquí: https://stackoverflow.com/a/24928437/2678219
fuente
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; }
fuente
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')
fuente
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.
fuente
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 ) ) ) -------------
fuente
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%'
fuente
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