如果SQLite中不存在ALTER TABLE ADD COLUMN


89

最近,我们需要将列添加到一些现有的SQLite数据库表中。可以使用来完成ALTER TABLE ADD COLUMN。当然,如果表格已被更改,我们希望不加更改。不幸的是,SQLite不支持上的IF NOT EXISTS子句ALTER TABLE

我们当前的解决方法是执行ALTER TABLE语句,并忽略所有“重复的列名”错误,就像此Python示例一样(但在C ++中)。

但是,我们通常的设置数据库模式的方法是使.sql脚本包含CREATE TABLE IF NOT EXISTSCREATE INDEX IF NOT EXISTS语句,可以使用sqlite3_execsqlite3命令行工具来执行该脚本。我们不能放入ALTER TABLE这些脚本文件,因为如果该语句失败,它将不执行任何操作。

我想将表定义放在一个地方,而不要在.sql和.cpp文件之间分割。有没有办法ALTER TABLE ADD COLUMN IF NOT EXISTS在纯SQLite SQL中编写变通办法?

Answers:


64

我有99%的纯SQL方法。这个想法是对您的架构进行版本控制。您可以通过两种方式执行此操作:

  • 使用'user_version'pragma命令(PRAGMA user_version)存储数据库模式版本的增量编号。

  • 将版本号存储在自己定义的表中。

这样,在启动软件时,它可以检查数据库架构,并在需要时运行ALTER TABLE查询,然后增加存储的版本。到目前为止,这比尝试“盲目”进行各种更新要好得多,尤其是如果您的数据库多年来增长和更改几次。


7
的初始值是user_version多少?我假设为零,但很高兴看到有记载。
2014年

即使这样,由于sqlite不支持IFALTER TABLE没有条件,因此可以在纯SQL中完成吗?“ 99%纯SQL”是什么意思?
Craig McQueen 2014年

1
@CraigMcQueen至于的初始值user_version,它似乎是0,但实际上是用户定义的值,因此您可以设置自己的初始值。
MPelletier 2014年

7
user_version当您有一个现有数据库时,有关初始值的问题user_version就很重要,您以前从未使用过,但是您想开始使用它,因此需要假定sqlite将其设置为特定的初始值。
Craig McQueen 2014年

1
@CraigMcQueen我同意,但是似乎没有记录在案。
MPelletier

30

一种解决方法是仅创建列并捕获如果该列已存在则发生的异常/错误。添加多个列时,请将它们添加到单独的ALTER TABLE语句中,这样一个重复项就不会阻止其他重复项的创建。

使用sqlite-net,我们做了类似的事情。这不是完美的,因为我们无法将重复的sqlite错误与其他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));
    }
}

28

SQLite还支持一种称为“ table_info”的编译指示,该语句返回表中每列具有该列名称(以及有关该列的其他信息)的一行。您可以在查询中使用它来检查缺少的列,如果不存在,请更改表。

PRAGMA table_info(foo_table_name)

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


30
如果您提供完成搜索的代码而不仅仅是链接,那么您的答案将更加出色。
Michael Alan Huff 2014年

PRAGMA table_info(表名称)。此命令会将table_name的每一列作为结果列出。根据此结果,可以确定该列是否存在。
阮阮

2
是否可以通过在较大的SQL语句的一部分中组合编译指示来实现此目的,以便仅在单个查询中添加不存在但不存在的列?
Michael

1
@迈克尔。据我所知,不,你不能。PRAGMA命令的问题在于您无法对其进行查询。该命令不向SQL引擎提供数据,而是直接返回结果
Kowlown

1
这不是创造竞争条件吗?假设我检查了列名,发现我的列丢失了,但是与此同时,另一个过程添加了该列。然后,我将尝试添加该列,但会因为它已经存在而出现错误。我想我应该先锁定数据库吗?我是sqlite的菜鸟,恐怕:)。
Ben Farmer

25

如果您在数据库升级语句中执行此操作,则最简单的方法可能是仅在捕获试图添加可能已经存在的字段时捕获引发的异常。

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}

2
我不喜欢异常风格的编程,但这非常干净。也许你让我有些摇摆。
斯蒂芬·J

