我们最近需要向一些现有的 SQLite 数据库表中添加列。这可以通过 ALTER TABLE ADD COLUMN
完成。当然,如果表已经改变,我们想不管它。不幸的是,SQLite 不支持 ALTER TABLE
上的 IF NOT EXISTS
子句。
我们当前的解决方法是执行 ALTER TABLE 语句并忽略任何“重复的列名”错误,就像 this Python example 一样(但在 C++ 中)。
但是,我们设置数据库模式的常用方法是使用包含 CREATE TABLE IF NOT EXISTS
和 CREATE INDEX IF NOT EXISTS
语句的 .sql 脚本,可以使用 sqlite3_exec
或 sqlite3
命令行工具执行它们。我们不能将 ALTER TABLE
放在这些脚本文件中,因为如果该语句失败,则不会执行它之后的任何内容。
我想将表定义放在一个地方,而不是在 .sql 和 .cpp 文件之间拆分。有没有办法在纯 SQLite SQL 中编写 ALTER TABLE ADD COLUMN IF NOT EXISTS
的解决方法?
我有一个 99% 纯 SQL 方法。这个想法是对您的架构进行版本控制。您可以通过两种方式做到这一点:
使用 'user_version' pragma 命令 (PRAGMA user_version) 存储数据库模式版本的增量编号。
将您的版本号存储在您自己定义的表中。
这样,当软件启动时,它可以检查数据库架构,如果需要,运行您的 ALTER TABLE
查询,然后增加存储的版本。这比“盲目地”尝试各种更新要好得多,尤其是如果您的数据库多年来增长和更改了几次。
一种解决方法是仅创建列并捕获列已存在时出现的异常/错误。添加多个列时,将它们添加到单独的 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));
}
}
SQLite 还支持名为“table_info”的编译指示语句,该语句在表中每列返回一行,并带有列名(以及有关列的其他信息)。您可以在查询中使用它来检查缺少的列,如果不存在则更改表。
PRAGMA table_info(foo_table_name)
样本输出:
cid name type notnull dflt_value pk 0 id integer 0 null 1 1 type text 0 null 0 2 data json 0 null 0
http://www.sqlite.org/pragma.html#pragma_table_info
如果您在数据库升级语句中执行此操作,也许最简单的方法是在您尝试添加可能已经存在的字段时捕获抛出的异常。
try {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}
PRAGMA 的一个方法是 table_info(table_name),它返回 table 的所有信息。
这是实现如何使用它来检查列是否存在,
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);
we give no shit about performance
:))。
SELECT * FROM pragma_table_info(...)
(注意编译指示和表信息之间的 SELECT 和下划线)。不确定他们实际添加了哪个版本,它在 3.16.0 上不起作用,但在 3.22.0 上有效。
对于那些想要将 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 的回复。我将其发布为新答案的原因是我没有足够的声誉将其发布为评论。
如果您在 flex/adobe air 中遇到此问题并首先发现自己在这里,我找到了解决方案,并将其发布在相关问题上:ADD COLUMN to sqlite db IF NOT EXISTS - flex/air sqlite?
我在这里的评论:https://stackoverflow.com/a/24928437/2678219
我在 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;
}
您也可以将 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')
这是我的解决方案,但在 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 命令添加列。
如果您一次执行一行,所有这些答案都很好。但是,最初的问题是输入一个 sql 脚本,该脚本将由单个 db execute 执行,并且所有解决方案(例如提前检查列是否存在)都需要执行程序要么知道哪些表,要么正在更改/添加列或对输入脚本进行预处理和解析以确定此信息。通常,您不会实时或经常运行它。所以捕获异常的想法是可以接受的,然后继续。这就是问题所在……如何继续前进。幸运的是,错误消息为我们提供了执行此操作所需的所有信息。这个想法是如果在alter table调用上出现异常则执行sql,我们可以在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 ) )
)
-------------
我想出了这个查询
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。
根据结果,更改列
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%'
我在 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; }
}
user_version
的初始值是多少?我假设为零,但很高兴看到记录在案。IF
并且ALTER TABLE
没有条件? “99% 纯 SQL”是什么意思?user_version
的初始值,貌似是0,其实是用户自定义的值,所以可以自己做初始值。user_version
初始值的问题与您已有数据库相关,并且您以前从未使用过user_version
,但您想开始使用它,因此您需要假设 sqlite 将其设置为特定的初始值。