imean-cassandra-orm 3.1.0 → 3.2.0

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.
package/dist/mod.cjs CHANGED
@@ -655,6 +655,20 @@ function shouldWrite(op, conf) {
655
655
  return Array.isArray(conf) && conf.includes(op);
656
656
  }
657
657
 
658
+ // src/indexes.ts
659
+ function createIndex(keyspace, tableName, fieldName, options) {
660
+ const indexName = `${tableName}_${String(fieldName).toLowerCase()}_sai_idx`;
661
+ const config = options !== true ? options : {
662
+ case_sensitive: false,
663
+ normalize: true,
664
+ ascii: true
665
+ };
666
+ const optionsString = JSON.stringify(config).replace(/"/g, "'");
667
+ return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${keyspace}.${tableName} (${String(
668
+ fieldName
669
+ )}) USING 'sai' WITH OPTIONS = ${optionsString}`;
670
+ }
671
+
658
672
  // src/helper.ts
659
673
  function dropTable(keyspace, tableName) {
660
674
  return `DROP TABLE IF EXISTS ${keyspace}.${tableName};`;
@@ -1194,28 +1208,10 @@ function buildSelectByIndexParams(query, fieldConfigs) {
1194
1208
  function createIndexes(schema) {
1195
1209
  const indexQueries = [];
1196
1210
  if (schema.indexes) {
1197
- Object.entries(schema.indexes).forEach(([fieldName, indexConfig]) => {
1198
- if (indexConfig) {
1199
- const indexName = `${fieldName.toLowerCase()}_sai_idx`;
1200
- let indexQuery = `CREATE INDEX ${indexName} ON ${schema.keyspace}.${schema.tableName} (${fieldName}) USING 'sai'`;
1201
- if (typeof indexConfig === "object" && Object.keys(indexConfig).length > 0) {
1202
- const options = [];
1203
- Object.entries(indexConfig).forEach(([key, value]) => {
1204
- if (value !== void 0 && value !== null) {
1205
- if (typeof value === "string") {
1206
- options.push(`'${key}': '${value}'`);
1207
- } else {
1208
- options.push(`'${key}': ${value}`);
1209
- }
1210
- }
1211
- });
1212
- if (options.length > 0) {
1213
- indexQuery += ` WITH OPTIONS = { ${options.join(", ")} }`;
1214
- }
1215
- }
1216
- indexQuery += ";";
1217
- indexQueries.push(indexQuery);
1218
- }
1211
+ Object.entries(schema.indexes).forEach(([fieldName, options]) => {
1212
+ indexQueries.push(
1213
+ createIndex(schema.keyspace, schema.tableName, fieldName, options)
1214
+ );
1219
1215
  });
1220
1216
  }
1221
1217
  return indexQueries;
@@ -1224,69 +1220,54 @@ function syncIndexes(schema, existingIndexes) {
1224
1220
  const operations = [];
1225
1221
  const existingIndexMap = /* @__PURE__ */ new Map();
1226
1222
  existingIndexes.forEach((index) => {
1227
- existingIndexMap.set(index.index_name, {
1228
- options: index.options
1223
+ existingIndexMap.set(index.options.target, {
1224
+ case_sensitive: Boolean(index.options.case_sensitive),
1225
+ normalize: Boolean(index.options.normalize),
1226
+ ascii: Boolean(index.options.ascii),
1227
+ similarity_function: index.options.similarity_function ?? void 0
1229
1228
  });
1230
1229
  });
1231
1230
  const newIndexMap = /* @__PURE__ */ new Map();
1232
1231
  if (schema.indexes) {
1233
- Object.entries(schema.indexes).forEach(([fieldName, indexConfig]) => {
1234
- if (indexConfig) {
1235
- const indexName = `${fieldName.toLowerCase()}_sai_idx`;
1236
- let options = "";
1237
- if (typeof indexConfig === "object" && Object.keys(indexConfig).length > 0) {
1238
- const optionStrings = [];
1239
- Object.entries(indexConfig).forEach(([key, value]) => {
1240
- if (value !== void 0 && value !== null) {
1241
- if (typeof value === "string") {
1242
- optionStrings.push(`'${key}': '${value}'`);
1243
- } else {
1244
- optionStrings.push(`'${key}': ${value}`);
1245
- }
1246
- }
1247
- });
1248
- if (optionStrings.length > 0) {
1249
- options = `{ ${optionStrings.join(", ")} }`;
1250
- }
1251
- }
1252
- newIndexMap.set(indexName, { fieldName, config: indexConfig, options });
1253
- }
1232
+ Object.entries(schema.indexes).forEach(([fieldName]) => {
1233
+ const options = typeof schema.indexes?.[fieldName] === "boolean" ? {
1234
+ case_sensitive: false,
1235
+ normalize: true,
1236
+ ascii: true
1237
+ } : schema.indexes?.[fieldName];
1238
+ newIndexMap.set(fieldName, options);
1254
1239
  });
1255
1240
  }
1256
- existingIndexMap.forEach((indexInfo, indexName) => {
1241
+ existingIndexMap.forEach((_, indexName) => {
1257
1242
  if (!newIndexMap.has(indexName)) {
1258
1243
  operations.push(`DROP INDEX IF EXISTS ${schema.keyspace}.${indexName};`);
1259
1244
  }
1260
1245
  });
1261
- newIndexMap.forEach((indexInfo, indexName) => {
1262
- const existingIndex = existingIndexMap.get(indexName);
1263
- if (!existingIndex) {
1264
- let createQuery = `CREATE INDEX ${indexName} ON ${schema.keyspace}.${schema.tableName} (${indexInfo.fieldName}) USING 'sai'`;
1265
- if (indexInfo.options) {
1266
- createQuery += ` WITH OPTIONS = ${indexInfo.options}`;
1267
- }
1268
- createQuery += ";";
1246
+ newIndexMap.forEach((_, fieldName) => {
1247
+ const options = newIndexMap.get(fieldName);
1248
+ const existingOptions = existingIndexMap.get(fieldName);
1249
+ if (!existingOptions) {
1250
+ let createQuery = createIndex(
1251
+ schema.keyspace,
1252
+ schema.tableName,
1253
+ fieldName,
1254
+ options
1255
+ );
1269
1256
  operations.push(createQuery);
1270
1257
  } else {
1271
- const existingOptions = existingIndex.options;
1272
- const newOptions = indexInfo.options;
1273
- const normalizeOptions = (opts) => {
1274
- if (!opts) return "";
1275
- let normalized = opts.replace(/^"(.*)"$/, "$1").trim();
1276
- normalized = normalized.replace(/\s+/g, " ");
1277
- return normalized;
1278
- };
1279
- const normalizedExisting = normalizeOptions(existingOptions);
1280
- const normalizedNew = normalizeOptions(newOptions);
1281
- if (normalizedExisting !== normalizedNew) {
1258
+ if (JSON.stringify(existingOptions) !== JSON.stringify(options)) {
1282
1259
  operations.push(
1283
- `DROP INDEX IF EXISTS ${schema.keyspace}.${indexName};`
1260
+ dropIndex(
1261
+ schema.keyspace,
1262
+ `${schema.tableName}_${fieldName.toLowerCase()}_sai_idx`
1263
+ )
1264
+ );
1265
+ let createQuery = createIndex(
1266
+ schema.keyspace,
1267
+ schema.tableName,
1268
+ fieldName,
1269
+ options
1284
1270
  );
1285
- let createQuery = `CREATE INDEX ${indexName} ON ${schema.keyspace}.${schema.tableName} (${indexInfo.fieldName}) USING 'sai'`;
1286
- if (indexInfo.options) {
1287
- createQuery += ` WITH OPTIONS = ${indexInfo.options}`;
1288
- }
1289
- createQuery += ";";
1290
1271
  operations.push(createQuery);
1291
1272
  }
1292
1273
  }
@@ -1364,6 +1345,691 @@ var createTableSchema = (config) => {
1364
1345
  return new TableSchema(config);
1365
1346
  };
1366
1347
 
1348
+ // src/model-synchronizer.ts
1349
+ var ModelSynchronizer = class {
1350
+ constructor(model) {
1351
+ this.model = model;
1352
+ }
1353
+ /**
1354
+ * 检测表结构变更
1355
+ */
1356
+ async detectChanges() {
1357
+ const existingTableInfo = await this.getExistingTableInfo();
1358
+ const schemaTableInfo = await this.convertSchemaToTableInfo();
1359
+ const addedFields = [];
1360
+ const modifiedFields = [];
1361
+ const deletedFields = [];
1362
+ const addedIndexes = [];
1363
+ const deletedIndexes = [];
1364
+ const rebuiltIndexes = [];
1365
+ for (const schemaColumn of schemaTableInfo.columns) {
1366
+ const existingColumn = existingTableInfo.columns.find(
1367
+ (c) => c.column_name.toLowerCase() === schemaColumn.column_name.toLowerCase()
1368
+ );
1369
+ if (!existingColumn) {
1370
+ addedFields.push({
1371
+ name: schemaColumn.column_name,
1372
+ type: schemaColumn.type,
1373
+ kind: schemaColumn.kind
1374
+ });
1375
+ } else {
1376
+ if (schemaColumn.type.toLowerCase() !== existingColumn.type.toLowerCase()) {
1377
+ modifiedFields.push({
1378
+ name: schemaColumn.column_name,
1379
+ oldType: existingColumn.type,
1380
+ newType: schemaColumn.type
1381
+ });
1382
+ }
1383
+ if (schemaColumn.kind !== existingColumn.kind) {
1384
+ if (schemaColumn.column_name.toLowerCase() === existingColumn.column_name.toLowerCase()) {
1385
+ const kindChangedField = {
1386
+ name: existingColumn.column_name,
1387
+ type: existingColumn.type,
1388
+ kind: existingColumn.kind
1389
+ };
1390
+ if (!deletedFields.some(
1391
+ (f) => f.name.toLowerCase() === kindChangedField.name.toLowerCase()
1392
+ )) {
1393
+ deletedFields.push(kindChangedField);
1394
+ }
1395
+ const kindChangedFieldNew = {
1396
+ name: schemaColumn.column_name,
1397
+ type: schemaColumn.type,
1398
+ kind: schemaColumn.kind
1399
+ };
1400
+ if (!addedFields.some(
1401
+ (f) => f.name.toLowerCase() === kindChangedFieldNew.name.toLowerCase()
1402
+ )) {
1403
+ addedFields.push(kindChangedFieldNew);
1404
+ }
1405
+ }
1406
+ }
1407
+ }
1408
+ }
1409
+ for (const existingColumn of existingTableInfo.columns) {
1410
+ const schemaColumn = schemaTableInfo.columns.find(
1411
+ (c) => c.column_name.toLowerCase() === existingColumn.column_name.toLowerCase()
1412
+ );
1413
+ if (!schemaColumn) {
1414
+ if (!deletedFields.some(
1415
+ (f) => f.name.toLowerCase() === existingColumn.column_name.toLowerCase()
1416
+ )) {
1417
+ deletedFields.push({
1418
+ name: existingColumn.column_name,
1419
+ type: existingColumn.type,
1420
+ kind: existingColumn.kind
1421
+ });
1422
+ }
1423
+ }
1424
+ }
1425
+ for (const schemaIndex of schemaTableInfo.indexes) {
1426
+ const existingIndex = existingTableInfo.indexes.find(
1427
+ (i) => i.index_name.toLowerCase() === schemaIndex.index_name.toLowerCase()
1428
+ );
1429
+ if (!existingIndex) {
1430
+ addedIndexes.push({
1431
+ name: schemaIndex.index_name,
1432
+ options: schemaIndex.options
1433
+ });
1434
+ } else {
1435
+ if (!this.areIndexOptionsEqual(existingIndex.options, schemaIndex.options)) {
1436
+ rebuiltIndexes.push({
1437
+ name: schemaIndex.index_name,
1438
+ oldOptions: existingIndex.options,
1439
+ newOptions: schemaIndex.options
1440
+ });
1441
+ }
1442
+ }
1443
+ }
1444
+ for (const existingIndex of existingTableInfo.indexes) {
1445
+ const schemaIndex = schemaTableInfo.indexes.find(
1446
+ (i) => i.index_name.toLowerCase() === existingIndex.index_name.toLowerCase()
1447
+ );
1448
+ if (!schemaIndex) {
1449
+ deletedIndexes.push({
1450
+ name: existingIndex.index_name,
1451
+ options: existingIndex.options
1452
+ });
1453
+ }
1454
+ }
1455
+ const hasChanges = addedFields.length > 0 || modifiedFields.length > 0 || deletedFields.length > 0 || addedIndexes.length > 0 || deletedIndexes.length > 0 || rebuiltIndexes.length > 0;
1456
+ return {
1457
+ addedFields,
1458
+ modifiedFields,
1459
+ deletedFields,
1460
+ addedIndexes,
1461
+ deletedIndexes,
1462
+ rebuiltIndexes,
1463
+ hasChanges
1464
+ };
1465
+ }
1466
+ /**
1467
+ * 打印变更信息(简洁列表格式)
1468
+ */
1469
+ printChangesTable(changes) {
1470
+ if (!changes.hasChanges) {
1471
+ return;
1472
+ }
1473
+ console.log(`
1474
+ \u{1F4CA} \u8868\u7ED3\u6784\u53D8\u5316
1475
+ `);
1476
+ const stats = [];
1477
+ const addedRegularFields = changes.addedFields.filter(
1478
+ (f) => f.kind === "regular"
1479
+ ).length;
1480
+ const addedPartitionKeys = changes.addedFields.filter(
1481
+ (f) => f.kind === "partition_key"
1482
+ ).length;
1483
+ const addedClusteringKeys = changes.addedFields.filter(
1484
+ (f) => f.kind === "clustering_key"
1485
+ ).length;
1486
+ const deletedRegularFields = changes.deletedFields.filter(
1487
+ (f) => f.kind === "regular"
1488
+ ).length;
1489
+ const deletedPartitionKeys = changes.deletedFields.filter(
1490
+ (f) => f.kind === "partition_key"
1491
+ ).length;
1492
+ const deletedClusteringKeys = changes.deletedFields.filter(
1493
+ (f) => f.kind === "clustering_key"
1494
+ ).length;
1495
+ if (addedRegularFields > 0) stats.push(`\u65B0\u589E${addedRegularFields}\u5B57\u6BB5`);
1496
+ if (addedPartitionKeys > 0) stats.push(`\u65B0\u589E\u5206\u533A\u952E${addedPartitionKeys}`);
1497
+ if (addedClusteringKeys > 0) stats.push(`\u65B0\u589E\u805A\u7C7B\u952E${addedClusteringKeys}`);
1498
+ if (changes.modifiedFields.length > 0)
1499
+ stats.push(`\u4FEE\u6539${changes.modifiedFields.length}\u5B57\u6BB5`);
1500
+ if (deletedRegularFields > 0) stats.push(`\u5220\u9664${deletedRegularFields}\u5B57\u6BB5`);
1501
+ if (deletedPartitionKeys > 0)
1502
+ stats.push(`\u5220\u9664\u5206\u533A\u952E${deletedPartitionKeys}`);
1503
+ if (deletedClusteringKeys > 0)
1504
+ stats.push(`\u5220\u9664\u805A\u7C7B\u952E${deletedClusteringKeys}`);
1505
+ if (changes.addedIndexes.length > 0)
1506
+ stats.push(`\u65B0\u589E\u7D22\u5F15${changes.addedIndexes.length}`);
1507
+ if (changes.rebuiltIndexes.length > 0)
1508
+ stats.push(`\u91CD\u5EFA\u7D22\u5F15${changes.rebuiltIndexes.length}`);
1509
+ if (changes.deletedIndexes.length > 0)
1510
+ stats.push(`\u5220\u9664\u7D22\u5F15${changes.deletedIndexes.length}`);
1511
+ if (stats.length > 0) {
1512
+ console.log(` \u7EDF\u8BA1\uFF08${stats.join("\uFF0C")}\uFF09`);
1513
+ }
1514
+ changes.addedFields.forEach((field) => {
1515
+ console.log(` \u{1F7E2} \u65B0\u589E\u5B57\u6BB5 ${field.name} = ${field.type}`);
1516
+ });
1517
+ changes.modifiedFields.forEach((field) => {
1518
+ console.log(
1519
+ ` \u{1F7E1} \u4FEE\u6539\u5B57\u6BB5 ${field.name} = ${field.oldType} \u2192 ${field.newType}`
1520
+ );
1521
+ });
1522
+ changes.deletedFields.forEach((field) => {
1523
+ console.log(` \u{1F534} \u5220\u9664\u5B57\u6BB5 ${field.name} = ${field.type}`);
1524
+ });
1525
+ changes.addedIndexes.forEach((index) => {
1526
+ const optionsStr = this.formatIndexOptions(index.options);
1527
+ console.log(
1528
+ ` \u{1F7E2} \u65B0\u589E\u7D22\u5F15 ${index.name}${optionsStr ? ` (${optionsStr})` : ""}`
1529
+ );
1530
+ });
1531
+ changes.rebuiltIndexes.forEach((index) => {
1532
+ console.log(` \u{1F7E1} \u91CD\u5EFA\u7D22\u5F15 ${index.name}`);
1533
+ });
1534
+ changes.deletedIndexes.forEach((index) => {
1535
+ console.log(` \u{1F534} \u5220\u9664\u7D22\u5F15 ${index.name}`);
1536
+ });
1537
+ }
1538
+ /**
1539
+ * 打印表结构信息(简洁格式)
1540
+ */
1541
+ printTableStructure(schema) {
1542
+ console.log(`
1543
+ \u{1F4CA} \u8868\u7ED3\u6784:
1544
+ `);
1545
+ Object.entries(schema.fields.shape).forEach(([fieldName, fieldSchema]) => {
1546
+ const type = convertZodToCassandraType(fieldSchema);
1547
+ const kind = schema.partitionKeyFields.includes(fieldName) ? "partition_key" : schema.clusteringKeyFields.some((ck) => ck.field === fieldName) ? "clustering_key" : "regular";
1548
+ const kindLabel = kind === "partition_key" ? " [\u5206\u533A\u952E]" : kind === "clustering_key" ? " [\u805A\u7C7B\u952E]" : "";
1549
+ console.log(` \u2022 ${fieldName} = ${type}${kindLabel}`);
1550
+ });
1551
+ if (schema.indexes && Object.keys(schema.indexes).length > 0) {
1552
+ console.log(`
1553
+ \u{1F4C7} \u7D22\u5F15:`);
1554
+ Object.entries(schema.indexes).forEach(([fieldName, options]) => {
1555
+ const indexName = `${schema.tableName}_${fieldName}_sai_idx`.toLowerCase();
1556
+ const optionsStr = this.formatIndexOptions(
1557
+ typeof options === "boolean" ? { case_sensitive: false, normalize: true, ascii: true } : options
1558
+ );
1559
+ console.log(
1560
+ ` \u2022 ${indexName} (${fieldName})${optionsStr ? ` - ${optionsStr}` : ""}`
1561
+ );
1562
+ });
1563
+ }
1564
+ }
1565
+ /**
1566
+ * 同步表结构(根据检测结果执行相应的变更)
1567
+ */
1568
+ async sync(forceRecreate = false) {
1569
+ const { schema, client } = this.model;
1570
+ const tableName = `${schema.keyspace}.${schema.tableName}`;
1571
+ console.log(`
1572
+ \u{1F504} \u5F00\u59CB\u540C\u6B65\u8868\u7ED3\u6784: ${tableName}`);
1573
+ console.log("=".repeat(56));
1574
+ await client.execute(
1575
+ queryHelper.ensureKeyspace(schema.keyspace, schema.keyspaceConfig)
1576
+ );
1577
+ console.log(`\u2705 Keyspace ${schema.keyspace} \u5DF2\u786E\u4FDD\u5B58\u5728`);
1578
+ const changes = await this.detectChanges();
1579
+ const tableExists = await this.checkTableExists();
1580
+ if (tableExists) {
1581
+ this.printChangesTable(changes);
1582
+ }
1583
+ if (!tableExists) {
1584
+ console.log(`
1585
+ \u{1F4CB} \u8868\u4E0D\u5B58\u5728\uFF0C\u5C06\u521B\u5EFA\u65B0\u8868`);
1586
+ this.printTableStructure(schema);
1587
+ try {
1588
+ console.log(`
1589
+ \u{1F680} \u521B\u5EFA\u8868 ${tableName}`);
1590
+ await client.execute(queryHelper.createTable(schema));
1591
+ console.log(`\u2705 \u521B\u5EFA\u8868 ${tableName} \u6210\u529F`);
1592
+ } catch (error) {
1593
+ const errorMessage = error instanceof Error ? error.message : String(error);
1594
+ console.error(`\u274C \u521B\u5EFA\u8868 ${tableName} \u5931\u8D25: ${errorMessage}`);
1595
+ throw error;
1596
+ }
1597
+ if (schema.indexes && Object.keys(schema.indexes).length > 0) {
1598
+ const indexQueries = queryHelper.createIndexes(schema);
1599
+ const indexFields = Object.keys(schema.indexes);
1600
+ for (let i = 0; i < indexQueries.length; i++) {
1601
+ const indexQuery = indexQueries[i];
1602
+ const fieldName = indexFields[i];
1603
+ const indexName = `${schema.tableName}_${fieldName}_sai_idx`.toLowerCase();
1604
+ try {
1605
+ console.log(`\u{1F680} \u6DFB\u52A0\u7D22\u5F15 ${indexName}`);
1606
+ await client.execute(indexQuery);
1607
+ console.log(`\u2705 \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u6210\u529F`);
1608
+ } catch (error) {
1609
+ const errorMessage = error instanceof Error ? error.message : String(error);
1610
+ console.error(`\u274C \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u5931\u8D25: ${errorMessage}`);
1611
+ throw error;
1612
+ }
1613
+ }
1614
+ }
1615
+ console.log(`
1616
+ \u2705 \u8868\u7ED3\u6784\u540C\u6B65\u5B8C\u6210: ${tableName}
1617
+ `);
1618
+ return;
1619
+ }
1620
+ if (!changes.hasChanges) {
1621
+ console.log(`
1622
+ \u2705 \u8868\u7ED3\u6784\u5DF2\u662F\u6700\u65B0\u72B6\u6001\uFF0C\u65E0\u9700\u53D8\u66F4
1623
+ `);
1624
+ return;
1625
+ }
1626
+ const needsRecreate = this.shouldRecreateTable(changes, forceRecreate);
1627
+ if (needsRecreate && !forceRecreate) {
1628
+ const reasons = [];
1629
+ const primaryKeyChanges = [
1630
+ ...changes.deletedFields.filter(
1631
+ (f) => f.kind === "partition_key" || f.kind === "clustering_key"
1632
+ ),
1633
+ ...changes.addedFields.filter(
1634
+ (f) => f.kind === "partition_key" || f.kind === "clustering_key"
1635
+ )
1636
+ ];
1637
+ if (primaryKeyChanges.length > 0) {
1638
+ const partitionKeyChanges = primaryKeyChanges.filter(
1639
+ (f) => f.kind === "partition_key"
1640
+ );
1641
+ const clusteringKeyChanges = primaryKeyChanges.filter(
1642
+ (f) => f.kind === "clustering_key"
1643
+ );
1644
+ if (partitionKeyChanges.length > 0) {
1645
+ reasons.push(`\u4E3B\u952E\u53D8\u66F4: \u5206\u533A\u952E\u6570\u91CF\u4E0D\u4E00\u81F4`);
1646
+ } else if (clusteringKeyChanges.length > 0) {
1647
+ reasons.push(`\u4E3B\u952E\u53D8\u66F4: \u805A\u7C7B\u952E\u914D\u7F6E\u4E0D\u4E00\u81F4`);
1648
+ } else {
1649
+ const deletedKeys = primaryKeyChanges.map(
1650
+ (f) => `${f.name} (${f.kind === "partition_key" ? "\u5206\u533A\u952E" : "\u805A\u7C7B\u952E"})`
1651
+ );
1652
+ reasons.push(`\u4E3B\u952E\u53D8\u66F4: ${deletedKeys.join(", ")}`);
1653
+ }
1654
+ }
1655
+ const incompatibleTypeChanges = changes.modifiedFields.filter(
1656
+ (f) => !this.isTypeCompatible(f.oldType, f.newType)
1657
+ );
1658
+ if (incompatibleTypeChanges.length > 0) {
1659
+ const typeChangeDetails = incompatibleTypeChanges.map(
1660
+ (f) => `${f.name} (${f.oldType} \u2192 ${f.newType})`
1661
+ );
1662
+ reasons.push(`\u5B57\u6BB5\u7C7B\u578B\u53D8\u66F4: ${typeChangeDetails.map((d) => `\u5B57\u6BB5\u7C7B\u578B\u4E0D\u517C\u5BB9: ${d.split("(")[0]}`).join(", ")}`);
1663
+ }
1664
+ const errorMessage = `\u8868 ${tableName} \u7ED3\u6784\u5B58\u5728\u4E0D\u53EF\u53D8\u66F4\u7684\u4FEE\u6539\uFF0C\u9700\u8981\u4F7F\u7528 forceRecreate=true \u6765\u91CD\u5EFA\u8868\u3002
1665
+ \u4E0D\u53EF\u53D8\u66F4\u7684\u4FEE\u6539\u5305\u62EC\uFF1A
1666
+ ${reasons.map((r) => ` - ${r}`).join("\n")}
1667
+ \u5176\u4ED6\u53D8\u66F4\uFF1A
1668
+ - \u65B0\u589E\u5B57\u6BB5: ${changes.addedFields.filter((f) => f.kind === "regular").map((f) => f.name).join(", ") || "\u65E0"}
1669
+ - \u5220\u9664\u5B57\u6BB5: ${changes.deletedFields.filter((f) => f.kind === "regular").map((f) => f.name).join(", ") || "\u65E0"}
1670
+ - \u65B0\u589E\u7D22\u5F15: ${changes.addedIndexes.map((i) => i.name).join(", ") || "\u65E0"}
1671
+ - \u5220\u9664\u7D22\u5F15: ${changes.deletedIndexes.map((i) => i.name).join(", ") || "\u65E0"}
1672
+ - \u91CD\u5EFA\u7D22\u5F15: ${changes.rebuiltIndexes.map((i) => i.name).join(", ") || "\u65E0"}`;
1673
+ console.error(`
1674
+ \u274C ${errorMessage}
1675
+ `);
1676
+ throw new Error(errorMessage);
1677
+ }
1678
+ if (needsRecreate) {
1679
+ console.log(`
1680
+ \u{1F4CB} \u8868\u5DF2\u7ECF\u5B58\u5728\uFF0C\u9700\u8981\u91CD\u5EFA\u8868`);
1681
+ try {
1682
+ console.log(`
1683
+ \u{1F680} \u5220\u9664\u8868 ${tableName}`);
1684
+ await client.execute(
1685
+ queryHelper.dropTable(schema.keyspace, schema.tableName)
1686
+ );
1687
+ console.log(`\u2705 \u5220\u9664\u8868 ${tableName} \u6210\u529F`);
1688
+ } catch (error) {
1689
+ const errorMessage = error instanceof Error ? error.message : String(error);
1690
+ console.error(`\u274C \u5220\u9664\u8868 ${tableName} \u5931\u8D25: ${errorMessage}`);
1691
+ throw error;
1692
+ }
1693
+ try {
1694
+ console.log(`\u{1F680} \u521B\u5EFA\u8868 ${tableName}`);
1695
+ await client.execute(queryHelper.createTable(schema));
1696
+ console.log(`\u2705 \u521B\u5EFA\u8868 ${tableName} \u6210\u529F`);
1697
+ } catch (error) {
1698
+ const errorMessage = error instanceof Error ? error.message : String(error);
1699
+ console.error(`\u274C \u521B\u5EFA\u8868 ${tableName} \u5931\u8D25: ${errorMessage}`);
1700
+ throw error;
1701
+ }
1702
+ if (schema.indexes && Object.keys(schema.indexes).length > 0) {
1703
+ const indexQueries = queryHelper.createIndexes(schema);
1704
+ const indexFields = Object.keys(schema.indexes);
1705
+ for (let i = 0; i < indexQueries.length; i++) {
1706
+ const indexQuery = indexQueries[i];
1707
+ const fieldName = indexFields[i];
1708
+ const indexName = `${schema.tableName}_${fieldName}_sai_idx`.toLowerCase();
1709
+ try {
1710
+ console.log(`\u{1F680} \u6DFB\u52A0\u7D22\u5F15 ${indexName}`);
1711
+ await client.execute(indexQuery);
1712
+ console.log(`\u2705 \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u6210\u529F`);
1713
+ } catch (error) {
1714
+ const errorMessage = error instanceof Error ? error.message : String(error);
1715
+ console.error(`\u274C \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u5931\u8D25: ${errorMessage}`);
1716
+ throw error;
1717
+ }
1718
+ }
1719
+ }
1720
+ console.log(`\u2705 \u4FEE\u6539\u8868 ${tableName} \u6210\u529F`);
1721
+ console.log(`
1722
+ \u2705 \u8868\u7ED3\u6784\u540C\u6B65\u5B8C\u6210: ${tableName}
1723
+ `);
1724
+ return;
1725
+ }
1726
+ console.log(`
1727
+ \u{1F4CB} \u8868\u5DF2\u7ECF\u5B58\u5728\uFF0C\u5F00\u59CB\u66F4\u65B0`);
1728
+ if (changes.addedFields.length > 0) {
1729
+ const regularFields = changes.addedFields.filter(
1730
+ (f) => f.kind === "regular"
1731
+ );
1732
+ if (regularFields.length > 0) {
1733
+ try {
1734
+ regularFields.forEach((f) => {
1735
+ console.log(`\u{1F680} \u6DFB\u52A0\u5B57\u6BB5 ${f.name}`);
1736
+ });
1737
+ await client.execute(
1738
+ queryHelper.addColumns(
1739
+ schema,
1740
+ regularFields.map((f) => f.name)
1741
+ )
1742
+ );
1743
+ regularFields.forEach((f) => {
1744
+ console.log(`\u2705 \u6DFB\u52A0\u5B57\u6BB5 ${f.name} \u6210\u529F`);
1745
+ });
1746
+ } catch (error) {
1747
+ const errorMessage = error instanceof Error ? error.message : String(error);
1748
+ console.error(`\u274C \u6DFB\u52A0\u5B57\u6BB5\u5931\u8D25: ${errorMessage}`);
1749
+ throw error;
1750
+ }
1751
+ }
1752
+ }
1753
+ if (changes.deletedFields.length > 0) {
1754
+ const regularFields = changes.deletedFields.filter(
1755
+ (f) => f.kind === "regular"
1756
+ );
1757
+ if (regularFields.length > 0) {
1758
+ try {
1759
+ regularFields.forEach((f) => {
1760
+ console.log(`\u{1F680} \u5220\u9664\u5B57\u6BB5 ${f.name}`);
1761
+ });
1762
+ await client.execute(
1763
+ queryHelper.dropColumns(
1764
+ schema,
1765
+ regularFields.map((f) => f.name)
1766
+ )
1767
+ );
1768
+ regularFields.forEach((f) => {
1769
+ console.log(`\u2705 \u5220\u9664\u5B57\u6BB5 ${f.name} \u6210\u529F`);
1770
+ });
1771
+ } catch (error) {
1772
+ const errorMessage = error instanceof Error ? error.message : String(error);
1773
+ console.error(`\u274C \u5220\u9664\u5B57\u6BB5\u5931\u8D25: ${errorMessage}`);
1774
+ throw error;
1775
+ }
1776
+ }
1777
+ }
1778
+ if (changes.modifiedFields.length > 0) {
1779
+ const compatibleChanges = changes.modifiedFields.filter(
1780
+ (f) => this.isTypeCompatible(f.oldType, f.newType)
1781
+ );
1782
+ const incompatibleChanges = changes.modifiedFields.filter(
1783
+ (f) => !this.isTypeCompatible(f.oldType, f.newType)
1784
+ );
1785
+ if (incompatibleChanges.length > 0) {
1786
+ incompatibleChanges.forEach((f) => {
1787
+ console.log(`\u{1F680} \u4FEE\u6539\u5B57\u6BB5 ${f.name} (${f.oldType} \u2192 ${f.newType})`);
1788
+ });
1789
+ }
1790
+ if (compatibleChanges.length > 0) {
1791
+ compatibleChanges.forEach((f) => {
1792
+ console.log(`\u{1F680} \u4FEE\u6539\u5B57\u6BB5 ${f.name} (${f.oldType} \u2192 ${f.newType})`);
1793
+ });
1794
+ }
1795
+ }
1796
+ if (changes.deletedIndexes.length > 0) {
1797
+ for (const deletedIndex of changes.deletedIndexes) {
1798
+ try {
1799
+ console.log(`\u{1F680} \u5220\u9664\u7D22\u5F15 ${deletedIndex.name}`);
1800
+ await client.execute(
1801
+ queryHelper.dropIndex(schema.keyspace, deletedIndex.name)
1802
+ );
1803
+ console.log(`\u2705 \u5220\u9664\u7D22\u5F15 ${deletedIndex.name} \u6210\u529F`);
1804
+ } catch (error) {
1805
+ const errorMessage = error instanceof Error ? error.message : String(error);
1806
+ console.error(
1807
+ `\u274C \u5220\u9664\u7D22\u5F15 ${deletedIndex.name} \u5931\u8D25: ${errorMessage}`
1808
+ );
1809
+ throw error;
1810
+ }
1811
+ }
1812
+ }
1813
+ if (changes.rebuiltIndexes.length > 0) {
1814
+ for (const rebuiltIndex of changes.rebuiltIndexes) {
1815
+ try {
1816
+ console.log(`\u{1F680} \u91CD\u5EFA\u7D22\u5F15 ${rebuiltIndex.name}`);
1817
+ await client.execute(
1818
+ queryHelper.dropIndex(schema.keyspace, rebuiltIndex.name)
1819
+ );
1820
+ console.log(`\u2705 \u5220\u9664\u65E7\u7D22\u5F15 ${rebuiltIndex.name} \u6210\u529F`);
1821
+ } catch (error) {
1822
+ const errorMessage = error instanceof Error ? error.message : String(error);
1823
+ console.error(
1824
+ `\u274C \u5220\u9664\u65E7\u7D22\u5F15 ${rebuiltIndex.name} \u5931\u8D25: ${errorMessage}`
1825
+ );
1826
+ throw error;
1827
+ }
1828
+ }
1829
+ }
1830
+ if (changes.addedIndexes.length > 0 || changes.rebuiltIndexes.length > 0) {
1831
+ if (schema.indexes) {
1832
+ const indexQueries = queryHelper.createIndexes(schema);
1833
+ const indexFields = Object.keys(schema.indexes);
1834
+ for (let i = 0; i < indexQueries.length; i++) {
1835
+ const indexQuery = indexQueries[i];
1836
+ const fieldName = indexFields[i];
1837
+ const indexName = `${schema.tableName}_${fieldName}_sai_idx`.toLowerCase();
1838
+ try {
1839
+ console.log(`\u{1F680} \u6DFB\u52A0\u7D22\u5F15 ${indexName}`);
1840
+ await client.execute(indexQuery);
1841
+ console.log(`\u2705 \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u6210\u529F`);
1842
+ } catch (error) {
1843
+ const errorMessage = error instanceof Error ? error.message : String(error);
1844
+ console.error(`\u274C \u6DFB\u52A0\u7D22\u5F15 ${indexName} \u5931\u8D25: ${errorMessage}`);
1845
+ throw error;
1846
+ }
1847
+ }
1848
+ }
1849
+ }
1850
+ console.log(`\u2705 \u4FEE\u6539\u8868 ${tableName} \u6210\u529F`);
1851
+ console.log(`
1852
+ \u2705 \u8868\u7ED3\u6784\u540C\u6B65\u5B8C\u6210: ${tableName}
1853
+ `);
1854
+ }
1855
+ /**
1856
+ * 判断是否需要重建表
1857
+ */
1858
+ shouldRecreateTable(changes, forceRecreate) {
1859
+ if (forceRecreate && changes.hasChanges) {
1860
+ return true;
1861
+ }
1862
+ const hasPrimaryKeyChanges = changes.deletedFields.some(
1863
+ (f) => f.kind === "partition_key" || f.kind === "clustering_key"
1864
+ ) || changes.addedFields.some(
1865
+ (f) => f.kind === "partition_key" || f.kind === "clustering_key"
1866
+ );
1867
+ if (hasPrimaryKeyChanges) {
1868
+ return true;
1869
+ }
1870
+ const hasIncompatibleTypeChanges = changes.modifiedFields.some(
1871
+ (f) => !this.isTypeCompatible(f.oldType, f.newType)
1872
+ );
1873
+ if (hasIncompatibleTypeChanges) {
1874
+ return true;
1875
+ }
1876
+ return false;
1877
+ }
1878
+ /**
1879
+ * 检查类型是否兼容
1880
+ * 基于 Cassandra 5.0 的类型兼容性规则
1881
+ */
1882
+ isTypeCompatible(existingType, newType) {
1883
+ const normalizeType = (type) => type.toLowerCase().trim();
1884
+ const existing = normalizeType(existingType);
1885
+ const newT = normalizeType(newType);
1886
+ if (existing === newT) {
1887
+ return true;
1888
+ }
1889
+ const compatibleChanges = {
1890
+ // 文本类型之间的兼容性
1891
+ text: ["varchar", "ascii"],
1892
+ varchar: ["text", "ascii"],
1893
+ ascii: ["text", "varchar"],
1894
+ // 整数类型之间的兼容性
1895
+ int: ["bigint", "smallint", "tinyint", "varint"],
1896
+ bigint: ["int", "smallint", "tinyint", "varint"],
1897
+ smallint: ["int", "bigint", "tinyint", "varint"],
1898
+ tinyint: ["int", "bigint", "smallint", "varint"],
1899
+ varint: ["int", "bigint", "smallint", "tinyint"],
1900
+ // 浮点数类型之间的兼容性
1901
+ float: ["double"],
1902
+ double: ["float"],
1903
+ // 时间类型之间的兼容性
1904
+ timestamp: ["timeuuid"],
1905
+ // 某些场景下可以转换
1906
+ // UUID 类型
1907
+ uuid: ["timeuuid"],
1908
+ // 某些场景下可以转换
1909
+ timeuuid: ["uuid"]
1910
+ // 某些场景下可以转换
1911
+ };
1912
+ if (compatibleChanges[existing] && compatibleChanges[existing].includes(newT)) {
1913
+ return true;
1914
+ }
1915
+ if (compatibleChanges[newT] && compatibleChanges[newT].includes(existing)) {
1916
+ return true;
1917
+ }
1918
+ const existingBase = existing.split("<")[0];
1919
+ const newBase = newT.split("<")[0];
1920
+ if (existingBase === newBase && (existingBase === "list" || existingBase === "set" || existingBase === "map")) {
1921
+ return false;
1922
+ }
1923
+ return false;
1924
+ }
1925
+ /**
1926
+ * 检查表是否存在
1927
+ */
1928
+ async checkTableExists() {
1929
+ const { schema, client } = this.model;
1930
+ try {
1931
+ const result = await client.execute(queryHelper.tableMetadata(), [
1932
+ schema.keyspace,
1933
+ schema.tableName
1934
+ ]);
1935
+ return result.rows.length > 0;
1936
+ } catch (error) {
1937
+ return false;
1938
+ }
1939
+ }
1940
+ /**
1941
+ * 获取现有表信息
1942
+ */
1943
+ async getExistingTableInfo() {
1944
+ const { schema, client } = this.model;
1945
+ const tableColumnsRows = await client.execute(queryHelper.tableMetadata(), [
1946
+ schema.keyspace,
1947
+ schema.tableName
1948
+ ]);
1949
+ const columns = tableColumnsRows.rows.map((col) => ({
1950
+ ...col,
1951
+ // 标准化 kind 值:Cassandra 可能返回 "clustering" 而不是 "clustering_key"
1952
+ kind: col.kind === "clustering" ? "clustering_key" : col.kind
1953
+ }));
1954
+ const indexesRows = await client.execute(queryHelper.getTableIndexes(), [
1955
+ schema.keyspace,
1956
+ schema.tableName
1957
+ ]);
1958
+ const indexes = indexesRows.rows;
1959
+ return { columns, indexes };
1960
+ }
1961
+ /**
1962
+ * 将 Schema 转换为表信息
1963
+ */
1964
+ async convertSchemaToTableInfo() {
1965
+ const { schema } = this.model;
1966
+ const columns = Object.keys(schema.fields.shape).map((field) => {
1967
+ const kind = schema.partitionKeyFields.includes(field) ? "partition_key" : schema.clusteringKeyFields.some((ck) => ck.field === field) ? "clustering_key" : "regular";
1968
+ return {
1969
+ column_name: field,
1970
+ type: convertZodToCassandraType(schema.fields.shape[field]),
1971
+ kind
1972
+ };
1973
+ });
1974
+ const indexes = Object.keys(schema.indexes ?? {}).map((index) => {
1975
+ const options = schema.indexes?.[index];
1976
+ const indexName = `${schema.tableName}_${index}_sai_idx`.toLowerCase();
1977
+ const indexOptions = typeof options === "boolean" ? { case_sensitive: false, normalize: true, ascii: true } : options;
1978
+ return {
1979
+ index_name: indexName,
1980
+ options: indexOptions
1981
+ };
1982
+ });
1983
+ return { columns, indexes };
1984
+ }
1985
+ /**
1986
+ * 比较索引选项是否相等
1987
+ */
1988
+ areIndexOptionsEqual(options1, options2) {
1989
+ const normalizeOptions = (opts) => {
1990
+ if (!opts || typeof opts !== "object") {
1991
+ return { case_sensitive: false, normalize: true, ascii: true };
1992
+ }
1993
+ const toBool = (val) => {
1994
+ if (typeof val === "boolean") return val;
1995
+ if (typeof val === "string") return val.toLowerCase() === "true";
1996
+ return Boolean(val);
1997
+ };
1998
+ return {
1999
+ case_sensitive: toBool(opts.case_sensitive ?? false),
2000
+ normalize: toBool(opts.normalize ?? true),
2001
+ ascii: toBool(opts.ascii ?? true),
2002
+ similarity_function: opts.similarity_function ?? void 0
2003
+ };
2004
+ };
2005
+ const norm1 = normalizeOptions(options1);
2006
+ const norm2 = normalizeOptions(options2);
2007
+ return norm1.case_sensitive === norm2.case_sensitive && norm1.normalize === norm2.normalize && norm1.ascii === norm2.ascii && norm1.similarity_function === norm2.similarity_function;
2008
+ }
2009
+ /**
2010
+ * 格式化索引选项为字符串
2011
+ */
2012
+ formatIndexOptions(options) {
2013
+ if (!options || typeof options !== "object") {
2014
+ return "";
2015
+ }
2016
+ const parts = [];
2017
+ if (options.case_sensitive !== void 0) {
2018
+ parts.push(`case_sensitive: ${options.case_sensitive}`);
2019
+ }
2020
+ if (options.normalize !== void 0) {
2021
+ parts.push(`normalize: ${options.normalize}`);
2022
+ }
2023
+ if (options.ascii !== void 0) {
2024
+ parts.push(`ascii: ${options.ascii}`);
2025
+ }
2026
+ if (options.similarity_function) {
2027
+ parts.push(`similarity: ${options.similarity_function}`);
2028
+ }
2029
+ return parts.length > 0 ? parts.join(", ") : "";
2030
+ }
2031
+ };
2032
+
1367
2033
  // src/model.ts
1368
2034
  var Model = class _Model {
1369
2035
  schema;
@@ -1527,332 +2193,107 @@ var Model = class _Model {
1527
2193
  this.schema.fields
1528
2194
  );
1529
2195
  }
1530
- // 同步 Schema
2196
+ // 同步 Schema(使用新的 ModelSynchronizer)
1531
2197
  async syncSchema(force = false) {
1532
2198
  try {
1533
- console.log(
1534
- `\u{1F504} \u5F00\u59CB\u540C\u6B65\u8868: ${this.schema.keyspace}.${this.schema.tableName}`
1535
- );
1536
- const operations = await this.syncTableSchema(force);
1537
- console.log(
1538
- `\u2705 \u8868 ${this.schema.keyspace}.${this.schema.tableName} \u540C\u6B65\u5B8C\u6210`
1539
- );
1540
- return operations.join("\n");
1541
- } catch (error) {
1542
- console.error(
1543
- `\u274C \u8868 ${this.schema.keyspace}.${this.schema.tableName} \u540C\u6B65\u5931\u8D25:`,
1544
- error
1545
- );
1546
- throw error;
1547
- }
1548
- }
1549
- // 同步表结构
1550
- async syncTableSchema(forceRecreate = false) {
1551
- const operations = [];
1552
- try {
1553
- console.log(
1554
- ` \u{1F4CB} \u8868\u7ED3\u6784: ${this.schema.keyspace}.${this.schema.tableName}`
1555
- );
1556
- console.log(` \u{1F511} \u5206\u533A\u952E: [${this.schema.partitionKey.join(", ")}]`);
1557
- if (this.schema.clusteringKey) {
1558
- console.log(
1559
- ` \u{1F4CA} \u805A\u7C7B\u952E: ${Object.entries(this.schema.clusteringKey).map(([k, v]) => `${k}:${v}`).join(", ")}`
1560
- );
1561
- }
1562
- console.log(
1563
- ` \u{1F4DD} \u5B57\u6BB5\u6570\u91CF: ${Object.keys(this.schema.fields.shape).length}`
1564
- );
1565
- await this.client.execute(
1566
- queryHelper.ensureKeyspace(
1567
- this.schema.keyspace,
1568
- this.schema.keyspaceConfig
1569
- )
1570
- );
1571
- const tableExists = await this.checkTableExists();
2199
+ const synchronizer = new ModelSynchronizer({
2200
+ schema: this.schema,
2201
+ client: this.client
2202
+ });
2203
+ const changes = await synchronizer.detectChanges();
2204
+ const operations = [];
2205
+ const tableExists = await synchronizer.checkTableExists();
1572
2206
  if (!tableExists) {
1573
2207
  operations.push(queryHelper.createTable(this.schema));
1574
- const indexQueries = queryHelper.createIndexes(this.schema);
1575
- operations.push(...indexQueries);
2208
+ if (this.schema.indexes && Object.keys(this.schema.indexes).length > 0) {
2209
+ operations.push(...queryHelper.createIndexes(this.schema));
2210
+ }
2211
+ } else if (!changes.hasChanges) {
2212
+ await synchronizer.sync(force);
2213
+ return "";
1576
2214
  } else {
1577
- const tableChanges = await this.analyzeTableChanges();
1578
- const hasUnalterableChanges = tableChanges.unalterableReasons.length > 0;
1579
- if (forceRecreate && hasUnalterableChanges) {
1580
- console.log("\u68C0\u6D4B\u5230\u4E0D\u53EF\u53D8\u66F4\u7684\u8868\u7ED3\u6784\u4FEE\u6539\uFF0C\u5C06\u91CD\u5EFA\u8868");
1581
- console.log("\u4E0D\u53EF\u53D8\u66F4\u539F\u56E0:", tableChanges.unalterableReasons);
1582
- console.log("\u6240\u6709\u53D8\u66F4:", tableChanges.changes);
2215
+ const needsRecreate = synchronizer.shouldRecreateTable(
2216
+ changes,
2217
+ force
2218
+ );
2219
+ if (needsRecreate) {
1583
2220
  operations.push(
1584
- queryHelper.dropTable(this.schema.keyspace, this.schema.tableName)
2221
+ `DROP TABLE IF EXISTS ${this.schema.keyspace}.${this.schema.tableName};`
1585
2222
  );
1586
2223
  operations.push(queryHelper.createTable(this.schema));
1587
- const indexQueries = queryHelper.createIndexes(this.schema);
1588
- operations.push(...indexQueries);
1589
- } else if (forceRecreate && !hasUnalterableChanges) {
1590
- console.log("\u68C0\u6D4B\u5230\u53EF\u53D8\u66F4\u7684\u8868\u7ED3\u6784\u4FEE\u6539\uFF0C\u5C06\u4F7F\u7528 ALTER TABLE");
1591
- console.log("\u53D8\u66F4\u8BE6\u60C5:", tableChanges.changes);
1592
- operations.push(...tableChanges.operations);
1593
- const indexOperations = await this.syncIndexes();
1594
- operations.push(...indexOperations);
1595
- } else if (!forceRecreate && hasUnalterableChanges) {
1596
- const reasons = tableChanges.unalterableReasons.join("; ");
1597
- throw new Error(
1598
- `\u8868 ${this.schema.keyspace}.${this.schema.tableName} \u7ED3\u6784\u5B58\u5728\u4E0D\u53EF\u53D8\u66F4\u7684\u4FEE\u6539\uFF0C\u9700\u8981\u4F7F\u7528 forceRecreate=true \u6765\u91CD\u5EFA\u8868\u3002
1599
- \u4E0D\u53EF\u53D8\u66F4\u7684\u4FEE\u6539\u5305\u62EC\uFF1A
1600
- ${reasons}
1601
- \u5176\u4ED6\u53D8\u66F4\uFF1A${tableChanges.changes.join("; ")}`
1602
- );
2224
+ if (this.schema.indexes && Object.keys(this.schema.indexes).length > 0) {
2225
+ operations.push(...queryHelper.createIndexes(this.schema));
2226
+ }
1603
2227
  } else {
1604
- console.log("\u68C0\u6D4B\u5230\u53EF\u53D8\u66F4\u7684\u8868\u7ED3\u6784\u4FEE\u6539\uFF0C\u5C06\u4F7F\u7528 ALTER TABLE");
1605
- console.log("\u53D8\u66F4\u8BE6\u60C5:", tableChanges.changes);
1606
- operations.push(...tableChanges.operations);
1607
- const indexOperations = await this.syncIndexes();
1608
- operations.push(...indexOperations);
1609
- }
1610
- }
1611
- for (const operation of operations) {
1612
- await this.client.execute(operation);
1613
- }
1614
- return operations;
1615
- } catch (error) {
1616
- console.error("\u540C\u6B65\u8868\u7ED3\u6784\u65F6\u53D1\u751F\u9519\u8BEF:", error);
1617
- throw error;
1618
- }
1619
- }
1620
- // 检查表是否存在
1621
- async checkTableExists() {
1622
- try {
1623
- const result = await this.client.execute(queryHelper.tableMetadata(), [
1624
- this.schema.keyspace,
1625
- this.schema.tableName
1626
- ]);
1627
- return result.rows.length > 0;
1628
- } catch (error) {
1629
- return false;
1630
- }
1631
- }
1632
- // 分析表结构变更
1633
- async analyzeTableChanges() {
1634
- const operations = [];
1635
- const changes = [];
1636
- const unalterableReasons = [];
1637
- try {
1638
- const tableMetadata2 = await this.client.execute(queryHelper.tableMetadata(), [
1639
- this.schema.keyspace,
1640
- this.schema.tableName
1641
- ]).then((res) => res.rows);
1642
- const schemaFields = Object.keys(this.schema.fields.shape);
1643
- const newFields = this.getNewFields(schemaFields, tableMetadata2);
1644
- const deletedFields = this.getDeletedFields(schemaFields, tableMetadata2);
1645
- if (newFields.length > 0) {
1646
- changes.push(`\u65B0\u589E\u5B57\u6BB5: ${newFields.join(", ")}`);
1647
- operations.push(queryHelper.addColumns(this.schema, newFields));
1648
- }
1649
- if (deletedFields.length > 0) {
1650
- changes.push(`\u5220\u9664\u5B57\u6BB5: ${deletedFields.join(", ")}`);
1651
- operations.push(queryHelper.dropColumns(this.schema, deletedFields));
1652
- }
1653
- const primaryKeyChanges = await this.checkPrimaryKeyChanges();
1654
- if (primaryKeyChanges.hasChanges) {
1655
- unalterableReasons.push(
1656
- `\u4E3B\u952E\u53D8\u66F4: ${primaryKeyChanges.reasons.join(", ")}`
1657
- );
1658
- operations.push(
1659
- queryHelper.dropTable(this.schema.keyspace, this.schema.tableName)
1660
- );
1661
- operations.push(queryHelper.createTable(this.schema));
1662
- const indexQueries = queryHelper.createIndexes(this.schema);
1663
- operations.push(...indexQueries);
1664
- }
1665
- const typeChanges = await this.checkFieldTypeChanges(tableMetadata2);
1666
- if (typeChanges.hasChanges) {
1667
- unalterableReasons.push(
1668
- `\u5B57\u6BB5\u7C7B\u578B\u53D8\u66F4: ${typeChanges.reasons.join(", ")}`
1669
- );
1670
- operations.push(
1671
- queryHelper.dropTable(this.schema.keyspace, this.schema.tableName)
1672
- );
1673
- operations.push(queryHelper.createTable(this.schema));
1674
- const indexQueries = queryHelper.createIndexes(this.schema);
1675
- operations.push(...indexQueries);
1676
- }
1677
- const indexOperations = await this.syncIndexes();
1678
- operations.push(...indexOperations);
1679
- return { operations, changes, unalterableReasons };
1680
- } catch (error) {
1681
- throw new Error(
1682
- `\u5206\u6790\u8868 ${this.schema.keyspace}.${this.schema.tableName} \u7ED3\u6784\u53D8\u66F4\u5931\u8D25: ${error}`
1683
- );
1684
- }
1685
- }
1686
- // 智能同步索引
1687
- async syncIndexes() {
1688
- try {
1689
- const existingIndexes = await this.client.execute(queryHelper.getTableIndexes(), [
1690
- this.schema.keyspace,
1691
- this.schema.tableName
1692
- ]).then((res) => res.rows);
1693
- return queryHelper.syncIndexes(this.schema, existingIndexes);
1694
- } catch (error) {
1695
- console.error("\u540C\u6B65\u7D22\u5F15\u65F6\u53D1\u751F\u9519\u8BEF:", error);
1696
- return [];
1697
- }
1698
- }
1699
- // 获取新字段
1700
- getNewFields(schemaFields, tableMetadata2) {
1701
- const existingFields = tableMetadata2.map(
1702
- (t) => t.column_name.toLowerCase()
1703
- );
1704
- return schemaFields.filter(
1705
- (field) => !existingFields.includes(field.toLowerCase())
1706
- );
1707
- }
1708
- // 获取删除的字段
1709
- getDeletedFields(schemaFields, tableMetadata2) {
1710
- const schemaFieldSet = new Set(schemaFields.map((f) => f.toLowerCase()));
1711
- return tableMetadata2.filter((t) => !schemaFieldSet.has(t.column_name.toLowerCase())).map((t) => t.column_name);
1712
- }
1713
- // 检查主键变更
1714
- async checkPrimaryKeyChanges() {
1715
- try {
1716
- const partitionKeyResult = await this.client.execute(
1717
- "SELECT column_name, type FROM system_schema.columns WHERE keyspace_name = ? AND table_name = ? AND kind = 'partition_key' ALLOW FILTERING",
1718
- [this.schema.keyspace, this.schema.tableName]
1719
- );
1720
- const clusteringKeyResult = await this.client.execute(
1721
- "SELECT column_name, type FROM system_schema.columns WHERE keyspace_name = ? AND table_name = ? AND kind = 'clustering' ALLOW FILTERING",
1722
- [this.schema.keyspace, this.schema.tableName]
1723
- );
1724
- if (partitionKeyResult.rows.length !== this.schema.partitionKey.length) {
1725
- return { hasChanges: true, reasons: ["\u5206\u533A\u952E\u6570\u91CF\u4E0D\u4E00\u81F4"] };
1726
- }
1727
- const existingPartitionKeys = new Set(
1728
- partitionKeyResult.rows.map((row) => row.column_name.toLowerCase())
1729
- );
1730
- const schemaPartitionKeys = new Set(
1731
- this.schema.partitionKey.map((key) => String(key).toLowerCase())
1732
- );
1733
- if (existingPartitionKeys.size !== schemaPartitionKeys.size) {
1734
- return { hasChanges: true, reasons: ["\u5206\u533A\u952E\u5B57\u6BB5\u540D\u4E0D\u4E00\u81F4"] };
1735
- }
1736
- for (const key of existingPartitionKeys) {
1737
- if (!schemaPartitionKeys.has(key)) {
1738
- return { hasChanges: true, reasons: ["\u5206\u533A\u952E\u5B57\u6BB5\u540D\u4E0D\u4E00\u81F4"] };
1739
- }
1740
- }
1741
- if (this.schema.clusteringKey) {
1742
- const existingClusteringKeys = new Set(
1743
- clusteringKeyResult.rows.map(
1744
- (row) => row.column_name.toLowerCase()
1745
- )
1746
- );
1747
- const schemaClusteringKeys = new Set(
1748
- Object.keys(this.schema.clusteringKey).map((key) => key.toLowerCase())
1749
- );
1750
- if (existingClusteringKeys.size !== schemaClusteringKeys.size) {
1751
- return { hasChanges: true, reasons: ["\u805A\u7C7B\u952E\u914D\u7F6E\u4E0D\u4E00\u81F4"] };
1752
- }
1753
- for (const key of existingClusteringKeys) {
1754
- if (!schemaClusteringKeys.has(key)) {
1755
- return { hasChanges: true, reasons: ["\u805A\u7C7B\u952E\u914D\u7F6E\u4E0D\u4E00\u81F4"] };
2228
+ const regularAddedFields = changes.addedFields.filter(
2229
+ (f) => f.kind === "regular"
2230
+ );
2231
+ if (regularAddedFields.length > 0) {
2232
+ operations.push(
2233
+ queryHelper.addColumns(
2234
+ this.schema,
2235
+ regularAddedFields.map((f) => f.name)
2236
+ )
2237
+ );
1756
2238
  }
1757
- }
1758
- } else if (clusteringKeyResult.rows.length > 0) {
1759
- return { hasChanges: true, reasons: ["\u805A\u7C7B\u952E\u914D\u7F6E\u4E0D\u4E00\u81F4"] };
1760
- }
1761
- return { hasChanges: false, reasons: [] };
1762
- } catch (error) {
1763
- console.error("\u68C0\u67E5\u4E3B\u952E\u53D8\u66F4\u65F6\u53D1\u751F\u9519\u8BEF:", error);
1764
- return { hasChanges: false, reasons: [] };
1765
- }
1766
- }
1767
- // 检查字段类型变更
1768
- async checkFieldTypeChanges(tableMetadata2) {
1769
- try {
1770
- for (const field of Object.keys(this.schema.fields.shape)) {
1771
- const existingField = tableMetadata2.find(
1772
- (t) => t.column_name.toLowerCase() === field.toLowerCase()
1773
- );
1774
- if (existingField) {
1775
- const schemaType = this.getCassandraType(
1776
- this.schema.fields.shape[field]
2239
+ const regularDeletedFields = changes.deletedFields.filter(
2240
+ (f) => f.kind === "regular"
1777
2241
  );
1778
- const existingType = existingField.type.toLowerCase();
1779
- if (!this.isTypeCompatible(existingType, schemaType)) {
1780
- return {
1781
- hasChanges: true,
1782
- reasons: [
1783
- `\u5B57\u6BB5\u7C7B\u578B\u4E0D\u517C\u5BB9: ${field} (${existingType} \u2192 ${schemaType})`
1784
- ]
1785
- };
2242
+ if (regularDeletedFields.length > 0) {
2243
+ operations.push(
2244
+ queryHelper.dropColumns(
2245
+ this.schema,
2246
+ regularDeletedFields.map((f) => f.name)
2247
+ )
2248
+ );
2249
+ }
2250
+ for (const deletedIndex of changes.deletedIndexes) {
2251
+ operations.push(
2252
+ queryHelper.dropIndex(this.schema.keyspace, deletedIndex.name)
2253
+ );
2254
+ }
2255
+ for (const rebuiltIndex of changes.rebuiltIndexes) {
2256
+ operations.push(
2257
+ queryHelper.dropIndex(this.schema.keyspace, rebuiltIndex.name)
2258
+ );
2259
+ }
2260
+ for (const addedIndex of changes.addedIndexes) {
2261
+ const fieldName = addedIndex.name.replace(`${this.schema.tableName}_`, "").replace("_sai_idx", "");
2262
+ const indexOptions = this.schema.indexes?.[fieldName];
2263
+ if (indexOptions !== void 0) {
2264
+ const indexQuery = createIndex(
2265
+ this.schema.keyspace,
2266
+ this.schema.tableName,
2267
+ fieldName,
2268
+ typeof indexOptions === "boolean" ? true : indexOptions
2269
+ );
2270
+ operations.push(indexQuery);
2271
+ }
2272
+ }
2273
+ for (const rebuiltIndex of changes.rebuiltIndexes) {
2274
+ const fieldName = rebuiltIndex.name.replace(`${this.schema.tableName}_`, "").replace("_sai_idx", "");
2275
+ const indexOptions = this.schema.indexes?.[fieldName];
2276
+ if (indexOptions !== void 0) {
2277
+ const indexQuery = createIndex(
2278
+ this.schema.keyspace,
2279
+ this.schema.tableName,
2280
+ fieldName,
2281
+ typeof indexOptions === "boolean" ? true : indexOptions
2282
+ );
2283
+ operations.push(indexQuery);
2284
+ }
1786
2285
  }
1787
2286
  }
1788
2287
  }
1789
- return { hasChanges: false, reasons: [] };
2288
+ await synchronizer.sync(force);
2289
+ return operations.join("\n");
1790
2290
  } catch (error) {
1791
- console.error("\u68C0\u67E5\u5B57\u6BB5\u7C7B\u578B\u53D8\u66F4\u65F6\u53D1\u751F\u9519\u8BEF:", error);
1792
- return { hasChanges: false, reasons: [] };
1793
- }
1794
- }
1795
- // 获取 Cassandra 类型
1796
- getCassandraType(zodType) {
1797
- let baseType = zodType;
1798
- while (baseType._def && (baseType._def.typeName === "ZodOptional" || baseType._def.typeName === "ZodNullable" || baseType._def.typeName === "ZodDefault")) {
1799
- baseType = baseType._def.innerType;
1800
- }
1801
- if (baseType._cassandraType) {
1802
- return baseType._cassandraType;
1803
- }
1804
- if (baseType._def.typeName === "ZodString") {
1805
- if (baseType._def.checks && baseType._def.checks.some((c) => c.kind === "uuid")) {
1806
- return "uuid";
1807
- }
1808
- return "text";
1809
- } else if (baseType._def.typeName === "ZodNumber") {
1810
- return "double";
1811
- } else if (baseType._def.typeName === "ZodBoolean") {
1812
- return "boolean";
1813
- } else if (baseType._def.typeName === "ZodDate") {
1814
- return "timestamp";
1815
- } else if (baseType._def.typeName === "ZodArray") {
1816
- return "list";
1817
- } else if (baseType._def.typeName === "ZodSet") {
1818
- return "set";
1819
- } else if (baseType._def.typeName === "ZodRecord") {
1820
- return "map";
1821
- }
1822
- return "text";
1823
- }
1824
- // 检查类型是否兼容
1825
- isTypeCompatible(existingType, newType) {
1826
- if (existingType === newType) {
1827
- return true;
1828
- }
1829
- const compatibleChanges = {
1830
- text: ["varchar", "ascii"],
1831
- // text 可以兼容 varchar 和 ascii
1832
- varchar: ["text", "ascii"],
1833
- // varchar 可以兼容 text 和 ascii
1834
- ascii: ["text", "varchar"],
1835
- // ascii 可以兼容 text 和 varchar
1836
- int: ["bigint", "smallint", "tinyint"],
1837
- // int 可以兼容其他整数类型
1838
- bigint: ["int", "smallint", "tinyint"],
1839
- // bigint 可以兼容其他整数类型
1840
- smallint: ["int", "bigint", "tinyint"],
1841
- // smallint 可以兼容其他整数类型
1842
- tinyint: ["int", "bigint", "smallint"],
1843
- // tinyint 可以兼容其他整数类型
1844
- float: ["double"],
1845
- // float 可以兼容 double
1846
- double: ["float"]
1847
- // double 可以兼容 float
1848
- };
1849
- if (compatibleChanges[existingType] && compatibleChanges[existingType].includes(newType)) {
1850
- return true;
1851
- }
1852
- if (compatibleChanges[newType] && compatibleChanges[newType].includes(existingType)) {
1853
- return true;
2291
+ console.error(
2292
+ `\u274C \u8868 ${this.schema.keyspace}.${this.schema.tableName} \u540C\u6B65\u5931\u8D25:`,
2293
+ error
2294
+ );
2295
+ throw error;
1854
2296
  }
1855
- return false;
1856
2297
  }
1857
2298
  // 构建插入查询
1858
2299
  buildInsertQuery(data, options) {
@@ -1900,7 +2341,7 @@ function createModel(schema, client) {
1900
2341
  var uuid = () => cassandraDriver.types.Uuid.random().toString();
1901
2342
 
1902
2343
  // src/client.ts
1903
- var Client2 = class {
2344
+ var Client3 = class {
1904
2345
  constructor(options) {
1905
2346
  this.options = options;
1906
2347
  this.cassandraClient = new cassandraDriver.Client({ ...options });
@@ -2069,7 +2510,7 @@ var Types = {
2069
2510
  )
2070
2511
  };
2071
2512
 
2072
- exports.Client = Client2;
2513
+ exports.Client = Client3;
2073
2514
  exports.Model = Model;
2074
2515
  exports.TableSchema = TableSchema;
2075
2516
  exports.Types = Types;