小巧、快速、可靠。
選擇其中三項。
SQLite 內建的 printf()

1. 概述

SQLite 包含自行實作的字串格式化例程「printf()」,可透過以下介面存取:

SQLite 內部也使用相同的核心字串格式化器。

1.1. 優點

為什麼 SQLite 擁有自己私有的內建 printf() 實作?為什麼不使用標準 C 函式庫中的 printf() 實作?有幾個原因:

  1. 透過使用自己的內建實作,SQLite 保證在所有平台和所有地區設定下的輸出都相同。這對於一致性和測試非常重要。如果一台機器給出的答案是「5.25e+08」,而另一台機器給出的答案是「5.250e+008」,這將會造成問題。兩個答案都是正確的,但 SQLite 總是給出相同的答案會更好。

  2. 我們不知道有任何方法可以使用標準函式庫 printf() 的 C 介面來實作 SQLite 的 format() SQL 函式 功能。然而,內建的 printf() 實作可以輕鬆地適應該任務。

  3. SQLite 中的 printf() 支援新的非標準替換類型(%q%Q%w%z),以及增強的替換行為(%s 和 %z),這些對於 SQLite 內部和使用 SQLite 的應用程式都很有用。標準函式庫的 printf() 通常無法以這種方式擴充。

  4. 透過 sqlite3_mprintf()sqlite3_vmprintf() 介面,內建的 printf() 實作支援將任意長度的字串轉譯到從 sqlite3_malloc64() 取得的記憶體緩衝區中。這比嘗試預計算結果字串的上限大小、配置適當大小的緩衝區,然後呼叫 snprintf() 更安全且更不容易出錯。

  5. SQLite 特定的 printf() 支援一個新的旗標(!),稱為「替代形式 2」旗標。替代形式 2 旗標以微妙的方式改變了浮點數轉換的處理方式,使輸出始終是與 SQL 相容的浮點數文字表示形式 — 這是使用標準函式庫 printf() 無法實現的。對於字串替換,替代形式 2 旗標會使寬度和精度以字元而不是位元組來衡量,這簡化了包含多位元組 UTF8 字元的字串的處理。

  6. 內建的 SQLite 具有編譯時期選項,例如 SQLITE_PRINTF_PRECISION_LIMIT,可防止將 printf() 功能暴露給不受信任的使用者之應用程式遭受阻斷服務攻擊。

  7. 使用內建的 printf() 實作意味著 SQLite 對主機環境的依賴性更少,使其更具可攜性。

1.2. 缺點

平心而論,內建 printf() 的實作也有一些缺點。例如:

  1. 內建的 printf() 實作會使用額外的程式碼空間(在使用 -Os 旗標的 GCC 5.4 上約為 7800 位元組)。

  2. 內建 printf() 的浮點數轉文字子函式,精度限制為 16 位有效數字,或者如果使用「!」替代格式 2 旗標,則為 26 位有效數字。每個 IEEE-754 雙精度浮點數都可以*精確地*表示為十進位值,但對於許多雙精度浮點數,精確的十進位表示需要超過 16 或 26 位有效數字。SQLite 的 printf() 函式只呈現前 16 或 26 位有效數字,因為這樣可以有效率地完成,而且 16 位十進位數字足以區分每個可能的雙精度浮點數值。在需要精確十進位值的情況下,請使用十進位擴充功能來取得雙精度浮點數值的精確十進位等值。

  3. 內建 snprintf() 實作中緩衝區指標和緩衝區大小參數的順序與標準函式庫實作中使用的順序相反。

  4. 內建的 printf() 實作不處理 posix 位置參照修飾符,這些修飾符允許 printf() 的參數順序與 %-替代符號的順序不同。在內建的 printf() 中,參數的順序必須與 %-替代符號的順序完全匹配。

儘管存在這些缺點,開發人員認為在 SQLite 中內建 printf() 實作仍然是利大於弊的。

2. 格式化細節

printf() 的格式字串是用來產生字串的範本。每當格式字串中出現「%」字元時,就會進行替換。「%」後面跟著一個或多個描述替換的額外字元。每個替換都具有以下格式:

%[旗標][寬度][.精度][長度]類型

所有替換都以單個「%」開頭,並以單個類型字元結尾。替換的其他元素是可選的。

要在輸出中包含單個「%」字元,請在範本中放置兩個連續的「%」字元。

2.1. 替換類型

下表顯示 SQLite 支援的替換類型

