imodel-pg 0.3.0 → 0.3.2

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 (3) hide show
  1. package/index.d.mts +1 -1
  2. package/index.mjs +229 -13
  3. package/package.json +1 -1
package/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * imodel v0.3.0
2
+ * imodel v0.3.2
3
3
  * (c) 2019-2025 undefined
4
4
  * @license undefined
5
5
  */
package/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * imodel v0.3.0
2
+ * imodel v0.3.2
3
3
  * (c) 2019-2025 undefined
4
4
  * @license undefined
5
5
  */
@@ -934,6 +934,7 @@ WHERE ${Sql`AND`.glue(pKeys.map(f => Sql`${main.field(f)}=${tmp.field(f)}`), get
934
934
  /** @import { Environment, IConnection } from 'imodel' */
935
935
  /** @import { PgEnvTrans } from '../index.mjs' */
936
936
  /** @import { DBIndex, DBTable, DBField } from 'imodel' */
937
+ const tableType = 'BASE TABLE';
937
938
  /**
938
939
  *
939
940
  * @param {Environment<PgEnvTrans>} env
@@ -1065,7 +1066,7 @@ async function loadDefault(env, query, fields) {
1065
1066
  return fields;
1066
1067
  }
1067
1068
  const sql = Sql`
1068
- SELECT ${Sql`,`.glue(data.map((v, k) => Sql`${v} as ${Sql(`a${k}`)}`))}
1069
+ SELECT ${Sql`,`.glue(data.map((v, k) => Sql`${v.default} as ${Sql(`a${k}`)}`))}
1069
1070
  `;
1070
1071
  const values = await query(env, sql);
1071
1072
  for (const [index, field] of data.entries()) {
@@ -1143,7 +1144,7 @@ async function loadBaseTables(env, schema, query, tables) {
1143
1144
  WHERE
1144
1145
  table_schema = ${schema}
1145
1146
  AND
1146
- table_type = 'base table'
1147
+ table_type = ${tableType}
1147
1148
  AND
1148
1149
  table_name IN (${Sql`,`.glue(tables.map(t => Sql`${t}`))})`;
1149
1150
  const rows = await query(env, sql);
@@ -1194,11 +1195,14 @@ async function loadTables(env, schema, query, tables) {
1194
1195
  /** @import { DBIndex, DBTable, DBField } from 'imodel' */
1195
1196
  /**
1196
1197
  *
1197
- * @param {DBIndex[]} indexes
1198
- * @returns
1198
+ * @param {DBIndex[]} [indexes]
1199
+ * @returns {Record<string, DBIndex>}
1199
1200
  */
1200
1201
  function buildNewIndexes(indexes) {
1201
- /** @type {Record<string, {fields: string[]; includes: string[][]; unique: boolean;}>} */
1202
+ if (!indexes) {
1203
+ return {};
1204
+ }
1205
+ /** @type {Record<string, {fields: string[]; includes: string[][]; unique?: boolean;}>} */
1202
1206
  const indexMap = {};
1203
1207
  for (const {
1204
1208
  fields,
@@ -1217,12 +1221,12 @@ function buildNewIndexes(indexes) {
1217
1221
  if (!index) {
1218
1222
  indexMap[key] = {
1219
1223
  fields: keys,
1220
- includes: [includes],
1224
+ includes: [includes || []],
1221
1225
  unique
1222
1226
  };
1223
1227
  continue;
1224
1228
  }
1225
- index.includes.push(includes);
1229
+ index.includes.push(includes || []);
1226
1230
  index.unique ||= unique;
1227
1231
  }
1228
1232
  /** @type {Record<string, DBIndex>} */
@@ -1257,7 +1261,7 @@ async function createIndex(env, query, table, {
1257
1261
  CREATE ${unique ? Sql`UNIQUE` : Sql``}
1258
1262
  INDEX ON ${Sql.Table(table)}
1259
1263
  (${Sql`,`.glue(...fields.map(f => Sql.Field(f)))})
1260
- ${includes.length ? Sql`
1264
+ ${includes?.length ? Sql`
1261
1265
  INCLUDE(${Sql`,`.glue(...includes.map(f => Sql.Field(f)))})
1262
1266
  ` : Sql``}
1263
1267
  `;
@@ -1284,6 +1288,216 @@ async function createNewTable(env, query, {
1284
1288
  await createIndex(env, query, table, index);
1285
1289
  }
1286
1290
  }
1291
+ /**
1292
+ *
1293
+ * @param {{fields: string[]; includes: string[]?; unique?: boolean; name: string[]}} oldIndex
1294
+ * @param {DBIndex} [newIndex]
1295
+ * @returns
1296
+ */
1297
+ function indexIsEq({
1298
+ includes,
1299
+ name,
1300
+ unique
1301
+ }, newIndex) {
1302
+ if (!newIndex) {
1303
+ return false;
1304
+ }
1305
+ if (name?.length !== 1) {
1306
+ return false;
1307
+ }
1308
+ if (!includes) {
1309
+ return false;
1310
+ }
1311
+ if (Boolean(newIndex.unique) !== Boolean(unique)) {
1312
+ return false;
1313
+ }
1314
+ const oldIncludes = new Set(includes);
1315
+ const newIncludes = new Set(newIndex.includes);
1316
+ if (oldIncludes.size !== newIncludes.size) {
1317
+ return false;
1318
+ }
1319
+ for (const k of oldIncludes) {
1320
+ if (!newIncludes.has(k)) {
1321
+ return false;
1322
+ }
1323
+ }
1324
+ return true;
1325
+ }
1326
+ /**
1327
+ *
1328
+ * @param {DBIndex[]} [indexes]
1329
+ * @returns
1330
+ */
1331
+ function buildOldIndexes(indexes) {
1332
+ /** @type {Record<string, {fields: string[]; includes: string[]?; unique?: boolean; name: string[]}>} */
1333
+ const indexMap = {};
1334
+ if (!indexes) {
1335
+ return indexMap;
1336
+ }
1337
+ for (const {
1338
+ fields,
1339
+ includes,
1340
+ unique,
1341
+ name
1342
+ } of indexes) {
1343
+ const keys = [];
1344
+ for (const f of fields) {
1345
+ if (keys.includes(f)) {
1346
+ continue;
1347
+ }
1348
+ keys.push(f);
1349
+ }
1350
+ const key = keys.join('\n');
1351
+ const index = indexMap[key];
1352
+ if (!index) {
1353
+ indexMap[key] = {
1354
+ fields: keys,
1355
+ includes: includes || [],
1356
+ unique,
1357
+ name: name ? [name] : []
1358
+ };
1359
+ continue;
1360
+ }
1361
+ index.includes = null;
1362
+ if (name) {
1363
+ index.name.push(name);
1364
+ }
1365
+ }
1366
+ return indexMap;
1367
+ }
1368
+ function toTypeHad(type) {
1369
+ switch (type) {
1370
+ case 'ipv4':
1371
+ case 'ipv6':
1372
+ return 'ip';
1373
+ case 'ipnetv4':
1374
+ case 'ipnetv6':
1375
+ return 'ipnet';
1376
+ case 'object':
1377
+ return 'json';
1378
+ }
1379
+ return type;
1380
+ }
1381
+ /**
1382
+ *
1383
+ * @param {Environment<PgEnvTrans>} env
1384
+ * @param {<T extends pg.QueryResultRow>(env: Environment<PgEnvTrans>, sql: Sql) => Promise<T[]>} query
1385
+ * @param {DBTable} newTable
1386
+ * @param {DBTable} oldTable
1387
+ */
1388
+ async function changeTable(env, query, newTable, oldTable) {
1389
+ const {
1390
+ table
1391
+ } = newTable;
1392
+ const newIndexes = new Map(Object.entries(buildNewIndexes(newTable.indexes)));
1393
+ const oldIndexes = buildOldIndexes(oldTable.indexes);
1394
+ for (const [key, index] of Object.entries(oldIndexes)) {
1395
+ if (indexIsEq(index, newIndexes.get(key))) {
1396
+ newIndexes.delete(key);
1397
+ continue;
1398
+ }
1399
+ for (const n of index.name || []) {
1400
+ const sql = Sql`DROP INDEX ${Sql.Id(n, 'index')}`;
1401
+ await query(env, sql);
1402
+ }
1403
+ }
1404
+ // 改名
1405
+ if (table !== oldTable.table) {
1406
+ const sql = Sql`
1407
+ ALTER TABLE ${Sql.Table(oldTable.table)}
1408
+ RENAME TO ${Sql.Table(table)}
1409
+ `;
1410
+ await query(env, sql);
1411
+ }
1412
+ const primary = Object.entries(newTable.fields).filter(([, v]) => v.primary).sort(([, {
1413
+ primary: a = 0
1414
+ }], [, {
1415
+ primary: b = 0
1416
+ }]) => a - b).map(([v]) => v);
1417
+ const oldPrimary = Object.entries(oldTable.fields).filter(([, v]) => v.primary).sort(([, {
1418
+ primary: a = 0
1419
+ }], [, {
1420
+ primary: b = 0
1421
+ }]) => a - b).map(([v]) => v);
1422
+ const prEq = oldPrimary.length === primary.length && primary.join('\n') === oldPrimary.join('\n');
1423
+ if (!prEq && oldPrimary.length) {
1424
+ let name = oldTable.primary;
1425
+ if (!name) {
1426
+ const sql = Sql`
1427
+ SELECT constraint_name
1428
+ FROM information_schema.table_constraints
1429
+ WHERE table_name = ${table} AND constraint_type = 'PRIMARY KEY'
1430
+ `;
1431
+ name = (await query(env, sql).then(v => v[0]?.constraint_name)) || '';
1432
+ }
1433
+ // @ts-ignore
1434
+ // eslint-disable-next-line max-len
1435
+ const sql = Sql`ALTER TABLE ${Sql.Table(table)} DROP CONSTRAINT ${Sql.Id(name)}`;
1436
+ await query(env, sql);
1437
+ }
1438
+ const oldFields = new Map(Object.entries(oldTable.fields));
1439
+ // 列处理
1440
+ /** @type {Sql[]} */
1441
+ const COLUMNs = [];
1442
+ for (const [fieldName, field] of Object.entries(newTable.fields)) {
1443
+ const old = oldFields.get(fieldName);
1444
+ oldFields.delete(fieldName);
1445
+ if (!old) {
1446
+ COLUMNs.push(add(fieldName, field.type, field, env));
1447
+ continue;
1448
+ }
1449
+ const type = toTypeHad(field.type);
1450
+ const {
1451
+ size,
1452
+ scale,
1453
+ array,
1454
+ default: defaultValue = null,
1455
+ nullable
1456
+ } = field;
1457
+ if (type !== old.type || Number(array) !== Number(old.array)) ;
1458
+ const COLUMN = Sql`ALTER COLUMN ${Sql.Field(fieldName)}`;
1459
+ const newType = getType(type, {
1460
+ scale,
1461
+ size
1462
+ }, array);
1463
+ const oldType = getType(old.type, old, old.array);
1464
+ if (newType !== oldType) {
1465
+ COLUMNs.push(Sql`${COLUMN} TYPE ${Sql(newType)}`);
1466
+ // TODO: USING "description"::int2
1467
+ }
1468
+ if (Boolean(nullable) !== Boolean(old.nullable)) {
1469
+ COLUMNs.push(nullable ? Sql`${COLUMN} DROP NOT NULL` : Sql`${COLUMN} SET NOT NULL`);
1470
+ }
1471
+ if (defaultValue !== (old.default ?? null)) {
1472
+ COLUMNs.push(defaultValue !== null ? Sql`${COLUMN} SET DEFAULT ${defaultValue}` : Sql`${COLUMN} DROP DEFAULT`);
1473
+ }
1474
+ }
1475
+ {
1476
+ for (const [fieldName, {
1477
+ nullable
1478
+ }] of oldFields) {
1479
+ if (nullable) {
1480
+ continue;
1481
+ }
1482
+ COLUMNs.push(Sql`ALTER COLUMN ${Sql.Field(fieldName)} DROP NOT NULL`);
1483
+ }
1484
+ }
1485
+ if (COLUMNs.length) {
1486
+ const sql = Sql`ALTER TABLE ${Sql.Table(table)} ${Sql`,`.glue(COLUMNs)}`;
1487
+ await query(env, sql);
1488
+ }
1489
+ if (!prEq && primary.length) {
1490
+ const sql = Sql`
1491
+ ALTER TABLE ${Sql.Table(table)}
1492
+ ADD PRIMARY KEY ${Sql`,`.glue(primary.map(f => Sql.Field(f)))}
1493
+ `;
1494
+ await query(env, sql);
1495
+ }
1496
+ // 重建索引
1497
+ for (const index of newIndexes.values()) {
1498
+ await createIndex(env, query, table, index);
1499
+ }
1500
+ }
1287
1501
  /**
1288
1502
  *
1289
1503
  * @param {Environment<PgEnvTrans>} env
@@ -1296,12 +1510,14 @@ async function syncTables(env, schema, query, tables, del) {
1296
1510
  const tableNames = tables.map(v => v.table);
1297
1511
  const dbTables = await loadTables(env, schema, query, tableNames);
1298
1512
  const tableMap = new Map(dbTables.map(v => [v.table, v]));
1299
- tables.map(table => {
1513
+ for (const table of tables) {
1300
1514
  const old = tableMap.get(table.table);
1301
- if (old) ; else {
1302
- return createNewTable(env, query, table);
1515
+ if (old) {
1516
+ await changeTable(env, query, table, old);
1517
+ } else {
1518
+ await createNewTable(env, query, table);
1303
1519
  }
1304
- });
1520
+ }
1305
1521
  }
1306
1522
 
1307
1523
  /** @import { Environment, IConnection } from 'imodel' */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imodel-pg",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "dependencies": {
5
5
  "pg": "^8.13.3",
6
6
  "tagged-sql": "^0.9.0"