我也不喜欢,但是C ++是有史以来最异常的样式编程语言。因此,我猜可能仍然会认为它是“有效的”。
tmighty

我的SQLite用例=我不想为其他语言(MSSQL)的愚蠢简单/一个衬里做大量额外的编码。好的答案...虽然它是“异常样式编程”,但是在升级功能中/与外界隔离,因此我认为它是可以接受的。
maplemale

尽管其他人不喜欢它,但我认为这是最好的解决方案
Adam Varhegyi,

13

这是PRAGMA的一个方法,是table_info(table_name),它返回表的所有信息。

这是实现如何将其用于检查列是否存在,

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

您也可以在不使用循环的情况下使用此查询,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);

光标cursor = db.rawQuery(“ select * from tableName”,null); 列= cursor.getColumnNames();
Vahe Gharibyan

1
我猜您忘了关闭光标了:-)
Pecana

@VaheGharibyan,因此您只需选择数据库中的所有内容即可获取列名?!您只是在说we give no shit about performance:))。
Farid

注意,最后一个查询不正确。正确的查询是:(SELECT * FROM pragma_table_info(...)请注意,在pragma和表信息之间使用SELECT和下划线)。不知道他们实际添加了哪个版本,它在3.16.0上不起作用,但在3.22.0上可以起作用。
PressingOnAlways

3

对于那些想将pragma table_info()的结果用作较大SQL的一部分的人。

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

关键部分是使用pragma_table_info('<table_name>')而不是pragma table_info('<table_name>')


此答案的灵感来自@Robert Hawkey的回复。我将其发布为新答案的原因是我没有足够的声誉将其发布为评论。


1

我想出了这个查询

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • 如果列存在,内部查询将返回0或1。
  • 根据结果​​更改列

代码=错误(1),消息= System.Data.SQLite.SQLiteException(0x800007BF):SQL逻辑错误邻近“ALTER”:在System.Data.SQLite.SQLite3.Prepare语法错误
インコグニトアレクセイ


0

我在C#/。Net中接受了以上答案,并将其改写为Qt / C ++,虽然变化不大,但是我想将其留在这里供以后寻找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;
}

0

您也可以将CASE-WHEN TSQL语句与pragma_table_info结合使用,以了解是否存在列:

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

在这里,我们如何更改表?什么时候有列名匹配?
user2700767

0

这是我的解决方案,但是在python中(我尝试并未能找到与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)

我使用PRAGMA来获取表格信息。它返回一个充满有关列信息的多维数组-每列一个数组。我计算数组的数量以获得列数。如果没有足够的列,那么我使用ALTER TABLE命令添加列。


0

如果您一次执行一行,那么所有这些答案都很好。但是,最初的问题是输入将由单个db execute执行的sql脚本,并且所有解决方案(例如提前检查列是否存在)都将要求执行程序知道哪些表和列正在被更改/添加,或者对输入脚本进行预处理和解析以确定这些信息。通常,您不会实时或经常运行此程序。因此,捕获异常的想法是可以接受的,然后继续进行下去。问题就在这里...如何继续前进。幸运的是,错误消息为我们提供了执行此操作所需的所有信息。这个想法是执行SQL,如果它在alter table调用上异常,我们可以在sql中找到alter table行并返回剩余的行并执行,直到成功或找不到匹配的alter table行为止。这是一些示例代码,其中我们在数组中包含sql脚本。我们迭代执行每个脚本的数组。我们两次调用它,使alter table命令失败,但是程序成功,因为我们从sql中删除了alter table命令并重新执行更新的代码。

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

预期产量

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

0
select * from sqlite_master where type = 'table' and tbl_name = 'TableName' and sql like '%ColumnName%'

逻辑:sqlite_master中的sql列包含表定义,因此它当然包含带有列名的字符串。

在搜索子字符串时,它有明显的局限性。因此,我建议在ColumnName中使用更具限制性的子字符串,例如类似这样的东西(需要测试为“`”字符并不总是存在):

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

0

我在2个查询中解决了这个问题。这是我使用System.Data.SQLite的Unity3D脚本。

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; }
            }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.