ALTER TABLE TAMBAHKAN KOLOM JIKA TIDAK ADA di SQLite

92

Kami baru-baru ini perlu menambahkan kolom ke beberapa tabel database SQLite yang ada. Ini bisa dilakukan dengan ALTER TABLE ADD COLUMN. Tentu saja, jika tabel sudah diubah, kita ingin membiarkannya. Sayangnya, SQLite tidak mendukung IF NOT EXISTSklausul tentang ALTER TABLE.

Solusi kami saat ini adalah dengan mengeksekusi pernyataan ALTER TABLE dan mengabaikan kesalahan "nama kolom duplikat", seperti contoh Python ini (tetapi dalam C ++).

Namun, pendekatan biasa kami untuk menyiapkan skema database adalah memiliki skrip .sql yang berisi CREATE TABLE IF NOT EXISTSdan CREATE INDEX IF NOT EXISTSpernyataan, yang dapat dieksekusi menggunakan sqlite3_execatau sqlite3alat baris perintah. Kami tidak dapat memasukkan ALTER TABLEfile skrip ini karena jika pernyataan itu gagal, apa pun setelah itu tidak akan dijalankan.

Saya ingin memiliki definisi tabel di satu tempat dan tidak memisahkan antara file .sql dan .cpp. Apakah ada cara untuk menulis solusi ALTER TABLE ADD COLUMN IF NOT EXISTSdi SQLite SQL murni?

dan04
sumber

Jawaban:

65

Saya memiliki metode SQL murni 99%. Idenya adalah untuk membuat versi skema Anda. Anda dapat melakukannya dengan dua cara:

  • Gunakan perintah pragma 'user_version' ( PRAGMA user_version) untuk menyimpan nomor tambahan untuk versi skema database Anda.

  • Simpan nomor versi Anda di tabel yang Anda tentukan sendiri.

Dengan cara ini, saat perangkat lunak dimulai, ia dapat memeriksa skema database dan, jika perlu, menjalankan ALTER TABLEkueri Anda , lalu menaikkan versi yang disimpan. Ini jauh lebih baik daripada mencoba berbagai pembaruan "buta", terutama jika database Anda tumbuh dan berubah beberapa kali selama bertahun-tahun.

MPelletier
sumber
7
Berapakah nilai awalnya user_version? Saya berasumsi nol, tapi alangkah baiknya melihat itu didokumentasikan.
Craig McQueen
Bahkan dengan ini, dapatkah itu dilakukan dalam SQL murni, karena sqlite tidak mendukung IFdan ALTER TABLEtidak bersyarat? Apa yang Anda maksud dengan "99% SQL murni"?
Craig McQueen
1
@CraigMcQueen Untuk nilai awal user_version, tampaknya 0, tetapi sebenarnya ini adalah nilai yang ditentukan pengguna, jadi Anda dapat membuat nilai awal Anda sendiri.
MPelletier
7
Pertanyaan tentang user_versionnilai awal relevan ketika Anda memiliki database yang sudah ada, dan Anda belum pernah menggunakan user_versionsebelumnya, tetapi Anda ingin mulai menggunakannya, jadi Anda perlu menganggap sqlite menyetelnya ke nilai awal tertentu.
Craig McQueen
1
@CraigMcQueen Saya setuju, tetapi tampaknya tidak didokumentasikan.
MPelletier
31

Salah satu solusinya adalah dengan membuat kolom dan menangkap pengecualian / kesalahan yang muncul jika kolom tersebut sudah ada. Saat menambahkan beberapa kolom, tambahkan dalam pernyataan ALTER TABLE terpisah sehingga satu duplikat tidak mencegah yang lain dibuat.

Dengan sqlite-net , kami melakukan sesuatu seperti ini. Ini tidak sempurna, karena kami tidak dapat membedakan kesalahan sqlite duplikat dari kesalahan sqlite lainnya.

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
sumber
27

SQLite juga mendukung pernyataan pragma yang disebut "table_info" yang mengembalikan satu baris per kolom dalam tabel dengan nama kolom (dan informasi lain tentang kolom). Anda dapat menggunakan ini dalam kueri untuk memeriksa kolom yang hilang, dan jika tidak ada, ubah tabel.

PRAGMA table_info(foo_table_name)

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

