@xeplr/actions 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/index.js +51 -0
- package/lib/builtins/db/fetch.js +142 -0
- package/lib/builtins/db/list-columns.js +39 -0
- package/lib/builtins/db/list-procedures.js +36 -0
- package/lib/builtins/db/list-tables.js +38 -0
- package/lib/builtins/db/list-views.js +35 -0
- package/lib/builtins/db/push.js +107 -0
- package/lib/builtins/db/query.js +5 -0
- package/lib/builtins/file/move.js +8 -0
- package/lib/builtins/file/upload.js +123 -0
- package/lib/builtins/http-request.js +3 -0
- package/lib/builtins/index.js +40 -0
- package/lib/builtins/send-email.js +8 -0
- package/lib/builtins/spawn-program.js +4 -0
- package/lib/drivers/db/index.js +41 -0
- package/lib/drivers/db/mongo.js +8 -0
- package/lib/drivers/db/mssql.js +464 -0
- package/lib/drivers/db/mysql.js +425 -0
- package/lib/drivers/db/postgres.js +372 -0
- package/lib/drivers/file/google.js +8 -0
- package/lib/drivers/file/index.js +42 -0
- package/lib/drivers/file/local.js +68 -0
- package/lib/drivers/file/sftp.js +9 -0
- package/lib/drivers/file/sharepoint.js +9 -0
- package/lib/errors.js +37 -0
- package/lib/formats/_xlsx-stream.js +355 -0
- package/lib/formats/csv.js +54 -0
- package/lib/formats/excel.js +87 -0
- package/lib/formats/index.js +38 -0
- package/lib/formats/json.js +42 -0
- package/lib/formats/txt.js +6 -0
- package/lib/registry.js +75 -0
- package/lib/runner.js +81 -0
- package/lib/streaming/checkpoint.js +46 -0
- package/lib/streaming/index.js +10 -0
- package/lib/streaming/resume.js +35 -0
- package/lib/streaming/spool.js +253 -0
- package/lib/streaming/utils.js +82 -0
- package/lib/uploader/attach.js +69 -0
- package/lib/uploader/index.js +298 -0
- package/lib/uploader/infer.js +76 -0
- package/lib/uploader/meta-store-knex.js +136 -0
- package/lib/uploader/meta-store-noop.js +11 -0
- package/lib/uploader/reconcile.js +88 -0
- package/package.json +90 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// DB driver factory. Each action (fetch/push/query) calls getDriver(type)
|
|
2
|
+
// to get the right driver, then invokes the common driver interface.
|
|
3
|
+
//
|
|
4
|
+
// Adding a new DB type = add a file here + register it below.
|
|
5
|
+
|
|
6
|
+
var DRIVERS = {
|
|
7
|
+
postgres: function() { return require('./postgres'); },
|
|
8
|
+
mysql: function() { return require('./mysql'); },
|
|
9
|
+
mssql: function() { return require('./mssql'); },
|
|
10
|
+
mongo: function() { return require('./mongo'); }
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function getDriver(type) {
|
|
14
|
+
var loader = DRIVERS[type];
|
|
15
|
+
if (!loader) {
|
|
16
|
+
throw new Error('Unknown dbType: "' + type + '". Supported: ' + Object.keys(DRIVERS).join(', '));
|
|
17
|
+
}
|
|
18
|
+
return loader();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check a driver's peer deps and throw a friendly error if any are missing.
|
|
22
|
+
// Called at action execution time (not registration) since the dbType is
|
|
23
|
+
// only known when the Job fires.
|
|
24
|
+
function checkDriverRequires(actionName, dbType, driver) {
|
|
25
|
+
var requires = (driver && driver.requires) || [];
|
|
26
|
+
for (var i = 0; i < requires.length; i++) {
|
|
27
|
+
try { require.resolve(requires[i]); }
|
|
28
|
+
catch (_) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
actionName + ' requires "' + requires[i] + '" for dbType="' + dbType + '". ' +
|
|
31
|
+
'Install with: npm install ' + requires[i]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
getDriver: getDriver,
|
|
39
|
+
checkDriverRequires: checkDriverRequires,
|
|
40
|
+
SUPPORTED: Object.keys(DRIVERS)
|
|
41
|
+
};
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
// MS SQL Server driver — parity with db/postgres.js. SqlQueue owns retries +
|
|
2
|
+
// bisection + abort, so the driver just does connect / query / close and
|
|
3
|
+
// provides the SQL builders the uploader composes into batch INSERTs.
|
|
4
|
+
//
|
|
5
|
+
// Peer dep: mssql (declared optional in @xeplr/actions/package.json).
|
|
6
|
+
//
|
|
7
|
+
// Dialect differences from Postgres worth calling out:
|
|
8
|
+
// • Placeholders are named `@p0, @p1, …` (0-based, matching the order
|
|
9
|
+
// buildInsertSql pushes params). query() binds them via request.input.
|
|
10
|
+
// • Identifiers are bracket-quoted: [name] (embedded ] doubled).
|
|
11
|
+
// • No `CREATE TABLE IF NOT EXISTS` / `IF NOT EXISTS` on index/column, so we
|
|
12
|
+
// guard with `IF OBJECT_ID(...) IS NULL` / `IF COL_LENGTH(...) IS NULL` /
|
|
13
|
+
// `IF NOT EXISTS (SELECT … sys.indexes …)` — all single-batch statements.
|
|
14
|
+
// • The mssql package returns `{ recordset, rowsAffected }` — query()
|
|
15
|
+
// normalizes it to the Postgres-shaped `{ rows, rowCount }`.
|
|
16
|
+
// • HARD LIMIT: SQL Server allows max 2100 parameters per statement AND max
|
|
17
|
+
// 1000 rows per multi-row VALUES. maxParams=2000 keeps BOTH satisfied
|
|
18
|
+
// (worst case: 1 user col → 2 params/row → 1000 rows).
|
|
19
|
+
//
|
|
20
|
+
// Type mapping — deliberately narrow, mirroring the Postgres driver's intent:
|
|
21
|
+
// string → NVARCHAR(MAX)
|
|
22
|
+
// number → DECIMAL(38,10) (38 is SQL Server's max precision; > 10
|
|
23
|
+
// fractional digits are rounded)
|
|
24
|
+
// boolean → BIT
|
|
25
|
+
// date → DATETIME2 (user rule: always datetime, stored UTC)
|
|
26
|
+
// datetime→ DATETIME2
|
|
27
|
+
// object → NVARCHAR(MAX) (SQL Server has no native JSON type)
|
|
28
|
+
// array → NVARCHAR(MAX)
|
|
29
|
+
|
|
30
|
+
var TYPE_MAP = {
|
|
31
|
+
string: 'NVARCHAR(MAX)',
|
|
32
|
+
number: 'DECIMAL(38,10)',
|
|
33
|
+
boolean: 'BIT',
|
|
34
|
+
date: 'DATETIME2',
|
|
35
|
+
datetime: 'DATETIME2',
|
|
36
|
+
object: 'NVARCHAR(MAX)',
|
|
37
|
+
array: 'NVARCHAR(MAX)'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// INFORMATION_SCHEMA DATA_TYPE (lowercase) → our logical types, for reconcile.
|
|
41
|
+
var NATIVE_TO_LOGICAL = {
|
|
42
|
+
varchar: 'string', nvarchar: 'string', char: 'string', nchar: 'string',
|
|
43
|
+
text: 'string', ntext: 'string', uniqueidentifier: 'string', xml: 'string',
|
|
44
|
+
int: 'number', bigint: 'number', smallint: 'number', tinyint: 'number',
|
|
45
|
+
decimal: 'number', numeric: 'number', float: 'number', real: 'number',
|
|
46
|
+
money: 'number', smallmoney: 'number',
|
|
47
|
+
bit: 'boolean',
|
|
48
|
+
datetime: 'datetime', datetime2: 'datetime', smalldatetime: 'datetime',
|
|
49
|
+
date: 'datetime', time: 'datetime', datetimeoffset: 'datetime'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
var INTERNAL_ID_COL = '__xeplr_id__';
|
|
53
|
+
var INTERNAL_MOVEMENT_COL = '__xeplr_movement_id__';
|
|
54
|
+
|
|
55
|
+
// See header note — 2000 satisfies both the 2100-param and 1000-row caps.
|
|
56
|
+
var MAX_PARAMS = 2000;
|
|
57
|
+
|
|
58
|
+
async function connect(config) {
|
|
59
|
+
var sql = require('mssql');
|
|
60
|
+
var pool = new sql.ConnectionPool({
|
|
61
|
+
server: config.host || config.server || 'localhost',
|
|
62
|
+
port: config.port || 1433,
|
|
63
|
+
user: config.user,
|
|
64
|
+
password: config.password,
|
|
65
|
+
database: config.database,
|
|
66
|
+
pool: { max: config.maxConnections || 10 },
|
|
67
|
+
options: Object.assign(
|
|
68
|
+
{ encrypt: true, trustServerCertificate: true },
|
|
69
|
+
config.options || {}
|
|
70
|
+
)
|
|
71
|
+
});
|
|
72
|
+
await pool.connect();
|
|
73
|
+
return pool;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function close(pool) {
|
|
77
|
+
if (pool) await pool.close();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Bind params as @p0..@pN and normalize `{ recordset, rowsAffected }` to
|
|
81
|
+
// Postgres shape. NOTE: mssql infers the SQL type from each JS value; a null
|
|
82
|
+
// with no sibling non-null value in the batch falls back to NVARCHAR, which is
|
|
83
|
+
// fine for our INSERTs since the column type is already declared.
|
|
84
|
+
async function query(pool, sql, params) {
|
|
85
|
+
var request = pool.request();
|
|
86
|
+
params = params || [];
|
|
87
|
+
for (var i = 0; i < params.length; i++) {
|
|
88
|
+
request.input('p' + i, params[i]);
|
|
89
|
+
}
|
|
90
|
+
var r = await request.query(sql);
|
|
91
|
+
var rows = r.recordset || [];
|
|
92
|
+
var rowCount = Array.isArray(r.rowsAffected)
|
|
93
|
+
? r.rowsAffected.reduce(function(a, b) { return a + b; }, 0)
|
|
94
|
+
: rows.length;
|
|
95
|
+
return { rows: rows, rowCount: rowCount };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toMssqlType(type) { return TYPE_MAP[type] || 'NVARCHAR(MAX)'; }
|
|
99
|
+
|
|
100
|
+
function dataTypeToLogical(dataType) {
|
|
101
|
+
return NATIVE_TO_LOGICAL[String(dataType).toLowerCase()] || 'string';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Bracket-quote a SQL Server identifier, escaping embedded closing brackets.
|
|
105
|
+
function quoteIdent(name) {
|
|
106
|
+
return '[' + String(name).replace(/]/g, ']]') + ']';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A string literal for use inside OBJECT_ID(N'…') / COL_LENGTH — single quotes
|
|
110
|
+
// doubled. NOT for values (those are always parameterized).
|
|
111
|
+
function quoteLiteral(s) {
|
|
112
|
+
return "N'" + String(s).replace(/'/g, "''") + "'";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildCreateTableSql(tableName, columns, primaryKeys) {
|
|
116
|
+
var defs = [];
|
|
117
|
+
defs.push(quoteIdent(INTERNAL_ID_COL) + ' BIGINT IDENTITY(1,1) PRIMARY KEY');
|
|
118
|
+
defs.push(quoteIdent(INTERNAL_MOVEMENT_COL) + ' NVARCHAR(255) NOT NULL');
|
|
119
|
+
for (var i = 0; i < columns.length; i++) {
|
|
120
|
+
defs.push(quoteIdent(columns[i].name) + ' ' + columnDef(columns[i], primaryKeys));
|
|
121
|
+
}
|
|
122
|
+
return 'IF OBJECT_ID(' + quoteLiteral(tableName) + ", 'U') IS NULL\n" +
|
|
123
|
+
'CREATE TABLE ' + quoteIdent(tableName) +
|
|
124
|
+
' (\n ' + defs.join(',\n ') + '\n)';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A string PK column can't be NVARCHAR(MAX) — SQL Server rejects LOB types as
|
|
128
|
+
// index key columns. UPSERT's MERGE needs a UNIQUE index on the PK cols, so
|
|
129
|
+
// bound PK string columns to NVARCHAR(255) (510 bytes; composite keys stay
|
|
130
|
+
// under the nonclustered-index key limit). Pre-create the table with a wider
|
|
131
|
+
// bounded NVARCHAR if your keys are longer.
|
|
132
|
+
function columnDef(col, primaryKeys) {
|
|
133
|
+
if (col.type === 'string' && primaryKeys && primaryKeys.indexOf(col.name) !== -1) {
|
|
134
|
+
return 'NVARCHAR(255)';
|
|
135
|
+
}
|
|
136
|
+
return toMssqlType(col.type);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildCreateErrorTableSql(tableName) {
|
|
140
|
+
var errTable = tableName + '_import_errors';
|
|
141
|
+
return 'IF OBJECT_ID(' + quoteLiteral(errTable) + ", 'U') IS NULL\n" +
|
|
142
|
+
'CREATE TABLE ' + quoteIdent(errTable) + ' (\n' +
|
|
143
|
+
' ' + quoteIdent(INTERNAL_ID_COL) + ' BIGINT IDENTITY(1,1) PRIMARY KEY,\n' +
|
|
144
|
+
' movement_id NVARCHAR(255) NOT NULL,\n' +
|
|
145
|
+
' row_num INT,\n' +
|
|
146
|
+
' error_description NVARCHAR(MAX),\n' +
|
|
147
|
+
' underlying_sql NVARCHAR(MAX),\n' +
|
|
148
|
+
' raw_row NVARCHAR(MAX),\n' +
|
|
149
|
+
' recorded_at DATETIME2 DEFAULT SYSUTCDATETIME()\n' +
|
|
150
|
+
')';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// One guarded `ALTER TABLE ADD` per column — idempotent via COL_LENGTH.
|
|
154
|
+
// (SQL Server uses `ADD`, not `ADD COLUMN`.)
|
|
155
|
+
function buildAlterTableAddSql(tableName, columns) {
|
|
156
|
+
return columns.map(function(c) {
|
|
157
|
+
return 'IF COL_LENGTH(' + quoteLiteral(tableName) + ', ' + quoteLiteral(c.name) + ') IS NULL\n' +
|
|
158
|
+
'ALTER TABLE ' + quoteIdent(tableName) +
|
|
159
|
+
' ADD ' + quoteIdent(c.name) + ' ' + toMssqlType(c.type);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Bulk INSERT (UPSERT via MERGE when primaryKeys given). Parameterized with
|
|
164
|
+
// @p0..@pN; values never concatenated into SQL.
|
|
165
|
+
function buildInsertSql(tableName, rows, columns, movementId, primaryKeys) {
|
|
166
|
+
var colNames = columns.map(function(c) { return c.name; });
|
|
167
|
+
var allCols = [INTERNAL_MOVEMENT_COL].concat(colNames);
|
|
168
|
+
|
|
169
|
+
var params = [];
|
|
170
|
+
var groups = [];
|
|
171
|
+
var idx = 0;
|
|
172
|
+
|
|
173
|
+
for (var r = 0; r < rows.length; r++) {
|
|
174
|
+
var row = rows[r];
|
|
175
|
+
var placeholders = ['@p' + idx];
|
|
176
|
+
params.push(movementId); idx++;
|
|
177
|
+
for (var c = 0; c < colNames.length; c++) {
|
|
178
|
+
placeholders.push('@p' + idx);
|
|
179
|
+
params.push(coerceValue(row[colNames[c]], columns[c].type));
|
|
180
|
+
idx++;
|
|
181
|
+
}
|
|
182
|
+
groups.push('(' + placeholders.join(',') + ')');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var quotedCols = allCols.map(quoteIdent);
|
|
186
|
+
|
|
187
|
+
if (primaryKeys && primaryKeys.length > 0) {
|
|
188
|
+
return { sql: buildMergeSql(tableName, quotedCols, colNames, groups, primaryKeys), params: params };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var sql = 'INSERT INTO ' + quoteIdent(tableName) +
|
|
192
|
+
' (' + quotedCols.join(', ') + ') VALUES ' + groups.join(',');
|
|
193
|
+
return { sql: sql, params: params };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// UPSERT for SQL Server. There is no ON CONFLICT / ON DUPLICATE KEY — MERGE is
|
|
197
|
+
// the portable primitive. Source rows come from a VALUES table constructor.
|
|
198
|
+
function buildMergeSql(tableName, quotedCols, colNames, valueGroups, primaryKeys) {
|
|
199
|
+
var target = quoteIdent(tableName);
|
|
200
|
+
var srcCols = quotedCols.join(', ');
|
|
201
|
+
|
|
202
|
+
var onClause = primaryKeys.map(function(pk) {
|
|
203
|
+
return 'T.' + quoteIdent(pk) + ' = S.' + quoteIdent(pk);
|
|
204
|
+
}).join(' AND ');
|
|
205
|
+
|
|
206
|
+
// Non-PK user columns update on match. __xeplr_movement_id__ ALWAYS updates
|
|
207
|
+
// so the latest movement "owns" the row and rollback finds it.
|
|
208
|
+
var updateCols = colNames.filter(function(c) { return primaryKeys.indexOf(c) === -1; });
|
|
209
|
+
var setColumns = updateCols.concat([INTERNAL_MOVEMENT_COL]);
|
|
210
|
+
var setClause = setColumns.map(function(c) {
|
|
211
|
+
return 'T.' + quoteIdent(c) + ' = S.' + quoteIdent(c);
|
|
212
|
+
}).join(', ');
|
|
213
|
+
|
|
214
|
+
var insertCols = quotedCols.join(', ');
|
|
215
|
+
var insertVals = quotedCols.map(function(c) { return 'S.' + c; }).join(', ');
|
|
216
|
+
|
|
217
|
+
var sql =
|
|
218
|
+
'MERGE INTO ' + target + ' AS T\n' +
|
|
219
|
+
'USING (VALUES ' + valueGroups.join(',') + ') AS S (' + srcCols + ')\n' +
|
|
220
|
+
'ON ' + onClause + '\n' +
|
|
221
|
+
(setClause ? 'WHEN MATCHED THEN UPDATE SET ' + setClause + '\n' : '') +
|
|
222
|
+
'WHEN NOT MATCHED THEN INSERT (' + insertCols + ') VALUES (' + insertVals + ')\n' +
|
|
223
|
+
';'; // MERGE must be terminated with a semicolon
|
|
224
|
+
return sql;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Coerce a JS value to the target column's effective type. Mirrors the
|
|
228
|
+
// Postgres driver; datetime is emitted as a UTC string (DATETIME2 parses ISO
|
|
229
|
+
// without the trailing Z). Bad values pass through so SQL Server rejects them
|
|
230
|
+
// and SqlQueue bisects the row into the error table.
|
|
231
|
+
function coerceValue(v, type) {
|
|
232
|
+
if (v === undefined || v === null) return null;
|
|
233
|
+
|
|
234
|
+
switch (type) {
|
|
235
|
+
case 'string':
|
|
236
|
+
if (v instanceof Date) return toMssqlDatetime(v);
|
|
237
|
+
if (Array.isArray(v)) return JSON.stringify(v);
|
|
238
|
+
if (typeof v === 'object') return JSON.stringify(v);
|
|
239
|
+
return String(v);
|
|
240
|
+
|
|
241
|
+
case 'number':
|
|
242
|
+
if (typeof v === 'number') return v;
|
|
243
|
+
if (typeof v === 'string') {
|
|
244
|
+
var n = Number(v);
|
|
245
|
+
return isNaN(n) ? v : n; // non-numeric string → let SQL Server reject
|
|
246
|
+
}
|
|
247
|
+
return v;
|
|
248
|
+
|
|
249
|
+
case 'boolean':
|
|
250
|
+
if (typeof v === 'boolean') return v;
|
|
251
|
+
if (typeof v === 'string') {
|
|
252
|
+
var lower = v.toLowerCase().trim();
|
|
253
|
+
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'y') return true;
|
|
254
|
+
if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'n') return false;
|
|
255
|
+
}
|
|
256
|
+
if (typeof v === 'number') return v !== 0;
|
|
257
|
+
return v; // let SQL Server reject
|
|
258
|
+
|
|
259
|
+
case 'date':
|
|
260
|
+
case 'datetime':
|
|
261
|
+
return toMssqlDatetime(v);
|
|
262
|
+
|
|
263
|
+
case 'object':
|
|
264
|
+
case 'array':
|
|
265
|
+
if (typeof v === 'string') return v; // assume valid JSON string
|
|
266
|
+
return JSON.stringify(v);
|
|
267
|
+
|
|
268
|
+
default:
|
|
269
|
+
return v;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 'YYYY-MM-DD HH:MM:SS.mmm' in UTC — DATETIME2 parses this unambiguously.
|
|
274
|
+
// Unparseable input passes through so SQL Server rejects it (→ error table).
|
|
275
|
+
function toMssqlDatetime(v) {
|
|
276
|
+
var d = (v instanceof Date) ? v : new Date(v);
|
|
277
|
+
if (isNaN(d.getTime())) return v;
|
|
278
|
+
return d.toISOString().slice(0, 23).replace('T', ' ');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function getTableSchema(pool, tableName, schema) {
|
|
282
|
+
var r = await query(pool,
|
|
283
|
+
'SELECT COLUMN_NAME AS name, DATA_TYPE AS dataType ' +
|
|
284
|
+
'FROM INFORMATION_SCHEMA.COLUMNS ' +
|
|
285
|
+
'WHERE TABLE_NAME = @p0 AND (@p1 IS NULL OR TABLE_SCHEMA = @p1) ' +
|
|
286
|
+
'ORDER BY ORDINAL_POSITION',
|
|
287
|
+
[tableName, schema || null]
|
|
288
|
+
);
|
|
289
|
+
return r.rows.map(function(row) {
|
|
290
|
+
return { name: row.name, dataType: row.dataType, udtName: row.dataType };
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Table/view/procedure listing — used by the db-list-* actions. A null/omitted
|
|
295
|
+
// `schema` returns every schema's objects (no dbo-only default) — SQL Server
|
|
296
|
+
// databases commonly spread real tables across several schemas, unlike
|
|
297
|
+
// Postgres/MySQL where a single default schema is the normal case.
|
|
298
|
+
async function listTables(pool, schema) {
|
|
299
|
+
var r = await query(pool,
|
|
300
|
+
'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES ' +
|
|
301
|
+
"WHERE TABLE_TYPE = 'BASE TABLE' AND (@p0 IS NULL OR TABLE_SCHEMA = @p0) " +
|
|
302
|
+
'ORDER BY TABLE_NAME',
|
|
303
|
+
[schema || null]
|
|
304
|
+
);
|
|
305
|
+
return r.rows.map(function(row) { return row.name; });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function listViews(pool, schema) {
|
|
309
|
+
var r = await query(pool,
|
|
310
|
+
'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES ' +
|
|
311
|
+
"WHERE TABLE_TYPE = 'VIEW' AND (@p0 IS NULL OR TABLE_SCHEMA = @p0) " +
|
|
312
|
+
'ORDER BY TABLE_NAME',
|
|
313
|
+
[schema || null]
|
|
314
|
+
);
|
|
315
|
+
return r.rows.map(function(row) { return row.name; });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ROUTINE_TYPE='PROCEDURE' only — excludes FUNCTION, same reasoning as the
|
|
319
|
+
// Postgres driver's listProcedures.
|
|
320
|
+
async function listProcedures(pool, schema) {
|
|
321
|
+
var r = await query(pool,
|
|
322
|
+
'SELECT ROUTINE_NAME AS name FROM INFORMATION_SCHEMA.ROUTINES ' +
|
|
323
|
+
"WHERE ROUTINE_TYPE = 'PROCEDURE' AND (@p0 IS NULL OR ROUTINE_SCHEMA = @p0) " +
|
|
324
|
+
'ORDER BY ROUTINE_NAME',
|
|
325
|
+
[schema || null]
|
|
326
|
+
);
|
|
327
|
+
return r.rows.map(function(row) { return row.name; });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Idempotently ensure __xeplr_movement_id__ exists (guarded single statement).
|
|
331
|
+
async function ensureMovementColumn(pool, tableName) {
|
|
332
|
+
await query(pool,
|
|
333
|
+
'IF COL_LENGTH(@p0, @p1) IS NULL\n' +
|
|
334
|
+
'ALTER TABLE ' + quoteIdent(tableName) +
|
|
335
|
+
' ADD ' + quoteIdent(INTERNAL_MOVEMENT_COL) + ' NVARCHAR(255)',
|
|
336
|
+
[tableName, INTERNAL_MOVEMENT_COL]
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Idempotently ensure the UNIQUE index that backs the MERGE match.
|
|
341
|
+
async function ensureUpsertIndex(pool, tableName, primaryKeys) {
|
|
342
|
+
if (!primaryKeys || primaryKeys.length === 0) return;
|
|
343
|
+
var idxName = tableName + '_upsert_uniq';
|
|
344
|
+
var cols = primaryKeys.map(quoteIdent).join(', ');
|
|
345
|
+
await query(pool,
|
|
346
|
+
'IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = @p0 AND object_id = OBJECT_ID(@p1))\n' +
|
|
347
|
+
'CREATE UNIQUE INDEX ' + quoteIdent(idxName) +
|
|
348
|
+
' ON ' + quoteIdent(tableName) + ' (' + cols + ')',
|
|
349
|
+
[idxName, tableName]
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Stream a result set WITHOUT buffering it in memory — the read primitive
|
|
354
|
+
// db-fetch composes. The mssql package streams via events (`row`/`error`/
|
|
355
|
+
// `done`) rather than a Node Readable, so we bridge those to an async iterator
|
|
356
|
+
// with real backpressure: pause the request once `batchSize` rows are buffered,
|
|
357
|
+
// resume after the consumer drains them.
|
|
358
|
+
//
|
|
359
|
+
// opts: { sql, params?, batchSize? } → async iterable<row>
|
|
360
|
+
async function* fetchStream(pool, opts) {
|
|
361
|
+
var high = Math.max(1, parseInt(opts.batchSize || 1000, 10));
|
|
362
|
+
var request = pool.request();
|
|
363
|
+
var params = opts.params || [];
|
|
364
|
+
for (var i = 0; i < params.length; i++) request.input('p' + i, params[i]);
|
|
365
|
+
request.stream = true;
|
|
366
|
+
|
|
367
|
+
var buffer = [];
|
|
368
|
+
var done = false;
|
|
369
|
+
var error = null;
|
|
370
|
+
var paused = false;
|
|
371
|
+
var wake = null; // resolver for the consumer's wait
|
|
372
|
+
|
|
373
|
+
function signal() { if (wake) { var w = wake; wake = null; w(); } }
|
|
374
|
+
|
|
375
|
+
request.on('row', function(row) {
|
|
376
|
+
buffer.push(row);
|
|
377
|
+
if (buffer.length >= high && !paused) { paused = true; request.pause(); }
|
|
378
|
+
signal();
|
|
379
|
+
});
|
|
380
|
+
request.on('error', function(err) { error = err; signal(); });
|
|
381
|
+
request.on('done', function() { done = true; signal(); });
|
|
382
|
+
|
|
383
|
+
// Fire the query — with stream=true this drives the events above. We don't
|
|
384
|
+
// await the returned promise; the events are the source of truth.
|
|
385
|
+
request.query(opts.sql).catch(function(err) { error = error || err; signal(); });
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
while (true) {
|
|
389
|
+
if (buffer.length) {
|
|
390
|
+
var batch = buffer;
|
|
391
|
+
buffer = [];
|
|
392
|
+
if (paused) { paused = false; request.resume(); }
|
|
393
|
+
for (var j = 0; j < batch.length; j++) yield batch[j];
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (error) throw error;
|
|
397
|
+
if (done) return;
|
|
398
|
+
await new Promise(function(res) { wake = res; });
|
|
399
|
+
}
|
|
400
|
+
} finally {
|
|
401
|
+
// If the consumer breaks out early, stop the underlying request.
|
|
402
|
+
if (!done && !error) { try { request.cancel(); } catch (_) {} }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function rollbackMovement(pool, tableName, movementId) {
|
|
407
|
+
var errTable = tableName + '_import_errors';
|
|
408
|
+
var result = { mainDeleted: 0, errorDeleted: 0 };
|
|
409
|
+
|
|
410
|
+
var r1 = await query(pool,
|
|
411
|
+
'DELETE FROM ' + quoteIdent(tableName) +
|
|
412
|
+
' WHERE ' + quoteIdent(INTERNAL_MOVEMENT_COL) + ' = @p0',
|
|
413
|
+
[movementId]
|
|
414
|
+
);
|
|
415
|
+
result.mainDeleted = r1.rowCount || 0;
|
|
416
|
+
|
|
417
|
+
// Error table might not exist yet — guard with OBJECT_ID so a missing table
|
|
418
|
+
// is a no-op rather than an error (parity with PG's 42P01 swallow).
|
|
419
|
+
var r2 = await query(pool,
|
|
420
|
+
'IF OBJECT_ID(' + quoteLiteral(errTable) + ", 'U') IS NOT NULL\n" +
|
|
421
|
+
'DELETE FROM ' + quoteIdent(errTable) + ' WHERE movement_id = @p0',
|
|
422
|
+
[movementId]
|
|
423
|
+
);
|
|
424
|
+
result.errorDeleted = r2.rowCount || 0;
|
|
425
|
+
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
module.exports = {
|
|
430
|
+
requires: ['mssql'],
|
|
431
|
+
maxParams: MAX_PARAMS,
|
|
432
|
+
|
|
433
|
+
// Connection lifecycle
|
|
434
|
+
connect: connect,
|
|
435
|
+
query: query,
|
|
436
|
+
close: close,
|
|
437
|
+
|
|
438
|
+
// SQL builders (pure functions, no DB access)
|
|
439
|
+
toMssqlType: toMssqlType,
|
|
440
|
+
dataTypeToLogical: dataTypeToLogical,
|
|
441
|
+
quoteIdent: quoteIdent,
|
|
442
|
+
buildCreateTableSql: buildCreateTableSql,
|
|
443
|
+
buildCreateErrorTableSql: buildCreateErrorTableSql,
|
|
444
|
+
buildAlterTableAddSql: buildAlterTableAddSql,
|
|
445
|
+
buildInsertSql: buildInsertSql,
|
|
446
|
+
|
|
447
|
+
// Idempotent DDL helpers (async — guarded single-batch statements)
|
|
448
|
+
ensureMovementColumn: ensureMovementColumn,
|
|
449
|
+
ensureUpsertIndex: ensureUpsertIndex,
|
|
450
|
+
|
|
451
|
+
// Streaming read primitive (used by db-fetch)
|
|
452
|
+
fetchStream: fetchStream,
|
|
453
|
+
|
|
454
|
+
// Introspection (used by uploader.reconcile + the db-list-* actions) + rollback
|
|
455
|
+
getTableSchema: getTableSchema,
|
|
456
|
+
listTables: listTables,
|
|
457
|
+
listViews: listViews,
|
|
458
|
+
listProcedures: listProcedures,
|
|
459
|
+
rollbackMovement: rollbackMovement,
|
|
460
|
+
|
|
461
|
+
// Constants
|
|
462
|
+
INTERNAL_ID_COL: INTERNAL_ID_COL,
|
|
463
|
+
INTERNAL_MOVEMENT_COL: INTERNAL_MOVEMENT_COL
|
|
464
|
+
};
|