fdb2 1.0.36 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +41 -0
  2. package/dist/public/explorer.css +533 -485
  3. package/dist/public/explorer.js +1277 -868
  4. package/dist/public/index.css +172 -34
  5. package/dist/public/index.js +103 -49
  6. package/dist/public/layout.js +3 -3
  7. package/dist/public/vue.js +1 -1
  8. package/dist/server/core/schema/__tests__/canonical.test.ts +195 -0
  9. package/dist/server/core/schema/__tests__/ddl-emitter.test.ts +456 -0
  10. package/dist/server/core/schema/__tests__/diff.test.ts +242 -0
  11. package/dist/server/core/schema/__tests__/roundtrip.property.test.ts +403 -0
  12. package/dist/server/core/schema/canonical.d.ts +50 -0
  13. package/dist/server/core/schema/canonical.d.ts.map +1 -0
  14. package/dist/server/core/schema/canonical.js +212 -0
  15. package/dist/server/core/schema/canonical.js.map +1 -0
  16. package/dist/server/core/schema/canonical.ts +223 -0
  17. package/dist/server/core/schema/ddl-emitter.d.ts +68 -0
  18. package/dist/server/core/schema/ddl-emitter.d.ts.map +1 -0
  19. package/dist/server/core/schema/ddl-emitter.js +813 -0
  20. package/dist/server/core/schema/ddl-emitter.js.map +1 -0
  21. package/dist/server/core/schema/ddl-emitter.ts +925 -0
  22. package/dist/server/core/schema/diff.d.ts +16 -0
  23. package/dist/server/core/schema/diff.d.ts.map +1 -0
  24. package/dist/server/core/schema/diff.js +115 -0
  25. package/dist/server/core/schema/diff.js.map +1 -0
  26. package/dist/server/core/schema/diff.ts +134 -0
  27. package/dist/server/core/schema/type-defaults.d.ts +27 -0
  28. package/dist/server/core/schema/type-defaults.d.ts.map +1 -0
  29. package/dist/server/core/schema/type-defaults.js +61 -0
  30. package/dist/server/core/schema/type-defaults.js.map +1 -0
  31. package/dist/server/core/schema/type-defaults.ts +79 -0
  32. package/dist/server/index.d.ts +1 -1
  33. package/dist/server/index.d.ts.map +1 -1
  34. package/dist/server/index.js +347 -124
  35. package/dist/server/index.js.map +1 -1
  36. package/dist/server/index.ts +336 -161
  37. package/dist/server/model/ddl-spec.d.ts +163 -0
  38. package/dist/server/model/ddl-spec.d.ts.map +1 -0
  39. package/dist/server/model/ddl-spec.js +32 -0
  40. package/dist/server/model/ddl-spec.js.map +1 -0
  41. package/dist/server/model/ddl-spec.ts +175 -0
  42. package/dist/server/service/connection.service.d.ts +20 -0
  43. package/dist/server/service/connection.service.d.ts.map +1 -1
  44. package/dist/server/service/connection.service.js +99 -25
  45. package/dist/server/service/connection.service.js.map +1 -1
  46. package/dist/server/service/connection.service.ts +104 -25
  47. package/dist/server/service/database/base.service.d.ts +0 -4
  48. package/dist/server/service/database/base.service.d.ts.map +1 -1
  49. package/dist/server/service/database/base.service.js.map +1 -1
  50. package/dist/server/service/database/base.service.ts +0 -5
  51. package/dist/server/service/database/clickhouse.service.d.ts +0 -5
  52. package/dist/server/service/database/clickhouse.service.d.ts.map +1 -1
  53. package/dist/server/service/database/clickhouse.service.js +0 -49
  54. package/dist/server/service/database/clickhouse.service.js.map +1 -1
  55. package/dist/server/service/database/clickhouse.service.ts +0 -52
  56. package/dist/server/service/database/cockroachdb.service.d.ts +0 -5
  57. package/dist/server/service/database/cockroachdb.service.d.ts.map +1 -1
  58. package/dist/server/service/database/cockroachdb.service.js +11 -142
  59. package/dist/server/service/database/cockroachdb.service.js.map +1 -1
  60. package/dist/server/service/database/cockroachdb.service.ts +11 -161
  61. package/dist/server/service/database/database.service.d.ts +55 -2
  62. package/dist/server/service/database/database.service.d.ts.map +1 -1
  63. package/dist/server/service/database/database.service.js +246 -4
  64. package/dist/server/service/database/database.service.js.map +1 -1
  65. package/dist/server/service/database/database.service.ts +295 -7
  66. package/dist/server/service/database/mongodb.service.d.ts +0 -6
  67. package/dist/server/service/database/mongodb.service.d.ts.map +1 -1
  68. package/dist/server/service/database/mongodb.service.js +0 -8
  69. package/dist/server/service/database/mongodb.service.js.map +1 -1
  70. package/dist/server/service/database/mongodb.service.ts +0 -8
  71. package/dist/server/service/database/mssql.service.d.ts +8 -4
  72. package/dist/server/service/database/mssql.service.d.ts.map +1 -1
  73. package/dist/server/service/database/mssql.service.js +27 -132
  74. package/dist/server/service/database/mssql.service.js.map +1 -1
  75. package/dist/server/service/database/mssql.service.ts +34 -156
  76. package/dist/server/service/database/mysql.service.d.ts +0 -4
  77. package/dist/server/service/database/mysql.service.d.ts.map +1 -1
  78. package/dist/server/service/database/mysql.service.js +11 -155
  79. package/dist/server/service/database/mysql.service.js.map +1 -1
  80. package/dist/server/service/database/mysql.service.ts +17 -184
  81. package/dist/server/service/database/oracle.service.d.ts +0 -4
  82. package/dist/server/service/database/oracle.service.d.ts.map +1 -1
  83. package/dist/server/service/database/oracle.service.js +11 -141
  84. package/dist/server/service/database/oracle.service.js.map +1 -1
  85. package/dist/server/service/database/oracle.service.ts +17 -166
  86. package/dist/server/service/database/postgres.service.d.ts +0 -4
  87. package/dist/server/service/database/postgres.service.d.ts.map +1 -1
  88. package/dist/server/service/database/postgres.service.js +11 -150
  89. package/dist/server/service/database/postgres.service.js.map +1 -1
  90. package/dist/server/service/database/postgres.service.ts +17 -174
  91. package/dist/server/service/database/sap.service.d.ts +0 -4
  92. package/dist/server/service/database/sap.service.d.ts.map +1 -1
  93. package/dist/server/service/database/sap.service.js +11 -137
  94. package/dist/server/service/database/sap.service.js.map +1 -1
  95. package/dist/server/service/database/sap.service.ts +11 -158
  96. package/dist/server/service/database/sql-type.helper.d.ts +28 -0
  97. package/dist/server/service/database/sql-type.helper.d.ts.map +1 -0
  98. package/dist/server/service/database/sql-type.helper.js +113 -0
  99. package/dist/server/service/database/sql-type.helper.js.map +1 -0
  100. package/dist/server/service/database/sql-type.helper.ts +115 -0
  101. package/dist/server/service/database/sqlite.service.d.ts +0 -5
  102. package/dist/server/service/database/sqlite.service.d.ts.map +1 -1
  103. package/dist/server/service/database/sqlite.service.js +11 -156
  104. package/dist/server/service/database/sqlite.service.js.map +1 -1
  105. package/dist/server/service/database/sqlite.service.ts +17 -183
  106. package/package.json +1 -1
  107. package/server.js +1 -1
