指令碼風格與選項
重建 CREATE TABLE 的排版只有 Metadata/Formatting/TSqlScriptRenderer 一份。 浮動預覽的指令碼分頁與 F12 都走它,差別只在傳進去的 SqlScriptOptions。 各自組一份的症狀發生過:同一張資料表在兩個表面上長得不一樣,而使用者會以為 其中一條壞了。
Fidelity 仍不是完整備份/往返還原工具。 目前擴充屬性值一律轉成文字,分割壓縮只 取第一個分割,檢視的觸發程序也未載入;需要完整保真時仍須逐項核對。
三層分工
| 層 | 型別 | 負責 |
|---|---|---|
| 選項 | Core/Scripting/SqlScriptOptions | 要不要多寫這幾個字。純資料,零相依,CLI 與測試直接引用 |
| 環境 | Metadata/Formatting/SqlScriptContext | 這一次連線與這個執行個體的事實:資料庫定序、換行、來源、健檢 |
| 排版 | Metadata/Formatting/TSqlScriptRenderer | 照選項把結構寫成 T-SQL |
選項是使用者調的,環境不是。混在一起的話,「資料庫定序」會變成一個調得動的 設定,而它一調就會讓「只在與資料庫不同時寫 COLLATE」拿錯誤的基準去比對。
「這一次的資料夠不夠」不在排版這一層:那由 SqlObjectStructure 回答, 不夠時整段換成註解(見中繼資料)。
三組風格
風格是選項的具名預設值,不是主題。使用者的自訂一律表達成「某個風格加上幾項 覆寫」,設定也只存那幾項覆寫——新增選項時已存過設定的人自動拿到它在該風格 底下的預設值,不必寫遷移。
| 風格 | 特徵 |
|---|---|
Fidelity | 條件約束獨立成敘述、每一段接 GO、型別與長度之間留空格、省略 ASC |
SsmsNative | SET 選項開頭、主索引鍵內嵌、Tab 縮排、明確寫出 ASC |
Minimal | 不加方括號、不寫定序與檔案群組、省略系統配的條件約束名稱 |
三組風格的括號裡都縮排,沒有一組頂格:頂格的話資料行與 CREATE TABLE 齊頭, 括號裡與括號外看起來同一層。只有 SsmsNative 用 Tab,因為它要與內建「編寫指令碼為」 逐字相同;其餘用四個空格,那份文字會被貼到各種 Tab 寬度的編輯器裡。要回到頂格就把 Indent 覆寫成空字串。
縮排只在 AppendColumns 套用一次,括號裡的每一行(資料行與內嵌的條件約束)都從 那裡出去。由各自組字串的地方自己補的症狀是新增一種括號內內容時漏掉一處,而那一行 會單獨頂格——看起來像跑出了括號。括號外的敘述不歸它管:IF 包起來的敘述維持頂格, 因為重新縮排要逐行加前綴,而 CHECK 的運算式裡可以有帶換行的字串常值,那一加就 改掉了常值本身。
Minimal 省略系統配的名稱,因此它的輸出不能拿來逐字比對重新產生的結果: 沒有名稱的條件約束在目的地會被引擎重配一個。要往返一致就用另外兩組。
選項不進 Unified Settings
設定頁只有風格加上三個最常被單獨調整的項目(資料行說明、健檢註解、檔頭)。 四十幾個開關各自成為一個設定的話,每一個都要動四處並過守門測試 (見設定結構),而設定頁會從五項變成五十項。 要再細調的人改的是 SqlScriptOptions 本身。
F12 那一條額外蓋掉三項,因為它們是可執行性的要求而不是風格偏好:ALTER PROCEDURE 必須是批次裡的第一個敘述所以要有 GO;計算資料行、篩選索引與索引 檢視對兩個 SET 的值有要求;而 F12 之後接著要做的事幾乎都是「改一下再執行」, 所以模組寫成 ALTER。SET 依 sys.tables 反推當初的值而不是固定 ON—— 一張在 OFF 之下建起來的資料表,用 ON 重建可能直接失敗。
幾個不能省的判斷
- 資料庫定序查不到時一律寫出資料行定序。 省略等於讓目的地用自己的資料庫 定序,而排序、比較與唯一索引的行為會跟著換,畫面上卻看不出差別。
- 識別值種子查不到時只寫
IDENTITY關鍵字。 猜一組(1, 1)出來是指令碼在 說謊:那張表可能是從別處匯入的。 TEXTIMAGE_ON問lob_data_space_id,不掃資料行型別。xml、CLR 型別與varchar(max)都算,漏一種就是一份與來源不同的資料表。它也從屬於「寫出檔案 群組」那個開關——只留半個儲存位置會指名一個在目的地不一定存在的檔案群組。- 分割配置查不到分割資料行時整段不寫。 只寫
ON [ps_x]是語法錯誤, 而少一個ON子句只是落到預設檔案群組——兩者都不對,但後者執行得起來。 - 停用的索引與
CHECK要跟著停回去。 不停的話那張表會多出一個來源上不存在 的索引,或開始擋掉來源允許的資料。 - 模組不包存在性判斷。 程序、函式、觸發程序與檢視必須是批次裡的第一個敘述, 包進
IF之後就不是了,只能改走EXEC('CREATE …')的動態 SQL——而那會把定義 原文變成一個字串,裡面的單引號要全部跳脫,存回去的定義從此與來源不同。 索引的判斷問sys.indexes而不是OBJECT_ID:索引名稱只在它所屬的資料表裡 唯一,OBJECT_ID對它一律回傳 NULL,包出來的IF會永遠成立。 - 模組即使關掉批次分隔也獨立一個批次。 觸發程序、程序、函式與檢視的前後都要 分開;只在結尾加
GO,仍會與前面的CREATE TABLE/SET混成不合法的批次。 - 停用 CHECK 保留名稱;附屬屬性只寫到已產生的目標。 否則
NOCHECK或擴充 屬性會指向已被風格省略的名稱。省略的屬性以註解交代,不猜新名稱。 - 索引選項只寫與預設值不同的那幾個。 全寫會讓每個索引掛七、八個
= OFF; 全不寫會漏掉真的調過的FILLFACTOR與DATA_COMPRESSION。預設值由SqlIndexOptions一份說了算。
資料不齊時交出什麼
模組(程序、函式、檢視、觸發程序)的指令碼就是 OBJECT_DEFINITION 的原文, 重組只會失真。取不到定義時整段換成註解,寫明原因(判別見 相容)並附上查得到的 欄位與參數——檢視同時是模組也有欄位,少了這一條就會掉進資料表那一支, 把一個檢視寫成 CREATE TABLE。那不只是難看,是指令碼在說謊:照著執行會多出 一張同名的資料表。
同義字與序列的定義不在 sys.sql_modules 裡,OBJECT_DEFINITION 對它們一律回傳 NULL;它們的定義就是目錄檢視上的那幾個欄位,由 SqlCatalogScript 組成 CREATE SYNONYM … FOR …; 與 CREATE SEQUENCE …;(見 metadata.md)。 到了指令碼分頁與 F12 之後,它們與模組拿到定義原文走的是同一條路。
資料表型別走自己的一支,寫成 CREATE TYPE ... AS TABLE。它也有欄位,不接走 就是同一個謊。主索引鍵寫成不具名的內嵌條件約束——CREATE TYPE 的括號裡 不收 CONSTRAINT 名稱,型別的條件約束一律命名不得,而查到的那個名字本來就是 引擎自己配的。其餘索引整組不寫,並在結尾註明省略了幾個:CREATE INDEX 與 ALTER TABLE 對型別都不合法,括號裡的內嵌 INDEX 又收不下 INCLUDE 與篩選條件。 一行都不交代的話,那份文字看起來就像那個型別只有主索引鍵。
條件約束(CHECK、DEFAULT、主索引鍵與唯一鍵、外來鍵)寫成父物件上的 ALTER TABLE … ADD CONSTRAINT,材料整份來自 SqlObjectStructure.Parent,排版仍然 只有 renderer 那幾支。父物件不齊或找不到那個名稱時整段註解,細節見 F12 指令碼。
欄位一列都沒有回來時(物件在清單被快取之後卸除,或這個登入對它的權限被收回), 資料表與資料表型別也是整段註解,格式與取不到定義的模組共用一份。要拿去執行的 輸出是全有或全無:少了欄位的 CREATE TABLE 只剩一對空括號,卻仍然貼得上去。 兩道判斷(種類、這一次的資料)見 metadata.md。
第四層查詢失敗是第三種:欄位照畫(那是第二層拿到的),指令碼分頁整段換成註解。 空的索引清單在這時候不是答案,而「沒有主索引鍵」會是一句謊話;分辨的方式見 相容與失敗。
計算欄位、已知 DEFAULT 或 CHECK 的運算式取不到時也整段註解。原因只有一個:這個 登入沒有 VIEW DEFINITION,名稱照給、運算式遮成 NULL。所以說死不給選項,並在欄位 摘要前逐項點名缺的那幾個。檔頭、健檢與降級 摘要共用逐行註解處理;名稱或運算式中的換行不能逃出註解。
快照
三組風格各有一份 tests/SqlAssist.Metadata.Tests/Golden/Loan.<風格>.sql, 逐字比對。快照是刻意脆弱的:「語意等價」沒有辦法自動判定,而排版正是三組風格 唯一的差別。輸出變了就要有人看過那份 diff,確認是想要的改變,再更新檔案:
$env:SQLASSIST_UPDATE_GOLDEN = '1'; .\tools\Run-CoreTests.ps1更新完一定要看 git diff。快照不能取代逐項的選項測試——它只說得出「整份變了」, 說不出是哪一個選項變的。