dsh-db-tool 1.5.91 → 1.7.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.
@@ -0,0 +1,311 @@
1
+ import { pgCfgWithDatabase, pgLikePoolCfg } from '../../sql-shared/pg-like.js';
2
+ import { assertIdent, quoteIdent } from '../../sql-shared/common.js';
3
+ import { safeDefaultExpr } from '../model.js';
4
+ /** information_schema data_type → 中间类型 */
5
+ export function pgTypeToMid(dataType) {
6
+ const t = dataType.toLowerCase();
7
+ if (t === 'smallint' || t === 'integer')
8
+ return 'int';
9
+ if (t === 'bigint')
10
+ return 'bigint';
11
+ if (t === 'numeric' || t === 'decimal')
12
+ return 'decimal';
13
+ if (t === 'real')
14
+ return 'float';
15
+ if (t === 'double precision')
16
+ return 'double';
17
+ if (t === 'boolean')
18
+ return 'boolean';
19
+ if (t === 'text')
20
+ return 'text';
21
+ if (t === 'bytea')
22
+ return 'bytes';
23
+ if (t === 'json' || t === 'jsonb')
24
+ return 'json';
25
+ if (t === 'date')
26
+ return 'date';
27
+ if (t.startsWith('time(') || t === 'time' || t === 'time without time zone' || t === 'time with time zone')
28
+ return 'time';
29
+ if (t === 'timestamp with time zone')
30
+ return 'timestamptz';
31
+ if (t.startsWith('timestamp'))
32
+ return 'datetime';
33
+ return 'string'; // varchar/char/uuid/xml/inet/cidr/money/自定义
34
+ }
35
+ /** 中间类型 → 目标列类型(GaussDB 兼容 PG 类型面;目标端不生成自增,见 mysql.ts 同注) */
36
+ export function midToPgType(c) {
37
+ switch (c.midType) {
38
+ case 'int': return 'INTEGER';
39
+ case 'bigint': return 'BIGINT';
40
+ case 'decimal': return `NUMERIC(${c.precision ?? 38},${c.scale ?? 10})`;
41
+ case 'float': return 'REAL';
42
+ case 'double': return 'DOUBLE PRECISION';
43
+ case 'boolean': return 'BOOLEAN';
44
+ case 'string': return `VARCHAR(${Math.min(Math.max(c.length ?? 255, 1), 10485760)})`;
45
+ case 'text': return 'TEXT';
46
+ case 'bytes': return 'BYTEA';
47
+ case 'json': return 'JSON';
48
+ case 'date': return 'DATE';
49
+ case 'time': return 'TIME';
50
+ case 'datetime': return 'TIMESTAMP';
51
+ case 'timestamptz': return 'TIMESTAMPTZ';
52
+ }
53
+ }
54
+ /** 绑定值形态:bigint→字符串(pg 不接受 bigint 绑定,BIGINT 列隐式转换保精度) */
55
+ export function toPgBind(c) {
56
+ if (typeof c === 'bigint')
57
+ return c.toString();
58
+ return c;
59
+ }
60
+ /** 单元格保真:pg 的 Date → ISO 字符串(datetime/timestamptz 统一 ISO 流转) */
61
+ function materialize(v) {
62
+ if (v === null || v === undefined)
63
+ return null;
64
+ if (v instanceof Date)
65
+ return v.toISOString();
66
+ return v;
67
+ }
68
+ export function createPgTransferReader(kind, pool, schema) {
69
+ const q = async (sql, params) => (await pool.query(sql, params ?? [])).rows;
70
+ const describe = async (table) => {
71
+ const tbl = table;
72
+ const rows = await q(`SELECT column_name, data_type, is_nullable, column_default, character_maximum_length,
73
+ numeric_precision, numeric_scale
74
+ FROM information_schema.columns
75
+ WHERE table_schema = $1 AND table_name = $2
76
+ ORDER BY ordinal_position`, [schema, tbl]);
77
+ if (rows.length === 0)
78
+ throw new Error(`表不存在: ${schema}.${tbl}`);
79
+ const pks = await q(`SELECT kcu.column_name FROM information_schema.table_constraints tc
80
+ JOIN information_schema.key_column_usage kcu
81
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
82
+ AND tc.table_name = kcu.table_name
83
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1 AND tc.table_name = $2`, [schema, tbl]);
84
+ const pkSet = new Set(pks.map((r) => String(r.column_name)));
85
+ const columns = rows.map((r) => ({
86
+ name: String(r.column_name),
87
+ midType: pgTypeToMid(String(r.data_type)),
88
+ sourceType: String(r.data_type),
89
+ nullable: r.is_nullable === 'YES',
90
+ primaryKey: pkSet.has(String(r.column_name)),
91
+ length: r.character_maximum_length == null ? undefined : Number(r.character_maximum_length),
92
+ precision: r.numeric_precision == null ? undefined : Number(r.numeric_precision),
93
+ scale: r.numeric_scale == null ? undefined : Number(r.numeric_scale),
94
+ default: r.column_default == null ? null : String(r.column_default),
95
+ }));
96
+ return { name: tbl, columns };
97
+ };
98
+ return {
99
+ kind,
100
+ listTables: async () => {
101
+ const rows = await q(`SELECT table_name FROM information_schema.tables
102
+ WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name`, [schema]);
103
+ return rows.map((r) => String(r.table_name));
104
+ },
105
+ describe,
106
+ count: async (table) => {
107
+ const rows = await q(`SELECT COUNT(*) AS n FROM ${quoteIdent(schema)}.${quoteIdent(table)}`);
108
+ return Number(rows[0]?.n ?? 0);
109
+ },
110
+ shardKey: async (table) => {
111
+ const t = await describe(table);
112
+ const pks = t.columns.filter((c) => c.primaryKey);
113
+ const pk = pks.length === 1 ? pks[0] : undefined;
114
+ const sharable = ['int', 'bigint', 'decimal', 'string', 'datetime', 'timestamptz'];
115
+ return pk && sharable.includes(pk.midType) ? pk.midType : null;
116
+ },
117
+ readBatch: async (table, cursor, limit) => {
118
+ const t = await describe(table);
119
+ const tbl = `${quoteIdent(schema)}.${quoteIdent(t.name)}`;
120
+ const cols = t.columns.map((c) => quoteIdent(c.name)).join(', ');
121
+ let sql;
122
+ let params;
123
+ if (cursor.mode === 'keyset') {
124
+ const key = quoteIdent(cursor.column);
125
+ sql = cursor.after === null
126
+ ? `SELECT ${cols} FROM ${tbl} ORDER BY ${key} LIMIT $1`
127
+ : `SELECT ${cols} FROM ${tbl} WHERE ${key} > $1 ORDER BY ${key} LIMIT $2`;
128
+ params = cursor.after === null ? [limit] : [toPgBind(cursor.after), limit];
129
+ }
130
+ else if (cursor.mode === 'range') {
131
+ const key = quoteIdent(cursor.column);
132
+ const conds = [];
133
+ const binds = [];
134
+ if (cursor.lo !== null) {
135
+ conds.push(`${key} ${cursor.loInclusive ? '>=' : '>'} $${binds.length + 1}`);
136
+ binds.push(toPgBind(cursor.lo));
137
+ }
138
+ if (cursor.hi !== null) {
139
+ conds.push(`${key} < $${binds.length + 1}`);
140
+ binds.push(toPgBind(cursor.hi));
141
+ }
142
+ const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
143
+ sql = `SELECT ${cols} FROM ${tbl}${where} ORDER BY ${key} LIMIT $${binds.length + 1}`;
144
+ params = [...binds, limit];
145
+ }
146
+ else {
147
+ sql = `SELECT ${cols} FROM ${tbl} LIMIT $1 OFFSET $2`;
148
+ params = [limit, cursor.offset];
149
+ }
150
+ const rows = await q(sql, params);
151
+ return rows.map((r) => t.columns.map((c) => materialize(r[c.name])));
152
+ },
153
+ keyAt: async (table, column, offset) => {
154
+ const rows = await q(`SELECT ${quoteIdent(assertIdent(column, '分片键'))} AS k FROM ${quoteIdent(schema)}.${quoteIdent(assertIdent(table, '表名'))}
155
+ ORDER BY ${quoteIdent(assertIdent(column, '分片键'))} LIMIT 1 OFFSET $1`, [offset]);
156
+ return materialize(rows[0]?.k ?? null);
157
+ },
158
+ close: async () => {
159
+ await pool.end();
160
+ },
161
+ };
162
+ }
163
+ export function createPgTransferWriter(kind, pool, schema, writeMode = 'insert') {
164
+ return {
165
+ kind,
166
+ tableExists: async (name) => {
167
+ const res = await pool.query(`SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2`, [schema, name]);
168
+ return Number(res.rows[0]?.n ?? 0) > 0;
169
+ },
170
+ dropTable: async (name) => {
171
+ await pool.query(`DROP TABLE ${quoteIdent(schema)}.${quoteIdent(name)}`);
172
+ },
173
+ // truncate 模式前置清空:TRUNCATE 重置表而无行级日志开销(PG 官方文档 TRUNCATE 节)
174
+ // 同连接先设 lock_timeout(管 MDL 与行锁):TRUNCATE 被长事务持有时 MDL 等待默认无限;
175
+ // TRUNCATE 拿不到锁(15s)降级 DELETE(行级锁,同一 lock_timeout 有限等待)
176
+ beforeLoad: async (table) => {
177
+ const client = await pool.connect();
178
+ try {
179
+ await client.query("SET lock_timeout = '15s'");
180
+ try {
181
+ await client.query(`TRUNCATE TABLE ${quoteIdent(schema)}.${quoteIdent(table.name)}`);
182
+ }
183
+ catch {
184
+ await client.query(`DELETE FROM ${quoteIdent(schema)}.${quoteIdent(table.name)}`);
185
+ }
186
+ }
187
+ finally {
188
+ client.release();
189
+ }
190
+ },
191
+ createTable: async (table) => {
192
+ const defs = table.columns.map((c) => {
193
+ let d = `${quoteIdent(c.name)} ${midToPgType(c)}`;
194
+ if (!c.nullable)
195
+ d += ' NOT NULL';
196
+ if (c.default != null) {
197
+ const expr = safeDefaultExpr(c.default);
198
+ if (expr != null)
199
+ d += ` DEFAULT ${expr}`;
200
+ }
201
+ return d;
202
+ });
203
+ const pkCols = table.columns.filter((c) => c.primaryKey).map((c) => quoteIdent(c.name));
204
+ const pk = pkCols.length > 0 ? `, PRIMARY KEY (${pkCols.join(', ')})` : '';
205
+ await pool.query(`CREATE TABLE ${quoteIdent(schema)}.${quoteIdent(table.name)} (${defs.join(', ')}${pk})`);
206
+ },
207
+ writeBatch: async (table, rows) => {
208
+ if (rows.length === 0)
209
+ return 0;
210
+ const cols = table.columns;
211
+ const width = cols.length;
212
+ for (const r of rows) {
213
+ if (r.length !== width) {
214
+ throw new Error(`行宽不匹配:期望 ${width} 列,实际 ${r.length} 列(表 ${table.name})`);
215
+ }
216
+ }
217
+ // ON CONFLICT 是 PG 系官方冲突语法(9.5+,GaussDB 兼容);replace 依赖主键做冲突判定
218
+ const pkCols = cols.filter((c) => c.primaryKey).map((c) => quoteIdent(c.name));
219
+ let action = '';
220
+ if (writeMode === 'ignore') {
221
+ action = ' ON CONFLICT DO NOTHING';
222
+ }
223
+ else if (writeMode === 'replace') {
224
+ if (pkCols.length === 0)
225
+ throw new Error(`replace 模式需要主键:表 ${table.name} 无主键`);
226
+ const updaters = cols
227
+ .filter((c) => !c.primaryKey)
228
+ .map((c) => `${quoteIdent(c.name)} = EXCLUDED.${quoteIdent(c.name)}`)
229
+ .join(', ');
230
+ action = ` ON CONFLICT (${pkCols.join(', ')}) DO UPDATE SET ${updaters}`;
231
+ }
232
+ const tbl = `${quoteIdent(schema)}.${quoteIdent(table.name)}`;
233
+ const colList = cols.map((c) => quoteIdent(c.name)).join(', ');
234
+ // 多行 VALUES 单语句(10-50x 于逐行):pg 扩展协议硬上限 65535 绑定参数,
235
+ // 每语句行数 = min(批大小, floor(65535/列数));事务包裹不变
236
+ const perStmt = Math.max(1, Math.floor(65535 / cols.length));
237
+ const client = await pool.connect();
238
+ try {
239
+ await client.query('BEGIN');
240
+ try {
241
+ let written = 0;
242
+ for (let i = 0; i < rows.length; i += perStmt) {
243
+ const chunk = rows.slice(i, i + perStmt);
244
+ const holders = chunk
245
+ .map((_, ri) => `(${cols.map((_, ci) => `$${ri * cols.length + ci + 1}`).join(', ')})`)
246
+ .join(', ');
247
+ const sql = `INSERT INTO ${tbl} (${colList}) VALUES ${holders}${action}`;
248
+ const res = await client.query(sql, chunk.flatMap((r) => r.map(toPgBind)));
249
+ written += res.rowCount ?? 0;
250
+ }
251
+ await client.query('COMMIT');
252
+ return written;
253
+ }
254
+ catch (e) {
255
+ await client.query('ROLLBACK').catch(() => { });
256
+ throw e;
257
+ }
258
+ }
259
+ finally {
260
+ client.release();
261
+ }
262
+ },
263
+ close: async () => {
264
+ await pool.end();
265
+ },
266
+ };
267
+ }
268
+ /** 传输专用独立池:建池前置与浏览适配器同源(pgLikePoolCfg:URL 规范化剥 jdbc:、
269
+ * 多主机逐台探测选台——驱动不能透传 jdbc: 与多台 connectionString,见 pg-like.ts);
270
+ * max 4 面向分片并行,避免占满服务器连接 */
271
+ async function openPgLikePool(kind, driver, rc) {
272
+ const { cfg } = await pgLikePoolCfg(kind, driver, rc, 4);
273
+ // pgLocate 跨库时覆盖的 fields.database 必须经 pgCfgWithDatabase 生效:驱动
274
+ // ConnectionParameters 按 Object.assign({}, config, parse(connectionString)) 合并,
275
+ // 直接叠加 database 字段会被 URL 里的原库名覆盖(静默连回原连接的库)
276
+ const db = rc.fields?.database != null ? String(rc.fields.database) : undefined;
277
+ const pool = new driver.Pool(db != null ? pgCfgWithDatabase(cfg, db) : cfg);
278
+ // 会话时区固定 UTC:timestamptz 列接收中间格式的无时区 UTC 墙钟串(mysql 源读出
279
+ // 形态)时按会话 TimeZone 解释,不钉死则目标库时区 ≠ UTC 静默偏移(同 oralike/
280
+ // mysql 读写端的会话前置约定)。同连接内部查询串行,SET 先于后续语句生效
281
+ pool.on('connect', (client) => {
282
+ void client.query("SET TIME ZONE '+00:00'").catch(() => { });
283
+ });
284
+ // pg 约定:无监听器的 pool error 会崩进程(pg-like.ts setupPool 同款兜底)
285
+ pool.on('error', () => { });
286
+ return pool;
287
+ }
288
+ function pgSchemaOf(rc) {
289
+ return rc.fields && typeof rc.fields.schema === 'string' && rc.fields.schema ? rc.fields.schema : 'public';
290
+ }
291
+ /** 定位解析:schema = loc.schema(非法标识符在 assertIdent 拒绝)?? 连接 schema;
292
+ * database ≠ 连接库时浅拷贝 rc 仅覆盖 fields.database(等价 pg-like poolFor
293
+ * 「同一份 cfg 仅覆盖库名」)。注意驱动 ConnectionParameters 按
294
+ * Object.assign({}, config, parse(connectionString)) 合并——connectionString 解析值
295
+ * 覆盖显式字段,故 URL 形态的跨库覆盖在 openPgLikePool 经 pgCfgWithDatabase
296
+ * 改写 URL path 段生效,此处只负责把目标库写进 fields.database */
297
+ export function pgLocate(rc, loc) {
298
+ const schema = loc?.schema != null ? assertIdent(loc.schema, '模式名') : pgSchemaOf(rc);
299
+ const cur = rc.fields?.database != null ? String(rc.fields.database) : undefined;
300
+ if (!loc?.database || loc.database === cur)
301
+ return { rc, schema };
302
+ return { rc: { ...rc, fields: { ...rc.fields, database: loc.database } }, schema };
303
+ }
304
+ export async function createPgTransferReaderFromConn(kind, driver, rc, loc) {
305
+ const located = pgLocate(rc, loc);
306
+ return createPgTransferReader(kind, await openPgLikePool(kind, driver, located.rc), located.schema);
307
+ }
308
+ export async function createPgTransferWriterFromConn(kind, driver, rc, writeMode, loc) {
309
+ const located = pgLocate(rc, loc);
310
+ return createPgTransferWriter(kind, await openPgLikePool(kind, driver, located.rc), located.schema, writeMode);
311
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * SQLite 传输读写端。
3
+ *
4
+ * 持 openRaw 的原始句柄(绕过浏览路径的 normalizeCell——BLOB 截断 / bigint 降位),
5
+ * bigint 经驱动的 readBigInts/safeIntegers 开关保真读取。sqlite 无主键表不做
6
+ * rowid 分片(评审结论:rowid 不在目标列、WITHOUT ROWID 表无此伪列),降级
7
+ * offset 单 worker 顺序读。
8
+ */
9
+ import { assertIdent, quoteIdent } from '../../sql-shared/common.js';
10
+ import { openRaw } from '../../sqlite/index.js';
11
+ import { safeDefaultExpr } from '../model.js';
12
+ /** 声明类型 → 中间类型。先按 SQLite 官方亲和性规则(INT/CHAR·TEXT/BLOB·空/REAL),
13
+ * 再按生态惯例特判日期关键词(sqlite 无日期类型,TEXT 存 ISO 字符串)。
14
+ * TEXT/CLOB 亲和归 'text'(值无长度上限,映射 'string' 会在目标端建成
15
+ * VARCHAR(255)——超长值 strict 报错、非 strict 静默截断);带声明长度的
16
+ * CHAR/VARCHAR 归 'string',长度由调用方从声明的 (N) 解析。 */
17
+ function declaredToMid(declared) {
18
+ const t = declared.toUpperCase();
19
+ if (t.includes('INT'))
20
+ return 'bigint'; // sqlite INTEGER 即 int64,一律按 bigint 防窄列截断
21
+ if (t.includes('CHAR') || t.includes('CLOB') || t.includes('TEXT')) {
22
+ if (t.includes('DATE'))
23
+ return 'date';
24
+ if (t.includes('DATETIME') || t.includes('TIMESTAMP'))
25
+ return 'datetime';
26
+ if (!t.includes('CHAR'))
27
+ return 'text'; // CLOB/TEXT/LONGTEXT 等无界文本
28
+ return 'string';
29
+ }
30
+ if (t.includes('BLOB') || t.trim() === '')
31
+ return 'bytes';
32
+ if (t.includes('REAL') || t.includes('FLOA') || t.includes('DOUB'))
33
+ return 'double';
34
+ if (t.includes('TIME'))
35
+ return 'datetime';
36
+ if (t.includes('DATE'))
37
+ return 'date';
38
+ return 'decimal'; // NUMERIC 亲和(含 DECIMAL/NUMBER/未知声明)
39
+ }
40
+ const SQLITE_TYPE_BY_MID = {
41
+ int: 'INTEGER',
42
+ bigint: 'INTEGER',
43
+ decimal: 'NUMERIC',
44
+ float: 'REAL',
45
+ double: 'REAL',
46
+ boolean: 'INTEGER',
47
+ string: 'TEXT',
48
+ text: 'TEXT',
49
+ json: 'TEXT',
50
+ bytes: 'BLOB',
51
+ date: 'TEXT',
52
+ time: 'TEXT',
53
+ datetime: 'TEXT',
54
+ timestamptz: 'TEXT',
55
+ };
56
+ function statementFor(db, sql) {
57
+ const stmt = db.prepare(sql);
58
+ // bigint 保真按驱动开关(失败不影响:SQLite 返回 number,>2^53 场景才有差)
59
+ try {
60
+ stmt.setReadBigInts?.(true);
61
+ stmt.safeIntegers?.(true);
62
+ }
63
+ catch {
64
+ // 驱动无该开关时保持默认
65
+ }
66
+ return stmt;
67
+ }
68
+ export function createSqliteTransferReader(db) {
69
+ const describe = async (table) => {
70
+ const tbl = assertIdent(table, '表名');
71
+ const raw = db.prepare(`PRAGMA table_info(${quoteIdent(tbl)})`).all();
72
+ if (raw.length === 0)
73
+ throw new Error(`表不存在: ${tbl}`);
74
+ const pkCount = raw.filter((r) => Number(r.pk) > 0).length;
75
+ const columns = raw.map((r) => {
76
+ const declared = String(r.type ?? '');
77
+ const isPk = Number(r.pk) > 0;
78
+ const midType = declaredToMid(declared);
79
+ // string 的声明长度(VARCHAR(N)/CHAR(N))→ 目标端按 N 建列,避免默认 255 截断
80
+ const declaredLen = /\((\d+)\)/.exec(declared);
81
+ return {
82
+ name: String(r.name),
83
+ midType,
84
+ sourceType: declared,
85
+ nullable: Number(r.notnull) === 0 && !isPk,
86
+ primaryKey: isPk,
87
+ // 单列整型主键 = rowid 别名,天然自增(WITHOUT ROWID 表误标无害:sqlite 写端不用该标记)
88
+ autoIncrement: isPk && pkCount === 1 && midType === 'bigint',
89
+ ...(midType === 'string' && declaredLen ? { length: Number(declaredLen[1]) } : {}),
90
+ default: r.dflt_value == null ? null : String(r.dflt_value),
91
+ };
92
+ });
93
+ return { name: tbl, columns };
94
+ };
95
+ return {
96
+ kind: 'sqlite',
97
+ listTables: async () => {
98
+ const raw = db
99
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
100
+ .all();
101
+ return raw.map((r) => String(r.name));
102
+ },
103
+ describe,
104
+ count: async (table) => {
105
+ const r = db.prepare(`SELECT COUNT(*) AS n FROM ${quoteIdent(assertIdent(table, '表名'))}`).get();
106
+ return Number(r?.n ?? 0);
107
+ },
108
+ shardKey: async (table) => {
109
+ const t = await describe(table);
110
+ const pks = t.columns.filter((c) => c.primaryKey);
111
+ const pk = pks.length === 1 ? pks[0] : undefined;
112
+ const sharable = ['int', 'bigint', 'decimal', 'string', 'datetime', 'timestamptz'];
113
+ if (pk && sharable.includes(pk.midType)) {
114
+ return pk.midType;
115
+ }
116
+ return null;
117
+ },
118
+ readBatch: async (table, cursor, limit) => {
119
+ const t = await describe(table);
120
+ const tbl = quoteIdent(t.name);
121
+ const cols = t.columns.map((c) => quoteIdent(c.name)).join(', ');
122
+ let stmt;
123
+ if (cursor.mode === 'keyset') {
124
+ const key = quoteIdent(cursor.column);
125
+ stmt =
126
+ cursor.after === null
127
+ ? statementFor(db, `SELECT ${cols} FROM ${tbl} ORDER BY ${key} LIMIT ?`)
128
+ : statementFor(db, `SELECT ${cols} FROM ${tbl} WHERE ${key} > ? ORDER BY ${key} LIMIT ?`);
129
+ const raw = cursor.after === null
130
+ ? stmt.all(limit)
131
+ : stmt.all(cursor.after, limit);
132
+ return raw.map((r) => t.columns.map((c) => r[c.name] ?? null));
133
+ }
134
+ if (cursor.mode === 'range') {
135
+ const key = quoteIdent(cursor.column);
136
+ const conds = [];
137
+ const binds = [];
138
+ if (cursor.lo !== null) {
139
+ conds.push(`${key} ${cursor.loInclusive ? '>=' : '>'} ?`);
140
+ binds.push(cursor.lo);
141
+ }
142
+ if (cursor.hi !== null) {
143
+ conds.push(`${key} < ?`);
144
+ binds.push(cursor.hi);
145
+ }
146
+ const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
147
+ stmt = statementFor(db, `SELECT ${cols} FROM ${tbl}${where} ORDER BY ${key} LIMIT ?`);
148
+ const raw = stmt.all(...binds, limit);
149
+ return raw.map((r) => t.columns.map((c) => r[c.name] ?? null));
150
+ }
151
+ stmt = statementFor(db, `SELECT ${cols} FROM ${tbl} LIMIT ? OFFSET ?`);
152
+ const raw = stmt.all(limit, cursor.offset);
153
+ return raw.map((r) => t.columns.map((c) => r[c.name] ?? null));
154
+ },
155
+ keyAt: async (table, column, offset) => {
156
+ const tbl = quoteIdent(assertIdent(table, '表名'));
157
+ const key = quoteIdent(assertIdent(column, '分片键'));
158
+ const rows = db
159
+ .prepare(`SELECT ${key} AS k FROM ${tbl} ORDER BY ${key} LIMIT 1 OFFSET ?`)
160
+ .all(offset);
161
+ return (rows[0]?.k ?? null);
162
+ },
163
+ close: async () => {
164
+ db.close();
165
+ },
166
+ };
167
+ }
168
+ export function createSqliteTransferWriter(db, writeMode = 'insert') {
169
+ const prefix = writeMode === 'ignore' ? 'INSERT OR IGNORE INTO ' : writeMode === 'replace' ? 'INSERT OR REPLACE INTO ' : 'INSERT INTO ';
170
+ return {
171
+ kind: 'sqlite',
172
+ tableExists: async (name) => {
173
+ const r = db.prepare(`SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name);
174
+ return Number(r?.n ?? 0) > 0;
175
+ },
176
+ dropTable: async (name) => {
177
+ db.exec(`DROP TABLE ${quoteIdent(assertIdent(name, '表名'))}`);
178
+ },
179
+ // truncate 模式前置清空:SQLite 官方无 TRUNCATE 语句,DELETE FROM 即全表清空
180
+ beforeLoad: async (table) => {
181
+ db.exec(`DELETE FROM ${quoteIdent(table.name)}`);
182
+ },
183
+ createTable: async (table) => {
184
+ const defs = table.columns.map((c) => {
185
+ let d = `${quoteIdent(c.name)} ${SQLITE_TYPE_BY_MID[c.midType]}`;
186
+ if (!c.nullable)
187
+ d += ' NOT NULL';
188
+ if (c.default != null) {
189
+ const expr = safeDefaultExpr(c.default);
190
+ if (expr != null)
191
+ d += ` DEFAULT ${expr}`;
192
+ }
193
+ return d;
194
+ });
195
+ // 主键用表级约束(支持复合主键;内联每列 PK 会在复合主键时报 Multiple primary key)
196
+ const pkCols = table.columns.filter((c) => c.primaryKey).map((c) => quoteIdent(c.name));
197
+ const pk = pkCols.length > 0 ? `, PRIMARY KEY (${pkCols.join(', ')})` : '';
198
+ db.exec(`CREATE TABLE ${quoteIdent(table.name)} (${defs.join(', ')}${pk})`);
199
+ },
200
+ writeBatch: async (table, rows) => {
201
+ if (rows.length === 0)
202
+ return 0;
203
+ const cols = table.columns;
204
+ const width = cols.length;
205
+ for (const r of rows) {
206
+ if (r.length !== width) {
207
+ throw new Error(`行宽不匹配:期望 ${width} 列,实际 ${r.length} 列(表 ${table.name})`);
208
+ }
209
+ }
210
+ // INSERT OR REPLACE 无唯一键时等同纯 INSERT(官方语义)——静默重复必须拦截
211
+ if (writeMode === 'replace' && !table.columns.some((c) => c.primaryKey)) {
212
+ throw new Error(`replace 模式需要主键:表 ${table.name} 无主键`);
213
+ }
214
+ const sql = `${prefix}${quoteIdent(table.name)} (${cols.map((c) => quoteIdent(c.name)).join(', ')}) VALUES (${cols.map(() => '?').join(', ')})`;
215
+ const stmt = db.prepare(sql);
216
+ let written = 0;
217
+ db.exec('BEGIN IMMEDIATE');
218
+ try {
219
+ for (const r of rows) {
220
+ const info = stmt.run(...r);
221
+ written += Number(info.changes ?? 0);
222
+ }
223
+ db.exec('COMMIT');
224
+ }
225
+ catch (e) {
226
+ try {
227
+ db.exec('ROLLBACK');
228
+ }
229
+ catch {
230
+ // 回滚失败时保留原始错误
231
+ }
232
+ throw e;
233
+ }
234
+ return written;
235
+ },
236
+ // sqlite 单列整型主键即 rowid 别名,目标端自增无需处理
237
+ close: async () => {
238
+ db.close();
239
+ },
240
+ };
241
+ }
242
+ /** 从连接配置创建(transfer/index.ts 分发入口用) */
243
+ export async function createSqliteTransferReaderFromConn(rc) {
244
+ const { db } = await openRaw(rc, true);
245
+ return createSqliteTransferReader(db);
246
+ }
247
+ export async function createSqliteTransferWriterFromConn(rc, writeMode) {
248
+ const { db } = await openRaw(rc, false);
249
+ return createSqliteTransferWriter(db, writeMode);
250
+ }
@@ -0,0 +1,77 @@
1
+ /** 源库 DEFAULT 表达式白名单:纯数字/单引号字符串/NULL/CURRENT_TIMESTAMP 才内联进
2
+ * 目标 DDL(default 文本来自源库系统表,白名单防意外表达式注入),其余丢弃。 */
3
+ export function safeDefaultExpr(raw) {
4
+ const s = raw.trim();
5
+ if (/^NULL$/i.test(s) || /^CURRENT_TIMESTAMP$/i.test(s))
6
+ return s.toUpperCase();
7
+ if (/^-?\d+(\.\d+)?$/.test(s))
8
+ return s;
9
+ if (/^'([^']|'')*'$/.test(s))
10
+ return s;
11
+ return null;
12
+ }
13
+ /** 分发:按 kind 创建读写端。mongo 在 kinds/mongo.ts;未注册的 kind 给出明确错误 */
14
+ export async function createTransferReader(rc, loc) {
15
+ const kind = rc.meta.kind;
16
+ if (kind === 'sqlite') {
17
+ const { createSqliteTransferReaderFromConn } = await import('./kinds/sqlite.js');
18
+ return createSqliteTransferReaderFromConn(rc); // sqlite 无层级,loc 忽略
19
+ }
20
+ if (kind === 'mysql') {
21
+ const { createMysqlTransferReaderFromConn } = await import('./kinds/mysql.js');
22
+ return createMysqlTransferReaderFromConn(rc, loc);
23
+ }
24
+ if (kind === 'postgresql' || kind === 'gaussdb') {
25
+ const { createPgTransferReaderFromConn } = await import('./kinds/pg.js');
26
+ const driver = kind === 'postgresql'
27
+ ? (await import('../postgresql/index.js')).getPgDriver()
28
+ : (await import('../gaussdb/index.js')).loadGaussDriver();
29
+ return createPgTransferReaderFromConn(kind, driver, rc, loc);
30
+ }
31
+ if (kind === 'oracle' || kind === 'dmdb') {
32
+ const oralike = await import('./kinds/oralike.js');
33
+ // oracledb.Pool / DmPoolLike 与 OraLikePool 结构兼容(驱动最小面),按契约收窄——
34
+ // 与 dmdb 适配器内部 as unknown as DmPoolLike 同款桥接
35
+ const pool = kind === 'oracle'
36
+ ? (await (await import('../oracle/index.js')).openOraclePool(rc))
37
+ : (await (await import('../dmdb/index.js')).openDmPool(rc));
38
+ // owner = 定位第一层(oracle/dm 单库多 schema 模型),缺省连接用户
39
+ return oralike.createOraLikeTransferReader(kind, pool, loc?.database ?? oralike.ownerOfFromConn(rc));
40
+ }
41
+ if (kind === 'mongodb') {
42
+ const { createMongoTransferReaderFromConn } = await import('./kinds/mongo.js');
43
+ return createMongoTransferReaderFromConn(rc, loc);
44
+ }
45
+ throw new Error(`数据传输暂不支持读取 ${kind}(待后续版本支持)`);
46
+ }
47
+ export async function createTransferWriter(rc, writeMode = 'insert', loc) {
48
+ const kind = rc.meta.kind;
49
+ if (kind === 'sqlite') {
50
+ const { createSqliteTransferWriterFromConn } = await import('./kinds/sqlite.js');
51
+ return createSqliteTransferWriterFromConn(rc, writeMode); // sqlite 无层级,loc 忽略
52
+ }
53
+ if (kind === 'mysql') {
54
+ const { createMysqlTransferWriterFromConn } = await import('./kinds/mysql.js');
55
+ return createMysqlTransferWriterFromConn(rc, writeMode, loc);
56
+ }
57
+ if (kind === 'postgresql' || kind === 'gaussdb') {
58
+ const { createPgTransferWriterFromConn } = await import('./kinds/pg.js');
59
+ const driver = kind === 'postgresql'
60
+ ? (await import('../postgresql/index.js')).getPgDriver()
61
+ : (await import('../gaussdb/index.js')).loadGaussDriver();
62
+ return createPgTransferWriterFromConn(kind, driver, rc, writeMode, loc);
63
+ }
64
+ if (kind === 'oracle' || kind === 'dmdb') {
65
+ const oralike = await import('./kinds/oralike.js');
66
+ const pool = kind === 'oracle'
67
+ ? (await (await import('../oracle/index.js')).openOraclePool(rc))
68
+ : (await (await import('../dmdb/index.js')).openDmPool(rc));
69
+ // 写入 owner = 定位第一层,缺省连接用户默认 schema(写端以 owner 前缀显式限定)
70
+ return oralike.createOraLikeTransferWriter(kind, pool, writeMode, loc?.database ?? oralike.ownerOfFromConn(rc));
71
+ }
72
+ if (kind === 'mongodb') {
73
+ const { createMongoTransferWriterFromConn } = await import('./kinds/mongo.js');
74
+ return createMongoTransferWriterFromConn(rc, writeMode, loc);
75
+ }
76
+ throw new Error(`数据传输暂不支持写入 ${kind}(待后续版本支持)`);
77
+ }