@@ -1,6 +1,6 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./layout.js","./rolldown-runtime.js","./vue.js","./layout.css","./explorer.js","./explorer.css","./vconsole.min.js"])))=>i.map(i=>d[i]);
2
2
  import { a as __toESM, n as __esmMin, r as __exportAll, t as __commonJSMin } from "./rolldown-runtime.js";
3
- import { $ as onScopeDispose, A as defineComponent, B as renderList, C as createBlock, F as nextTick, H as resolveComponent, I as onBeforeUnmount, J as getCurrentScope, L as onMounted, M as hasInjectionContext, O as createTextVNode, P as inject, Q as markRaw, R as onUnmounted, S as createBaseVNode, T as createElementBlock, U as resolveDynamicComponent, V as renderSlot, W as watch, X as isReactive, Z as isRef, a as init_vue_router, b as Fragment, c as createI18n, ct as toDisplayString, d as init_vue_runtime_esm_bundler, et as reactive, i as createWebHistory, it as unref, j as getCurrentInstance, k as createVNode, l as init_vue_i18n, n as useTitle, nt as toRaw, ot as normalizeClass, p as createApp, q as effectScope, r as createRouter, rt as toRefs, st as normalizeStyle, t as init_core, tt as ref, u as useI18n, w as createCommentVNode, x as computed, y as withModifiers, z as openBlock } from "./vue.js";
3
+ import { $ as markRaw, A as defineComponent, B as renderList, C as createBlock, F as nextTick, G as watch, H as resolveComponent, I as onBeforeUnmount, J as effectScope, L as onMounted, M as hasInjectionContext, O as createTextVNode, P as inject, Q as isRef, R as onUnmounted, S as createBaseVNode, T as createElementBlock, U as resolveDynamicComponent, V as renderSlot, W as useSlots, Y as getCurrentScope, Z as isReactive, a as init_vue_router, at as unref, b as Fragment, c as createI18n, ct as normalizeStyle, d as init_vue_runtime_esm_bundler, et as onScopeDispose, i as createWebHistory, it as toRefs, j as getCurrentInstance, k as createVNode, l as init_vue_i18n, lt as toDisplayString, n as useTitle, nt as ref, p as createApp, r as createRouter, rt as toRaw, st as normalizeClass, t as init_core, tt as reactive, u as useI18n, w as createCommentVNode, x as computed, y as withModifiers, z as openBlock } from "./vue.js";
4
4
  import { i as init_bootstrap, n as init_bootstrap_esm, r as init_bootstrap_icons, t as Modal } from "./bootstrap.js";
5
5
  //#region \0vite/modulepreload-polyfill.js
