befly 3.15.0 → 3.15.1

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.
@@ -10,6 +10,20 @@ import { getDialectByName, getSyncTableColumnsInfoQuery, getSyncTableIndexesQuer
10
10
  import { Logger } from "../lib/logger";
11
11
  import { normalizeFieldDefinition } from "../utils/normalizeFieldDefinition";
12
12
  import { snakeCase } from "../utils/util";
13
+ function createRuntimeForIO(dbDialect, db, dbName = "") {
14
+ return {
15
+ dbDialect: dbDialect,
16
+ db: db,
17
+ dbName: dbName
18
+ };
19
+ }
20
+ function buildRuntimeIoError(operation, tableName, error) {
21
+ const errMsg = String(error?.message || error);
22
+ const outErr = new Error(`同步表:读取元信息失败,操作=${operation},表=${tableName},错误=${errMsg}`);
23
+ if (error?.sqlInfo)
24
+ outErr.sqlInfo = error.sqlInfo;
25
+ return outErr;
26
+ }
13
27
  /**
14
28
  * 数据库同步命令入口(函数模式)
15
29
  */
@@ -18,39 +32,22 @@ export const syncTable = (async (ctx, items) => {
18
32
  // 记录处理过的表名(用于清理缓存)
19
33
  const processedTables = [];
20
34
  if (!Array.isArray(items)) {
21
- throw new Error("syncTable(items) 参数必须是数组");
35
+ throw new Error("同步表:请传入多个表定义组成的数组");
22
36
  }
23
- if (!ctx) {
24
- throw new Error("syncTable(ctx, items) 缺少 ctx");
25
- }
26
- if (!ctx.db) {
27
- throw new Error("syncTable(ctx, items) 缺少 ctx.db");
37
+ if (!ctx?.db) {
38
+ throw new Error("同步表:ctx.db 未初始化");
28
39
  }
29
40
  if (!ctx.redis) {
30
- throw new Error("syncTable(ctx, items) 缺少 ctx.redis");
41
+ throw new Error("同步表:ctx.redis 未初始化");
31
42
  }
32
43
  if (!ctx.config) {
33
- throw new Error("syncTable(ctx, items) 缺少 ctx.config");
34
- }
35
- // DbDialect 归一化(允许值与映射关系):
36
- //
37
- // | ctx.config.db.type 输入 | 归一化 dbDialect |
38
- // |------------------------|------------------|
39
- // | mysql / 其他 / 空值 | mysql |
40
- // | postgres / postgresql | postgresql |
41
- // | sqlite | sqlite |
42
- //
43
- // 约束:后续若新增方言,必须同步更新:
44
- // - 这里的归一化
45
- // - ensureDbVersion / runtime I/O / DDL 分支
46
- const dbType = String(ctx.config.db?.type || "mysql").toLowerCase();
47
- let dbDialect = "mysql";
48
- if (dbType === "postgres" || dbType === "postgresql") {
49
- dbDialect = "postgresql";
44
+ throw new Error("同步表:ctx.config 未初始化");
50
45
  }
51
- else if (dbType === "sqlite") {
52
- dbDialect = "sqlite";
46
+ if (!ctx.config.db?.dialect) {
47
+ throw new Error("同步表:ctx.config.db.dialect 缺失");
53
48
  }
49
+ // DbDialect(按项目约定:正常启动时会先通过 checkConfig,因此这里直接使用配置值)
50
+ const dbDialect = ctx.config.db.dialect;
54
51
  // 检查数据库版本(复用 ctx.db 的现有连接/事务)
55
52
  await ensureDbVersion(dbDialect, ctx.db);
56
53
  const databaseName = ctx.config.db?.database || "";
@@ -64,32 +61,17 @@ export const syncTable = (async (ctx, items) => {
64
61
  if (!item || item.type !== "table") {
65
62
  continue;
66
63
  }
67
- if (item.source !== "app" && item.source !== "addon" && item.source !== "core") {
68
- Logger.warn(`syncTable 跳过未知来源表定义: source=${String(item.source)} fileName=${String(item.fileName)}`);
69
- continue;
70
- }
71
64
  // 确定表名:
72
65
  // - addon 表:addon_{addonName}_{fileName}
73
66
  // - app/core 表:{fileName}
74
- const baseTableName = snakeCase(item.fileName);
75
- let tableName = baseTableName;
76
- if (item.source === "addon") {
77
- if (!item.addonName || String(item.addonName).trim() === "") {
78
- throw new Error(`syncTable addon 表缺少 addonName: fileName=${String(item.fileName)}`);
79
- }
80
- tableName = `addon_${snakeCase(item.addonName)}_${baseTableName}`;
81
- }
82
- const tableDefinition = item.content;
83
- if (!tableDefinition || typeof tableDefinition !== "object") {
84
- throw new Error(`syncTable 表定义无效: table=${tableName}`);
85
- }
67
+ const tableName = item.source === "addon" ? `addon_${snakeCase(item.addonName)}_${snakeCase(item.fileName)}` : snakeCase(item.fileName);
68
+ const tableFields = item.content;
86
69
  // 为字段属性设置默认值:表定义来自 JSON/扫描结果,字段可能缺省。
87
70
  // 缺省会让 diff/DDL 生成出现 undefined vs null 等差异,导致错误的变更判断。
88
- for (const fieldDef of Object.values(tableDefinition)) {
89
- applyFieldDefaults(fieldDef);
71
+ for (const fieldDef of Object.values(tableFields)) {
72
+ normalizeFieldDefinitionInPlace(fieldDef);
90
73
  }
91
74
  const existsTable = await tableExistsRuntime(runtime, tableName);
92
- const tableFields = tableDefinition;
93
75
  if (existsTable) {
94
76
  await modifyTableRuntime(runtime, tableName, tableFields);
95
77
  }
@@ -205,7 +187,7 @@ const SYNC_TABLE_TEST_KIT = {
205
187
  getTypeMapping: getTypeMapping,
206
188
  quoteIdentifier: quoteIdentifier,
207
189
  escapeComment: escapeComment,
208
- applyFieldDefaults: applyFieldDefaults,
190
+ normalizeFieldDefinitionInPlace: normalizeFieldDefinitionInPlace,
209
191
  isStringOrArrayType: isStringOrArrayType,
210
192
  getSqlType: getSqlType,
211
193
  resolveDefaultValue: resolveDefaultValue,
@@ -219,13 +201,7 @@ const SYNC_TABLE_TEST_KIT = {
219
201
  tableExistsRuntime: tableExistsRuntime,
220
202
  getTableColumnsRuntime: getTableColumnsRuntime,
221
203
  getTableIndexesRuntime: getTableIndexesRuntime,
222
- createRuntime: (dbDialect, db, dbName = "") => {
223
- return {
224
- dbDialect: dbDialect,
225
- db: db,
226
- dbName: dbName
227
- };
228
- }
204
+ createRuntime: createRuntimeForIO
229
205
  };
230
206
  // 测试能力挂载(避免导出零散函数,同时确保运行时存在)
231
207
  syncTable.TestKit = SYNC_TABLE_TEST_KIT;
@@ -282,88 +258,19 @@ function normalizeColumnDefaultValue(value) {
282
258
  // 注意:这里刻意不封装“logFieldChange/formatFieldList”之类的一次性工具函数,
283
259
  // 以减少抽象层级(按项目要求:能直写就直写)。
284
260
  /**
285
- * 为字段定义应用默认值
261
+ * 为字段定义应用默认值(就地归一化)
286
262
  */
287
- function isJsonValue(value) {
288
- if (value === null)
289
- return true;
290
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
291
- return true;
292
- if (Array.isArray(value)) {
293
- return value.every((v) => isJsonValue(v));
294
- }
295
- if (typeof value === "object") {
296
- for (const v of Object.values(value)) {
297
- if (v === undefined)
298
- continue;
299
- if (!isJsonValue(v))
300
- return false;
301
- }
302
- return true;
303
- }
304
- return false;
305
- }
306
- function applyFieldDefaults(fieldDef) {
307
- if (!fieldDef || typeof fieldDef !== "object")
308
- return;
309
- const record = fieldDef;
310
- const name = record["name"];
311
- const type = record["type"];
312
- if (typeof name !== "string" || typeof type !== "string")
313
- return;
314
- const minRaw = record["min"];
315
- const maxRaw = record["max"];
316
- const defaultRaw = record["default"];
317
- const detailRaw = record["detail"];
318
- const indexRaw = record["index"];
319
- const uniqueRaw = record["unique"];
320
- const nullableRaw = record["nullable"];
321
- const unsignedRaw = record["unsigned"];
322
- const regexpRaw = record["regexp"];
323
- const input = {
324
- name: name,
325
- type: type
326
- };
327
- if (typeof detailRaw === "string") {
328
- input.detail = detailRaw;
329
- }
330
- if (typeof minRaw === "number" || minRaw === null) {
331
- input.min = minRaw;
332
- }
333
- if (typeof maxRaw === "number" || maxRaw === null) {
334
- input.max = maxRaw;
335
- }
336
- if (defaultRaw === null) {
337
- input.default = null;
338
- }
339
- else if (isJsonValue(defaultRaw)) {
340
- input.default = defaultRaw;
341
- }
342
- if (typeof indexRaw === "boolean") {
343
- input.index = indexRaw;
344
- }
345
- if (typeof uniqueRaw === "boolean") {
346
- input.unique = uniqueRaw;
347
- }
348
- if (typeof nullableRaw === "boolean") {
349
- input.nullable = nullableRaw;
350
- }
351
- if (typeof unsignedRaw === "boolean") {
352
- input.unsigned = unsignedRaw;
353
- }
354
- if (typeof regexpRaw === "string" || regexpRaw === null) {
355
- input.regexp = regexpRaw;
356
- }
357
- const normalized = normalizeFieldDefinition(input);
358
- record["detail"] = normalized.detail;
359
- record["min"] = normalized.min;
360
- record["max"] = normalized.max;
361
- record["default"] = normalized.default;
362
- record["index"] = normalized.index;
363
- record["unique"] = normalized.unique;
364
- record["nullable"] = normalized.nullable;
365
- record["unsigned"] = normalized.unsigned;
366
- record["regexp"] = normalized.regexp;
263
+ function normalizeFieldDefinitionInPlace(fieldDef) {
264
+ const normalized = normalizeFieldDefinition(fieldDef);
265
+ fieldDef.detail = normalized.detail;
266
+ fieldDef.min = normalized.min;
267
+ fieldDef.max = normalized.max;
268
+ fieldDef.default = normalized.default;
269
+ fieldDef.index = normalized.index;
270
+ fieldDef.unique = normalized.unique;
271
+ fieldDef.nullable = normalized.nullable;
272
+ fieldDef.unsigned = normalized.unsigned;
273
+ fieldDef.regexp = normalized.regexp;
367
274
  }
368
275
  /**
369
276
  * 判断是否为字符串或数组类型(需要长度参数)
@@ -620,41 +527,45 @@ function isCompatibleTypeChange(currentType, newType) {
620
527
  // 读:表是否存在
621
528
  // ---------------------------------------------------------------------------
622
529
  async function tableExistsRuntime(runtime, tableName) {
623
- const db = runtime.db;
624
- if (!db)
625
- throw new Error("SQL 执行器未初始化");
626
530
  try {
627
531
  // 统一交由方言层构造 SQL;syncTable 仅决定“要查哪个 schema/db”。
628
532
  // - MySQL:传 runtime.dbName(information_schema.table_schema)
629
533
  // - PostgreSQL:固定 public(项目约定)
630
534
  // - SQLite:忽略 schema
631
- let schema = undefined;
632
- if (runtime.dbDialect === "mysql") {
633
- schema = runtime.dbName;
634
- }
635
- else if (runtime.dbDialect === "postgresql") {
636
- schema = "public";
637
- }
535
+ const schema = runtime.dbDialect === "mysql" ? runtime.dbName : runtime.dbDialect === "postgresql" ? "public" : undefined;
638
536
  const q = getDialectByName(runtime.dbDialect).tableExistsQuery(tableName, schema);
639
- const res = await db.unsafe(q.sql, q.params);
537
+ const res = await runtime.db.unsafe(q.sql, q.params);
640
538
  return (res.data?.[0]?.count || 0) > 0;
641
539
  }
642
540
  catch (error) {
643
- const errMsg = String(error?.message || error);
644
- const outErr = new Error(`runtime I/O 失败: op=tableExists table=${tableName} err=${errMsg}`);
645
- if (error?.sqlInfo)
646
- outErr.sqlInfo = error.sqlInfo;
647
- throw outErr;
541
+ throw buildRuntimeIoError("检查表是否存在", tableName, error);
648
542
  }
649
543
  }
650
544
  // ---------------------------------------------------------------------------
651
545
  // 读:列信息
652
546
  // ---------------------------------------------------------------------------
653
547
  async function getTableColumnsRuntime(runtime, tableName) {
548
+ // 返回的列数据示例
549
+ // [{
550
+ // is_system: {
551
+ // type: "bigint",
552
+ // columnType: "bigint unsigned",
553
+ // length: null,
554
+ // max: null,
555
+ // nullable: false,
556
+ // defaultValue: "0",
557
+ // comment: '',
558
+ // },
559
+ // description: {
560
+ // type: "varchar",
561
+ // columnType: "varchar(500)",
562
+ // length: 500,
563
+ // max: 500,
564
+ // nullable: false,
565
+ // defaultValue: "",
566
+ // comment: '',
567
+ // }]
654
568
  const columns = {};
655
- const db = runtime.db;
656
- if (!db)
657
- throw new Error("SQL 执行器未初始化");
658
569
  try {
659
570
  // 方言差异说明:
660
571
  // - MySQL:information_schema.columns 最完整,包含 COLUMN_TYPE 与 COLUMN_COMMENT。
@@ -662,31 +573,32 @@ async function getTableColumnsRuntime(runtime, tableName) {
662
573
  // - SQLite:PRAGMA table_info 仅提供 type/notnull/default 等有限信息,无列注释。
663
574
  if (runtime.dbDialect === "mysql") {
664
575
  const q = getSyncTableColumnsInfoQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
665
- const result = await db.unsafe(q.columns.sql, q.columns.params);
576
+ const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
666
577
  for (const row of result.data) {
667
578
  const defaultValue = normalizeColumnDefaultValue(row.COLUMN_DEFAULT);
668
579
  columns[row.COLUMN_NAME] = {
669
- type: row.DATA_TYPE,
670
- columnType: row.COLUMN_TYPE,
580
+ // 防御性:某些 driver/编码设置可能导致字符串字段不是 string(如 Buffer/number/null)。
581
+ type: String(row.DATA_TYPE ?? ""),
582
+ columnType: String(row.COLUMN_TYPE ?? ""),
671
583
  length: row.CHARACTER_MAXIMUM_LENGTH,
672
584
  max: row.CHARACTER_MAXIMUM_LENGTH,
673
585
  nullable: row.IS_NULLABLE === "YES",
674
586
  defaultValue: defaultValue,
675
- comment: row.COLUMN_COMMENT
587
+ comment: row.COLUMN_COMMENT === null ? null : String(row.COLUMN_COMMENT)
676
588
  };
677
589
  }
678
590
  }
679
591
  else if (runtime.dbDialect === "postgresql") {
680
592
  const q = getSyncTableColumnsInfoQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
681
- const result = await db.unsafe(q.columns.sql, q.columns.params);
682
- const comments = q.comments ? (await db.unsafe(q.comments.sql, q.comments.params)).data : [];
593
+ const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
594
+ const comments = q.comments ? (await runtime.db.unsafe(q.comments.sql, q.comments.params)).data : [];
683
595
  const commentMap = {};
684
596
  for (const r of comments)
685
597
  commentMap[r.column_name] = r.column_comment;
686
598
  for (const row of result.data) {
687
599
  columns[row.column_name] = {
688
- type: row.data_type,
689
- columnType: row.data_type,
600
+ type: String(row.data_type ?? ""),
601
+ columnType: String(row.data_type ?? ""),
690
602
  length: row.character_maximum_length,
691
603
  max: row.character_maximum_length,
692
604
  nullable: String(row.is_nullable).toUpperCase() === "YES",
@@ -697,24 +609,18 @@ async function getTableColumnsRuntime(runtime, tableName) {
697
609
  }
698
610
  else if (runtime.dbDialect === "sqlite") {
699
611
  const q = getSyncTableColumnsInfoQuery({ dialect: "sqlite", table: tableName, dbName: runtime.dbName });
700
- const result = await db.unsafe(q.columns.sql, q.columns.params);
612
+ const result = await runtime.db.unsafe(q.columns.sql, q.columns.params);
701
613
  for (const row of result.data) {
702
614
  let baseType = String(row.type || "").toUpperCase();
703
615
  let max = null;
704
616
  const m = /^(\w+)\s*\((\d+)\)/.exec(baseType);
705
- if (m) {
706
- const base = m[1];
707
- const maxText = m[2];
708
- if (typeof base === "string") {
709
- baseType = base;
710
- }
711
- if (typeof maxText === "string") {
712
- max = Number(maxText);
713
- }
617
+ if (m && m[1] && m[2]) {
618
+ baseType = m[1];
619
+ max = Number(m[2]);
714
620
  }
715
621
  columns[row.name] = {
716
- type: baseType.toLowerCase(),
717
- columnType: baseType.toLowerCase(),
622
+ type: String(baseType).toLowerCase(),
623
+ columnType: String(baseType).toLowerCase(),
718
624
  length: max,
719
625
  max: max,
720
626
  nullable: row.notnull === 0,
@@ -726,21 +632,22 @@ async function getTableColumnsRuntime(runtime, tableName) {
726
632
  return columns;
727
633
  }
728
634
  catch (error) {
729
- const errMsg = String(error?.message || error);
730
- const outErr = new Error(`runtime I/O 失败: op=getTableColumns table=${tableName} err=${errMsg}`);
731
- if (error?.sqlInfo)
732
- outErr.sqlInfo = error.sqlInfo;
733
- throw outErr;
635
+ throw buildRuntimeIoError("读取列信息", tableName, error);
734
636
  }
735
637
  }
736
638
  // ---------------------------------------------------------------------------
737
639
  // 读:索引信息(单列索引)
738
640
  // ---------------------------------------------------------------------------
739
641
  async function getTableIndexesRuntime(runtime, tableName) {
642
+ // 索引返回示例
643
+ // {
644
+ // code: [ "code" ],
645
+ // idx_created_at: [ "created_at" ],
646
+ // idx_group: [ "group" ],
647
+ // idx_state: [ "state" ],
648
+ // idx_updated_at: [ "updated_at" ],
649
+ // }
740
650
  const indexes = {};
741
- const db = runtime.db;
742
- if (!db)
743
- throw new Error("SQL 执行器未初始化");
744
651
  try {
745
652
  // 方言差异说明:
746
653
  // - MySQL:information_schema.statistics 直接给出 index -> column 映射。
@@ -748,7 +655,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
748
655
  // - SQLite:PRAGMA index_list + index_info;同样仅收集单列索引,避免多列索引误判。
749
656
  if (runtime.dbDialect === "mysql") {
750
657
  const q = getSyncTableIndexesQuery({ dialect: "mysql", table: tableName, dbName: runtime.dbName });
751
- const result = await db.unsafe(q.sql, q.params);
658
+ const result = await runtime.db.unsafe(q.sql, q.params);
752
659
  for (const row of result.data) {
753
660
  const indexName = row.INDEX_NAME;
754
661
  const current = indexes[indexName];
@@ -762,7 +669,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
762
669
  }
763
670
  else if (runtime.dbDialect === "postgresql") {
764
671
  const q = getSyncTableIndexesQuery({ dialect: "postgresql", table: tableName, dbName: runtime.dbName });
765
- const result = await db.unsafe(q.sql, q.params);
672
+ const result = await runtime.db.unsafe(q.sql, q.params);
766
673
  for (const row of result.data) {
767
674
  const m = /\(([^)]+)\)/.exec(row.indexdef);
768
675
  if (m) {
@@ -774,10 +681,10 @@ async function getTableIndexesRuntime(runtime, tableName) {
774
681
  }
775
682
  else if (runtime.dbDialect === "sqlite") {
776
683
  const quotedTable = quoteIdentifier("sqlite", tableName);
777
- const list = await db.unsafe(`PRAGMA index_list(${quotedTable})`);
684
+ const list = await runtime.db.unsafe(`PRAGMA index_list(${quotedTable})`);
778
685
  for (const idx of list.data) {
779
686
  const quotedIndex = quoteIdentifier("sqlite", idx.name);
780
- const info = await db.unsafe(`PRAGMA index_info(${quotedIndex})`);
687
+ const info = await runtime.db.unsafe(`PRAGMA index_info(${quotedIndex})`);
781
688
  const cols = info.data.map((r) => r.name);
782
689
  if (cols.length === 1)
783
690
  indexes[idx.name] = cols;
@@ -786,11 +693,7 @@ async function getTableIndexesRuntime(runtime, tableName) {
786
693
  return indexes;
787
694
  }
788
695
  catch (error) {
789
- const errMsg = String(error?.message || error);
790
- const outErr = new Error(`runtime I/O 失败: op=getTableIndexes table=${tableName} err=${errMsg}`);
791
- if (error?.sqlInfo)
792
- outErr.sqlInfo = error.sqlInfo;
793
- throw outErr;
696
+ throw buildRuntimeIoError("读取索引信息", tableName, error);
794
697
  }
795
698
  }
796
699
  // ---------------------------------------------------------------------------
@@ -800,39 +703,37 @@ async function getTableIndexesRuntime(runtime, tableName) {
800
703
  * 数据库版本检查(按方言)
801
704
  */
802
705
  async function ensureDbVersion(dbDialect, db) {
803
- if (!db)
804
- throw new Error("SQL 执行器未初始化");
805
706
  if (dbDialect === "mysql") {
806
707
  const r = await db.unsafe("SELECT VERSION() AS version");
807
708
  if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
808
- throw new Error("无法获取 MySQL 版本信息");
709
+ throw new Error("同步表:无法获取 MySQL 版本信息");
809
710
  }
810
711
  const version = r.data[0].version;
811
712
  const majorPart = String(version).split(".")[0] || "0";
812
713
  const majorVersion = parseInt(majorPart, 10);
813
714
  if (!Number.isFinite(majorVersion) || majorVersion < DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR) {
814
- throw new Error(`此脚本仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+,当前版本: ${version}`);
715
+ throw new Error(`同步表:仅支持 MySQL ${DB_VERSION_REQUIREMENTS.MYSQL_MIN_MAJOR}.0+(当前版本:${version})`);
815
716
  }
816
717
  return;
817
718
  }
818
719
  if (dbDialect === "postgresql") {
819
720
  const r = await db.unsafe("SELECT version() AS version");
820
721
  if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
821
- throw new Error("无法获取 PostgreSQL 版本信息");
722
+ throw new Error("同步表:无法获取 PostgreSQL 版本信息");
822
723
  }
823
724
  const versionText = r.data[0].version;
824
725
  const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
825
726
  const majorText = m ? m[1] : undefined;
826
727
  const major = typeof majorText === "string" ? parseInt(majorText, 10) : NaN;
827
728
  if (!Number.isFinite(major) || major < DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR) {
828
- throw new Error(`此脚本要求 PostgreSQL >= ${DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR},当前: ${versionText}`);
729
+ throw new Error(`同步表:要求 PostgreSQL >= ${DB_VERSION_REQUIREMENTS.POSTGRES_MIN_MAJOR}(当前:${versionText})`);
829
730
  }
830
731
  return;
831
732
  }
832
733
  if (dbDialect === "sqlite") {
833
734
  const r = await db.unsafe("SELECT sqlite_version() AS version");
834
735
  if (!r.data || r.data.length === 0 || !r.data[0]?.version) {
835
- throw new Error("无法获取 SQLite 版本信息");
736
+ throw new Error("同步表:无法获取 SQLite 版本信息");
836
737
  }
837
738
  const version = r.data[0].version;
838
739
  const parts = String(version)
@@ -843,7 +744,7 @@ async function ensureDbVersion(dbDialect, db) {
843
744
  const patch = parts[2] ?? 0;
844
745
  const vnum = maj * 10000 + min * 100 + patch;
845
746
  if (!Number.isFinite(vnum) || vnum < DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION_NUM) {
846
- throw new Error(`此脚本要求 SQLite >= ${DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION},当前: ${version}`);
747
+ throw new Error(`同步表:要求 SQLite >= ${DB_VERSION_REQUIREMENTS.SQLITE_MIN_VERSION}(当前:${version})`);
847
748
  }
848
749
  return;
849
750
  }
@@ -869,7 +770,7 @@ function compareFieldDefinition(dbDialect, existingColumn, fieldDef) {
869
770
  }
870
771
  }
871
772
  if (dbDialect !== "sqlite") {
872
- const currentComment = existingColumn.comment || "";
773
+ const currentComment = existingColumn.comment === null || existingColumn.comment === undefined ? "" : String(existingColumn.comment);
873
774
  if (currentComment !== normalized.name) {
874
775
  changes.push({
875
776
  type: "comment",
@@ -881,10 +782,26 @@ function compareFieldDefinition(dbDialect, existingColumn, fieldDef) {
881
782
  const typeMapping = getTypeMapping(dbDialect);
882
783
  const mapped = typeMapping[normalized.type];
883
784
  if (typeof mapped !== "string") {
884
- throw new Error(`未知字段类型映射:dialect=${dbDialect} type=${String(normalized.type)}`);
785
+ throw new Error(`同步表:未知字段类型映射(方言=${dbDialect},类型=${String(normalized.type)})`);
885
786
  }
886
787
  const expectedType = mapped.toLowerCase();
887
- const currentType = existingColumn.type.toLowerCase();
788
+ // 防御性:理论上 ColumnInfo.type/columnType 都应为 string,但线上偶发出现 number/null/Buffer 等导致崩溃。
789
+ // 同时:columnType 可能包含长度/unsigned(如 varchar(255), bigint unsigned),这里归一化为“基础类型”再比较。
790
+ let rawType = "";
791
+ if (typeof existingColumn.type === "string" && existingColumn.type.trim() !== "") {
792
+ rawType = existingColumn.type;
793
+ }
794
+ else if (typeof existingColumn.columnType === "string" && existingColumn.columnType.trim() !== "") {
795
+ rawType = existingColumn.columnType;
796
+ }
797
+ else {
798
+ rawType = String(existingColumn.type ?? "");
799
+ }
800
+ const currentType = rawType
801
+ .toLowerCase()
802
+ .replace(/\s*unsigned/gi, "")
803
+ .replace(/\([^)]*\)/g, "")
804
+ .trim();
888
805
  if (currentType !== expectedType) {
889
806
  changes.push({
890
807
  type: "datatype",
@@ -922,7 +839,7 @@ async function rebuildSqliteTable(runtime, tableName, fields) {
922
839
  // - 只复制 targetCols 与 existingCols 的交集,避免因新增列/删除列导致 INSERT 失败。
923
840
  // - 不做额外的数据转换/回填:保持迁移路径尽量“纯结构同步”。
924
841
  if (runtime.dbDialect !== "sqlite") {
925
- throw new Error(`rebuildSqliteTable 仅支持 sqlite 方言,当前: ${String(runtime.dbDialect)}`);
842
+ throw new Error(`同步表:SQLite 重建表仅支持 sqlite 方言(当前:${String(runtime.dbDialect)})`);
926
843
  }
927
844
  const quotedSourceTable = quoteIdentifier("sqlite", tableName);
928
845
  const info = await runtime.db.unsafe(`PRAGMA table_info(${quotedSourceTable})`);
@@ -21,8 +21,8 @@ export type BeflyRuntimeEnv = Record<string, string | undefined>;
21
21
  export interface DatabaseConfig {
22
22
  /** 是否启用数据库 (0: 关闭, 1: 开启) @default 0 */
23
23
  enable?: number;
24
- /** 数据库类型 ('mysql' | 'postgres' | 'sqlite') @default 'sqlite' */
25
- type?: string;
24
+ /** 数据库方言 ('mysql' | 'postgresql' | 'sqlite') @default 'sqlite' */
25
+ dialect?: "mysql" | "postgresql" | "sqlite";
26
26
  /** 数据库主机 @default '127.0.0.1' */
27
27
  host?: string;
28
28
  /** 数据库端口 @default 3306 */
@@ -18,7 +18,7 @@ export type DbTableName = keyof DbRowMap & string;
18
18
  /**
19
19
  * 数据库类型
20
20
  */
21
- export type DatabaseType = "mysql" | "postgres" | "sqlite";
21
+ export type DatabaseType = "mysql" | "postgresql" | "sqlite";
22
22
  /**
23
23
  * 查询选项
24
24
  */
@@ -28,7 +28,7 @@ export interface CreateTableOptions {
28
28
  */
29
29
  export interface SqlHelperOptions {
30
30
  /** 数据库类型 */
31
- type: "mysql" | "postgres" | "sqlite";
31
+ type: "mysql" | "postgresql" | "sqlite";
32
32
  /** 表名前缀 */
33
33
  prefix?: string;
34
34
  }
@@ -46,7 +46,7 @@ export interface FieldDefinition {
46
46
  unique?: boolean;
47
47
  /**
48
48
  * 是否可为 NULL。
49
- * - 缺省默认:true
49
+ * - 缺省默认:false
50
50
  */
51
51
  nullable?: boolean;
52
52
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.15.0",
4
- "gitHead": "3a9dc64d2eb69e646f4fd6eebd5d8dbe6516cb69",
3
+ "version": "3.15.1",
4
+ "gitHead": "540d9551c589cb33d711c593e561f0fffc0aac0c",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
7
7
  "keywords": [