簡介
有時,您需要從 SQL 語句中訪問操作系統功能。例如,您可能希望將一些信息寫到文件、與特定的進程建立通信鏈路或執行駐留在數據庫服務器上的另一個程序。IBM® DB2® Universal Database™(UDB)的用戶自定義函數(UDF)可以使您方便地完成這些任務。您可以使用各種不同的編程語言(包括 SQL、Java™ 或 C)實現 UDF。在本文中,我將用一些示例向您展示,用 C 編寫的 UDF 如何在數據庫服務器上被用來直接執行“外部”任務。特別地,我將向您展示如何:
訪問駐留在數據庫服務器上的文件
調用另一個可執行文件。
處理文件系統中文件的方法基本上有兩種:從文件中讀取或寫入文件。在 SQL 中使用 UDF,可以輕松地完成這些任務。例如,要讀取文件,您可以將該文件的名稱指定為 SQL 語句中的表達式,而 UDF 可以將該文件名作為輸入,並將文件的內容作為其結果返回。可以象對待 SQL 語句中的任何其它值那樣對待這個結果:可以將它插入表、以結果集的形式返回給客戶機或由其它函數進一步處理它。同樣,通過指定文件名和字符串本身,可以從 SQL 啟動將字符串寫入到文件的操作。函數打開文件,寫入字符串,然後返回成功或錯誤狀態。
從 SQL 調用數據庫服務器上的可執行文件或腳本可能會派上用場。例如,您可能希望根據數據庫中的事件或條件啟動一些任務。譬如,表中的行太多您可能想開始進行備份,或者您可能想使用另一個程序來執行某一任務(譬如向管理員發送電子郵件)。另一種可能的選擇是在獨立的事務中執行 SQL 腳本。
在我向您展示這些 UDF 之前,讓我們簡要地回顧一下如何編譯 UDF 的 C/C++ 代碼並將它鏈接到共享庫,以便 DB2 此後能夠使用該函數。
構建用戶自定義函數
要構建本文中描述的函數,編譯源文件 os_calls.c 並將它鏈接到共享庫。如果您願意,還可以定義導出文件,它定義該庫的入口點。您必須根據平台使用不同樣式的導出文件。Windows® 系統需要導出文件 library.def 中具有如 清單 1所示的內容。
清單 1. Windows 系統上的導出文件
LIBRARY systemCallUDF
EXPORTS
readFileToClob
readFileToTable
writeFileFromParams
systemCall
UNIX® 系統使用 .exp 作為導出文件 library.exp 的擴展名,該文件中的結構非常簡單。它只列出將要導出的函數的入口點,而不包含更多的信息。 清單 2顯示了這樣一個示例。
清單 2. UNIX 和 Linux 系統上的導出文件
readFileToClob
readFileToTable
writeFileFromParams
systemCall
使用相應的導出文件和源文件來編譯和鏈接代碼。要完成這項任務,您必須確保在用來開發這些函數的系統上安裝了 C 編譯器和 DB2 應用程序開發客戶機(Application Development ClIEnt)。注:在打算使用這些函數的 生產系統上不需要有 C 編譯器或 DB2 應用程序開發客戶機。為了簡化構建過程,可以使用實例目錄中的 sqllib/samples/c/bldrtn 腳本(bldrtn 表示 構建例程,build routine)。如果使用腳本,就必須提供導出文件。
注:對於版本 DB2 UDB 7.x,在 UNIX 系統上使用 bldudf 工具,在 Windows 系統上使用 bldmudf 工具。語法略有不同;請參考文件中的描述以獲取更多詳細信息。
要在文件 os_calls.c 中構建 UDF,只要執行:
INSTHOME
/sqllib/samples/c/bldrtn os_calls
其中 INSTHOME 是到實例主目錄的路徑。例如:
c:\\program files\\ibm\\sqllib\\samples\\c\\bldrtn os_calls
或
/home/stolze/sqllib/samples/c/bldrtn os_calls
該腳本的最後一步將共享庫復制到實例目錄中的 sqllib/function 目錄。
從文件中讀取
當讀取文件時,如何用 SQL 表示文件內容有幾種選擇。例如,整個文件內容可以變成一個標量 VARCHAR 或 CLOB 值(二進制文件可以轉換成 BLOB),或者如果文件有良好定義的結構,那麼該結構就可以被映射成關系表。在第一種情況下,外部的標量函數可以完成該任務;在第二種情況下,我們使用 DB2 表函數來執行從文件內部結構到數據庫系統中的表結構的映射。
首先,我們處理將文件的整個內容表示為單個 CLOB 值的基本情況。這意味著讀取該文件的整個內容並將它存儲到 LOB 值中,然後將該 LOB 值返回到數據庫引擎。該函數的輸入是絕對文件名,輸出是包含結果的 CLOB。使用 清單 3中的 SQL 語句在數據庫中創建函數。
清單 3. 創建將文件讀入 CLOB 中的函數
CREATE FUNCTION readFile(fileName VARCHAR(255))
RETURNS CLOB(1M)
SPECIFIC readFileToClob
EXTERNAL NAME 'os_calls!readFileToClob'
LANGUAGE C
PARAMETER STYLE SQL
DETERMINISTIC
NOT FENCED
RETURNS NULL ON NULL INPUT
NO SQL
NO EXTERNAL ACTION
NO SCRATCHPAD
ALLOW PARALLEL
NO FINAL CALL;
GRANT EXECUTE ON FUNCTION readFile TO PUBLIC;
注:僅有 DB2 UDB V8 支持 GRANT EXECUTE 語句。同樣,對於 DB2 UDB V7,必須將 PARAMETERSTYLE SQL 語句替換成 PARAMETER STYLE DB2SQL。
實現該函數的相應 C 代碼也很簡單,如 清單 4所示。該代碼打開指定文件、驗證文件的大小以確保所有數據都能復制到 CLOB,然後在關閉該文件之前將數據實際復制到 CLOB。
清單 4. 將文件讀入 CLOB 中的 C 代碼
#include <stdio.h>
#include <sqludf.h>
void SQL_API_FN readFileToClob(
SQLUDF_VARCHAR *fileName,
SQLUDF_CLOB *fileData, /* output */
/* null indicators */
SQLUDF_NULLIND *fileName_ind,
SQLUDF_NULLIND *fileData_ind,
SQLUDF_TRAIL_ARGS)
{
int rc = 0;
long fileSize = 0;
size_t readCnt = 0;
FILE *f = NULL;
f = fopen(fileName, "r");
if (!f) {
strcpy(SQLUDF_MSGTX, "Could not open file ");
strncat(SQLUDF_MSGTX, fileName,
SQLUDF_MSGTEXT_LEN - strlen(SQLUDF_MSGTX)-1);
strncpy(SQLUDF_STATE, "38100", SQLUDF_SQLSTATE_LEN);
return;
}
rc = fseek(f, 0, SEEK_END);
if (rc) {
sprintf(SQLUDF_MSGTX, "fseek() failed with rc = %d", rc);
strncpy(SQLUDF_STATE, "38101", SQLUDF_SQLSTATE_LEN);
return;
}
/* verify the file size */
fileSize = ftell(f);
if (fileSize > fileData->length) {
strcpy(SQLUDF_MSGTX, "File too large");
strncpy(SQLUDF_STATE, "38102", SQLUDF_SQLSTATE_LEN);
return;
}
/* go to the beginning and read the entire file */
rc = fseek(f, 0, 0);
if (rc) {
sprintf(SQLUDF_MSGTX, "fseek() failed with rc = %d", rc);
strncpy(SQLUDF_STATE, "38103", SQLUDF_SQLSTATE_LEN);
return;
}
readCnt = fread(fileData->data, 1, fileSize, f);
if (readCnt != fileSize) {
/* raise a warning that something weird is going on */
sprintf(SQLUDF_MSGTX, "Could not read entire file "
"(%d vs %d)", readCnt, fileSize);
strncpy(SQLUDF_STATE, "01H10", SQLUDF_SQLSTATE_LEN);
*fileData_ind = -1;
}
else {
fileData->length = readCnt;
*fileData_ind = 0;
}
}
在編譯並安裝了包含 UDF 的共享庫之後,讓我們執行一些非常基本的成功和錯誤用例的測試:
/home/stolze $ echo "test" > test_file
/home/stolze $ db2 "VALUES readFile( '/home/stolze/test_file' )"
1
--------------------------------------------------------
test
1 record(s) selected.
/home/stolze $ db2 "VALUES readFile( '/home/stolze/test_file123' )"
1
--------------------------------------------------------
SQL0443N Routine "READFILE" (specific name "READFILETOCLOB") has
returned an error SQLSTATE with diagnostic text "Could not open
file /home/stolze/test_file123". SQLSTATE=38100
此後,就可以在 SQL 語句中象使用其它函數一樣使用這個 UDF,例如在下列 INSERT 中:
/home/stolze $ db2 "CREATE TABLE test_table ( text CLOB(2M) )"
DB20000I The SQL statement completed successfully.
/home/stolze $ db2 "INSERT INTO test_table VALUES ( readFile( '/home/stolze/test_file' ) )"
DB20000I The SQL statement completed successfully.
/home/stolze $ db2 "SELECT SUBSTR(text, 2, 3) FROM test_table"
1
---
est
1 record(s) selected.
使用表函數
接下來將說明如何將具有良好定義結構的文件表示為關系表。在這種情況下,我將其作為 opaque 數據,實現了從文件內容到表的映射,而不是簡單地將整個文件復制到單個 LOB 中。
對於我們的示例,假定文件有下列結構:
該文件由一系列統一的結構化記錄組成。
每個記錄總共包含 60 字節。
前 10 個字節包含(以文本表示的)32 位帶符號的整數值。
後 50 個字節表示一個定長字符串。
從文件結構到關系結構的簡單映射就是把文件中的所有記錄作為一個具有兩列的表返回。第一列的類型是 INTEGER,第二列的類型是 VARCHAR(50)。文件中的每個記錄將成為表中的一行。注:其它文件可能采用不同的文件格式,每種格式都需要屬於自己的專門映射。您需要為支持每種映射而實現單獨的函數。
定義了映射之後,讓我們創建數據庫系統中的函數。 清單 5向您展示了創建的方法。
清單 5. 創建讀取文件並將它表示為表的函數
CREATE FUNCTION readFileTable(fileName VARCHAR(256))
RETURNS TABLE ( intCol INTEGER, string VARCHAR(50) )
SPECIFIC readFileToTable
EXTERNAL NAME 'os_calls!readFileToTable'
LANGUAGE C
PARAMETER STYLE SQL
DETERMINISTIC
NOT FENCED
RETURNS NULL ON NULL INPUT
NO SQL
NO EXTERNAL ACTION
SCRATCHPAD
NO FINAL CALL
DISALLOW PARALLEL;
GRANT EXECUTE ON FUNCTION readFileTable TO PUBLIC;
清單 6展示了實現從文件格式到關系表映射的 C 代碼。
清單 6. 將文件內容表示為表的 C 代碼
#include <stdio.h>
#include <sqludf.h>
#include <sqlstate.h>
void SQL_API_FN readFileToTable(
SQLUDF_VARCHAR *fileName, /* input */
SQLUDF_INTEGER *intCol, /* output */
SQLUDF_VARCHAR *charCol, /* output */
/* null indicators */
SQLUDF_NULLIND *fileName_ind,
SQLUDF_NULLIND *intCol_ind,
SQLUDF_NULLIND *charCol_ind,
SQLUDF_TRAIL_ARGS_ALL)
{
size_t readCnt = 0;
char intValue[10+1] = { '\\0' };
char strValue[50+1] = { '\\0' };
struct scratchMap {
FILE *f;
int rowNumber;
};
/* map the scratchpad */
struct scratchMap *scratch =
(struct scratchMap *)SQLUDF_SCRAT->data;
*intCol_ind = -1;
*charCol_ind = -1;
switch (SQLUDF_CALLT) {
case SQLUDF_TF_OPEN:
/* open file and store the pointer on the scratchpad */
scratch->f = fopen(fileName, "r");
if (!scratch->f) {
strcpy(SQLUDF_MSGTX, "Could not open file ");
strncat(SQLUDF_MSGTX, fileName, SQLUDF_MSGTEXT_LEN -
strlen(SQLUDF_MSGTX)-1);
strncpy(SQLUDF_STATE, "38200", SQLUDF_SQLSTATE_LEN);
return;
}
scratch->rowNumber = 0;
break;
case SQLUDF_TF_FETCH:
/* count the row */
scratch->rowNumber++;
/* read the integer */
readCnt = fread(intValue, 1, 10, scratch->f);
if (readCnt == 0) {
/* end of file reached */
/* end of file reached */
strncpy(SQLUDF_STATE, SQL_NODATA_EXCEPTION,
SQLUDF_SQLSTATE_LEN);
return;
}
else if (readCnt != 10) {
sprintf(SQLUDF_MSGTX, "Could not read int value "
"in line %d", scratch->rowNumber);
strncpy(SQLUDF_STATE, "38201", SQLUDF_SQLSTATE_LEN);
return;
}
intValue[10] = '\\0';
if (sscanf(intValue, "%d", intCol) != 1) {
sprintf(SQLUDF_MSGTX, "Invalid integer value %s "
" in row %d", intValue, scratch->rowNumber);
strncpy(SQLUDF_STATE, "38202", SQLUDF_SQLSTATE_LEN);
return;
}
*intCol_ind = 0;
/* read the string (allow truncations at EOF) */
readCnt = fread(strValue, 1, 50, scratch->f);
strValue[readCnt] = '\\0';
strcpy(charCol, strValue);
*charCol_ind = 0;
break;
case SQLUDF_TF_CLOSE:
/* close the file */
fclose(scratch->f);
scratch->f = NULL;
scratch->rowNumber = 0;
}
}
現在,讓我們構造一個符合上面指定文件格式的文件。用此類文件,可以驗證 UDF 是否正常運行。注:我們一定不能在文件中附加行結束符,因為這樣做可能不符合所要求的文件格式。用於 echo 命令的 -n 選項意味著我們未終止該字符串。生成文件的方式因所用的操作系統而異。
/home/stolze $ echo -n "1234567890" > test_file
/home/stolze $ echo -n "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx" >> test_file
/home/stolze $ echo -n " 987654" >> test_file
/home/stolze $ echo -n "12345678901234567890123456789012345678901234567890" >> test_file
/home/stolze $ echo -n " -2" >> test_file
/home/stolze $ echo -n " test string " >> test_file
調用表函數的語法與調用標量函數的不同。以下 SELECT 語句演示了如何在 FROM 子句中使用表函數。接下來,您會看到一些非常基本的對該函數的測試。
/home/stolze $ db2 "SELECT * FROM TABLE ( readFileTable( '/home/stolze/test_file' ) ) AS t"
INTCOL STRING
----------- --------------------------------------------------
1234567890 abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwx
987654 12345678901234567890123456789012345678901234567890
-2 test string
3 record(s) selected.
/home/stolze $ db2 "SELECT * FROM TABLE ( readFileTable( '/home/stolze/test_file123' ) ) AS t"
INTCOL STRING
----------- --------------------------------------------------
SQL0443N Routine "READFILETABLE" (specific name "*LETOTABLE") has
returned an error SQLSTATE with diagnostic text "Could not open
file /home/stolze/test_file123". SQLSTATE=38200
/home/stolze $ echo -n "123" > test_file
/home/stolze $ db2 "SELECT * FROM TABLE ( readFileTable( '/home/stolze/test_file' ) ) AS t"
INTCOL STRING
----------- --------------------------------------------------
SQL0443N Routine "READFILETABLE" (specific name
"READFILETOTABLE") has returned an error SQLSTATE with diagnostic
text "Could not read int value in line 1". SQLSTATE=38201
/home/stolze $ touch empty_test
/home/stolze $ db2 "SELECT * FROM TABLE ( readFileTable( '/home/stolze/empty_test' ) ) AS t"
INTCOL STRING
----------- --------------------------------------------------
0 record(s) selected.
寫入文件
文章“ 從 SQL 中進行文件輸出和調試的 UDF”展示了一種常見情形,在那種情形下如果能夠從 SQL 語句內部將數據寫到文件將是非常有幫助的。該文章應用了本節所介紹的概念。
寫入文件的操作基本上是從文件讀取的逆過程。必須實現文件格式與將要寫入的信息之間的映射。同樣,用戶自定義函數可以用很方便的方式執行這些任務。在下列示例中,我假定所采用的文件格式定義與我們上面用過的相同。
該文件由一系列統一的結構化記錄組成。
每個記錄總共包含 60 字節。
前 10 個字節包含以文本表示的 32 位帶符號的整數值。
後 50 個字節表示定長字符串。
實現寫操作的用戶自定義函數接受兩個輸入參數。第一個參數的類型是 INTEGER,第二個參數的類型是 VARCHAR(50)。我們由此實現了如 清單 7所示的 SQL 語句,用來注冊數據庫中的函數。
清單 7. 創建將數據寫入到文件的函數
CREATE FUNCTION writeFile( fileName VARCHAR(255),
intValue INTEGER, strValue VARCHAR(50) )
RETURNS INTEGER
SPECIFIC writeFile
EXTERNAL NAME 'os_calls!writeFileFromParams'
LANGUAGE C
PARAMETER STYLE SQL
DETERMINISTIC
NOT FENCED
RETURNS NULL ON NULL INPUT
NO SQL
EXTERNAL ACTION
SCRATCHPAD
DISALLOW PARALLEL
FINAL CALL;
GRANT EXECUTE ON FUNCTION writeFile TO PUBLIC;
使用高速暫存(scratchpad)來將打開文件描述符從一個 UDF 調用傳遞到下一個調用。這樣,如果要在單個 SQL 語句向文件寫入多個記錄,我們就可以節省反復打開和關閉文件所用的時間。實現寫操作的 C 代碼也很簡單。 清單 8顯示了詳細信息。必須對調用類型給予特別關注,但除此以外,只需以正確的格式將數據寫入文件。
清單 8. 將數據寫入文件的 C 代碼
#include <stdio.h>
#include <string.h>
#include <sqludf.h>
void SQL_API_FN writeFileFromParams(
SQLUDF_VARCHAR *fileName, /* input */
SQLUDF_INTEGER *intCol, /* input */
SQLUDF_VARCHAR *charCol, /* input */
SQLUDF_INTEGER *result, /* output */
/* null indicators */
SQLUDF_NULLIND *fileName_ind, SQLUDF_NULLIND *intCol_ind,
SQLUDF_NULLIND *charCol_ind, SQLUDF_NULLIND *result_ind,
SQLUDF_TRAIL_ARGS_ALL)
{
int rc = 0;
int writtenLen = 0;
char row[60+1] = { '\\0' };
struct scratchMap {
FILE *f;
};
/* map the scratchpad */
struct scratchMap *scratch =
struct scratchMap *scratch =
(struct scratchMap *)SQLUDF_SCRAT->data;
*result_ind = -1;
switch (SQLUDF_CALLT) {
case SQLUDF_FIRST_CALL:
/* open file and store the pointer on the scratchpad */
scratch->f = fopen(fileName, "wb");
if (!scratch->f) {
strcpy(SQLUDF_MSGTX, "Could not open file ");
strncat(SQLUDF_MSGTX, fileName, SQLUDF_MSGTEXT_LEN -
strlen(SQLUDF_MSGTX)-1);
strncpy(SQLUDF_STATE, "38300", SQLUDF_SQLSTATE_LEN);
rc = -1;
goto cleanup;
}
}
case SQLUDF_NORMAL_CALL:
if (*intCol < -999999999) {
/* a number of -1000000000 or smaller needs
11 bytes, which is too long for our format */
sprintf(SQLUDF_MSGTX, "The number %d is too long.",
*intCol);
strncpy(SQLUDF_STATE, "38301", SQLUDF_SQLSTATE_LEN);
rc = -2;
goto cleanup;
}
if (strlen(charCol) > 50) {
/* strings must not be longer than 50 characters */
sprintf(SQLUDF_MSGTX, "The string %s is too long.",
charCol);
strncpy(SQLUDF_STATE, "38302", SQLUDF_SQLSTATE_LEN);
rc = -3;
goto cleanup;
}
/* write the given integer and string */
writtenLen = fprintf(scratch->f, "%10d%-50s", *intCol,
charCol);
if (writtenLen != 60) {
sprintf(SQLUDF_MSGTX, "Did not write 60 bytes "
"(wrote %d).", writtenLen);
strncpy(SQLUDF_STATE, "38303", SQLUDF_SQLSTATE_LEN);
rc = -4;
goto cleanup;
}
*result = 0;
*result_ind = 0;
break;
}
cleanup:
switch (SQLUDF_CALLT) {
case SQLUDF_FIRST_CALL:
if (rc == 0) {
break;
}
/* we must close the file descriptor upon an error in
the FIRST call; so we fall through to the final call
processing below */
case SQLUDF_FINAL_CALL:
case SQLUDF_FINAL_CRA:
/* close the file */
fclose(scratch->f);
fclose(scratch->f);
scratch->f = NULL;
}
}
編譯了代碼,並鏈接到共享庫之後,就要測試該函數。將該函數與上面介紹的 UDF readFileTable() 一起使用,我們還可以通過再次從該文件讀取數據,並將它表示為數據庫中的表,來驗證數據是否是以正確的格式寫入文件的。
/home/stolze $ db2 "VALUES writeFile( '/home/stolze/write.file', 10, 'some text' )"
1
-----------
0
1 record(s) selected.
/home/stolze $ more write.file
10some text
/home/stolze $ db2 "VALUES writeFile( '/home/stolze/write.file', -1234567890, 'some text' )"
1
-----------
SQL0443N Routine "STOLZE.WRITEFILE" (specific name "WRITEFILE") has
returned an error SQLSTATE with diagnostic text "The number
-1234567890 is too long." SQLSTATE=38301
/home/stolze $ db2 "CREATE TABLE t ( id INTEGER, string VARCHAR(50) )"
/home/stolze $ db2 "INSERT INTO t VALUES ( 1, 'the first text' ), ( 2, 'another text' )"
下列 SELECT 語句中的 UDF 執行的文件寫操作處理了名為“t”的表中的兩行。如果表包含更多行,則其它行也會被寫入文件。請注意,我沒有在生成的文件 write.file 中設置行結束符。因此,該文件將在向標准輸出轉儲時只顯示一個長字符串,並且只在一行上顯示。
/home/stolze $ db2 "SELECT writeFile( '/home/stolze/write.file', id, string ) FROM t"
/home/stolze $ more write.file
1the first text 2another text
/home/stolze $ db2 "SELECT * FROM TABLE ( readFileTable( '/home/stolze/write.file' ) ) AS x"
INTCOL STRING
----------- --------------------------------------------------
1 the first text
2 another text
2 record(s) selected.