db-model-router 1.0.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/.env +7 -0
- package/LICENSE +201 -0
- package/README.md +505 -0
- package/docker-compose.yml +141 -0
- package/docs/README.md +208 -0
- package/docs/SKILL.md +202 -0
- package/docs/adapters/cockroachdb.md +49 -0
- package/docs/adapters/dynamodb.md +53 -0
- package/docs/adapters/mongodb.md +56 -0
- package/docs/adapters/mssql.md +55 -0
- package/docs/adapters/oracle.md +52 -0
- package/docs/adapters/postgres.md +50 -0
- package/docs/adapters/redis.md +53 -0
- package/docs/adapters/sqlite3.md +43 -0
- package/package.json +109 -0
- package/src/cli/generate-app.js +359 -0
- package/src/cli/generate-model.js +760 -0
- package/src/cli/generate-openapi.js +237 -0
- package/src/cli/generate-route.js +346 -0
- package/src/cockroachdb/db.js +563 -0
- package/src/commons/function.js +165 -0
- package/src/commons/model.js +444 -0
- package/src/commons/route.js +214 -0
- package/src/commons/validator.js +172 -0
- package/src/dynamodb/db.js +552 -0
- package/src/index.js +57 -0
- package/src/mongodb/db.js +381 -0
- package/src/mssql/db.js +461 -0
- package/src/mysql/db.js +527 -0
- package/src/oracle/db.js +855 -0
- package/src/oracle/sql_translator.js +406 -0
- package/src/postgres/db.js +666 -0
- package/src/postgres/ddl_translator.js +69 -0
- package/src/postgres/sql_translator.js +396 -0
- package/src/redis/db.js +448 -0
- package/src/serve.js +90 -0
- package/src/sqlite3/db.js +346 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL → Oracle SQL Translation Layer
|
|
3
|
+
* Preprocesses SQL strings before execution to convert common MySQL patterns to Oracle equivalents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MYSQL_TO_ORACLE_DATE_FORMATS = {
|
|
7
|
+
'%Y': 'YYYY', '%y': 'YY', '%m': 'MM', '%d': 'DD',
|
|
8
|
+
'%H': 'HH24', '%h': 'HH', '%i': 'MI', '%s': 'SS',
|
|
9
|
+
'%p': 'AM', '%M': 'MONTH', '%b': 'MON', '%W': 'DAY', '%a': 'DY',
|
|
10
|
+
'%j': 'DDD', '%T': 'HH24:MI:SS', '%r': 'HH:MI:SS AM',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function convertDateFormat(mysqlFmt) {
|
|
14
|
+
let oracleFmt = mysqlFmt;
|
|
15
|
+
const keys = Object.keys(MYSQL_TO_ORACLE_DATE_FORMATS).sort((a, b) => b.length - a.length);
|
|
16
|
+
for (const k of keys) {
|
|
17
|
+
oracleFmt = oracleFmt.split(k).join(MYSQL_TO_ORACLE_DATE_FORMATS[k]);
|
|
18
|
+
}
|
|
19
|
+
return oracleFmt;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ORACLE_RESERVED = new Set(['access','add','all','alter','and','any','as','asc','audit','between','by','char','check','cluster','column','comment','compress','connect','create','current','date','decimal','default','delete','desc','distinct','drop','else','exclusive','exists','file','float','for','from','grant','group','having','identified','immediate','in','increment','index','initial','insert','integer','intersect','into','is','level','like','lock','long','maxextents','minus','mlslabel','mode','modify','noaudit','nocompress','not','nowait','null','number','of','offline','on','online','option','or','order','pctfree','prior','public','raw','rename','resource','revoke','row','rowid','rownum','rows','select','session','set','share','size','smallint','start','successful','synonym','sysdate','table','then','to','trigger','type','uid','union','unique','update','user','validate','values','varchar','varchar2','view','whenever','where','with']);
|
|
23
|
+
|
|
24
|
+
function stripBackticks(sql) {
|
|
25
|
+
return sql.replace(/`([^`]+)`/g, (_, name) => {
|
|
26
|
+
const clean = name.replace(/\s+/g, '_');
|
|
27
|
+
return ORACLE_RESERVED.has(clean.toLowerCase()) ? '"' + clean.toUpperCase() + '"' : clean;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Parse balanced-paren args for FUNC_NAME(...) starting at the open paren. */
|
|
32
|
+
function parseFuncArgs(s, argsStart) {
|
|
33
|
+
let depth = 1, i = argsStart, args = [], argStart = argsStart;
|
|
34
|
+
while (i < s.length && depth > 0) {
|
|
35
|
+
if (s[i] === '(') depth++;
|
|
36
|
+
else if (s[i] === ')') { depth--; if (depth === 0) { args.push(s.slice(argStart, i).trim()); break; } }
|
|
37
|
+
else if (s[i] === ',' && depth === 1) { args.push(s.slice(argStart, i).trim()); argStart = i + 1; }
|
|
38
|
+
else if (s[i] === "'") { i++; while (i < s.length && s[i] !== "'") i++; }
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
return { args, end: i };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Replace a named function call using balanced-paren arg parsing. Calls replacer(args) → string|null. */
|
|
45
|
+
function replaceFuncCall(sql, funcName, replacer) {
|
|
46
|
+
const re = new RegExp('\\b' + funcName + '\\s*\\(', 'i');
|
|
47
|
+
let result = '';
|
|
48
|
+
while (true) {
|
|
49
|
+
const m = re.exec(sql);
|
|
50
|
+
if (!m) { result += sql; break; }
|
|
51
|
+
result += sql.slice(0, m.index);
|
|
52
|
+
const { args, end } = parseFuncArgs(sql, m.index + m[0].length);
|
|
53
|
+
const processedArgs = args.map(a => replaceFuncCall(a, funcName, replacer));
|
|
54
|
+
const replacement = replacer(processedArgs);
|
|
55
|
+
if (replacement != null) {
|
|
56
|
+
result += replacement;
|
|
57
|
+
} else {
|
|
58
|
+
result += m[0] + processedArgs.join(', ') + ')';
|
|
59
|
+
}
|
|
60
|
+
sql = sql.slice(end + 1);
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function translateFunctions(sql) {
|
|
66
|
+
// NOW() → SYSDATE
|
|
67
|
+
sql = sql.replace(/\bNOW\s*\(\s*\)/gi, 'SYSDATE');
|
|
68
|
+
|
|
69
|
+
// CURRENT_TIMESTAMP (not in DEFAULT context) → SYSTIMESTAMP
|
|
70
|
+
sql = sql.replace(/\bCURRENT_TIMESTAMP\b(?!\s+ON)/gi, 'SYSTIMESTAMP');
|
|
71
|
+
|
|
72
|
+
// IFNULL(a, b) → NVL(a, b)
|
|
73
|
+
sql = sql.replace(/\bIFNULL\s*\(/gi, 'NVL(');
|
|
74
|
+
|
|
75
|
+
// GROUP_CONCAT(col SEPARATOR ',') → LISTAGG(col, ',') WITHIN GROUP (ORDER BY col)
|
|
76
|
+
sql = sql.replace(
|
|
77
|
+
/\bGROUP_CONCAT\s*\(\s*([^)]+?)\s+SEPARATOR\s+'([^']*)'\s*\)/gi,
|
|
78
|
+
(_, col, sep) => `LISTAGG(${col.trim()}, '${sep}') WITHIN GROUP (ORDER BY ${col.trim()})`
|
|
79
|
+
);
|
|
80
|
+
// GROUP_CONCAT(col) → LISTAGG(col, ',') WITHIN GROUP (ORDER BY col)
|
|
81
|
+
sql = sql.replace(
|
|
82
|
+
/\bGROUP_CONCAT\s*\(\s*([^)]+?)\s*\)/gi,
|
|
83
|
+
(_, col) => `LISTAGG(${col.trim()}, ',') WITHIN GROUP (ORDER BY ${col.trim()})`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// UNIX_TIMESTAMP()*1000 → Oracle epoch millis
|
|
87
|
+
sql = sql.replace(
|
|
88
|
+
/\bUNIX_TIMESTAMP\s*\(\s*\)\s*\*\s*1000/gi,
|
|
89
|
+
"((CAST(SYS_EXTRACT_UTC(SYSTIMESTAMP) AS DATE) - DATE '1970-01-01') * 86400000)"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// CAST(x AS UNSIGNED) → CAST(x AS NUMBER)
|
|
93
|
+
sql = sql.replace(/\bCAST\s*\((.+?)\s+AS\s+UNSIGNED\s*\)/gi, 'CAST($1 AS NUMBER)');
|
|
94
|
+
// CAST(x AS SIGNED) → CAST(x AS NUMBER)
|
|
95
|
+
sql = sql.replace(/\bCAST\s*\((.+?)\s+AS\s+SIGNED\s*\)/gi, 'CAST($1 AS NUMBER)');
|
|
96
|
+
|
|
97
|
+
// CURDATE() → TRUNC(SYSDATE)
|
|
98
|
+
sql = sql.replace(/\bCURDATE\s*\(\s*\)/gi, 'TRUNC(SYSDATE)');
|
|
99
|
+
|
|
100
|
+
// FROM_UNIXTIME(expr, fmt) → TO_CHAR(TO_DATE('1970-01-01','YYYY-MM-DD') + expr/86400, fmt)
|
|
101
|
+
// FROM_UNIXTIME(expr) → TO_DATE('1970-01-01','YYYY-MM-DD') + expr/86400
|
|
102
|
+
sql = replaceFuncCall(sql, 'FROM_UNIXTIME', (args) => {
|
|
103
|
+
if (args.length === 2) {
|
|
104
|
+
const fmt = args[1].match(/^'([^']*)'$/);
|
|
105
|
+
return fmt
|
|
106
|
+
? `TO_CHAR(TO_DATE('1970-01-01','YYYY-MM-DD') + (${args[0]})/86400, '${convertDateFormat(fmt[1])}')`
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
if (args.length === 1) return `(TO_DATE('1970-01-01','YYYY-MM-DD') + (${args[0]})/86400)`;
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// JSON_ARRAY_APPEND(arr, '$', val) → append val to JSON array
|
|
114
|
+
// Oracle: use string manipulation to append to JSON array
|
|
115
|
+
sql = replaceFuncCall(sql, 'JSON_ARRAY_APPEND', (args) => {
|
|
116
|
+
if (args.length === 3) {
|
|
117
|
+
const arr = args[0], val = args[2];
|
|
118
|
+
return `CASE WHEN ${arr} IS NULL OR ${arr} = '[]' THEN '[' || ${val} || ']' ELSE SUBSTR(${arr}, 1, LENGTH(${arr})-1) || ',' || ${val} || ']' END`;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// JSON_ARRAY() → '[]'
|
|
124
|
+
sql = sql.replace(/\bJSON_ARRAY\s*\(\s*\)/gi, "'[]'");
|
|
125
|
+
|
|
126
|
+
// CAST(? AS JSON) → ?
|
|
127
|
+
sql = sql.replace(/\bCAST\s*\(\s*\?\s+AS\s+JSON\s*\)/gi, '?');
|
|
128
|
+
|
|
129
|
+
// LAST_INSERT_ID() — not directly supported, handled by db.js RETURNING clause
|
|
130
|
+
sql = sql.replace(/\bLAST_INSERT_ID\s*\(\s*\)/gi, '0');
|
|
131
|
+
|
|
132
|
+
return sql;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function translateLimit(sql) {
|
|
136
|
+
// LIMIT n OFFSET m → OFFSET m ROWS FETCH NEXT n ROWS ONLY
|
|
137
|
+
sql = sql.replace(
|
|
138
|
+
/\bLIMIT\s+(\d+)\s+OFFSET\s+(\d+)/gi,
|
|
139
|
+
(_, limit, offset) => `OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY`
|
|
140
|
+
);
|
|
141
|
+
// LIMIT n,m (MySQL alternate syntax: LIMIT offset, count)
|
|
142
|
+
sql = sql.replace(
|
|
143
|
+
/\bLIMIT\s+(\d+)\s*,\s*(\d+)/gi,
|
|
144
|
+
(_, offset, limit) => `OFFSET ${offset} ROWS FETCH NEXT ${limit} ROWS ONLY`
|
|
145
|
+
);
|
|
146
|
+
// LIMIT n (no offset) → FETCH FIRST n ROWS ONLY
|
|
147
|
+
sql = sql.replace(
|
|
148
|
+
/\bLIMIT\s+(\d+)(?!\s+OFFSET)\b/gi,
|
|
149
|
+
(_, limit) => `FETCH FIRST ${limit} ROWS ONLY`
|
|
150
|
+
);
|
|
151
|
+
// Handle placeholder-based LIMIT ? OFFSET ?
|
|
152
|
+
sql = sql.replace(
|
|
153
|
+
/\bLIMIT\s+\?\s+OFFSET\s+\?/gi,
|
|
154
|
+
'OFFSET ? ROWS FETCH NEXT ? ROWS ONLY'
|
|
155
|
+
);
|
|
156
|
+
sql = sql.replace(
|
|
157
|
+
/\bLIMIT\s+\?\s*,\s*\?/gi,
|
|
158
|
+
'OFFSET ? ROWS FETCH NEXT ? ROWS ONLY'
|
|
159
|
+
);
|
|
160
|
+
// LIMIT ? (single placeholder) → FETCH FIRST ? ROWS ONLY
|
|
161
|
+
sql = sql.replace(
|
|
162
|
+
/\bLIMIT\s+\?(?!\s*,)(?!\s+OFFSET)/gi,
|
|
163
|
+
'FETCH FIRST ? ROWS ONLY'
|
|
164
|
+
);
|
|
165
|
+
return sql;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Convert INTERVAL arithmetic to Oracle date arithmetic.
|
|
169
|
+
* Oracle uses: date + N (days), date + N/24 (hours), date + N/1440 (minutes), date + N/86400 (seconds)
|
|
170
|
+
*/
|
|
171
|
+
function intervalToOracle(expr, n, unit, op) {
|
|
172
|
+
const u = unit.toUpperCase();
|
|
173
|
+
switch (u) {
|
|
174
|
+
case 'SECOND': return `(${expr} ${op} (${n}/86400))`;
|
|
175
|
+
case 'MINUTE': return `(${expr} ${op} (${n}/1440))`;
|
|
176
|
+
case 'HOUR': return `(${expr} ${op} (${n}/24))`;
|
|
177
|
+
case 'DAY': return `(${expr} ${op} ${n})`;
|
|
178
|
+
case 'WEEK': return `(${expr} ${op} (${n}*7))`;
|
|
179
|
+
case 'MONTH': return `ADD_MONTHS(${expr}, ${op === '+' ? n : '-' + n})`;
|
|
180
|
+
case 'YEAR': return `ADD_MONTHS(${expr}, ${op === '+' ? n + '*12' : '-' + n + '*12'})`;
|
|
181
|
+
default: return `(${expr} ${op} ${n})`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function translateDateFunctions(sql) {
|
|
186
|
+
// DATE_FORMAT(col, '%Y-%m-%d') → TO_CHAR(col, 'YYYY-MM-DD')
|
|
187
|
+
// Use balanced-paren parsing to handle nested calls like DATE_FORMAT(STR_TO_DATE(...), '%Y-%m-%d')
|
|
188
|
+
sql = replaceFuncCall(sql, 'DATE_FORMAT', (args) => {
|
|
189
|
+
if (args.length !== 2) return null;
|
|
190
|
+
const fmt = args[1].match(/^'([^']*)'$/);
|
|
191
|
+
return fmt ? `TO_CHAR(${args[0]}, '${convertDateFormat(fmt[1])}')` : null;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// STR_TO_DATE(str, '%Y-%m-%d') → TO_DATE(str, 'YYYY-MM-DD')
|
|
195
|
+
sql = replaceFuncCall(sql, 'STR_TO_DATE', (args) => {
|
|
196
|
+
if (args.length !== 2) return null;
|
|
197
|
+
const fmt = args[1].match(/^'([^']*)'$/);
|
|
198
|
+
return fmt ? `TO_DATE(${args[0]}, '${convertDateFormat(fmt[1])}')` : null;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// DATE_ADD(expr, INTERVAL n UNIT) → Oracle arithmetic (handles any expr, not just NOW())
|
|
202
|
+
sql = sql.replace(
|
|
203
|
+
/\bDATE_ADD\s*\(\s*(.+?)\s*,\s*INTERVAL\s+(\?|\d+)\s+(SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)\s*\)/gi,
|
|
204
|
+
(_, expr, n, unit) => intervalToOracle(expr.trim(), n, unit, '+')
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// DATE_SUB(expr, INTERVAL n UNIT) → Oracle arithmetic (handles any expr, not just NOW())
|
|
208
|
+
sql = sql.replace(
|
|
209
|
+
/\bDATE_SUB\s*\(\s*(.+?)\s*,\s*INTERVAL\s+(\?|\d+)\s+(SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)\s*\)/gi,
|
|
210
|
+
(_, expr, n, unit) => intervalToOracle(expr.trim(), n, unit, '-')
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Bare INTERVAL: expr +/- INTERVAL n UNIT (without DATE_ADD/DATE_SUB wrapper)
|
|
214
|
+
sql = sql.replace(
|
|
215
|
+
/(\+|-)\s*INTERVAL\s+(\?|\d+)\s+(SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)\b/gi,
|
|
216
|
+
(_, op, n, unit) => {
|
|
217
|
+
const u = unit.toUpperCase();
|
|
218
|
+
switch (u) {
|
|
219
|
+
case 'SECOND': return `${op} (${n}/86400)`;
|
|
220
|
+
case 'MINUTE': return `${op} (${n}/1440)`;
|
|
221
|
+
case 'HOUR': return `${op} (${n}/24)`;
|
|
222
|
+
case 'DAY': return `${op} ${n}`;
|
|
223
|
+
case 'WEEK': return `${op} (${n}*7)`;
|
|
224
|
+
default: return `${op} ${n}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return sql;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function translateJsonFunctions(sql) {
|
|
233
|
+
// JSON_UNQUOTE(expr) → expr (Oracle JSON_VALUE already returns unquoted text)
|
|
234
|
+
sql = replaceFuncCall(sql, 'JSON_UNQUOTE', (args) => args.length === 1 ? args[0] : null);
|
|
235
|
+
// JSON_SET(col, '$.key', val) → JSON_MERGEPATCH(col, '{"key": ' || val || '}')
|
|
236
|
+
// For simple single-key updates; nested paths use dot notation
|
|
237
|
+
sql = replaceFuncCall(sql, 'JSON_SET', (args) => {
|
|
238
|
+
if (args.length < 3 || args.length % 2 === 0) return null;
|
|
239
|
+
let result = args[0];
|
|
240
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
241
|
+
const pathMatch = args[i].match(/^['"]?\$\.([^'"]+)['"]?$/);
|
|
242
|
+
if (!pathMatch) return null;
|
|
243
|
+
const key = pathMatch[1];
|
|
244
|
+
const val = args[i + 1];
|
|
245
|
+
result = `JSON_MERGEPATCH(${result}, '{"${key}":' || ${val} || '}')`;
|
|
246
|
+
}
|
|
247
|
+
return result;
|
|
248
|
+
});
|
|
249
|
+
// JSON_EXTRACT(col, '$.key') → JSON_VALUE(col, '$.key')
|
|
250
|
+
sql = sql.replace(/\bJSON_EXTRACT\s*\(/gi, 'JSON_VALUE(');
|
|
251
|
+
|
|
252
|
+
// JSON_OBJECT('key', val, ...) → JSON_OBJECT(KEY 'key' VALUE val, ...)
|
|
253
|
+
sql = replaceFuncCall(sql, 'JSON_OBJECT', (args) => {
|
|
254
|
+
if (args.length === 0) return 'JSON_OBJECT()';
|
|
255
|
+
if (args.length % 2 !== 0) return null;
|
|
256
|
+
const pairs = [];
|
|
257
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
258
|
+
pairs.push(`KEY ${args[i]} VALUE ${args[i + 1]}`);
|
|
259
|
+
}
|
|
260
|
+
return `JSON_OBJECT(${pairs.join(', ')})`;
|
|
261
|
+
});
|
|
262
|
+
// JSON_CONTAINS(col, val, '$.path') → JSON_EXISTS(col, '$.path')
|
|
263
|
+
sql = sql.replace(
|
|
264
|
+
/\bJSON_CONTAINS\s*\(\s*([^,]+?)\s*,\s*[^,]+?\s*,\s*'\$\.([^']+)'\s*\)/gi,
|
|
265
|
+
(_, col, path) => `JSON_EXISTS(${col.trim()}, '$.${path}')`
|
|
266
|
+
);
|
|
267
|
+
// col->>'$.path' → JSON_VALUE(col, '$.path') — handle alias.col pattern
|
|
268
|
+
// Also handle double-quoted "$.path" (MySQL JSON shorthand): convert to single-quoted for Oracle
|
|
269
|
+
sql = sql.replace(/((?:\w+\.)?\w+)\s*->>\s*"(\$\.[^"]+)"/g, "JSON_VALUE($1, '$2')");
|
|
270
|
+
sql = sql.replace(/((?:\w+\.)?\w+)\s*->>\s*('[^']*')/g, 'JSON_VALUE($1, $2)');
|
|
271
|
+
// col->'$.path' → JSON_VALUE(col, '$.path')
|
|
272
|
+
sql = sql.replace(/((?:\w+\.)?\w+)\s*->\s*"(\$\.[^"]+)"/g, "JSON_VALUE($1, '$2')");
|
|
273
|
+
sql = sql.replace(/((?:\w+\.)?\w+)\s*->\s*('[^']*')/g, 'JSON_VALUE($1, $2)');
|
|
274
|
+
// Fix COALESCE type mismatch: COALESCE(JSON_VALUE(...), 60) → COALESCE(JSON_VALUE(...), '60')
|
|
275
|
+
// But don't fix when inside CAST (e.g. CAST(COALESCE(JSON_VALUE(...), 0) + 1 AS NUMBER))
|
|
276
|
+
sql = sql.replace(/COALESCE\s*\(\s*JSON_VALUE\(([^)]+)\)\s*,\s*(\d+)\s*\)/gi,
|
|
277
|
+
(match, jv, num, offset) => {
|
|
278
|
+
// Check if this COALESCE is inside a CAST
|
|
279
|
+
const before = sql.slice(Math.max(0, offset - 50), offset);
|
|
280
|
+
if (/CAST\s*\(\s*$/i.test(before)) return match;
|
|
281
|
+
return `COALESCE(JSON_VALUE(${jv}), '${num}')`;
|
|
282
|
+
});
|
|
283
|
+
// WHERE 1 (MySQL truthy) → WHERE 1=1
|
|
284
|
+
sql = sql.replace(/\bWHERE\s+1\b(?!\s*=)/gi, 'WHERE 1=1');
|
|
285
|
+
return sql;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Translate INSERT ... ON DUPLICATE KEY UPDATE → MERGE INTO
|
|
290
|
+
*/
|
|
291
|
+
function translateOnDuplicateKey(sql) {
|
|
292
|
+
const match = sql.match(
|
|
293
|
+
/INSERT\s+INTO\s+(\w+)\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)\s+(?:AS\s+(\w+)\s+)?ON\s+DUPLICATE\s+KEY\s+UPDATE\s+(.+)$/i
|
|
294
|
+
);
|
|
295
|
+
if (!match) return sql;
|
|
296
|
+
|
|
297
|
+
const [, table, colStr, valuesStr, alias, updateClause] = match;
|
|
298
|
+
const columns = colStr.split(',').map(c => c.trim());
|
|
299
|
+
const values = valuesStr.split(',').map(v => v.trim());
|
|
300
|
+
|
|
301
|
+
// Parse update assignments: "col = V.col" or "col = VALUES(col)"
|
|
302
|
+
const updates = updateClause.split(',').map(a => {
|
|
303
|
+
const m = a.trim().match(/(\w+)\s*=\s*(?:\w+\.(\w+)|VALUES\s*\(\s*(\w+)\s*\))/i);
|
|
304
|
+
return m ? m[1].trim() : null;
|
|
305
|
+
}).filter(Boolean);
|
|
306
|
+
|
|
307
|
+
// ON clause columns = all columns NOT in the update set
|
|
308
|
+
const updateSet = new Set(updates);
|
|
309
|
+
const onColumns = columns.filter(c => !updateSet.has(c));
|
|
310
|
+
|
|
311
|
+
// Build MERGE with inline values (not bind placeholders)
|
|
312
|
+
const usingCols = columns.map((c, i) => `${values[i]} AS ${c}`).join(', ');
|
|
313
|
+
const onClause = onColumns.map(k => `t.${k} = s.${k}`).join(' AND ');
|
|
314
|
+
const updateSetSql = updates.map(c => `t.${c} = s.${c}`).join(', ');
|
|
315
|
+
const insertCols = columns.join(', ');
|
|
316
|
+
const insertVals = columns.map(c => `s.${c}`).join(', ');
|
|
317
|
+
|
|
318
|
+
return `MERGE INTO ${table} t USING (SELECT ${usingCols} FROM DUAL) s ON (${onClause})` +
|
|
319
|
+
(updateSetSql ? ` WHEN MATCHED THEN UPDATE SET ${updateSetSql}` : '') +
|
|
320
|
+
` WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals})`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* MySQL: UPDATE t1 [AS a] INNER JOIN t2 [AS b] ON cond SET ... WHERE ...
|
|
325
|
+
* Oracle: MERGE INTO t1 a USING t2 b ON (cond AND whereCond) WHEN MATCHED THEN UPDATE SET ...
|
|
326
|
+
*/
|
|
327
|
+
function translateUpdateJoin(sql) {
|
|
328
|
+
const m = sql.match(
|
|
329
|
+
/^(\s*)UPDATE\s+(\w+)(\s+AS\s+(\w+))?\s+INNER\s+JOIN\s+(\w+)(\s+AS\s+(\w+))?\s+ON\s+([\s\S]+?)\s+SET\s+([\s\S]+?)\s+WHERE\s+([\s\S]+)$/i
|
|
330
|
+
);
|
|
331
|
+
if (!m) return sql;
|
|
332
|
+
const [, ws, t1, , a1, t2, , a2, onCond, setCols, whereCond] = m;
|
|
333
|
+
const alias1 = a1 || t1;
|
|
334
|
+
const alias2 = a2 || t2;
|
|
335
|
+
// Strip table alias prefix from SET columns
|
|
336
|
+
const cleanSet = setCols.replace(new RegExp('\\b' + alias1 + '\\.', 'g'), alias1 + '.');
|
|
337
|
+
return `${ws}MERGE INTO ${t1} ${alias1} USING ${t2} ${alias2} ON (${onCond.trim()} AND ${whereCond.trim()}) WHEN MATCHED THEN UPDATE SET ${cleanSet}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function translateInsertIgnore(sql) {
|
|
341
|
+
// INSERT IGNORE INTO table (cols) VALUES (...) →
|
|
342
|
+
// Wrap in PL/SQL block: BEGIN INSERT INTO ... ; EXCEPTION WHEN DUP_VAL_ON_INDEX THEN NULL; END;
|
|
343
|
+
const match = sql.match(/\bINSERT\s+IGNORE\s+INTO\s+([\s\S]+)$/i);
|
|
344
|
+
if (match) {
|
|
345
|
+
return `BEGIN INSERT INTO ${match[1].replace(/;\s*$/, '')}; EXCEPTION WHEN DUP_VAL_ON_INDEX THEN NULL; END;`;
|
|
346
|
+
}
|
|
347
|
+
return sql;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function translate(sql) {
|
|
351
|
+
if (!sql || typeof sql !== "string") return sql;
|
|
352
|
+
// Escape hatch: skip translation for native Oracle SQL
|
|
353
|
+
if (sql.trimStart().startsWith("/* ORACLE_NATIVE */")) return sql;
|
|
354
|
+
|
|
355
|
+
sql = stripBackticks(sql);
|
|
356
|
+
sql = translateUpdateJoin(sql);
|
|
357
|
+
sql = translateDateFunctions(sql); // Must run before translateFunctions (which converts NOW() to SYSDATE)
|
|
358
|
+
sql = translateFunctions(sql);
|
|
359
|
+
sql = translateLimit(sql);
|
|
360
|
+
sql = translateJsonFunctions(sql);
|
|
361
|
+
sql = translateOnDuplicateKey(sql);
|
|
362
|
+
sql = translateInsertIgnore(sql);
|
|
363
|
+
// Quote Oracle reserved words used as column aliases
|
|
364
|
+
// Strategy: replace all "as word" then restore those inside CAST(... AS type)
|
|
365
|
+
sql = sql.replace(
|
|
366
|
+
/\bas\s+(user|date|number|level|comment|size|type)\b/gi,
|
|
367
|
+
(m, word) => `as "${word}"`
|
|
368
|
+
);
|
|
369
|
+
// Restore CAST(... as "TYPE") back to CAST(... AS TYPE)
|
|
370
|
+
// Use balanced-paren search to handle any nesting depth
|
|
371
|
+
function fixCastQuoting(s) {
|
|
372
|
+
const re = /\bCAST\s*\(/gi;
|
|
373
|
+
let result = '', lastEnd = 0, m;
|
|
374
|
+
while ((m = re.exec(s)) !== null) {
|
|
375
|
+
result += s.slice(lastEnd, m.index);
|
|
376
|
+
let depth = 1, i = m.index + m[0].length;
|
|
377
|
+
while (i < s.length && depth > 0) {
|
|
378
|
+
if (s[i] === '(') depth++;
|
|
379
|
+
else if (s[i] === ')') depth--;
|
|
380
|
+
else if (s[i] === "'") { i++; while (i < s.length && s[i] !== "'") i++; }
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
383
|
+
const inner = s.slice(m.index + m[0].length, i - 1);
|
|
384
|
+
const asMatch = inner.match(/^([\s\S]+)\s+as\s+"(\w+)"$/i);
|
|
385
|
+
if (asMatch) {
|
|
386
|
+
result += `CAST(${asMatch[1]} AS ${asMatch[2]})`;
|
|
387
|
+
} else {
|
|
388
|
+
result += s.slice(m.index, i);
|
|
389
|
+
}
|
|
390
|
+
lastEnd = i;
|
|
391
|
+
re.lastIndex = result.length; // adjust for next search
|
|
392
|
+
}
|
|
393
|
+
result += s.slice(lastEnd);
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
sql = fixCastQuoting(sql);
|
|
397
|
+
// Remove trailing semicolons (Oracle doesn't want them in execute())
|
|
398
|
+
// But preserve semicolons inside PL/SQL blocks (BEGIN...END;)
|
|
399
|
+
if (!/\bBEGIN\b/i.test(sql)) {
|
|
400
|
+
sql = sql.replace(/;\s*$/, "");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return sql;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = { translate, stripBackticks, translateFunctions, translateLimit, translateDateFunctions, translateJsonFunctions, translateOnDuplicateKey, translateInsertIgnore, convertDateFormat };
|