6
6
  (function polyfill() {
@@ -4510,6 +4510,13 @@ function getVSCodeBridge() {
4510
4510
  if (!bridgeInstance) bridgeInstance = new VSCodeBridge();
4511
4511
  return bridgeInstance;
4512
4512
  }
4513
+ /**
4514
+ * 判断是否在 VSCode 环境中
4515
+ */
4516
+ function isVSCodeEnvironment() {
4517
+ if (!bridgeInstance) bridgeInstance = new VSCodeBridge();
4518
+ return bridgeInstance.isVSCodeEnvironment();
4519
+ }
4513
4520
  var VSCodeBridge, bridgeInstance;
4514
4521
  var init_bridge = __esmMin((() => {
4515
4522
  VSCodeBridge = class {
@@ -4535,10 +4542,15 @@ var init_bridge = __esmMin((() => {
4535
4542
  }
4536
4543
  /**
4537
4544
  * 监听来自 VSCode 扩展的消息
4545
+ *
4546
+ * 这里刻意不按 event.origin 过滤:VSCode 各版本经 webview.postMessage 下发时
4547
+ * 携带的 origin 并不稳定(部分版本为非空值),一旦过滤就会把宿主消息整批丢掉,
4548
+ * 表现为「连接下拉为空 / 数据库列表不展示」这类面板级静默失效。
4549
+ * rpc.ts 与 entry/explorer.ts 的监听同样不过滤,这里与之保持一致。
4550
+ * webview 内 CSP 已限定脚本来源、宿主是唯一消息来源,该过滤没有实际收益。
4538
4551
  */
4539
4552
  onMessage(callback) {
4540
4553
  window.addEventListener("message", (event) => {
4541
- if (event.origin) return;
4542
4554
  callback(event.data);
4543
4555
  });
4544
4556
  }
@@ -5079,6 +5091,12 @@ var init_zh_CN = __esmMin((() => {
5079
5091
  quickActions: "快速操作",
5080
5092
  exportSchema: "导出架构",
5081
5093
  viewLogs: "查看日志",
5094
+ schemaExported: "架构已导出({count} 个数据库){path}",
5095
+ schemaExportNoDatabase: "当前连接下没有可导出的数据库",
5096
+ schemaExportFailed: "架构导出失败",
5097
+ logsTitle: "数据库日志",
5098
+ logsEmpty: "没有可展示的日志记录",
5099
+ logsLoadFailed: "日志读取失败",
5082
5100
  searchDatabase: "搜索数据库...",
5083
5101
  loadingDatabases: "加载数据库列表...",
5084
5102
  noDatabase: "暂无数据库",
@@ -5110,6 +5128,13 @@ var init_zh_CN = __esmMin((() => {
5110
5128
  basicInfo: "基本信息",
5111
5129
  connectionConfig: "连接配置",
5112
5130
  sqliteConfig: "SQLite配置",
5131
+ remoteConfig: "远程代理服务",
5132
+ remoteUrl: "服务地址",
5133
+ remoteUrlPlaceholder: "http://192.168.1.10:9800",
5134
+ remoteToken: "访问令牌(可选)",
5135
+ remoteTokenPlaceholder: "与服务端 DB_TOOL_REMOTE_TOKEN 一致",
5136
+ remoteConfigHint: "保存后,该远程服务上的所有数据库连接会自动出现在连接列表中,可像本地数据库一样直接访问。",
5137
+ remoteUrlRequired: "请填写远程服务地址",
5113
5138
  authInfo: "认证信息",
5114
5139
  otherOptions: "其他选项",
5115
5140
  host: "主机地址",
@@ -5163,7 +5188,6 @@ var init_zh_CN = __esmMin((() => {
5163
5188
  refreshDatabase: "刷新数据库",
5164
5189
  deleteDatabase: "删除数据库",
5165
5190
  refreshTable: "刷新表",
5166
- viewStructure: "查看结构",
5167
5191
  defaultTitle: "数据库浏览器",
5168
5192
  defaultDesc: "请从左侧选择一个连接、数据库或表来查看详细信息",
5169
5193
  rowsSuffix: "行"
@@ -5220,6 +5244,7 @@ var init_zh_CN = __esmMin((() => {
5220
5244
  tableModifySuccess: "表结构修改成功",
5221
5245
  tableCreateFailed: "创建表失败",
5222
5246
  tableModifyFailed: "表结构修改失败",
5247
+ getTableInfoFailed: "获取表结构失败",
5223
5248
  size: "大小"
5224
5249
  },
5225
5250
  tableDetail: {
@@ -5274,7 +5299,6 @@ var init_zh_CN = __esmMin((() => {
5274
5299
  operationFailed: "操作失败",
5275
5300
  structureModifySuccess: "表结构修改成功",
5276
5301
  structureModifyFailed: "表结构修改失败",
5277
- confirmDeleteColumn: "确定要删除列 {name} 吗?",
5278
5302
  confirmDeleteIndex: "确定要删除索引 {name} 吗?",
5279
5303
  confirmDeleteFK: "确定要删除外键 {name} 吗?",
5280
5304
  noInsertField: "没有可插入的字段",
@@ -5339,6 +5363,8 @@ var init_zh_CN = __esmMin((() => {
5339
5363
  operationsHeader: "操作",
5340
5364
  columnNamePlaceholder: "字段名",
5341
5365
  selectType: "选择类型",
5366
+ customType: "自定义类型",
5367
+ customTypePlaceholder: "如 ENUM('a','b')、DECIMAL(10,2)",
5342
5368
  lengthPlaceholder: "长度",
5343
5369
  precisionPlaceholder: "精度",
5344
5370
  scalePlaceholder: "小数",
@@ -5599,7 +5625,8 @@ var init_zh_CN = __esmMin((() => {
5599
5625
  syncSuccess: "数据同步成功,{success}/{total} 个表同步完成,共同步 {rows} 行数据",
5600
5626
  syncSuccessToast: "数据同步成功,{success}/{total} 个表同步完成",
5601
5627
  loadTablesFailed: "加载表列表失败",
5602
- sqlSent: "已发送 SQL 查询",
5628
+ execFailed: "执行失败",
5629
+ selectConnectionFirst: "请先选择数据库连接",
5603
5630
  featureInDev: "{feature}功能开发中...",
5604
5631
  healthNormal: "正常",
5605
5632
  healthCheckConnection: "连接状态",
@@ -5832,6 +5859,12 @@ var init_en_US = __esmMin((() => {
5832
5859
  quickActions: "Quick Actions",
5833
5860
  exportSchema: "Export Schema",
5834
5861
  viewLogs: "View Logs",
5862
+ schemaExported: "Schema exported ({count} database(s)){path}",
5863
+ schemaExportNoDatabase: "No database available to export for this connection",
5864
+ schemaExportFailed: "Failed to export schema",
5865
+ logsTitle: "Database Logs",
5866
+ logsEmpty: "No log records to display",
5867
+ logsLoadFailed: "Failed to load logs",
5835
5868
  searchDatabase: "Search databases...",
5836
5869
  loadingDatabases: "Loading databases...",
5837
5870
  noDatabase: "No databases yet",
@@ -5863,6 +5896,13 @@ var init_en_US = __esmMin((() => {
5863
5896
  basicInfo: "Basic Information",
5864
5897
  connectionConfig: "Connection Configuration",
5865
5898
  sqliteConfig: "SQLite Configuration",
5899
+ remoteConfig: "Remote Proxy Service",
5900
+ remoteUrl: "Service URL",
5901
+ remoteUrlPlaceholder: "http://192.168.1.10:9800",
5902
+ remoteToken: "Access Token (optional)",
5903
+ remoteTokenPlaceholder: "Same as the server DB_TOOL_REMOTE_TOKEN",
5904
+ remoteConfigHint: "After saving, all database connections on that remote service appear in the connection list and can be accessed like local databases.",
5905
+ remoteUrlRequired: "Remote service URL is required",
5866
5906
  authInfo: "Authentication",
5867
5907
  otherOptions: "Other Options",
5868
5908
  host: "Host",
@@ -5916,7 +5956,6 @@ var init_en_US = __esmMin((() => {
5916
5956
  refreshDatabase: "Refresh Database",
5917
5957
  deleteDatabase: "Delete Database",
5918
5958
  refreshTable: "Refresh Table",
5919
- viewStructure: "View Structure",
5920
5959
  defaultTitle: "Database Explorer",
5921
5960
  defaultDesc: "Select a connection, database, or table from the left panel to view details",
5922
5961
  rowsSuffix: "rows"
@@ -5972,6 +6011,7 @@ var init_en_US = __esmMin((() => {
5972
6011
  tableModifySuccess: "Table structure modified successfully",
5973
6012
  tableCreateFailed: "Failed to create table",
5974
6013
  tableModifyFailed: "Failed to modify table structure",
6014
+ getTableInfoFailed: "Failed to get table structure",
5975
6015
  size: "Size"
5976
6016
  },
5977
6017
  tableDetail: {
@@ -6024,7 +6064,6 @@ var init_en_US = __esmMin((() => {
6024
6064
  operationFailed: "Operation failed",
6025
6065
  structureModifySuccess: "Table structure modified successfully",
6026
6066
  structureModifyFailed: "Failed to modify table structure",
6027
- confirmDeleteColumn: "Are you sure you want to delete column {name}?",
6028
6067
  confirmDeleteIndex: "Are you sure you want to delete index {name}?",
6029
6068
  confirmDeleteFK: "Are you sure you want to delete foreign key {name}?",
6030
6069
  noInsertField: "No insertable fields",
@@ -6089,6 +6128,8 @@ var init_en_US = __esmMin((() => {
6089
6128
  operationsHeader: "Actions",
6090
6129
  columnNamePlaceholder: "Column name",
6091
6130
  selectType: "Select type",
6131
+ customType: "Custom type",
6132
+ customTypePlaceholder: "e.g. ENUM('a','b'), DECIMAL(10,2)",
6092
6133
  lengthPlaceholder: "Length",
6093
6134
  precisionPlaceholder: "Precision",
6094
6135
  scalePlaceholder: "Scale",
@@ -6349,7 +6390,8 @@ var init_en_US = __esmMin((() => {
6349
6390
  syncSuccess: "Data sync successful, {success}/{total} tables synced, {rows} rows total",
6350
6391
  syncSuccessToast: "Data sync successful, {success}/{total} tables synced",
6351
6392
  loadTablesFailed: "Failed to load table list",
6352
- sqlSent: "SQL query sent",
6393
+ execFailed: "Execution failed",
6394
+ selectConnectionFirst: "Please select a database connection first",
6353
6395
  featureInDev: "{feature} feature is under development...",
6354
6396
  healthNormal: "Normal",
6355
6397
  healthCheckConnection: "Connection Status",
@@ -6791,7 +6833,7 @@ var init_toast_vue_vue_type_script_setup_true_lang = __esmMin((() => {
6791
6833
  }, [toast.title ? (openBlock(), createElementBlock("div", _hoisted_2$4, [createBaseVNode("strong", _hoisted_3$3, toDisplayString(toast.title), 1), createBaseVNode("button", {
6792
6834
  type: "button",
6793
6835
  class: "btn-close",
6794
- onClick: ($event) => removeToast(index)
6836
+ onClick: ($event) => removeToast(toast)
6795
6837
  }, null, 8, _hoisted_4$2)])) : createCommentVNode("", true), createBaseVNode("div", _hoisted_5$2, toDisplayString(toast.message), 1)], 2);
6796
6838
  }), 128))]);
6797
6839
  };
@@ -6799,8 +6841,8 @@ var init_toast_vue_vue_type_script_setup_true_lang = __esmMin((() => {
6799
6841
  });
6800
6842
  }));
6801
6843
  //#endregion
6802
- //#region src/components/toast/toast.vue?vue&type=style&index=0&scoped=7d639c44&lang.css
6803
- var init_toast_vue_vue_type_style_index_0_scoped_7d639c44_lang = __esmMin((() => {}));
6844
+ //#region src/components/toast/toast.vue?vue&type=style&index=0&scoped=daf8ceb9&lang.css
6845
+ var init_toast_vue_vue_type_style_index_0_scoped_daf8ceb9_lang = __esmMin((() => {}));
6804
6846
  //#endregion
6805
6847
  //#region \0plugin-vue:export-helper
6806
6848
  var _plugin_vue_export_helper_default;
@@ -6817,9 +6859,9 @@ var toast_default$1;
6817
6859
  var init_toast$2 = __esmMin((() => {
6818
6860
  init_toast_vue_vue_type_script_setup_true_lang();
6819
6861
  init_toast_vue_vue_type_script_setup_true_lang();
6820
- init_toast_vue_vue_type_style_index_0_scoped_7d639c44_lang();
6862
+ init_toast_vue_vue_type_style_index_0_scoped_daf8ceb9_lang();
6821
6863
  init__plugin_vue_export_helper();
6822
- toast_default$1 = /*#__PURE__*/ _plugin_vue_export_helper_default(toast_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-7d639c44"]]);
6864
+ toast_default$1 = /*#__PURE__*/ _plugin_vue_export_helper_default(toast_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-daf8ceb9"]]);
6823
6865
  }));
6824
6866
  //#endregion
6825
6867
  //#region src/utils/toast.ts
@@ -6946,7 +6988,7 @@ var init_loading = __esmMin((() => {
6946
6988
  }));
6947
6989
  //#endregion
6948
6990
  //#region src/components/modal/index.vue?vue&type=script&setup=true&lang.ts
6949
- var _hoisted_1$2, _hoisted_2$2, _hoisted_3$2, _hoisted_4$1, _hoisted_5$1, _hoisted_6$1, _hoisted_7$1, _hoisted_8$1, _hoisted_9$1, index_vue_vue_type_script_setup_true_lang_default$1;
6991
+ var _hoisted_1$2, _hoisted_2$2, _hoisted_3$2, _hoisted_4$1, _hoisted_5$1, _hoisted_6$1, _hoisted_7$1, _hoisted_8$1, index_vue_vue_type_script_setup_true_lang_default$1;
6950
6992
  var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
6951
6993
  init_vue_runtime_esm_bundler();
6952
6994
  init_vue_i18n();
@@ -6961,18 +7003,17 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
6961
7003
  class: "modal-body text-wrap"
6962
7004
  };
6963
7005
  _hoisted_3$2 = { key: 1 };
6964
- _hoisted_4$1 = { class: "d-flex align-items-center mb-3" };
6965
- _hoisted_5$1 = { class: "flex-grow-1 text-wrap" };
6966
- _hoisted_6$1 = {
7006
+ _hoisted_4$1 = { class: "flex-grow-1 text-wrap" };
7007
+ _hoisted_5$1 = {
6967
7008
  key: 0,
6968
7009
  class: "error-details-toggle mt-2"
6969
7010
  };
6970
- _hoisted_7$1 = {
7011
+ _hoisted_6$1 = {
6971
7012
  key: 0,
6972
7013
  class: "error-details-content mt-2"
6973
7014
  };
6974
- _hoisted_8$1 = { class: "error-json" };
6975
- _hoisted_9$1 = { class: "modal-footer" };
7015
+ _hoisted_7$1 = { class: "error-json" };
7016
+ _hoisted_8$1 = { class: "modal-footer" };
6976
7017
  index_vue_vue_type_script_setup_true_lang_default$1 = /*@__PURE__*/ defineComponent({
6977
7018
  __name: "index",
6978
7019
  props: {
@@ -7038,23 +7079,17 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7038
7079
  const dynamicShowCancel = ref(false);
7039
7080
  const detailsExpanded = ref(false);
7040
7081
  const dynamicErrorDetails = ref(null);
7041
- const isMobile = ref(false);
7042
- const checkScreenSize = () => {
7043
- isMobile.value = window.innerWidth < 768;
7044
- };
7045
- checkScreenSize();
7046
- window.addEventListener("resize", checkScreenSize);
7047
- onUnmounted(() => {
7048
- window.removeEventListener("resize", checkScreenSize);
7049
- });
7082
+ const slots = useSlots();
7083
+ const isAutoSize = computed(() => !slots.default && !props.isFullScreen);
7050
7084
  const dynamicStyle = computed(() => {
7051
7085
  return {
7052
- maxWidth: isMobile.value ? "100vw" : "80vw!important",
7053
- width: "auto",
7054
- minWidth: "300px",
7086
+ ...isAutoSize.value ? {} : {
7087
+ width: "auto",
7088
+ minWidth: "300px"
7089
+ },
7055
7090
  wordWrap: "break-word",
7056
7091
  overflowWrap: "break-word",
7057
- zIndex: 1060,
7092
+ zIndex: "1060",
7058
7093
  ...Object.keys(props.style).length ? props.style : {},
7059
7094
  ...dynamicOptions.value?.style || {}
7060
7095
  };
@@ -7192,6 +7227,22 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7192
7227
  }
7193
7228
  return Promise.resolve();
7194
7229
  }
7230
+ /**
7231
+ * 强制收起弹窗(兜底)。
7232
+ * Bootstrap 的 hide() 依赖自身 _isShown / 过渡回调,在个别宿主环境(VSCode webview)
7233
+ * 里可能出现「点了关闭但状态没翻转」的情况,此时直接摘掉 show 类并移除遮罩,
7234
+ * 保证关闭按钮一定生效。
7235
+ */
7236
+ function forceHide() {
7237
+ const el = modalContainer.value;
7238
+ if (!el) return false;
7239
+ if (!(el.classList.contains("show") || getComputedStyle(el).display !== "none")) return false;
7240
+ el.classList.remove("show", "fade");
7241
+ el.style.display = "none";
7242
+ document.querySelectorAll(".modal-backdrop").forEach((b) => b.remove());
7243
+ ModalHide();
7244
+ return true;
7245
+ }
7195
7246
  function hide() {
7196
7247
  if (isHiding.value) {
7197
7248
  console.log("Modal already hiding, skipping");
@@ -7209,6 +7260,9 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7209
7260
  };
7210
7261
  modalInstance.hide();
7211
7262
  setTimeout(checkHidden, 100);
7263
+ setTimeout(() => {
7264
+ if (forceHide()) resolve();
7265
+ }, 800);
7212
7266
  });
7213
7267
  }
7214
7268
  return Promise.resolve();
@@ -7270,16 +7324,16 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7270
7324
  tabindex: "-1",
7271
7325
  "aria-labelledby": "modalLabel"
7272
7326
  }, [createBaseVNode("div", {
7273
- class: normalizeClass(["modal-dialog modal-dialog-centered", { "modal-fullscreen": __props.isFullScreen }]),
7327
+ class: normalizeClass(["modal-dialog modal-dialog-centered", {
7328
+ "modal-fullscreen": __props.isFullScreen,
7329
+ "modal-dialog-auto": isAutoSize.value
7330
+ }]),
7274
7331
  style: normalizeStyle(dynamicStyle.value)
7275
7332
  }, [createBaseVNode("div", {
7276
7333
  class: normalizeClass(["modal-content", contentClass.value]),
7277
7334
  style: { "margin": "auto" }
7278
7335
  }, [
7279
- createBaseVNode("div", {
7280
- class: normalizeClass(["modal-header", headerClass.value]),
7281
- style: { "padding": "1rem 1.5rem" }
7282
- }, [createBaseVNode("h5", _hoisted_1$2, [typeIcon.value ? (openBlock(), createElementBlock("i", {
7336
+ createBaseVNode("div", { class: normalizeClass(["modal-header", headerClass.value]) }, [createBaseVNode("h5", _hoisted_1$2, [typeIcon.value ? (openBlock(), createElementBlock("i", {
7283
7337
  key: 0,
7284
7338
  class: normalizeClass([typeIcon.value, "me-2"])
7285
7339
  }, null, 2)) : createCommentVNode("", true), createTextVNode(" " + toDisplayString(dynamicTitle.value || props.title), 1)]), createBaseVNode("button", {
@@ -7287,15 +7341,15 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7287
7341
  class: "btn-close",
7288
7342
  "aria-label": "Close",
7289
7343
  onClick: cancel
7290
- })], 2),
7291
- modalShow.value ? (openBlock(), createElementBlock("div", _hoisted_2$2, [_ctx.$slots.default ? renderSlot(_ctx.$slots, "default", { key: 0 }, void 0, true) : (openBlock(), createElementBlock("div", _hoisted_3$2, [createBaseVNode("div", _hoisted_4$1, [typeIcon.value ? (openBlock(), createElementBlock("i", {
7344
+ }, [..._cache[0] || (_cache[0] = [createBaseVNode("i", { class: "bi bi-x-lg" }, null, -1)])])], 2),
7345
+ modalShow.value ? (openBlock(), createElementBlock("div", _hoisted_2$2, [_ctx.$slots.default ? renderSlot(_ctx.$slots, "default", { key: 0 }, void 0, true) : (openBlock(), createElementBlock("div", _hoisted_3$2, [createBaseVNode("div", { class: normalizeClass(["d-flex align-items-center", shouldShowDetails() ? "mb-2" : "mb-0"]) }, [typeIcon.value ? (openBlock(), createElementBlock("i", {
7292
7346
  key: 0,
7293
- class: normalizeClass([typeIcon.value, "me-3 fs-1"])
7294
- }, null, 2)) : createCommentVNode("", true), createBaseVNode("div", _hoisted_5$1, toDisplayString(dynamicContent.value), 1)]), shouldShowDetails() ? (openBlock(), createElementBlock("div", _hoisted_6$1, [createBaseVNode("button", {
7347
+ class: normalizeClass([typeIcon.value, "me-3 modal-type-icon"])
7348
+ }, null, 2)) : createCommentVNode("", true), createBaseVNode("div", _hoisted_4$1, toDisplayString(dynamicContent.value), 1)], 2), shouldShowDetails() ? (openBlock(), createElementBlock("div", _hoisted_5$1, [createBaseVNode("button", {
7295
7349
  class: "btn btn-sm btn-outline-secondary",
7296
7350
  onClick: toggleDetails
7297
- }, [createTextVNode(toDisplayString(detailsExpanded.value ? _ctx.$t("common.hide") : _ctx.$t("common.details")) + " ", 1), _cache[0] || (_cache[0] = createBaseVNode("i", { class: "bi bi-chevron-{{detailsExpanded ? 'up' : 'down'}} ms-1" }, null, -1))]), detailsExpanded.value ? (openBlock(), createElementBlock("div", _hoisted_7$1, [createBaseVNode("pre", _hoisted_8$1, toDisplayString(formatErrorDetails()), 1)])) : createCommentVNode("", true)])) : createCommentVNode("", true)]))])) : createCommentVNode("", true),
7298
- createBaseVNode("div", _hoisted_9$1, [_ctx.$slots.footer ? renderSlot(_ctx.$slots, "footer", { key: 0 }, void 0, true) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [dynamicShowCancel.value || props.closeButton.show ? (openBlock(), createElementBlock("button", {
7351
+ }, [createTextVNode(toDisplayString(detailsExpanded.value ? _ctx.$t("common.hide") : _ctx.$t("common.details")) + " ", 1), _cache[1] || (_cache[1] = createBaseVNode("i", { class: "bi bi-chevron-{{detailsExpanded ? 'up' : 'down'}} ms-1" }, null, -1))]), detailsExpanded.value ? (openBlock(), createElementBlock("div", _hoisted_6$1, [createBaseVNode("pre", _hoisted_7$1, toDisplayString(formatErrorDetails()), 1)])) : createCommentVNode("", true)])) : createCommentVNode("", true)]))])) : createCommentVNode("", true),
7352
+ createBaseVNode("div", _hoisted_8$1, [_ctx.$slots.footer ? renderSlot(_ctx.$slots, "footer", { key: 0 }, void 0, true) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [dynamicShowCancel.value || props.closeButton.show ? (openBlock(), createElementBlock("button", {
7299
7353
  key: 0,
7300
7354
  type: "button",
7301
7355
  class: "btn btn-secondary",
@@ -7316,17 +7370,17 @@ var init_index_vue_vue_type_script_setup_true_lang$1 = __esmMin((() => {
7316
7370
  });
7317
7371
  }));
7318
7372
  //#endregion
7319
- //#region src/components/modal/index.vue?vue&type=style&index=0&scoped=aa6b0e1f&lang.css
7320
- var init_index_vue_vue_type_style_index_0_scoped_aa6b0e1f_lang = __esmMin((() => {}));
7373
+ //#region src/components/modal/index.vue?vue&type=style&index=0&scoped=8c11135e&lang.css
7374
+ var init_index_vue_vue_type_style_index_0_scoped_8c11135e_lang = __esmMin((() => {}));
7321
7375
  //#endregion
7322
7376
  //#region src/components/modal/index.vue
7323
7377
  var modal_default$1;
7324
7378
  var init_modal$1 = __esmMin((() => {
7325
7379
  init_index_vue_vue_type_script_setup_true_lang$1();
7326
7380
  init_index_vue_vue_type_script_setup_true_lang$1();
7327
- init_index_vue_vue_type_style_index_0_scoped_aa6b0e1f_lang();
7381
+ init_index_vue_vue_type_style_index_0_scoped_8c11135e_lang();
7328
7382
  init__plugin_vue_export_helper();
7329
- modal_default$1 = /*#__PURE__*/ _plugin_vue_export_helper_default(index_vue_vue_type_script_setup_true_lang_default$1, [["__scopeId", "data-v-aa6b0e1f"]]);
7383
+ modal_default$1 = /*#__PURE__*/ _plugin_vue_export_helper_default(index_vue_vue_type_script_setup_true_lang_default$1, [["__scopeId", "data-v-8c11135e"]]);
7330
7384
  }));
7331
7385
  //#endregion
7332
7386
  //#region src/components/dataGrid/pagination.vue?vue&type=script&setup=true&lang.ts
@@ -7770,4 +7824,4 @@ var init_modal = __esmMin((() => {
7770
7824
  app.mount("#app");
7771
7825
  })))();
7772
7826
  //#endregion
7773
- export { defineStore as A, init_rpc as C, vscodeSaveFile as D, vscodePickSaveFolder as E, init_detect as O, blobToBase64 as S, vscodeNotifyExported as T, init__plugin_vue_export_helper as _, dataGrid_default as a, init_base as b, pagination_default as c, init_loading as d, init_toast$1 as f, _plugin_vue_export_helper_default as g, toast_default$1 as h, showConfirm as i, init_pinia as j, isVSCode as k, init_modal$1 as l, init_toast$2 as m, init_modal as n, init_dataGrid as o, toast as p, showAlert as r, init_pagination as s, getModalInstance as t, modal_default$1 as u, getT as v, isVscodeWebview as w, request as x, init_i18n as y };
7827
+ export { isVSCodeEnvironment as A, init_rpc as C, vscodeSaveFile as D, vscodePickSaveFolder as E, isVSCode as M, defineStore as N, getVSCodeBridge as O, init_pinia as P, blobToBase64 as S, vscodeNotifyExported as T, init__plugin_vue_export_helper as _, dataGrid_default as a, init_base as b, pagination_default as c, init_loading as d, init_toast$1 as f, _plugin_vue_export_helper_default as g, toast_default$1 as h, showConfirm as i, init_detect as j, init_bridge as k, init_modal$1 as l, init_toast$2 as m, init_modal as n, init_dataGrid as o, toast as p, showAlert as r, init_pagination as s, getModalInstance as t, modal_default$1 as u, getT as v, isVscodeWebview as w, request as x, init_i18n as y };
@@ -1,6 +1,6 @@
1
1
  import { n as __esmMin } from "./rolldown-runtime.js";
2
- import { A as defineComponent, B as renderList, G as withCtx, H as resolveComponent, O as createTextVNode, S as createBaseVNode, T as createElementBlock, W as watch, b as Fragment, ct as toDisplayString, d as init_vue_runtime_esm_bundler, f as Transition, it as unref, k as createVNode, l as init_vue_i18n, u as useI18n, w as createCommentVNode, y as withModifiers, z as openBlock } from "./vue.js";
3
- import { A as defineStore, _ as init__plugin_vue_export_helper, g as _plugin_vue_export_helper_default, j as init_pinia } from "./index.js";
2
+ import { A as defineComponent, B as renderList, G as watch, H as resolveComponent, K as withCtx, O as createTextVNode, S as createBaseVNode, T as createElementBlock, at as unref, b as Fragment, d as init_vue_runtime_esm_bundler, f as Transition, k as createVNode, l as init_vue_i18n, lt as toDisplayString, u as useI18n, w as createCommentVNode, y as withModifiers, z as openBlock } from "./vue.js";
3
+ import { N as defineStore, P as init_pinia, _ as init__plugin_vue_export_helper, g as _plugin_vue_export_helper_default } from "./index.js";
4
4
  //#region src/stores/locale.ts
5
5
  var LOCALE_KEY, useLocaleStore;
6
6
  var init_locale = __esmMin((() => {
@@ -230,7 +230,7 @@ var init_layout_vue_vue_type_script_setup_true_lang = __esmMin((() => {
230
230
  setup(__props) {
231
231
  const { t } = useI18n();
232
232
  const aboutStore = useAboutStore();
233
- const appVersion = "1.0.36";
233
+ const appVersion = "1.0.37";
234
234
  return (_ctx, _cache) => {
235
235
  const _component_router_link = resolveComponent("router-link");
236
236
  const _component_router_view = resolveComponent("router-view");
@@ -12963,4 +12963,4 @@ var init_core = __esmMin((() => {
12963
12963
  Number.POSITIVE_INFINITY;
12964
12964
  }));
12965
12965
  //#endregion
12966
- export { onScopeDispose as $, defineComponent as A, renderList as B, createBlock as C, createStaticVNode as D, createSlots as E, nextTick as F, withCtx as G, resolveComponent as H, onBeforeUnmount as I, getCurrentScope as J, withDirectives as K, onMounted as L, hasInjectionContext as M, init_runtime_core_esm_bundler as N, createTextVNode as O, inject as P, markRaw as Q, onUnmounted as R, createBaseVNode as S, createElementBlock as T, resolveDynamicComponent as U, renderSlot as V, watch as W, isReactive as X, init_reactivity_esm_bundler as Y, isRef as Z, vModelText as _, init_vue_router as a, init_shared_esm_bundler as at, Fragment as b, createI18n as c, toDisplayString$1 as ct, init_vue_runtime_esm_bundler as d, reactive as et, Transition as f, vModelSelect as g, vModelCheckbox as h, createWebHistory as i, unref as it, getCurrentInstance$1 as j, createVNode as k, init_vue_i18n as l, init_runtime_dom_esm_bundler as m, useTitle as n, toRaw as nt, useRoute as o, normalizeClass as ot, createApp as p, effectScope as q, createRouter as r, toRefs as rt, useRouter as s, normalizeStyle as st, init_core as t, ref as tt, useI18n as u, vShow as v, createCommentVNode as w, computed as x, withModifiers as y, openBlock as z };
12966
+ export { markRaw as $, defineComponent as A, renderList as B, createBlock as C, createStaticVNode as D, createSlots as E, nextTick as F, watch as G, resolveComponent as H, onBeforeUnmount as I, effectScope as J, withCtx as K, onMounted as L, hasInjectionContext as M, init_runtime_core_esm_bundler as N, createTextVNode as O, inject as P, isRef as Q, onUnmounted as R, createBaseVNode as S, createElementBlock as T, resolveDynamicComponent as U, renderSlot as V, useSlots as W, init_reactivity_esm_bundler as X, getCurrentScope as Y, isReactive as Z, vModelText as _, init_vue_router as a, unref as at, Fragment as b, createI18n as c, normalizeStyle as ct, init_vue_runtime_esm_bundler as d, onScopeDispose as et, Transition as f, vModelSelect as g, vModelCheckbox as h, createWebHistory as i, toRefs as it, getCurrentInstance$1 as j, createVNode as k, init_vue_i18n as l, toDisplayString$1 as lt, init_runtime_dom_esm_bundler as m, useTitle as n, ref as nt, useRoute as o, init_shared_esm_bundler as ot, createApp as p, withDirectives as q, createRouter as r, toRaw as rt, useRouter as s, normalizeClass as st, init_core as t, reactive as tt, useI18n as u, vShow as v, createCommentVNode as w, computed as x, withModifiers as y, openBlock as z };
@@ -0,0 +1,195 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ canonicalizeTableEntity,
4
+ canonicalizeTableSpec,
5
+ classifyDefault,
6
+ canonicalizeType,
7
+ diffColumnFields,
8
+ normalizeNumericLiteral
9
+ } from '../canonical';
10
+ import type { TableSpec } from '../../../model/ddl-spec';
11
+
12
+ describe('classifyDefault:三态 + 表达式分类(消灭 || 兜底吃掉假值)', () => {
13
+ it('0 / false / 空串是真实默认值,不得回落为 none', () => {
14
+ expect(classifyDefault(0)).toEqual({ kind: 'literal', text: '0' });
15
+ expect(classifyDefault('0')).toEqual({ kind: 'literal', text: '0' });
16
+ expect(classifyDefault(false)).toEqual({ kind: 'literal', text: 'false' });
17
+ expect(classifyDefault('')).toEqual({ kind: 'literal', text: '' }); // MySQL DEFAULT ''
18
+ });
19
+
20
+ it('无默认值 / DEFAULT NULL / 显式标记 三者可区分', () => {
21
+ expect(classifyDefault(undefined)).toEqual({ kind: 'none', text: '' });
22
+ expect(classifyDefault(null)).toEqual({ kind: 'none', text: '' });
23
+ expect(classifyDefault('NULL')).toEqual({ kind: 'null', text: '' });
24
+ expect(classifyDefault('null')).toEqual({ kind: 'null', text: '' });
25
+ expect(classifyDefault('x', false)).toEqual({ kind: 'none', text: '' }); // hasDefault=false 优先
26
+ expect(classifyDefault(undefined, true)).toEqual({ kind: 'literal', text: '' }); // DEFAULT ''(显式)
27
+ });
28
+
29
+ it('引号字面量与 PG 类型标注', () => {
30
+ expect(classifyDefault("'abc'")).toEqual({ kind: 'literal', text: 'abc' });
31
+ expect(classifyDefault("'0'::character varying")).toEqual({
32
+ kind: 'expression',
33
+ text: "'0'::CHARACTER VARYING"
34
+ });
35
+ });
36
+
37
+ it('表达式默认值:关键字与函数调用', () => {
38
+ expect(classifyDefault('CURRENT_TIMESTAMP')).toEqual({
39
+ kind: 'expression',
40
+ text: 'CURRENT_TIMESTAMP'
41
+ });
42
+ expect(classifyDefault('now()')).toEqual({ kind: 'expression', text: 'NOW()' });
43
+ expect(classifyDefault("nextval('s')")).toEqual({ kind: 'expression', text: "NEXTVAL('S')" });
44
+ });
45
+
46
+ it('数值书写差异归一:007→7、0.50→0.5、-0→0', () => {
47
+ expect(normalizeNumericLiteral('007')).toBe('7');
48
+ expect(normalizeNumericLiteral('0.50')).toBe('0.5');
49
+ expect(normalizeNumericLiteral('-0')).toBe('0');
50
+ expect(classifyDefault('0.50')).toEqual({ kind: 'literal', text: '0.5' });
51
+ expect(diffColumnBetween('0.5', '0.50')).toBe(false); // 同一默认值
52
+ });
53
+ });
54
+
55
+ describe('canonicalizeType:全串与拆分字段两种表示等价', () => {
56
+ it("实体 'varchar(255)' 与表单 {type:'VARCHAR', length:'255'} 等价", () => {
57
+ const a = canonicalizeType({ type: 'varchar(255)', length: 255 });
58
+ const b = canonicalizeType({ type: 'VARCHAR', length: '255' });
59
+ expect(a).toEqual(b);
60
+ expect(a.baseType).toBe('VARCHAR');
61
+ expect(a.length).toBe(255);
62
+ });
63
+
64
+ it('DECIMAL(10,0):scale=0 必须保留(假值不丢弃)', () => {
65
+ const a = canonicalizeType({ type: 'decimal(10,0)' });
66
+ const b = canonicalizeType({ type: 'DECIMAL', precision: '10', scale: '0' });
67
+ expect(a).toEqual(b);
68
+ expect(a.precision).toBe(10);
69
+ expect(a.scale).toBe(0);
70
+ });
71
+
72
+ it("单参数精度类型:'decimal(10)' 解析为 precision", () => {
73
+ expect(canonicalizeType({ type: 'decimal(10)' })).toEqual({
74
+ baseType: 'DECIMAL',
75
+ precision: 10,
76
+ scale: undefined
77
+ });
78
+ });
79
+
80
+ it("非数字参数(ENUM)整体原样保留", () => {
81
+ const a = canonicalizeType({ type: "enum('a','b')" });
82
+ const b = canonicalizeType({ type: 'ENUM', customType: "enum('a','b')" });
83
+ expect(a).toEqual(b);
84
+ expect(a.baseType).toBe("enum('a','b')");
85
+ });
86
+
87
+ it('大小写与空格归一', () => {
88
+ expect(canonicalizeType({ type: ' varchar ' }).baseType).toBe('VARCHAR');
89
+ });
90
+ });
91
+
92
+ describe('canonicalizeTable*:跨表示等价与自增 clamp', () => {
93
+ const entity = {
94
+ name: 'users',
95
+ comment: '用户',
96
+ columns: [
97
+ { name: 'id', type: 'int', nullable: false, isPrimary: true, isAutoIncrement: true, length: 11 },
98
+ { name: 'name', type: 'varchar(64)', nullable: true, defaultValue: '', comment: ' 名字 ' },
99
+ { name: 'amount', type: 'decimal(10,0)', nullable: true, defaultValue: 0 }
100
+ ]
101
+ };
102
+
103
+ /** Phase E 前端应做的回显:canonical → TableSpec */
104
+ function echoSpec(): TableSpec {
105
+ const t = canonicalizeTableEntity(entity);
106
+ const mapDefault = (i: number): Pick<TableSpec['columns'][number], 'hasDefault' | 'defaultValue'> => {
107
+ const c = t.columns[i]!;
108
+ switch (c.defaultKind) {
109
+ case 'none': return { hasDefault: false };
110
+ case 'null': return { hasDefault: true, defaultValue: 'NULL' };
111
+ case 'literal': return { hasDefault: true, defaultValue: c.defaultText };
112
+ case 'expression': return { hasDefault: true, defaultValue: c.defaultText };
113
+ }
114
+ };
115
+ const mk = (i: number, extra: Record<string, unknown> = {}) => {
116
+ const c = t.columns[i]!;
117
+ const full = c.baseType.includes('(');
118
+ return {
119
+ name: c.name,
120
+ ...(full ? { type: c.baseType, customType: c.baseType } : { type: c.baseType, length: c.length, precision: c.precision, scale: c.scale }),
121
+ nullable: c.nullable,
122
+ ...mapDefault(i),
123
+ comment: c.comment,
124
+ ...extra
125
+ };
126
+ };
127
+ return {
128
+ name: t.name,
129
+ comment: t.comment,
130
+ columns: [
131
+ mk(0, { autoIncrement: true }),
132
+ mk(1),
133
+ mk(2)
134
+ ],
135
+ primaryKey: t.primaryKey
136
+ };
137
+ }
138
+
139
+ it('「实体 → canonical → 表单回显」全程零 diff(往返幂等的基石)', () => {
140
+ const before = canonicalizeTableEntity(entity);
141
+ const after = canonicalizeTableSpec(echoSpec());
142
+ expect(diffColumnFields(before.columns[0]!, after.columns[0]!)).toEqual([]);
143
+ expect(diffColumnFields(before.columns[1]!, after.columns[1]!)).toEqual([]);
144
+ expect(diffColumnFields(before.columns[2]!, after.columns[2]!)).toEqual([]);
145
+ });
146
+
147
+ it('rawAutoIncrement 不被 clamp 吞掉(供"加主键时恢复自增")', () => {
148
+ const t = canonicalizeTableEntity({
149
+ name: 't',
150
+ columns: [{ name: 'id', type: 'int', nullable: false, isAutoIncrement: true, isPrimary: false }]
151
+ });
152
+ expect(t.columns[0]!.rawAutoIncrement).toBe(true);
153
+ expect(t.columns[0]!.effectiveAutoIncrement).toBe(false); // 无主键 → 不生效
154
+ });
155
+
156
+ it('clamp 是两侧同一规则:非主键列勾自增不产生列级 diff', () => {
157
+ const before = canonicalizeTableEntity({
158
+ name: 't',
159
+ columns: [{ name: 'id', type: 'int', nullable: false, isPrimary: false, isAutoIncrement: false }]
160
+ });
161
+ const after = canonicalizeTableSpec({
162
+ name: 't',
163
+ primaryKey: [],
164
+ columns: [{ name: 'id', type: 'INT', nullable: false, hasDefault: false, autoIncrement: true }] // 勾了自增但非主键
165
+ });
166
+ expect(diffColumnFields(before.columns[0]!, after.columns[0]!)).toEqual([]);
167
+ });
168
+
169
+ it('注释 trim:两侧空格差异不算改动', () => {
170
+ const before = canonicalizeTableEntity({
171
+ name: 't',
172
+ columns: [{ name: 'a', type: 'int', nullable: true, comment: 'x' }]
173
+ });
174
+ const after = canonicalizeTableSpec({
175
+ name: 't',
176
+ primaryKey: [],
177
+ columns: [{ name: 'a', type: 'INT', nullable: true, hasDefault: false, comment: ' x ' }]
178
+ });
179
+ expect(diffColumnFields(before.columns[0]!, after.columns[0]!)).toEqual([]);
180
+ });
181
+ });
182
+
183
+ function diffColumnBetween(aRaw: string, bRaw: string): boolean {
184
+ const col = (defaultValue: unknown) => ({
185
+ name: 'x', type: 'int', nullable: true, defaultValue, isPrimary: false
186
+ });
187
+ const before = canonicalizeTableEntity({ name: 't', columns: [col(aRaw)] }).columns[0]!;
188
+ const specCol = (defaultValue: string) => ({
189
+ name: 'x', type: 'INT', nullable: true, hasDefault: true, defaultValue
190
+ });
191
+ const after = canonicalizeTableSpec({
192
+ name: 't', primaryKey: [], columns: [specCol(bRaw)]
193
+ }).columns[0]!;
194
+ return diffColumnFields(before, after).includes('default');
195
+ }