imodel-pg 0.3.1 → 0.3.3

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 +238 -13
  3. package/package.json +1 -1
package/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * imodel v0.3.1
2
+ * imodel v0.3.3
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.1
2
+ * imodel v0.3.3
3
3
  * (c) 2019-2025 undefined
4
4
  * @license undefined
5
5
  */
@@ -128,18 +128,28 @@ function getType(s, {
128
128
  }
129
129
 
130
130
  /** @import { Environment, ColumnOptions } from 'imodel' */
131
+ /**
132
+ *
133
+ * @param {string} t
134
+ */
135
+ function toSqlString2(t) {
136
+ return `'${t.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`;
137
+ }
131
138
  /**
132
139
  *
133
140
  * @param {any} v
134
141
  * @param {string} type
135
142
  * @returns
136
143
  */
137
- function toSqlString(v, type) {
144
+ function toSql(v, type) {
145
+ if (type === 'json' || type === 'object') {
146
+ return `${toSqlString2(JSON.stringify(type))}::JSON`;
147
+ }
138
148
  if (typeof v === 'bigint' || typeof v === 'number') {
139
149
  return String(v);
140
150
  }
141
151
  if (typeof v === 'string') {
142
- return `'${v.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}'`;
152
+ return toSqlString2(v);
143
153
  }
144
154
  if (v === true) {
145
155
  return 'TRUE';
@@ -148,8 +158,9 @@ function toSqlString(v, type) {
148
158
  return 'FALSE';
149
159
  }
150
160
  if (v instanceof Date) {
151
- return `'${v.toISOString()}'`;
161
+ return v.toISOString();
152
162
  }
163
+ return null;
153
164
  }
154
165
  /**
155
166
  *
@@ -178,10 +189,10 @@ function getDefault(env, value, type, array) {
178
189
  }
179
190
  if (array) {
180
191
  const values = Array.isArray(v) ? v : [v];
181
- const t = values.map(v => toSqlString(v)).filter(Boolean).join(',');
182
- return Sql(`'{${t.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')}}'`);
192
+ const t = values.map(v => toSql(v, type)).filter(Boolean);
193
+ return Sql(`{${t.join(',')}}`);
183
194
  }
184
- const t = toSqlString(v);
195
+ const t = toSql(v, type);
185
196
  if (t) {
186
197
  return Sql(t);
187
198
  }
@@ -934,6 +945,7 @@ WHERE ${Sql`AND`.glue(pKeys.map(f => Sql`${main.field(f)}=${tmp.field(f)}`), get
934
945
  /** @import { Environment, IConnection } from 'imodel' */
935
946
  /** @import { PgEnvTrans } from '../index.mjs' */
936
947
  /** @import { DBIndex, DBTable, DBField } from 'imodel' */
948
+ const tableType = 'BASE TABLE';
937
949
  /**
938
950
  *
939
951
  * @param {Environment<PgEnvTrans>} env
@@ -1065,7 +1077,7 @@ async function loadDefault(env, query, fields) {
1065
1077
  return fields;
1066
1078
  }
1067
1079
  const sql = Sql`
1068
- SELECT ${Sql`,`.glue(data.map((v, k) => Sql`${v} as ${Sql(`a${k}`)}`))}
1080
+ SELECT ${Sql`,`.glue(data.map((v, k) => Sql(`${v.default} as ${k}`)))}
1069
1081
  `;
1070
1082
  const values = await query(env, sql);
1071
1083
  for (const [index, field] of data.entries()) {
@@ -1143,7 +1155,7 @@ async function loadBaseTables(env, schema, query, tables) {
1143
1155
  WHERE
1144
1156
  table_schema = ${schema}
1145
1157
  AND
1146
- table_type = 'base table'
1158
+ table_type = ${tableType}
1147
1159
  AND
1148
1160
  table_name IN (${Sql`,`.glue(tables.map(t => Sql`${t}`))})`;
1149
1161
  const rows = await query(env, sql);
@@ -1287,6 +1299,217 @@ async function createNewTable(env, query, {
1287
1299
  await createIndex(env, query, table, index);
1288
1300
  }
1289
1301
  }
1302
+ /**
1303
+ *
1304
+ * @param {{fields: string[]; includes: string[]?; unique?: boolean; name: string[]}} oldIndex
1305
+ * @param {DBIndex} [newIndex]
1306
+ * @returns
1307
+ */
1308
+ function indexIsEq({
1309
+ includes,
1310
+ name,
1311
+ unique
1312
+ }, newIndex) {
1313
+ if (!newIndex) {
1314
+ return false;
1315
+ }
1316
+ if (name?.length !== 1) {
1317
+ return false;
1318
+ }
1319
+ if (!includes) {
1320
+ return false;
1321
+ }
1322
+ if (Boolean(newIndex.unique) !== Boolean(unique)) {
1323
+ return false;
1324
+ }
1325
+ const oldIncludes = new Set(includes);
1326
+ const newIncludes = new Set(newIndex.includes);
1327
+ if (oldIncludes.size !== newIncludes.size) {
1328
+ return false;
1329
+ }
1330
+ for (const k of oldIncludes) {
1331
+ if (!newIncludes.has(k)) {
1332
+ return false;
1333
+ }
1334
+ }
1335
+ return true;
1336
+ }
1337
+ /**
1338
+ *
1339
+ * @param {DBIndex[]} [indexes]
1340
+ * @returns
1341
+ */
1342
+ function buildOldIndexes(indexes) {
1343
+ /** @type {Record<string, {fields: string[]; includes: string[]?; unique?: boolean; name: string[]}>} */
1344
+ const indexMap = {};
1345
+ if (!indexes) {
1346
+ return indexMap;
1347
+ }
1348
+ for (const {
1349
+ fields,
1350
+ includes,
1351
+ unique,
1352
+ name
1353
+ } of indexes) {
1354
+ const keys = [];
1355
+ for (const f of fields) {
1356
+ if (keys.includes(f)) {
1357
+ continue;
1358
+ }
1359
+ keys.push(f);
1360
+ }
1361
+ const key = keys.join('\n');
1362
+ const index = indexMap[key];
1363
+ if (!index) {
1364
+ indexMap[key] = {
1365
+ fields: keys,
1366
+ includes: includes || [],
1367
+ unique,
1368
+ name: name ? [name] : []
1369
+ };
1370
+ continue;
1371
+ }
1372
+ index.includes = null;
1373
+ if (name) {
1374
+ index.name.push(name);
1375
+ }
1376
+ }
1377
+ return indexMap;
1378
+ }
1379
+ function toTypeHad(type) {
1380
+ switch (type) {
1381
+ case 'ipv4':
1382
+ case 'ipv6':
1383
+ return 'ip';
1384
+ case 'ipnetv4':
1385
+ case 'ipnetv6':
1386
+ return 'ipnet';
1387
+ case 'object':
1388
+ return 'json';
1389
+ }
1390
+ return type;
1391
+ }
1392
+ /**
1393
+ *
1394
+ * @param {Environment<PgEnvTrans>} env
1395
+ * @param {<T extends pg.QueryResultRow>(env: Environment<PgEnvTrans>, sql: Sql) => Promise<T[]>} query
1396
+ * @param {DBTable} newTable
1397
+ * @param {DBTable} oldTable
1398
+ */
1399
+ async function changeTable(env, query, newTable, oldTable) {
1400
+ const {
1401
+ table
1402
+ } = newTable;
1403
+ const newIndexes = new Map(Object.entries(buildNewIndexes(newTable.indexes)));
1404
+ const oldIndexes = buildOldIndexes(oldTable.indexes);
1405
+ for (const [key, index] of Object.entries(oldIndexes)) {
1406
+ if (indexIsEq(index, newIndexes.get(key))) {
1407
+ newIndexes.delete(key);
1408
+ continue;
1409
+ }
1410
+ for (const n of index.name || []) {
1411
+ const sql = Sql`DROP INDEX ${Sql.Id(n, 'index')}`;
1412
+ await query(env, sql);
1413
+ }
1414
+ }
1415
+ // 改名
1416
+ if (table !== oldTable.table) {
1417
+ const sql = Sql`
1418
+ ALTER TABLE ${Sql.Table(oldTable.table)}
1419
+ RENAME TO ${Sql.Table(table)}
1420
+ `;
1421
+ await query(env, sql);
1422
+ }
1423
+ const primary = Object.entries(newTable.fields).filter(([, v]) => v.primary).sort(([, {
1424
+ primary: a = 0
1425
+ }], [, {
1426
+ primary: b = 0
1427
+ }]) => a - b).map(([v]) => v);
1428
+ const oldPrimary = Object.entries(oldTable.fields).filter(([, v]) => v.primary).sort(([, {
1429
+ primary: a = 0
1430
+ }], [, {
1431
+ primary: b = 0
1432
+ }]) => a - b).map(([v]) => v);
1433
+ const prEq = oldPrimary.length === primary.length && primary.join('\n') === oldPrimary.join('\n');
1434
+ if (!prEq && oldPrimary.length) {
1435
+ let name = oldTable.primary;
1436
+ if (!name) {
1437
+ const sql = Sql`
1438
+ SELECT constraint_name
1439
+ FROM information_schema.table_constraints
1440
+ WHERE table_name = ${table} AND constraint_type = 'PRIMARY KEY'
1441
+ `;
1442
+ name = (await query(env, sql).then(v => v[0]?.constraint_name)) || '';
1443
+ }
1444
+ // @ts-ignore
1445
+ // eslint-disable-next-line max-len
1446
+ const sql = Sql`ALTER TABLE ${Sql.Table(table)} DROP CONSTRAINT ${Sql.Id(name)}`;
1447
+ await query(env, sql);
1448
+ }
1449
+ const oldFields = new Map(Object.entries(oldTable.fields));
1450
+ // 列处理
1451
+ /** @type {Sql[]} */
1452
+ const COLUMNs = [];
1453
+ for (const [fieldName, field] of Object.entries(newTable.fields)) {
1454
+ const old = oldFields.get(fieldName);
1455
+ oldFields.delete(fieldName);
1456
+ if (!old) {
1457
+ COLUMNs.push(add(fieldName, field.type, field, env));
1458
+ continue;
1459
+ }
1460
+ const type = toTypeHad(field.type);
1461
+ const {
1462
+ size,
1463
+ scale,
1464
+ array,
1465
+ default: defaultValue = null,
1466
+ nullable
1467
+ } = field;
1468
+ if (type !== old.type || Number(array) !== Number(old.array)) ;
1469
+ const COLUMN = Sql`ALTER COLUMN ${Sql.Field(fieldName)}`;
1470
+ const newType = getType(type, {
1471
+ scale,
1472
+ size
1473
+ }, array);
1474
+ const oldType = getType(old.type, old, old.array);
1475
+ if (newType !== oldType) {
1476
+ COLUMNs.push(Sql`${COLUMN} TYPE ${Sql(newType)}`);
1477
+ // TODO: USING "description"::int2
1478
+ }
1479
+ if (Boolean(nullable) !== Boolean(old.nullable)) {
1480
+ COLUMNs.push(nullable ? Sql`${COLUMN} DROP NOT NULL` : Sql`${COLUMN} SET NOT NULL`);
1481
+ }
1482
+ if (defaultValue !== (old.default ?? null)) {
1483
+ const def = getDefault(env, defaultValue, type, array);
1484
+ COLUMNs.push(def ? Sql`${COLUMN} SET DEFAULT ${def}` : Sql`${COLUMN} DROP DEFAULT`);
1485
+ }
1486
+ }
1487
+ {
1488
+ for (const [fieldName, {
1489
+ nullable
1490
+ }] of oldFields) {
1491
+ if (nullable) {
1492
+ continue;
1493
+ }
1494
+ COLUMNs.push(Sql`ALTER COLUMN ${Sql.Field(fieldName)} DROP NOT NULL`);
1495
+ }
1496
+ }
1497
+ if (COLUMNs.length) {
1498
+ const sql = Sql`ALTER TABLE ${Sql.Table(table)} ${Sql`,`.glue(COLUMNs)}`;
1499
+ await query(env, sql);
1500
+ }
1501
+ if (!prEq && primary.length) {
1502
+ const sql = Sql`
1503
+ ALTER TABLE ${Sql.Table(table)}
1504
+ ADD PRIMARY KEY ${Sql`,`.glue(primary.map(f => Sql.Field(f)))}
1505
+ `;
1506
+ await query(env, sql);
1507
+ }
1508
+ // 重建索引
1509
+ for (const index of newIndexes.values()) {
1510
+ await createIndex(env, query, table, index);
1511
+ }
1512
+ }
1290
1513
  /**
1291
1514
  *
1292
1515
  * @param {Environment<PgEnvTrans>} env
@@ -1299,12 +1522,14 @@ async function syncTables(env, schema, query, tables, del) {
1299
1522
  const tableNames = tables.map(v => v.table);
1300
1523
  const dbTables = await loadTables(env, schema, query, tableNames);
1301
1524
  const tableMap = new Map(dbTables.map(v => [v.table, v]));
1302
- tables.map(table => {
1525
+ for (const table of tables) {
1303
1526
  const old = tableMap.get(table.table);
1304
- if (old) ; else {
1305
- return createNewTable(env, query, table);
1527
+ if (old) {
1528
+ await changeTable(env, query, table, old);
1529
+ } else {
1530
+ await createNewTable(env, query, table);
1306
1531
  }
1307
- });
1532
+ }
1308
1533
  }
1309
1534
 
1310
1535
  /** @import { Environment, IConnection } from 'imodel' */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imodel-pg",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "dependencies": {
5
5
  "pg": "^8.13.3",
6
6
  "tagged-sql": "^0.9.0"