替換類型意義
% 連續兩個「%」字元會在輸出中轉換為單個「%」,而不替換任何值。
d, i參數是一個帶正負號的整數,以十進位顯示。
u參數是一個無正負號的整數,以十進位顯示。
f參數是一個雙精度浮點數,以十進位顯示。
e, E參數是一個雙精度浮點數,以科學記號顯示。指數字元是 'e' 或 'E',取決於類型。
g, G參數是一個雙精度浮點數,以一般十進位表示法顯示,或者如果指數不接近零,則以科學記號顯示。
x, X參數是一個整數,以十六進位顯示。%x 使用小寫十六進位,%X 使用大寫十六進位。
o參數是一個整數,以八進位顯示。
s, z 參數是一個以零結尾的字串,會被顯示出來,或者是一個空指標,會被視為空字串。對於 C 語言介面中的 %z 類型,在字串複製到輸出後,會呼叫 sqlite3_free() 釋放字串。對於 SQL printf() 函式,%s 和 %z 替換是相同的,NULL 參數會被視為空字串。

%s 替換在 printf 函式中是通用的,但 %z 替換和對空指標的安全處理是 SQLite 的增強功能,在其他 printf() 實作中找不到。
c對於 C 語言介面,參數是一個整數,它被解釋為一個字元。對於 format() SQL 函式,參數是一個字串,從中提取第一個字元並顯示。
p參數是一個指標,它以十六進位位址顯示。由於 SQL 語言沒有指標的概念,因此 format() SQL 函式 的 %p 替換功能類似於 %x。
n參數是一個指向整數的指標。此替換類型不顯示任何內容。相反,參數指向的整數會被生成的字串中的字元數覆蓋,該字元數是由 %n 左側的所有格式符號產生的。
q, Q 參數是一個以零結尾的字串。該字串會將所有單引號 (') 字元加倍列印,以便該字串可以安全地顯示在 SQL 字串字面值中。%Q 替換類型還會在替換字串的兩端加上單引號。

如果 %Q 的參數為空指標,則輸出為未加引號的「NULL」。換句話說,空指標會產生 SQL NULL,而非空指標會產生有效的 SQL 字串字面值。如果 %q 的參數為空指標,則不會產生任何輸出。因此,指向 %q 的空指標與空字串相同。

對於這些替換,精度是從參數中獲取的位元組或字元數,而不是寫入輸出的位元組或字元數。

%q 和 %Q 替換是 SQLite 的增強功能,在大多數其他 printf() 實作中找不到。
w 此替換的工作方式與 %q 類似,不同之處在於它會將所有雙引號 (") 加倍,而不是單引號,使結果適用於在 SQL 陳述式中與雙引號括住的識別碼名稱一起使用。

%w 替換是 SQLite 的增強功能,在大多數其他 printf() 實作中找不到。

2.2. 選用的長度欄位

參數值的長度可以由替換類型字母之前的字母指定。在 SQLite 中,長度僅對整數類型有效。對於始終使用 64 位元值的 format() SQL 函式,長度會被忽略。下表顯示 SQLite 允許的長度說明符

長度說明符意義
(預設) 「int」或「unsigned int」。在所有現代系統上為 32 位元。
l「long int」或「long unsigned int」。在所有現代系統上也為 32 位元。
ll「long long int」或「long long unsigned」或「sqlite3_int64」或「sqlite3_uint64」值。在所有現代系統上,這些都是 64 位元整數。

只有「ll」長度修飾符對 SQLite 才會有影響。而且它僅在使用 C 語言介面時才會有影響。

2.3. 選用的寬度欄位

寬度欄位指定替換值在輸出中的最小寬度。如果寫入輸出的字串或數字短於寬度,則會填充該值。預設情況下,填充在左側(值為右對齊)。如果使用「-」旗標,則填充在右側,值為左對齊。

預設情況下,寬度以位元組為單位。但是,如果存在「!」旗標,則寬度以字元為單位。這僅對多位元組 utf-8 字元有影響,而這些字元僅出現在字串替換中。

如果寬度是單個「*」字元而不是數字,則實際寬度值會從參數列表中讀取為整數。如果讀取的值為負數,則絕對值用於寬度,並且值左對齊,如同存在「-」旗標一樣。

如果要替換的值大於寬度,則完整值會添加到輸出中。換句話說,寬度是值在輸出中呈現的最小寬度。

2.4. 選用的精度欄位

如果存在精度欄位,則必須在寬度後面加上單個 "." 字元。如果沒有寬度,則引入精度的 "." 將緊跟在旗標(如果有的話)或初始 "%" 之後。

對於字串替換 %s、%z、%q、%Q 或 %w,精度是從參數中使用的位元組數或字元數。預設情況下,數量是位元組數,但如果存在 "!" 旗標,則數量是字元數。如果沒有精度,則替換整個字串。範例:"%.3s" 替換參數字串的前 3 個位元組。"%!.3s" 替換參數字串的前三個字元。

對於整數替換 %d、%i、%x、%X、%o 和 %p,精度指定要顯示的最小位數。如有必要,將添加前導零,以將輸出擴展到最小位數。

對於浮點數替換 %e、%E 和 %f,精度指定要顯示在小數點右側的位數。使用 %g 和 %G 時,精度是有效位數的總數,如果指定的精度為 0,則向上捨入為 1。

對於字元替換 %c,大於 1 的精度 N 會導致字元重複 N 次。這是僅在 SQLite 中找到的非標準擴展。

如果精度是單個 "*" 字元而不是數字,則實際精度值將作為整數從參數列表中讀取。

2.5. 選項旗標欄位

旗標由緊跟在引入替換的 "%" 之後的零個或多個字元組成。各種旗標及其含義如下:

旗標意義
- 在輸出中左對齊值。預設值是右對齊。如果寬度為零或小於要替換的值的長度,則沒有填充,"-" 旗標是無操作的。
+ 對於帶符號的數字替換,在正數前面加上 "+" 號。無論旗標設定如何,負數前面總是會出現 "-" 號。
(空格) 對於帶符號的數字替換,在正數前面加上一個空格。
0 (零填充選項)根據需要在數字替換前面加上盡可能多的 "0" 字元,以將值擴展到指定的寬度。如果省略了寬度欄位,則此旗標是無操作的。無限大和 NaN(非數字)浮點值通常分別呈現為 "Inf" 和 "NaN",但在啟用零填充選項的情況下,它們會呈現為 "9.0e+999" 和 "null"。換句話說,使用零填充選項時,浮點無限大和 NaN 會呈現為有效的 SQL 和 JSON 字面量。
# 這是「替代形式 1」旗標。對於 %g 和 %G 替換,這會導致移除尾隨零。此旗標強制所有浮點數替換都顯示小數點。對於 %o、%x 和 %X 替換,「替代形式 1」旗標會導致值分別以 "0"、"0x" 或 "0X" 作為字首。
, 逗號選項會導致在小數點左邊每隔 3 位數就在數字替換(%d、%f 等)的輸出中添加逗號分隔符。小數點右邊的數字不會添加逗號。這可以幫助人們更輕鬆地辨別大整數值的大小。例如,值 2147483647 使用 "%d" 會呈現為 "2147483647",但使用 "%,d" 會顯示為 "2,147,483,647"。此旗標是非標準擴展。
! 這是「替代形式 2」旗標。對於字串替換,此旗標會導致寬度和精度以字元而不是位元組來理解。對於浮點數替換,「替代形式 2」旗標將顯示的有效位數的最大值從 16 增加到 26,強制顯示小數點,並使小數點後至少顯示一位數字。

alternate-form-2 旗標是一個非標準的擴充功能,據我們所知,在其他 printf() 實作中並不存在。

3. 實作與歷史

核心的字串格式化例程是位於 printf.c 原始碼檔案中的 sqlite3VXPrintf() 函式。所有不同的介面(有時是間接地)都會呼叫這個核心函式。sqlite3VXPrintf() 函式最初是由 SQLite 的第一作者(Hipp)在 1980 年代後期於杜克大學就讀研究所時所編寫的程式碼。Hipp 將這個 printf() 實作保留在他的個人工具箱中,直到他在 2000 年開始著手開發 SQLite。該程式碼於 2000 年 10 月 8 日 併入 SQLite 原始碼樹,用於 SQLite 1.0.9 版。

Fossil 版本控制系統 使用自己的 printf() 實作,該實作衍生自 SQLite printf() 實作的早期版本,但這兩個實作後來已經分歧。

sqlite3_snprintf() 函式的緩衝區指標和緩衝區大小參數與標準 C 函式庫 snprintf() 例程中的順序相反。這是因為在 Hipp 最初實作他的版本時,標準 C 函式庫中沒有 snprintf() 例程,而他選擇了與標準 C 函式庫設計者不同的順序。