postgresql核心開發之add_months函式實戰
postgresql核心內部函式實現詳解
pg前文通過實現的helloworld入門,實際上就是實現了一個函式,但這個函式只有演示意義,也沒有詳細介紹函式實現細節,本文將詳細介紹在postgresql內部如何完成一個函式的實現。
在oracle中,add_months函式能基於當前時間增加或減去幾個月得出一個新日期,postgresql沒有這樣的函式,我們就以add_months有例子介紹下postgresql的函式增加方法。在C語言中,實現一個函式很簡單,只要把函式主體實現就行,在其它檔案中使用的話include標頭檔案中聲名就可以了,這兩個步驟在postgresql核心中自然也要有,但還要增加一個動作,註冊到pg_proc系統表中,pg_proc是postgresql中函式的系統表。
首先實現函式,因為是時間相關的函式,可以放到src/backend/utils/adt/date.c中,add_months_date和add_months_timestamp,標頭檔案src/include/utils/date.h 增加聲名,程式碼如下:
Datum
add_months_date(PG_FUNCTION_ARGS)
{
DateADT date = PG_GETARG_DATEADT(0);
int added_mon = PG_GETARG_INT32(1);
struct pg_tm tt;
struct pg_tm * tm = &tt;
bool isLastDay = false;
int LastDay;
int year;
int mon;
int day;
DateADT result = 0;
/* check range */
if (DATE_NOT_FINITE(date))
{
PG_RETURN_DATEADT(date);
}
j2date((date + POSTGRES_EPOCH_JDATE), &(tm->tm_year), &(tm->tm_mon), &(tm->tm_mday));
year = tm->tm_year;
mon = tm->tm_mon;
day = tm->tm_mday;
isLastDay = (day == day_tab[isleap(year)][mon - 1]);
mon += added_mon;
if (mon > 12)
{
year += ((mon - 1) / 12);
mon = (((mon - 1) % 12) + 1);
}
else if (mon < 1)
{
year += ((mon / 12) - 1);
mon = ((mon % 12) + 12);
}
LastDay = day_tab[isleap(year)][mon - 1];
day = isLastDay ? LastDay : ((day > LastDay) ? LastDay : day);
if (!IS_VALID_JULIAN(year, mon, day))
{
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("date out of range")));
}
result = (DateADT)(date2j(year, mon, day) - POSTGRES_EPOCH_JDATE);
PG_RETURN_DATEADT(result);
}
Datum
add_months_timestamp(PG_FUNCTION_ARGS)
{
Timestamp timestamp = PG_GETARG_TIMESTAMP(0);
int added_mon = PG_GETARG_INT32(1);
struct pg_tm tt;
struct pg_tm * tm = &tt;
bool isLastDay = FALSE;
int LastDay;
int year;
int mon;
int day;
int tz;
fsec_t fsec;
Timestamp result = 0;
if (TIMESTAMP_NOT_FINITE(timestamp))
{
PG_RETURN_TIMESTAMP(timestamp);
}
if (timestamp2tm(timestamp, &tz, tm, &fsec, NULL, NULL) != 0)
{
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("timestamp out of range")));
}
year = tm->tm_year;
mon = tm->tm_mon;
day = tm->tm_mday;
isLastDay = (day == day_tab[isleap(year)][mon - 1]);
mon += added_mon;
if (mon > 12)
{
year += ((mon - 1) / 12);
mon = (((mon - 1) % 12) + 1);
}
else if (mon < 1)
{
year += ((mon / 12) - 1);
mon = ((mon % 12) + 12);
}
LastDay = day_tab[isleap(year)][mon - 1];
day = isLastDay ? LastDay : ((day > LastDay) ? LastDay : day);
tm->tm_year = year;
tm->tm_mon = mon;
tm->tm_mday = day;
if (tm2timestamp(tm, fsec, NULL, &result) != 0)
{
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("timestamp out of range")));
}
PG_RETURN_TIMESTAMP(result);
}
src/include/utils/date.h
extern Datum add_months_date(PG_FUNCTION_ARGS);
extern Datum add_months_timestamp(PG_FUNCTION_ARGS);
這裡不對add_months函式內部邏輯深入,有興趣的可以研究下oracle該函式表現對照下,重點在於完成了函式體實現,只要再註冊到pg_proc中即可實現新增加函式。如何註冊呢?
在src/include/catalog/pg_proc.h 新增程式碼如下:
DATA(insert OID = 9998 (add_months PGNSP PGUID 12 1 0 0 0 f f f f t f i f 2 0 1082 "1082 23" _null_ _null_ _null_ _null_ _null_ add_months_date _null_ _null_ _null_));
DATA(insert OID = 9997 (add_months PGNSP PGUID 12 1 0 0 0 f f f f t f i f 2 0 1114 "1114 23" _null_ _null_ _null_ _null_ _null_ add_months_timestamp _null_ _null_ _null_));
DATA(insert OID = 9996 (add_months PGNSP PGUID 14 1 0 0 0 f f f f t f i f 2 0 1114 "1184 23" _null_ _null_ _null_ _null_ _null_ "select add_months($1::timestamp,$2)" _null_ _null_ _null_));
OK,全部需要增加的程式碼全部完成,編譯,安裝,重新初始化庫,再執行測試如下:
postgres=# \d a
Table "public.a"
Column | Type | Modifiers
--------+-----------------------------+-----------
a | date |
b | timestamp without time zone |
postgres=# select *from a;
a | b
------------+----------------------------
2016-12-04 | 2016-12-04 22:17:28.447829
2016-02-28 | 2016-02-29 00:00:00
(2 rows)
postgres=# select a,add_months(a,3),b,add_months(b,4) from a;
a | add_months | b | add_months
------------+------------+----------------------------+----------------------------
2016-12-04 | 2017-03-04 | 2016-12-04 22:17:28.447829 | 2017-04-05 06:17:28.447829
2016-02-28 | 2016-05-28 | 2016-02-29 00:00:00 | 2016-06-30 08:00:00
(2 rows)
add_months功能正常,至此一個內部函式增加完畢,下面再以提問的方式詳細介紹下一些可能的疑問。
第一個問題,為什麼要增加兩個函式,不是隻一個add_months嗎?
事實上,一共增加了三個。這三個外部呼叫都是add_months函式,根據引數不相同實現了過載,在pg_proc.h註冊時說明了這些引數。(後面還有詳細說明)
第二個問題,註冊是什麼意思?
在模板庫中插入記錄,使任何庫一建立就有。對於系統表來說,在它的標頭檔案裡有對這個系統表的詳細定義說明,在這個標頭檔案下面能按格式預置一些記錄,這些記錄在initdb時會插入到模板庫的對應系統表中。具體來說,這些預置的記錄,在編譯過程中,會被perl指令碼轉換到postgres.bki中,這個bki檔案在安裝目錄的share資料夾,當initdb時,會載入這個bki並解析成一條條sql執行,創建出一個個系統表,插入初始記錄,把初始的資訊準備好。有興趣者可以看看initdb模組中 bootstrap_template1函式,初始化模組時第1件事就是載入postgres.bki檔案翻譯解析執行。
第三個問題,在pg_proc.h中插入的記錄是什麼含義?
以第一行為例詳細說明如下:
DATA(insert OID = 9998 (add_months PGNSP PGUID 12 1 0 0 0 f f f f t f i f 2 0 1082 “1082 23” null null null null null add_months_date null null null));
9998–OID使用核心中未使用的OID即可(src/include/catalog下unused_oids,可以顯示未使用的oid) postgres內部預留了1W多個oid給系統用,選一個沒有的就行,如果不知道哪些可用,在\src\include\catalog\ 下有個指令碼檔案unused_oids,執行一下就能找出哪些oid可用,但要這是一個linux指令碼,需要在linux下執行。
add_months–函式名,我們在SQL中用的外部函式,但對應內部功能函式實現可能不止一個
PGNSP–函式所屬的名字空間的OID,PGNSP即pg_catalog(oid=11),內建函式新增此值固定
PGUID–函式的擁有者OID,PGUID及initdb時指定使用者(oid=10),內建函式新增此值固定
12–實現語言或該函式的呼叫介面,內建函式使用12(internal),SQL用14,如第三個函式實現
1–估計的執行代價,如果proretset為真,這是每行返回的代價
0–估計的結果行數量(如果proretset為假,該值為0)
0–可變陣列引數的元素的資料型別,如果函式沒有可變引數則為0
0–呼叫該函式時可以通過此列指定的函式來簡化
f–函式是否為一個聚集函式
f–函式是否為一個視窗函式
f–函式是一個安全性定義者(即,一個”setuid”函式)
f–該函式沒有副作用。除了通過返回值,沒有關於引數的資訊被傳播。任何會丟擲基於其引數值的錯誤資訊的函式都不是洩露驗證的。
t–當任意呼叫函式為空時,函式是否會返回空值。在那種情況下函式實際上根本不會被呼叫。非”strict”函式必須準備好處理空值輸入。
f–函式是否返回一個集合(即,指定資料型別的多個值)
i–provolatile說明函式是僅僅只依賴於它的輸入引數,還是會被外部因素影響。值i表示”不變的”函式,它對於相同的輸入總是輸出相同的結果。值s表示”穩定的”函式,它的結果(對於固定輸入)在一次掃描內不會變化。值v表示”不穩定的”函式,它的結果在任何時候都可能變化(使用v頁表示函式具有副作用,所以對它們的呼叫無法得到優化)
f–函式型別(此項為我們開發過程中新增欄位,表示函式型別,f代表函式,p代表過程,t代表觸發器),postgresql核心沒有此引數,可忽略
2–輸入引數的個數,對應後面的1082 23兩個引數
0–具有預設值的引數個數
1082–返回值的資料型別
“1082 23”–函式引數的資料型別的陣列,這隻包括輸入引數(含INOUT和VARIADIC引數),因此也表現了函式的呼叫特徵 過載函式也憑這區別,如add_months函式,如果有多個,引數肯定不同,這個不同即可以是數量不同,也可以是型別不同,1082 23 就是代表型別,如下:
postgres=# select oid,typname from pg_type where oid in (1082,23,1114,1184) ; oid | typname ------+------------- 23 | int4 1082 | date 1114 | timestamp 1184 | timestamptz (4 rows)
_null_–函式引數的資料型別的陣列,這包括所有引數(含OUT和INOUT引數)。但是,如果所有引數都是IN引數,這個域將為空。注意下標是從1開始 ,然而由於歷史原因proargtypes的下標是從0開始
_null_–函式引數的模式的陣列。編碼為: i表示IN引數 , o表示OUT引數, b表示INOUT引數, v表示VARIADIC引數, t表示TABLE引數。 如果所有的引數都是IN引數,這個域為空。注意這裡的下標對應著proallargtypes而不是proargtypes中的位置
_null_–函式引數的名字的陣列。沒有名字的引數在陣列中設定為空字串。如果沒有一個引數有名字,這個域為空。注意這裡的下標對應著proallargtypes而不是proargtypes中的位置
_null_–預設值的表示式樹(按照
nodeToString()
的表現方式)。這是一個pronargdefaults元素的列表,對應於最後N個input引數(即最後N個proargtypes位置)。如果沒有一個引數具有預設值,這個域為空_null_–資料型別OID為了應用轉換
add_months_date–函式處理者如何呼叫該函式。它可能是針對解釋型語言的真實原始碼、一個符號連結、一個檔名或任何其他東西,這取決於實現語言/呼叫習慣,簡單來說,就是add_months真正執行的東西,第一個是add_months_date ,第二個是調了add_months_timestamp函式,第三個是一個sql語句select add_months(
1::timestamp, 2)。雖然我們寫sql語句時,並不是太關心相關的型別,但是在執行時,肯定是精確調某個函式來完成功能。第一個處理的是add_months(date,int), 第二個處理的是add_months(timestamp,int),第三個處理的是add_months(timestamptz,int),通過這樣的方式實現sql函式過載。_null_–關於如何呼叫函式的附加資訊。其解釋是與語言相關的
_null_–函式對於執行時配置變數的本地設定值
_null_–訪問許可權
第四個問題,Datum,PG_FUNCTION_ARGS,PG_GETARG_DATEADT,PG_RETURN_DATEADT,分別是什麼東西?
Datum是通用的型別,存什麼型別的值是什麼由接收的人自己解析。PG_FUNCTION_ARGS是引數的巨集,裡面有函式的資訊。PG_GETARG_DATEADT是取到前面的引數裡的對應位置的引數值。PG_RETURN_DATEADT將值轉成對應的型別返回,會將多餘的位元組清理掉。這些詳細的可以看看程式碼,尤其PG_FUNCTION_ARGS的定義用法,對理解函式的實現有很大幫助。
ok,這次就到這,希望能對大家有所幫助。