Robert Hawkey
sumber
32
Jawaban Anda akan jauh lebih baik jika Anda memberikan kode yang dapat digunakan untuk menyelesaikan penelusuran itu, bukan hanya tautan.
Michael Alan Huff
Info_tabel PRAGMA (nama_tabel). Perintah ini akan mencantumkan setiap kolom table_name sebagai baris di hasil. Berdasarkan hasil ini, Anda dapat menentukan apakah kolom tersebut ada atau tidak.
Hao Nguyen
2
Adakah cara untuk melakukan ini dengan menggabungkan pragma di bagian dari pernyataan SQL yang lebih besar sehingga kolom ditambahkan jika tidak ada tetapi sebaliknya tidak, hanya dalam satu kueri?
Michael
1
@Michael. Sejauh yang saya tahu, tidak, Anda tidak bisa. Masalah dengan perintah PRAGMA adalah Anda tidak dapat menanyakannya. perintah tidak menyajikan data ke mesin SQL, ia mengembalikan hasil secara langsung
Kowlown
1
Bukankah ini menciptakan kondisi balapan? Katakanlah saya memeriksa nama kolom, melihat bahwa kolom saya hilang, tetapi sementara itu proses lain menambahkan kolom tersebut. Kemudian saya akan mencoba menambahkan kolom tetapi akan mendapatkan error karena sudah ada. Saya kira saya harus mengunci database dulu atau sesuatu? Saya seorang noob untuk sqlite saya takut :).
Ben Farmer
26

Jika Anda melakukan ini dalam pernyataan pemutakhiran DB, mungkin cara paling sederhana adalah dengan hanya menangkap pengecualian yang dilemparkan jika Anda mencoba untuk menambahkan bidang yang mungkin sudah ada.

try {
   db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN foo TEXT default null");
} catch (SQLiteException ex) {
   Log.w(TAG, "Altering " + TABLE_NAME + ": " + ex.getMessage());
}
pengguna7896780
sumber
3
Saya tidak suka pemrograman gaya pengecualian, tapi ini luar biasa bersih. Mungkin Anda sedikit mempengaruhi saya.
Stephen J
Saya juga tidak menyukainya, tetapi C ++ adalah bahasa pemrograman gaya paling pengecualian yang pernah ada. Jadi saya rasa orang mungkin masih melihatnya sebagai "valid".
tmighty
Kasus penggunaan saya untuk SQLite = Saya tidak ingin melakukan banyak pengkodean tambahan untuk sesuatu yang sederhana / satu liner dalam bahasa lain (MSSQL). Jawaban bagus ... meskipun itu adalah "pemrograman gaya pengecualian" itu dalam fungsi peningkatan / terisolasi jadi saya kira itu dapat diterima.
maplemale
Sementara yang lain tidak menyukainya, saya pikir ini adalah solusi terbaik lol
Adam Varhegyi
13

threre adalah metode PRAGMA adalah table_info (nama_tabel), ia mengembalikan semua informasi tabel.

Berikut ini implementasi cara menggunakannya agar kolom centang ada atau tidak,

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

Anda juga dapat menggunakan kueri ini tanpa menggunakan loop,

cursor = db.rawQuery("PRAGMA table_info("+ table +") where name = " + column, null);
Krunal Shah
sumber
Cursor cursor = db.rawQuery ("pilih * dari nama tabel", null); kolom = cursor.getColumnNames ();
Vahe Gharibyan
1
Saya kira Anda lupa menutup kursor :-)
Pecana
@VaheGharibyan, jadi Anda cukup memilih semua yang ada di DB Anda hanya untuk mendapatkan nama kolom ?! Apa yang Anda katakan adalah we give no shit about performance:)).
Farid
Catatan, kueri terakhir salah. Kueri yang tepat adalah: SELECT * FROM pragma_table_info(...)(perhatikan SELECT dan garis bawahi antara pragma dan info tabel). Tidak yakin versi apa yang sebenarnya mereka tambahkan, ini tidak berfungsi pada 3.16.0 tetapi berfungsi pada 3.22.0.
PressingOnAlways
3

Bagi mereka yang ingin menggunakan pragma table_info()result sebagai bagian dari SQL yang lebih besar.

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

Bagian utama adalah untuk menggunakan pragma_table_info('<table_name>')bukan pragma table_info('<table_name>').


Jawaban ini terinspirasi dari balasan @Robert Hawkey. Alasan saya mempostingnya sebagai jawaban baru adalah saya tidak memiliki reputasi yang cukup untuk mempostingnya sebagai komentar.

