1. 程式人生 > >postgresql核心開發之add_months函式實戰

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元素的列表,對應於最後Ninput引數(即最後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,這次就到這,希望能對大家有所幫助。