Matahari
sumber
1

Saya datang dengan pertanyaan ini

SELECT CASE (SELECT count(*) FROM pragma_table_info(''product'') c WHERE c.name = ''purchaseCopy'') WHEN 0 THEN ALTER TABLE product ADD purchaseCopy BLOB END
  • Kueri dalam akan mengembalikan 0 atau 1 jika kolom ada.
  • Berdasarkan hasil, ubah kolom
Aravin
sumber
kode = Kesalahan (1), pesan = System.Data.SQLite.SQLiteException (0x800007BF): kesalahan logika SQL di dekat "ALTER": kesalahan sintaks di System.Data.SQLite.SQLite3.Prepare
イ ン コ グ ニ ト ア レ ク セ イ
Anda mengalami kesalahan ketik dengan 2 tanda kutip sederhana di sekitar string (product dan purchaseCopy) tetapi saya tidak dapat membuatnya berfungsi karena "LALU ALTER TABLE". Apakah kamu yakin itu mungkin? Jika ini berhasil, itu harus menjadi jawaban yang diterima.
Neekobus
0

Saya mengambil jawaban di atas di C # /. Net, dan menulis ulang untuk Qt / C ++, tidak terlalu banyak berubah, tetapi saya ingin meninggalkannya di sini untuk siapa pun di masa depan yang mencari jawaban 'ish' 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
sumber
0

Sebagai alternatif, Anda dapat menggunakan pernyataan CASE-WHEN TSQL yang dikombinasikan dengan pragma_table_info untuk mengetahui apakah ada kolom:

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
sumber
di sini bagaimana kita mengubah tabel? bila ada nama kolom yang cocok?
pengguna2700767
0

Ini solusi saya, tetapi dengan python (saya mencoba dan gagal menemukan posting apa pun tentang topik yang terkait dengan 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)

Saya menggunakan PRAGMA untuk mendapatkan informasi tabel. Ini mengembalikan array multidimensi yang penuh dengan informasi tentang kolom - satu array per kolom. Saya menghitung jumlah array untuk mendapatkan jumlah kolom. Jika kolom tidak cukup, maka saya menambahkan kolom menggunakan perintah ALTER TABLE.

Thomas Weeks
sumber
0

Semua jawaban ini baik-baik saja jika Anda mengeksekusi satu baris dalam satu waktu. Namun, pertanyaan aslinya adalah memasukkan skrip sql yang akan dieksekusi oleh satu db yang dieksekusi dan semua solusi (seperti memeriksa untuk melihat apakah kolom ada di sana sebelumnya) akan memerlukan program pelaksana baik memiliki pengetahuan tentang tabel apa dan kolom sedang diubah / ditambahkan atau lakukan pra-pemrosesan dan parsing dari skrip input untuk menentukan informasi ini. Biasanya Anda tidak akan menjalankan ini secara realtime atau sering. Jadi gagasan menangkap pengecualian dapat diterima dan kemudian dilanjutkan. Di situlah letak masalahnya ... bagaimana melanjutkan. Untungnya pesan kesalahan memberi kita semua informasi yang kita butuhkan untuk melakukan ini. Idenya adalah untuk mengeksekusi sql jika pengecualian pada panggilan tabel alter, kita dapat menemukan baris tabel alter di sql dan mengembalikan baris yang tersisa dan mengeksekusi sampai berhasil atau tidak ada lagi baris tabel perubahan yang cocok dapat ditemukan. Inilah beberapa contoh kode di mana kita memiliki skrip sql dalam sebuah array. Kami mengulang array yang mengeksekusi setiap skrip. Kami menyebutnya dua kali untuk membuat perintah alter table gagal tetapi program berhasil karena kami menghapus perintah alter table dari sql dan mengeksekusi ulang kode yang diperbarui.

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

keluaran yang diharapkan

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

Logika: kolom sql di sqlite_master berisi definisi tabel, jadi pasti berisi string dengan nama kolom.

Saat Anda mencari sub-string, ia memiliki batasan yang jelas. Jadi saya akan menyarankan untuk menggunakan sub-string yang lebih ketat di ColumnName, misalnya sesuatu seperti ini (tunduk pada pengujian karena karakter '"' tidak selalu ada):

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

Saya menyelesaikannya dalam 2 pertanyaan. Ini adalah skrip Unity3D saya menggunakan 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; }
            }
イ ン コ グ ニ ト ア レ ク セ イ
sumber