@rex0220/kintone-sql-tools 1.1.1 → 1.2.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/README.md +190 -190
- package/dist-cli/ksql.js +477 -376
- package/dist-mcp/ksql-mcp.js +37830 -0
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +19 -6
package/dist-cli/ksql.js
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
shouldExitOnEmpty: () => shouldExitOnEmpty
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_fs2 = require("fs");
|
|
38
38
|
var import_path = require("path");
|
|
39
39
|
var import_readline = require("readline");
|
|
40
40
|
var import_os = require("os");
|
|
@@ -3299,6 +3299,62 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
|
|
|
3299
3299
|
};
|
|
3300
3300
|
}
|
|
3301
3301
|
|
|
3302
|
+
// src/core/optimization/wherePredicatePushdown.ts
|
|
3303
|
+
function extractTableCondition(where, tableAlias) {
|
|
3304
|
+
switch (where.type) {
|
|
3305
|
+
case "BINARY":
|
|
3306
|
+
if (!isSingleTableField(where.left, tableAlias)) return null;
|
|
3307
|
+
if (!isPushDownableRight(where.right)) return null;
|
|
3308
|
+
return where;
|
|
3309
|
+
case "NULL_CHECK":
|
|
3310
|
+
if (!isSingleTableField(where.field, tableAlias)) return null;
|
|
3311
|
+
return where;
|
|
3312
|
+
case "LOGICAL":
|
|
3313
|
+
if (where.op === "AND") {
|
|
3314
|
+
const left = extractTableCondition(where.left, tableAlias);
|
|
3315
|
+
const right = extractTableCondition(where.right, tableAlias);
|
|
3316
|
+
if (left && right) return { ...where, left, right };
|
|
3317
|
+
return left ?? right ?? null;
|
|
3318
|
+
}
|
|
3319
|
+
return referencesOnlyTable(where, tableAlias) ? where : null;
|
|
3320
|
+
case "NOT":
|
|
3321
|
+
case "GROUP":
|
|
3322
|
+
return referencesOnlyTable(where, tableAlias) ? where : null;
|
|
3323
|
+
case "EXISTS":
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
function isSingleTableField(field, tableAlias) {
|
|
3328
|
+
if (field.type !== "FIELD") return false;
|
|
3329
|
+
return field.tableAlias === tableAlias;
|
|
3330
|
+
}
|
|
3331
|
+
function isPushDownableRight(value) {
|
|
3332
|
+
switch (value.type) {
|
|
3333
|
+
case "STRING":
|
|
3334
|
+
case "NUMBER":
|
|
3335
|
+
case "KINTONE_FUNC":
|
|
3336
|
+
case "IN_LIST":
|
|
3337
|
+
return true;
|
|
3338
|
+
default:
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
function referencesOnlyTable(expr, tableAlias) {
|
|
3343
|
+
switch (expr.type) {
|
|
3344
|
+
case "BINARY":
|
|
3345
|
+
return isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
|
|
3346
|
+
case "NULL_CHECK":
|
|
3347
|
+
return isSingleTableField(expr.field, tableAlias);
|
|
3348
|
+
case "LOGICAL":
|
|
3349
|
+
return referencesOnlyTable(expr.left, tableAlias) && referencesOnlyTable(expr.right, tableAlias);
|
|
3350
|
+
case "NOT":
|
|
3351
|
+
case "GROUP":
|
|
3352
|
+
return referencesOnlyTable(expr.expr, tableAlias);
|
|
3353
|
+
case "EXISTS":
|
|
3354
|
+
return false;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3302
3358
|
// src/engine/process.ts
|
|
3303
3359
|
function flatten(record, alias) {
|
|
3304
3360
|
const row = {};
|
|
@@ -3998,7 +4054,21 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
3998
4054
|
const parallel = options.fetchParallel ?? 1;
|
|
3999
4055
|
await resolveSubqueries(stmt.where, client, options, cacheContext);
|
|
4000
4056
|
await resolveSubqueries(stmt.having, client, options, cacheContext);
|
|
4001
|
-
const
|
|
4057
|
+
const tableConditions = /* @__PURE__ */ new Map();
|
|
4058
|
+
if (stmt.where !== null) {
|
|
4059
|
+
if (stmt.from.alias) {
|
|
4060
|
+
const cond = extractTableCondition(stmt.where, stmt.from.alias);
|
|
4061
|
+
if (cond) tableConditions.set(stmt.from.alias, cond);
|
|
4062
|
+
}
|
|
4063
|
+
for (const join2 of stmt.joins) {
|
|
4064
|
+
if (join2.table.alias) {
|
|
4065
|
+
const cond = extractTableCondition(stmt.where, join2.table.alias);
|
|
4066
|
+
if (cond) tableConditions.set(join2.table.alias, cond);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
|
|
4071
|
+
const mainFetch = fetchTableRecordsForFullScan(
|
|
4002
4072
|
stmt,
|
|
4003
4073
|
stmt.from,
|
|
4004
4074
|
client,
|
|
@@ -4006,11 +4076,39 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
4006
4076
|
parallel,
|
|
4007
4077
|
true,
|
|
4008
4078
|
options.onLimitReached ?? "error",
|
|
4009
|
-
warnings
|
|
4079
|
+
warnings,
|
|
4080
|
+
mainPushDown
|
|
4010
4081
|
);
|
|
4082
|
+
const parallelJoins = [];
|
|
4083
|
+
const onOptJoins = [];
|
|
4084
|
+
for (const join2 of stmt.joins) {
|
|
4085
|
+
const jCond = join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
|
|
4086
|
+
if (jCond !== null) {
|
|
4087
|
+
parallelJoins.push({
|
|
4088
|
+
join: join2,
|
|
4089
|
+
promise: fetchTableRecordsForFullScan(
|
|
4090
|
+
stmt,
|
|
4091
|
+
join2.table,
|
|
4092
|
+
client,
|
|
4093
|
+
maxRecords,
|
|
4094
|
+
parallel,
|
|
4095
|
+
false,
|
|
4096
|
+
options.onLimitReached ?? "error",
|
|
4097
|
+
warnings,
|
|
4098
|
+
jCond
|
|
4099
|
+
)
|
|
4100
|
+
});
|
|
4101
|
+
} else {
|
|
4102
|
+
onOptJoins.push(join2);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
const mainRecords = await mainFetch;
|
|
4011
4106
|
const tables = /* @__PURE__ */ new Map();
|
|
4012
4107
|
tables.set(stmt.from.alias, mainRecords);
|
|
4013
|
-
const
|
|
4108
|
+
for (const { join: join2, promise } of parallelJoins) {
|
|
4109
|
+
tables.set(join2.table.alias, await promise);
|
|
4110
|
+
}
|
|
4111
|
+
await Promise.all(onOptJoins.map(async (join2) => {
|
|
4014
4112
|
const optimized = await tryFetchJoinRecordsBySourceKeys(
|
|
4015
4113
|
stmt,
|
|
4016
4114
|
join2,
|
|
@@ -4019,7 +4117,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
4019
4117
|
maxRecords,
|
|
4020
4118
|
parallel,
|
|
4021
4119
|
options.onLimitReached ?? "error",
|
|
4022
|
-
warnings
|
|
4120
|
+
warnings,
|
|
4121
|
+
null
|
|
4023
4122
|
);
|
|
4024
4123
|
const joinRecords = optimized ?? await fetchTableRecordsForFullScan(
|
|
4025
4124
|
stmt,
|
|
@@ -4029,11 +4128,11 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
4029
4128
|
parallel,
|
|
4030
4129
|
false,
|
|
4031
4130
|
options.onLimitReached ?? "error",
|
|
4032
|
-
warnings
|
|
4131
|
+
warnings,
|
|
4132
|
+
null
|
|
4033
4133
|
);
|
|
4034
4134
|
tables.set(join2.table.alias, joinRecords);
|
|
4035
|
-
});
|
|
4036
|
-
await Promise.all(joinFetches);
|
|
4135
|
+
}));
|
|
4037
4136
|
const scalarCache = await resolveScalarColumns(stmt.columns, client, options, cacheContext);
|
|
4038
4137
|
const optionOrders = await buildOptionOrdersForSelect(stmt, client, cacheContext);
|
|
4039
4138
|
const sortKinds = await buildSortKindsForSelect(stmt, client, cacheContext);
|
|
@@ -4246,13 +4345,15 @@ function processRowToKintoneRecord(row) {
|
|
|
4246
4345
|
Object.entries(row).map(([k, v]) => [k, { value: v ?? "" }])
|
|
4247
4346
|
);
|
|
4248
4347
|
}
|
|
4249
|
-
async function fetchTableRecordsForFullScan(stmt, table, client, maxRecords, parallel, isMainTable, onLimit, warnings) {
|
|
4348
|
+
async function fetchTableRecordsForFullScan(stmt, table, client, maxRecords, parallel, isMainTable, onLimit, warnings, pushDownCond = null) {
|
|
4250
4349
|
const fields = selectToFetchAllFields(stmt, table);
|
|
4251
4350
|
const onTruncate = (max) => {
|
|
4252
4351
|
warnings.add(`\u53D6\u5F97\u4E0A\u9650\uFF08${max} \u4EF6\uFF09\u306B\u9054\u3057\u305F\u305F\u3081\u3001${max} \u4EF6\u3067\u6253\u3061\u5207\u3063\u3066\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002`);
|
|
4253
4352
|
};
|
|
4254
4353
|
if (!table.subtableCode) {
|
|
4255
|
-
const
|
|
4354
|
+
const baseQuery = isMainTable ? selectToFetchAllParams(stmt, table.appId).query : "";
|
|
4355
|
+
const pushQuery = pushDownCond !== null ? whereToKintone(pushDownCond) : "";
|
|
4356
|
+
const query = baseQuery && pushQuery ? `(${baseQuery}) and (${pushQuery})` : baseQuery || pushQuery;
|
|
4256
4357
|
const resolved = await fetchRecordsForSharedPlan(client.getRecords, table.appId, query, fields, {
|
|
4257
4358
|
parallel,
|
|
4258
4359
|
maxRecords,
|
|
@@ -4287,7 +4388,7 @@ function splitChunks(items, size) {
|
|
|
4287
4388
|
var JOIN_IN_CHUNK_SIZE = 50;
|
|
4288
4389
|
var JOIN_IN_MAX_CHUNKS = 6;
|
|
4289
4390
|
var JOIN_IN_MAX_KEYS = JOIN_IN_CHUNK_SIZE * JOIN_IN_MAX_CHUNKS;
|
|
4290
|
-
async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxRecords, parallel, onLimit, warnings) {
|
|
4391
|
+
async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxRecords, parallel, onLimit, warnings, pushDownCond = null) {
|
|
4291
4392
|
if (join2.type !== "INNER") return null;
|
|
4292
4393
|
if (!join2.table.alias) return null;
|
|
4293
4394
|
if (join2.table.subtableCode) return null;
|
|
@@ -4320,7 +4421,7 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
|
|
|
4320
4421
|
if (values.length === 0) return [];
|
|
4321
4422
|
if (values.length > JOIN_IN_MAX_KEYS) {
|
|
4322
4423
|
warnings.add(
|
|
4323
|
-
`JOIN\u30AD\u30FC\u304C ${values.length} \u4EF6\u306E\u305F\u3081
|
|
4424
|
+
`JOIN\u30AD\u30FC\u304C ${values.length} \u4EF6\u306E\u305F\u3081 ON \u6700\u9069\u5316\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3001JOIN\u5148\u3092\u5168\u4EF6\u53D6\u5F97\u3057\u307E\u3059\uFF08\u4E0A\u9650 ${JOIN_IN_MAX_KEYS} \u4EF6\uFF09\u3002`
|
|
4324
4425
|
);
|
|
4325
4426
|
return null;
|
|
4326
4427
|
}
|
|
@@ -4332,7 +4433,8 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
|
|
|
4332
4433
|
const merged = [];
|
|
4333
4434
|
const seen = /* @__PURE__ */ new Set();
|
|
4334
4435
|
for (const chunk2 of chunks) {
|
|
4335
|
-
const
|
|
4436
|
+
const inClause = `${joinField} in (${chunk2.map(sqlQuote).join(",")})`;
|
|
4437
|
+
const query = pushDownCond !== null ? `(${inClause}) and (${whereToKintone(pushDownCond)})` : inClause;
|
|
4336
4438
|
const resolved = await fetchRecordsForSharedPlan(client.getRecords, join2.table.appId, query, fields, {
|
|
4337
4439
|
parallel,
|
|
4338
4440
|
maxRecords,
|
|
@@ -5148,19 +5250,21 @@ function buildSelectPlan(stmt, label) {
|
|
|
5148
5250
|
lines.push(` kintone query: ${params.query || "(\u306A\u3057)"}`);
|
|
5149
5251
|
lines.push(` fields: ${params.fields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : params.fields.join(", ")}`);
|
|
5150
5252
|
} else {
|
|
5151
|
-
const mainParams = selectToFetchAllParams(stmt, stmt.from.appId);
|
|
5152
5253
|
const mainFields = selectToFetchAllFields(stmt, stmt.from);
|
|
5153
|
-
const
|
|
5154
|
-
const
|
|
5155
|
-
|
|
5254
|
+
const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
|
|
5255
|
+
const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
|
|
5256
|
+
const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
|
|
5257
|
+
lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
|
|
5156
5258
|
lines.push(` kintone query: ${mainQ}`);
|
|
5157
5259
|
lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
|
|
5158
5260
|
for (const join2 of stmt.joins) {
|
|
5159
5261
|
const joinFields = selectToFetchAllFields(stmt, join2.table);
|
|
5160
|
-
const
|
|
5262
|
+
const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
|
|
5161
5263
|
const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
|
|
5162
|
-
|
|
5163
|
-
|
|
5264
|
+
const joinPushDown = join2.table.alias && stmt.where ? extractTableCondition(stmt.where, join2.table.alias) : null;
|
|
5265
|
+
const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
|
|
5266
|
+
lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
|
|
5267
|
+
lines.push(` kintone query: ${joinQ}`);
|
|
5164
5268
|
lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
|
|
5165
5269
|
}
|
|
5166
5270
|
}
|
|
@@ -5716,114 +5820,349 @@ function detectSortKind(fieldType, calcFormat) {
|
|
|
5716
5820
|
return void 0;
|
|
5717
5821
|
}
|
|
5718
5822
|
|
|
5719
|
-
// src/
|
|
5720
|
-
var
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
const
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
debugUrl: false,
|
|
5800
|
-
debugHeaders: false,
|
|
5801
|
-
exitOnEmpty: false,
|
|
5802
|
-
allowDml: false,
|
|
5803
|
-
yes: false,
|
|
5804
|
-
allowWithoutWhere: false,
|
|
5805
|
-
dmlMaxRows: null,
|
|
5806
|
-
userFormat: null,
|
|
5807
|
-
arrayFormat: null,
|
|
5808
|
-
tableFormat: null,
|
|
5809
|
-
dateFormat: null,
|
|
5810
|
-
attachmentFormat: null
|
|
5823
|
+
// src/node/appProfiles.ts
|
|
5824
|
+
var import_fs = require("fs");
|
|
5825
|
+
function parseTokenMap(raw) {
|
|
5826
|
+
const out = {};
|
|
5827
|
+
if (!raw.trim()) return out;
|
|
5828
|
+
const pairs = raw.split(",");
|
|
5829
|
+
for (const pair of pairs) {
|
|
5830
|
+
const idx = pair.indexOf("=");
|
|
5831
|
+
if (idx <= 0) throw new Error("ArgumentError: --token-map must be APPxxx=token pairs.");
|
|
5832
|
+
const key = normalizeAppKey(pair.slice(0, idx).trim());
|
|
5833
|
+
const value = pair.slice(idx + 1).trim();
|
|
5834
|
+
if (!value) throw new Error(`ArgumentError: token is empty for ${key}.`);
|
|
5835
|
+
out[key] = value;
|
|
5836
|
+
}
|
|
5837
|
+
return out;
|
|
5838
|
+
}
|
|
5839
|
+
function parseTokenFile(path) {
|
|
5840
|
+
const raw = (0, import_fs.readFileSync)(path, "utf-8");
|
|
5841
|
+
const parsed = JSON.parse(raw);
|
|
5842
|
+
const out = {};
|
|
5843
|
+
for (const [k, v] of Object.entries(parsed)) out[normalizeAppKey(k)] = String(v);
|
|
5844
|
+
return out;
|
|
5845
|
+
}
|
|
5846
|
+
function normalizeAppKey(v) {
|
|
5847
|
+
const m1 = v.match(/^APP(\d+)$/i);
|
|
5848
|
+
if (m1) return `APP${m1[1]}`;
|
|
5849
|
+
const m2 = v.match(/^(\d+)$/);
|
|
5850
|
+
if (m2) return `APP${m2[1]}`;
|
|
5851
|
+
throw new Error(`ArgumentError: invalid app key "${v}"`);
|
|
5852
|
+
}
|
|
5853
|
+
function extractAppIds(sql) {
|
|
5854
|
+
const out = /* @__PURE__ */ new Set();
|
|
5855
|
+
for (const m of sql.matchAll(/\bAPP(\d+)\b/gi)) out.add(Number(m[1]));
|
|
5856
|
+
return [...out];
|
|
5857
|
+
}
|
|
5858
|
+
function isSqlIdentContinue(ch) {
|
|
5859
|
+
if (!ch) return false;
|
|
5860
|
+
const cp = ch.codePointAt(0);
|
|
5861
|
+
return cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp >= 48 && cp <= 57 || cp === 95 || cp === 36 || cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 63744 && cp <= 64255 || cp >= 65281 && cp <= 65376;
|
|
5862
|
+
}
|
|
5863
|
+
function isProfileNameChar(ch) {
|
|
5864
|
+
if (!ch) return false;
|
|
5865
|
+
const cp = ch.codePointAt(0);
|
|
5866
|
+
return cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp >= 48 && cp <= 57 || ch === "_" || ch === "-" || ch === "." || ch === "$";
|
|
5867
|
+
}
|
|
5868
|
+
function tryParseAppProfileToken(sql, start) {
|
|
5869
|
+
const head = sql.slice(start, start + 3);
|
|
5870
|
+
if (head.toUpperCase() !== "APP") return null;
|
|
5871
|
+
const prev = start > 0 ? sql[start - 1] : "";
|
|
5872
|
+
if (isSqlIdentContinue(prev)) return null;
|
|
5873
|
+
let i = start + 3;
|
|
5874
|
+
const digitStart = i;
|
|
5875
|
+
while (i < sql.length && /[0-9]/.test(sql[i])) i++;
|
|
5876
|
+
const digitEnd = i;
|
|
5877
|
+
if (digitEnd === digitStart) return null;
|
|
5878
|
+
if (sql[i] === "$") {
|
|
5879
|
+
i++;
|
|
5880
|
+
const subStart = i;
|
|
5881
|
+
while (i < sql.length && isSqlIdentContinue(sql[i])) i++;
|
|
5882
|
+
if (i === subStart) return null;
|
|
5883
|
+
}
|
|
5884
|
+
const appEnd = i;
|
|
5885
|
+
let profile = null;
|
|
5886
|
+
if (sql[i] === "@") {
|
|
5887
|
+
i++;
|
|
5888
|
+
const pStart = i;
|
|
5889
|
+
while (i < sql.length && isProfileNameChar(sql[i])) i++;
|
|
5890
|
+
if (i === pStart) return null;
|
|
5891
|
+
profile = sql.slice(pStart, i);
|
|
5892
|
+
}
|
|
5893
|
+
const next = i < sql.length ? sql[i] : "";
|
|
5894
|
+
if (isSqlIdentContinue(next)) return null;
|
|
5895
|
+
return {
|
|
5896
|
+
appId: Number(sql.slice(digitStart, digitEnd)),
|
|
5897
|
+
profile,
|
|
5898
|
+
start,
|
|
5899
|
+
digitStart,
|
|
5900
|
+
digitEnd,
|
|
5901
|
+
appEnd,
|
|
5902
|
+
fullEnd: i
|
|
5811
5903
|
};
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5904
|
+
}
|
|
5905
|
+
function collectAppProfileTokens(sql) {
|
|
5906
|
+
const tokens = [];
|
|
5907
|
+
let i = 0;
|
|
5908
|
+
while (i < sql.length) {
|
|
5909
|
+
const ch = sql[i];
|
|
5910
|
+
if (ch === "'") {
|
|
5911
|
+
i++;
|
|
5912
|
+
while (i < sql.length) {
|
|
5913
|
+
if (sql[i] === "'") {
|
|
5914
|
+
i++;
|
|
5915
|
+
if (i < sql.length && sql[i] === "'") {
|
|
5916
|
+
i++;
|
|
5917
|
+
continue;
|
|
5918
|
+
}
|
|
5919
|
+
break;
|
|
5920
|
+
}
|
|
5921
|
+
i++;
|
|
5922
|
+
}
|
|
5816
5923
|
continue;
|
|
5817
5924
|
}
|
|
5818
|
-
if (
|
|
5819
|
-
|
|
5925
|
+
if (ch === "`") {
|
|
5926
|
+
i++;
|
|
5927
|
+
while (i < sql.length && sql[i] !== "`") i++;
|
|
5928
|
+
if (i < sql.length) i++;
|
|
5820
5929
|
continue;
|
|
5821
5930
|
}
|
|
5822
|
-
if (
|
|
5823
|
-
|
|
5931
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
5932
|
+
i += 2;
|
|
5933
|
+
while (i < sql.length && sql[i] !== "\n") i++;
|
|
5824
5934
|
continue;
|
|
5825
5935
|
}
|
|
5826
|
-
if (
|
|
5936
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
5937
|
+
i += 2;
|
|
5938
|
+
while (i < sql.length) {
|
|
5939
|
+
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
5940
|
+
i += 2;
|
|
5941
|
+
break;
|
|
5942
|
+
}
|
|
5943
|
+
i++;
|
|
5944
|
+
}
|
|
5945
|
+
continue;
|
|
5946
|
+
}
|
|
5947
|
+
const parsed = tryParseAppProfileToken(sql, i);
|
|
5948
|
+
if (!parsed) {
|
|
5949
|
+
i++;
|
|
5950
|
+
continue;
|
|
5951
|
+
}
|
|
5952
|
+
tokens.push(parsed);
|
|
5953
|
+
i = parsed.fullEnd;
|
|
5954
|
+
}
|
|
5955
|
+
return tokens;
|
|
5956
|
+
}
|
|
5957
|
+
function nextVirtualAppId(used) {
|
|
5958
|
+
let id = 9e8;
|
|
5959
|
+
while (used.has(id)) id++;
|
|
5960
|
+
used.add(id);
|
|
5961
|
+
return id;
|
|
5962
|
+
}
|
|
5963
|
+
function normalizeSqlAppProfiles(sql, defaultProfile = "dev") {
|
|
5964
|
+
const tokens = collectAppProfileTokens(sql);
|
|
5965
|
+
const hasProfileSyntax = tokens.some((t) => t.profile !== null);
|
|
5966
|
+
const profilesByApp = /* @__PURE__ */ new Map();
|
|
5967
|
+
const normalizedProfile = (profile) => profile ?? defaultProfile;
|
|
5968
|
+
for (const t of tokens) {
|
|
5969
|
+
const p = normalizedProfile(t.profile);
|
|
5970
|
+
let set = profilesByApp.get(t.appId);
|
|
5971
|
+
if (!set) {
|
|
5972
|
+
set = /* @__PURE__ */ new Set();
|
|
5973
|
+
profilesByApp.set(t.appId, set);
|
|
5974
|
+
}
|
|
5975
|
+
set.add(p.toLowerCase());
|
|
5976
|
+
}
|
|
5977
|
+
const usedAppIds = new Set(tokens.map((t) => t.appId));
|
|
5978
|
+
const pairToMapped = /* @__PURE__ */ new Map();
|
|
5979
|
+
const appBindingByMappedApp = /* @__PURE__ */ new Map();
|
|
5980
|
+
for (const [appId, pSet] of profilesByApp.entries()) {
|
|
5981
|
+
const profiles = [...pSet].sort();
|
|
5982
|
+
if (profiles.length <= 1) continue;
|
|
5983
|
+
for (const pLower of profiles) {
|
|
5984
|
+
const mapped = nextVirtualAppId(usedAppIds);
|
|
5985
|
+
pairToMapped.set(`${appId}@${pLower}`, mapped);
|
|
5986
|
+
appBindingByMappedApp.set(mapped, { appId, profile: pLower });
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
const out = [];
|
|
5990
|
+
let cursor = 0;
|
|
5991
|
+
for (const t of tokens) {
|
|
5992
|
+
const p = normalizedProfile(t.profile);
|
|
5993
|
+
const pLower = p.toLowerCase();
|
|
5994
|
+
const mapped = pairToMapped.get(`${t.appId}@${pLower}`) ?? t.appId;
|
|
5995
|
+
appBindingByMappedApp.set(mapped, { appId: t.appId, profile: pLower });
|
|
5996
|
+
out.push(sql.slice(cursor, t.start));
|
|
5997
|
+
out.push(sql.slice(t.start, t.digitStart));
|
|
5998
|
+
out.push(String(mapped));
|
|
5999
|
+
out.push(sql.slice(t.digitEnd, t.appEnd));
|
|
6000
|
+
cursor = t.fullEnd;
|
|
6001
|
+
}
|
|
6002
|
+
out.push(sql.slice(cursor));
|
|
6003
|
+
return {
|
|
6004
|
+
normalizedSql: out.join(""),
|
|
6005
|
+
hasProfileSyntax,
|
|
6006
|
+
appBindingByMappedApp
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
6009
|
+
function buildCacheContext(defaultProfile, appBindingByMappedApp) {
|
|
6010
|
+
if (appBindingByMappedApp.size === 0) return `default:${defaultProfile.toLowerCase()}`;
|
|
6011
|
+
const pairs = [...appBindingByMappedApp.entries()].sort((a, b) => a[0] - b[0]).map(([mappedAppId, b]) => `M${mappedAppId}=APP${b.appId}@${b.profile}`);
|
|
6012
|
+
return `apps:${pairs.join(",")}`;
|
|
6013
|
+
}
|
|
6014
|
+
function formatResolvedAppProfiles(sql, defaultProfile) {
|
|
6015
|
+
const parsed = normalizeSqlAppProfiles(sql, defaultProfile);
|
|
6016
|
+
if (parsed.appBindingByMappedApp.size === 0) return "(none)";
|
|
6017
|
+
return [...parsed.appBindingByMappedApp.values()].map((b) => `APP${b.appId}->${b.profile}`).join(", ");
|
|
6018
|
+
}
|
|
6019
|
+
|
|
6020
|
+
// src/node/dmlGuard.ts
|
|
6021
|
+
function getStatementType(stmt) {
|
|
6022
|
+
if (!stmt || typeof stmt !== "object") return "UNKNOWN";
|
|
6023
|
+
const obj = stmt;
|
|
6024
|
+
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
6025
|
+
}
|
|
6026
|
+
function isDmlType(type) {
|
|
6027
|
+
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
6028
|
+
}
|
|
6029
|
+
function hasWhereClause(stmt) {
|
|
6030
|
+
if (!stmt || typeof stmt !== "object") return false;
|
|
6031
|
+
const obj = stmt;
|
|
6032
|
+
return obj.where !== null && obj.where !== void 0;
|
|
6033
|
+
}
|
|
6034
|
+
function isNoFromSelectStatement(stmt) {
|
|
6035
|
+
if (!stmt || typeof stmt !== "object") return false;
|
|
6036
|
+
const obj = stmt;
|
|
6037
|
+
return obj.type === "SELECT" && obj.from?.appId === 0 && obj.from?.cteName === "__NO_FROM__";
|
|
6038
|
+
}
|
|
6039
|
+
function getInsertValuesCount(stmt) {
|
|
6040
|
+
if (!stmt || typeof stmt !== "object") return null;
|
|
6041
|
+
const obj = stmt;
|
|
6042
|
+
if (obj.type !== "INSERT") return null;
|
|
6043
|
+
return Array.isArray(obj.values) ? obj.values.length : null;
|
|
6044
|
+
}
|
|
6045
|
+
function collectDmlTargetFields(stmt) {
|
|
6046
|
+
if (!stmt || typeof stmt !== "object") return [];
|
|
6047
|
+
const obj = stmt;
|
|
6048
|
+
if (!obj.type) return [];
|
|
6049
|
+
if (obj.type === "UPDATE") {
|
|
6050
|
+
return (obj.assignments ?? []).map((a) => a.field).filter((f) => Boolean(f));
|
|
6051
|
+
}
|
|
6052
|
+
if (obj.type === "INSERT" || obj.type === "INSERT_SELECT" || obj.type === "UPSERT" || obj.type === "UPSERT_SELECT") {
|
|
6053
|
+
return [...obj.fields ?? [], ...obj.keyFields ?? []];
|
|
6054
|
+
}
|
|
6055
|
+
return [];
|
|
6056
|
+
}
|
|
6057
|
+
|
|
6058
|
+
// src/cli/index.ts
|
|
6059
|
+
var HELP_TEXT = `ksql - Execute SQL against kintone apps
|
|
6060
|
+
|
|
6061
|
+
Usage:
|
|
6062
|
+
ksql [options]
|
|
6063
|
+
ksql -e "<SQL>"
|
|
6064
|
+
ksql -f <file.sql>
|
|
6065
|
+
|
|
6066
|
+
Options:
|
|
6067
|
+
-e, --execute <sql> Execute SQL string
|
|
6068
|
+
-f, --file <path> Execute SQL file
|
|
6069
|
+
--console Start interactive console mode
|
|
6070
|
+
--dry-run Parse and show execution plan only
|
|
6071
|
+
--format <type> Output format: table | json | jsonl | csv | markdown | md
|
|
6072
|
+
--max-records <n> Max records to fetch (default: 500)
|
|
6073
|
+
--on-limit <mode> On record limit: error | truncate
|
|
6074
|
+
--timeout <ms> Request timeout in milliseconds (default: 30000)
|
|
6075
|
+
--config <path> Config file path (default: ./ksql.config.json)
|
|
6076
|
+
--profile <name> Profile name in config
|
|
6077
|
+
--base-url <url> kintone base URL
|
|
6078
|
+
--guest-space-id <id> Guest space ID (uses /k/guest/<id>/v1 APIs)
|
|
6079
|
+
--auth <type> Auth type: token | userpass | auto
|
|
6080
|
+
--username <name> Login username (for userpass auth)
|
|
6081
|
+
--password <pass> Login password (for userpass auth)
|
|
6082
|
+
--token <token> Single-app token
|
|
6083
|
+
--token-map <mapping> App token map (APP100=...,APP101=...)
|
|
6084
|
+
--token-file <path> JSON file for app token map
|
|
6085
|
+
--app <id> Default app id context
|
|
6086
|
+
--diag-record-id <id> Diagnostic: GET record.json by app+id
|
|
6087
|
+
--no-header Hide table header
|
|
6088
|
+
--pretty Pretty-print JSON output
|
|
6089
|
+
--user-format <mode> User field format: full | name | code
|
|
6090
|
+
--array-format <mode> Array field format: full | join
|
|
6091
|
+
--table-format <mode> Subtable format: full | count
|
|
6092
|
+
--date-format <mode> Date format: full | local
|
|
6093
|
+
--attachment-format <mode> Attachment format: full | name | fileKey
|
|
6094
|
+
--output <path> Write output to file
|
|
6095
|
+
--no-color Disable ANSI colors
|
|
6096
|
+
--quiet Suppress non-result logs
|
|
6097
|
+
--debug Show request/response debug logs
|
|
6098
|
+
--debug-url Show only HTTP request URL debug logs
|
|
6099
|
+
--debug-headers Show request headers in debug logs (masked)
|
|
6100
|
+
--exit-on-empty Return exit code 1 when rowCount is 0
|
|
6101
|
+
--allow-dml Enable UPDATE/DELETE/INSERT/UPSERT/REORDER execution
|
|
6102
|
+
--yes Skip DML confirmation prompt
|
|
6103
|
+
--allow-without-where Allow UPDATE/DELETE without WHERE
|
|
6104
|
+
--dml-max-rows <n> Max affected rows for DML guard (default: 100)
|
|
6105
|
+
-h, --help Show help
|
|
6106
|
+
-v, --version Show version
|
|
6107
|
+
`;
|
|
6108
|
+
function parseArgs(argv) {
|
|
6109
|
+
const out = {
|
|
6110
|
+
help: false,
|
|
6111
|
+
version: false,
|
|
6112
|
+
executeSql: null,
|
|
6113
|
+
filePath: null,
|
|
6114
|
+
console: false,
|
|
6115
|
+
dryRun: false,
|
|
6116
|
+
format: null,
|
|
6117
|
+
maxRecords: null,
|
|
6118
|
+
onLimit: null,
|
|
6119
|
+
timeout: null,
|
|
6120
|
+
configPath: null,
|
|
6121
|
+
profile: null,
|
|
6122
|
+
baseUrl: null,
|
|
6123
|
+
guestSpaceId: null,
|
|
6124
|
+
auth: null,
|
|
6125
|
+
username: null,
|
|
6126
|
+
password: null,
|
|
6127
|
+
token: null,
|
|
6128
|
+
tokenMap: {},
|
|
6129
|
+
tokenFile: null,
|
|
6130
|
+
app: null,
|
|
6131
|
+
diagRecordId: null,
|
|
6132
|
+
noHeader: false,
|
|
6133
|
+
pretty: false,
|
|
6134
|
+
outputPath: null,
|
|
6135
|
+
noColor: false,
|
|
6136
|
+
quiet: false,
|
|
6137
|
+
debug: false,
|
|
6138
|
+
debugUrl: false,
|
|
6139
|
+
debugHeaders: false,
|
|
6140
|
+
exitOnEmpty: false,
|
|
6141
|
+
allowDml: false,
|
|
6142
|
+
yes: false,
|
|
6143
|
+
allowWithoutWhere: false,
|
|
6144
|
+
dmlMaxRows: null,
|
|
6145
|
+
userFormat: null,
|
|
6146
|
+
arrayFormat: null,
|
|
6147
|
+
tableFormat: null,
|
|
6148
|
+
dateFormat: null,
|
|
6149
|
+
attachmentFormat: null
|
|
6150
|
+
};
|
|
6151
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6152
|
+
const a = argv[i];
|
|
6153
|
+
if (a === "-h" || a === "--help") {
|
|
6154
|
+
out.help = true;
|
|
6155
|
+
continue;
|
|
6156
|
+
}
|
|
6157
|
+
if (a === "-v" || a === "--version") {
|
|
6158
|
+
out.version = true;
|
|
6159
|
+
continue;
|
|
6160
|
+
}
|
|
6161
|
+
if (a === "--console") {
|
|
6162
|
+
out.console = true;
|
|
6163
|
+
continue;
|
|
6164
|
+
}
|
|
6165
|
+
if (a === "--dry-run") {
|
|
5827
6166
|
out.dryRun = true;
|
|
5828
6167
|
continue;
|
|
5829
6168
|
}
|
|
@@ -6024,216 +6363,14 @@ function parseArgs(argv) {
|
|
|
6024
6363
|
}
|
|
6025
6364
|
function getVersion() {
|
|
6026
6365
|
const pkgPath = (0, import_path.resolve)(__dirname, "../package.json");
|
|
6027
|
-
const raw = (0,
|
|
6366
|
+
const raw = (0, import_fs2.readFileSync)(pkgPath, "utf-8");
|
|
6028
6367
|
const pkg = JSON.parse(raw);
|
|
6029
6368
|
return pkg.version ?? "0.0.0";
|
|
6030
6369
|
}
|
|
6031
6370
|
function loadConfig(configPath) {
|
|
6032
|
-
const raw = (0,
|
|
6371
|
+
const raw = (0, import_fs2.readFileSync)(configPath, "utf-8");
|
|
6033
6372
|
return JSON.parse(raw);
|
|
6034
6373
|
}
|
|
6035
|
-
function parseTokenMap(raw) {
|
|
6036
|
-
const out = {};
|
|
6037
|
-
if (!raw.trim()) return out;
|
|
6038
|
-
const pairs = raw.split(",");
|
|
6039
|
-
for (const pair of pairs) {
|
|
6040
|
-
const idx = pair.indexOf("=");
|
|
6041
|
-
if (idx <= 0) throw new Error("ArgumentError: --token-map must be APPxxx=token pairs.");
|
|
6042
|
-
const key = normalizeAppKey(pair.slice(0, idx).trim());
|
|
6043
|
-
const value = pair.slice(idx + 1).trim();
|
|
6044
|
-
if (!value) throw new Error(`ArgumentError: token is empty for ${key}.`);
|
|
6045
|
-
out[key] = value;
|
|
6046
|
-
}
|
|
6047
|
-
return out;
|
|
6048
|
-
}
|
|
6049
|
-
function parseTokenFile(path) {
|
|
6050
|
-
const raw = (0, import_fs.readFileSync)(path, "utf-8");
|
|
6051
|
-
const parsed = JSON.parse(raw);
|
|
6052
|
-
const out = {};
|
|
6053
|
-
for (const [k, v] of Object.entries(parsed)) out[normalizeAppKey(k)] = String(v);
|
|
6054
|
-
return out;
|
|
6055
|
-
}
|
|
6056
|
-
function normalizeAppKey(v) {
|
|
6057
|
-
const m1 = v.match(/^APP(\d+)$/i);
|
|
6058
|
-
if (m1) return `APP${m1[1]}`;
|
|
6059
|
-
const m2 = v.match(/^(\d+)$/);
|
|
6060
|
-
if (m2) return `APP${m2[1]}`;
|
|
6061
|
-
throw new Error(`ArgumentError: invalid app key "${v}"`);
|
|
6062
|
-
}
|
|
6063
|
-
function extractAppIds(sql) {
|
|
6064
|
-
const out = /* @__PURE__ */ new Set();
|
|
6065
|
-
for (const m of sql.matchAll(/\bAPP(\d+)\b/gi)) out.add(Number(m[1]));
|
|
6066
|
-
return [...out];
|
|
6067
|
-
}
|
|
6068
|
-
function isSqlIdentContinue(ch) {
|
|
6069
|
-
if (!ch) return false;
|
|
6070
|
-
const cp = ch.codePointAt(0);
|
|
6071
|
-
return cp >= 65 && cp <= 90 || // A-Z
|
|
6072
|
-
cp >= 97 && cp <= 122 || // a-z
|
|
6073
|
-
cp >= 48 && cp <= 57 || // 0-9
|
|
6074
|
-
cp === 95 || // _
|
|
6075
|
-
cp === 36 || // $
|
|
6076
|
-
cp >= 12352 && cp <= 12543 || cp >= 13312 && cp <= 40959 || cp >= 63744 && cp <= 64255 || cp >= 65281 && cp <= 65376;
|
|
6077
|
-
}
|
|
6078
|
-
function isProfileNameChar(ch) {
|
|
6079
|
-
if (!ch) return false;
|
|
6080
|
-
const cp = ch.codePointAt(0);
|
|
6081
|
-
return cp >= 65 && cp <= 90 || // A-Z
|
|
6082
|
-
cp >= 97 && cp <= 122 || // a-z
|
|
6083
|
-
cp >= 48 && cp <= 57 || // 0-9
|
|
6084
|
-
ch === "_" || ch === "-" || ch === "." || ch === "$";
|
|
6085
|
-
}
|
|
6086
|
-
function tryParseAppProfileToken(sql, start) {
|
|
6087
|
-
const head = sql.slice(start, start + 3);
|
|
6088
|
-
if (head.toUpperCase() !== "APP") return null;
|
|
6089
|
-
const prev = start > 0 ? sql[start - 1] : "";
|
|
6090
|
-
if (isSqlIdentContinue(prev)) return null;
|
|
6091
|
-
let i = start + 3;
|
|
6092
|
-
const digitStart = i;
|
|
6093
|
-
while (i < sql.length && /[0-9]/.test(sql[i])) i++;
|
|
6094
|
-
const digitEnd = i;
|
|
6095
|
-
if (digitEnd === digitStart) return null;
|
|
6096
|
-
if (sql[i] === "$") {
|
|
6097
|
-
i++;
|
|
6098
|
-
const subStart = i;
|
|
6099
|
-
while (i < sql.length && isSqlIdentContinue(sql[i])) i++;
|
|
6100
|
-
if (i === subStart) return null;
|
|
6101
|
-
}
|
|
6102
|
-
const appEnd = i;
|
|
6103
|
-
let profile = null;
|
|
6104
|
-
if (sql[i] === "@") {
|
|
6105
|
-
i++;
|
|
6106
|
-
const pStart = i;
|
|
6107
|
-
while (i < sql.length && isProfileNameChar(sql[i])) i++;
|
|
6108
|
-
if (i === pStart) return null;
|
|
6109
|
-
profile = sql.slice(pStart, i);
|
|
6110
|
-
}
|
|
6111
|
-
const next = i < sql.length ? sql[i] : "";
|
|
6112
|
-
if (isSqlIdentContinue(next)) return null;
|
|
6113
|
-
return {
|
|
6114
|
-
appId: Number(sql.slice(digitStart, digitEnd)),
|
|
6115
|
-
profile,
|
|
6116
|
-
start,
|
|
6117
|
-
digitStart,
|
|
6118
|
-
digitEnd,
|
|
6119
|
-
appEnd,
|
|
6120
|
-
fullEnd: i
|
|
6121
|
-
};
|
|
6122
|
-
}
|
|
6123
|
-
function collectAppProfileTokens(sql) {
|
|
6124
|
-
const tokens = [];
|
|
6125
|
-
let i = 0;
|
|
6126
|
-
while (i < sql.length) {
|
|
6127
|
-
const ch = sql[i];
|
|
6128
|
-
if (ch === "'") {
|
|
6129
|
-
i++;
|
|
6130
|
-
while (i < sql.length) {
|
|
6131
|
-
if (sql[i] === "'") {
|
|
6132
|
-
i++;
|
|
6133
|
-
if (i < sql.length && sql[i] === "'") {
|
|
6134
|
-
i++;
|
|
6135
|
-
continue;
|
|
6136
|
-
}
|
|
6137
|
-
break;
|
|
6138
|
-
}
|
|
6139
|
-
i++;
|
|
6140
|
-
}
|
|
6141
|
-
continue;
|
|
6142
|
-
}
|
|
6143
|
-
if (ch === "`") {
|
|
6144
|
-
i++;
|
|
6145
|
-
while (i < sql.length && sql[i] !== "`") i++;
|
|
6146
|
-
if (i < sql.length) i++;
|
|
6147
|
-
continue;
|
|
6148
|
-
}
|
|
6149
|
-
if (ch === "-" && sql[i + 1] === "-") {
|
|
6150
|
-
i += 2;
|
|
6151
|
-
while (i < sql.length && sql[i] !== "\n") i++;
|
|
6152
|
-
continue;
|
|
6153
|
-
}
|
|
6154
|
-
if (ch === "/" && sql[i + 1] === "*") {
|
|
6155
|
-
i += 2;
|
|
6156
|
-
while (i < sql.length) {
|
|
6157
|
-
if (sql[i] === "*" && sql[i + 1] === "/") {
|
|
6158
|
-
i += 2;
|
|
6159
|
-
break;
|
|
6160
|
-
}
|
|
6161
|
-
i++;
|
|
6162
|
-
}
|
|
6163
|
-
continue;
|
|
6164
|
-
}
|
|
6165
|
-
const parsed = tryParseAppProfileToken(sql, i);
|
|
6166
|
-
if (!parsed) {
|
|
6167
|
-
i++;
|
|
6168
|
-
continue;
|
|
6169
|
-
}
|
|
6170
|
-
tokens.push(parsed);
|
|
6171
|
-
i = parsed.fullEnd;
|
|
6172
|
-
}
|
|
6173
|
-
return tokens;
|
|
6174
|
-
}
|
|
6175
|
-
function nextVirtualAppId(used) {
|
|
6176
|
-
let id = 9e8;
|
|
6177
|
-
while (used.has(id)) id++;
|
|
6178
|
-
used.add(id);
|
|
6179
|
-
return id;
|
|
6180
|
-
}
|
|
6181
|
-
function normalizeSqlAppProfiles(sql, defaultProfile = "dev") {
|
|
6182
|
-
const tokens = collectAppProfileTokens(sql);
|
|
6183
|
-
const hasProfileSyntax = tokens.some((t) => t.profile !== null);
|
|
6184
|
-
const profilesByApp = /* @__PURE__ */ new Map();
|
|
6185
|
-
const normalizedProfile = (profile) => profile ?? defaultProfile;
|
|
6186
|
-
for (const t of tokens) {
|
|
6187
|
-
const p = normalizedProfile(t.profile);
|
|
6188
|
-
let set = profilesByApp.get(t.appId);
|
|
6189
|
-
if (!set) {
|
|
6190
|
-
set = /* @__PURE__ */ new Set();
|
|
6191
|
-
profilesByApp.set(t.appId, set);
|
|
6192
|
-
}
|
|
6193
|
-
set.add(p.toLowerCase());
|
|
6194
|
-
}
|
|
6195
|
-
const usedAppIds = new Set(tokens.map((t) => t.appId));
|
|
6196
|
-
const pairToMapped = /* @__PURE__ */ new Map();
|
|
6197
|
-
const appBindingByMappedApp = /* @__PURE__ */ new Map();
|
|
6198
|
-
for (const [appId, pSet] of profilesByApp.entries()) {
|
|
6199
|
-
const profiles = [...pSet].sort();
|
|
6200
|
-
if (profiles.length <= 1) continue;
|
|
6201
|
-
for (const pLower of profiles) {
|
|
6202
|
-
const mapped = nextVirtualAppId(usedAppIds);
|
|
6203
|
-
pairToMapped.set(`${appId}@${pLower}`, mapped);
|
|
6204
|
-
appBindingByMappedApp.set(mapped, { appId, profile: pLower });
|
|
6205
|
-
}
|
|
6206
|
-
}
|
|
6207
|
-
const out = [];
|
|
6208
|
-
let cursor = 0;
|
|
6209
|
-
for (const t of tokens) {
|
|
6210
|
-
const p = normalizedProfile(t.profile);
|
|
6211
|
-
const pLower = p.toLowerCase();
|
|
6212
|
-
const mapped = pairToMapped.get(`${t.appId}@${pLower}`) ?? t.appId;
|
|
6213
|
-
appBindingByMappedApp.set(mapped, { appId: t.appId, profile: pLower });
|
|
6214
|
-
out.push(sql.slice(cursor, t.start));
|
|
6215
|
-
out.push(sql.slice(t.start, t.digitStart));
|
|
6216
|
-
out.push(String(mapped));
|
|
6217
|
-
out.push(sql.slice(t.digitEnd, t.appEnd));
|
|
6218
|
-
cursor = t.fullEnd;
|
|
6219
|
-
}
|
|
6220
|
-
out.push(sql.slice(cursor));
|
|
6221
|
-
return {
|
|
6222
|
-
normalizedSql: out.join(""),
|
|
6223
|
-
hasProfileSyntax,
|
|
6224
|
-
appBindingByMappedApp
|
|
6225
|
-
};
|
|
6226
|
-
}
|
|
6227
|
-
function buildCacheContext(defaultProfile, appBindingByMappedApp) {
|
|
6228
|
-
if (appBindingByMappedApp.size === 0) return `default:${defaultProfile.toLowerCase()}`;
|
|
6229
|
-
const pairs = [...appBindingByMappedApp.entries()].sort((a, b) => a[0] - b[0]).map(([mappedAppId, b]) => `M${mappedAppId}=APP${b.appId}@${b.profile}`);
|
|
6230
|
-
return `apps:${pairs.join(",")}`;
|
|
6231
|
-
}
|
|
6232
|
-
function formatResolvedAppProfiles(sql, defaultProfile) {
|
|
6233
|
-
const parsed = normalizeSqlAppProfiles(sql, defaultProfile);
|
|
6234
|
-
if (parsed.appBindingByMappedApp.size === 0) return "(none)";
|
|
6235
|
-
return [...parsed.appBindingByMappedApp.values()].map((b) => `APP${b.appId}->${b.profile}`).join(", ");
|
|
6236
|
-
}
|
|
6237
6374
|
function resolveTokenValue(raw) {
|
|
6238
6375
|
if (raw.startsWith("env:")) {
|
|
6239
6376
|
const envKey = raw.slice(4);
|
|
@@ -6275,42 +6412,6 @@ function envAuth(name) {
|
|
|
6275
6412
|
if (v === "token" || v === "userpass" || v === "auto") return v;
|
|
6276
6413
|
return null;
|
|
6277
6414
|
}
|
|
6278
|
-
function isDmlType(type) {
|
|
6279
|
-
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT";
|
|
6280
|
-
}
|
|
6281
|
-
function hasWhereClause(stmt) {
|
|
6282
|
-
if (!stmt || typeof stmt !== "object") return false;
|
|
6283
|
-
const obj = stmt;
|
|
6284
|
-
return obj.where !== null && obj.where !== void 0;
|
|
6285
|
-
}
|
|
6286
|
-
function getStatementType(stmt) {
|
|
6287
|
-
if (!stmt || typeof stmt !== "object") return "UNKNOWN";
|
|
6288
|
-
const obj = stmt;
|
|
6289
|
-
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
6290
|
-
}
|
|
6291
|
-
function isNoFromSelectStatement(stmt) {
|
|
6292
|
-
if (!stmt || typeof stmt !== "object") return false;
|
|
6293
|
-
const obj = stmt;
|
|
6294
|
-
return obj.type === "SELECT" && obj.from?.appId === 0 && obj.from?.cteName === "__NO_FROM__";
|
|
6295
|
-
}
|
|
6296
|
-
function getInsertValuesCount(stmt) {
|
|
6297
|
-
if (!stmt || typeof stmt !== "object") return null;
|
|
6298
|
-
const obj = stmt;
|
|
6299
|
-
if (obj.type !== "INSERT") return null;
|
|
6300
|
-
return Array.isArray(obj.values) ? obj.values.length : null;
|
|
6301
|
-
}
|
|
6302
|
-
function collectDmlTargetFields(stmt) {
|
|
6303
|
-
if (!stmt || typeof stmt !== "object") return [];
|
|
6304
|
-
const obj = stmt;
|
|
6305
|
-
if (!obj.type) return [];
|
|
6306
|
-
if (obj.type === "UPDATE") {
|
|
6307
|
-
return (obj.assignments ?? []).map((a) => a.field).filter((f) => Boolean(f));
|
|
6308
|
-
}
|
|
6309
|
-
if (obj.type === "INSERT" || obj.type === "INSERT_SELECT" || obj.type === "UPSERT" || obj.type === "UPSERT_SELECT") {
|
|
6310
|
-
return [...obj.fields ?? [], ...obj.keyFields ?? []];
|
|
6311
|
-
}
|
|
6312
|
-
return [];
|
|
6313
|
-
}
|
|
6314
6415
|
function normalizeUnique(values) {
|
|
6315
6416
|
const out = [];
|
|
6316
6417
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6695,9 +6796,9 @@ function getHistoryPath() {
|
|
|
6695
6796
|
}
|
|
6696
6797
|
function loadHistory(maxItems = 200) {
|
|
6697
6798
|
const p = getHistoryPath();
|
|
6698
|
-
if (!(0,
|
|
6799
|
+
if (!(0, import_fs2.existsSync)(p)) return [];
|
|
6699
6800
|
try {
|
|
6700
|
-
const raw = (0,
|
|
6801
|
+
const raw = (0, import_fs2.readFileSync)(p, "utf-8");
|
|
6701
6802
|
const lines = raw.split(/\r?\n/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6702
6803
|
return lines.slice(-maxItems);
|
|
6703
6804
|
} catch {
|
|
@@ -6707,26 +6808,26 @@ function loadHistory(maxItems = 200) {
|
|
|
6707
6808
|
function appendHistory(sql) {
|
|
6708
6809
|
const p = getHistoryPath();
|
|
6709
6810
|
try {
|
|
6710
|
-
(0,
|
|
6811
|
+
(0, import_fs2.appendFileSync)(p, `${sql.replace(/\s+/g, " ").trim()}
|
|
6711
6812
|
`, "utf-8");
|
|
6712
6813
|
} catch {
|
|
6713
6814
|
}
|
|
6714
6815
|
}
|
|
6715
6816
|
function editBufferWithExternalEditor(current) {
|
|
6716
6817
|
const editor = process.env.KSQL_EDITOR ?? process.env.VISUAL ?? process.env.EDITOR ?? (process.platform === "win32" ? "notepad" : "vi");
|
|
6717
|
-
const dir = (0,
|
|
6818
|
+
const dir = (0, import_fs2.mkdtempSync)((0, import_path.join)((0, import_os.tmpdir)(), "ksql-edit-"));
|
|
6718
6819
|
const filePath = (0, import_path.join)(dir, "query.sql");
|
|
6719
6820
|
try {
|
|
6720
|
-
(0,
|
|
6821
|
+
(0, import_fs2.writeFileSync)(filePath, current, "utf-8");
|
|
6721
6822
|
const cmd = `"${editor}" "${filePath}"`;
|
|
6722
6823
|
const res = (0, import_child_process.spawnSync)(cmd, { stdio: "inherit", shell: true });
|
|
6723
6824
|
if (res.error) throw res.error;
|
|
6724
6825
|
if ((res.status ?? 0) !== 0) {
|
|
6725
6826
|
throw new Error(`Editor exited with code ${res.status ?? 1}`);
|
|
6726
6827
|
}
|
|
6727
|
-
return (0,
|
|
6828
|
+
return (0, import_fs2.readFileSync)(filePath, "utf-8").replace(/\r\n/g, "\n");
|
|
6728
6829
|
} finally {
|
|
6729
|
-
(0,
|
|
6830
|
+
(0, import_fs2.rmSync)(dir, { recursive: true, force: true });
|
|
6730
6831
|
}
|
|
6731
6832
|
}
|
|
6732
6833
|
async function runConsole(base) {
|
|
@@ -6908,11 +7009,11 @@ async function runConsole(base) {
|
|
|
6908
7009
|
}
|
|
6909
7010
|
try {
|
|
6910
7011
|
if (meta.append) {
|
|
6911
|
-
(0,
|
|
7012
|
+
(0, import_fs2.appendFileSync)(meta.path, lastOutput, "utf-8");
|
|
6912
7013
|
process.stdout.write(`saved (append): ${meta.path}
|
|
6913
7014
|
`);
|
|
6914
7015
|
} else {
|
|
6915
|
-
(0,
|
|
7016
|
+
(0, import_fs2.writeFileSync)(meta.path, lastOutput, "utf-8");
|
|
6916
7017
|
process.stdout.write(`saved: ${meta.path}
|
|
6917
7018
|
`);
|
|
6918
7019
|
}
|
|
@@ -7023,7 +7124,7 @@ async function run() {
|
|
|
7023
7124
|
let isDmlStatement = false;
|
|
7024
7125
|
if (args.diagRecordId === null) {
|
|
7025
7126
|
sql = args.executeSql;
|
|
7026
|
-
if (!sql && args.filePath) sql = (0,
|
|
7127
|
+
if (!sql && args.filePath) sql = (0, import_fs2.readFileSync)(args.filePath, "utf-8");
|
|
7027
7128
|
if (!sql || !sql.trim()) {
|
|
7028
7129
|
process.stderr.write("ArgumentError: SQL is empty.\n");
|
|
7029
7130
|
return 2;
|
|
@@ -7045,7 +7146,7 @@ async function run() {
|
|
|
7045
7146
|
isDmlStatement = isDmlType(stmtType);
|
|
7046
7147
|
hasWhere = hasWhereClause(stmt);
|
|
7047
7148
|
insertValuesCount = getInsertValuesCount(stmt);
|
|
7048
|
-
const supported = stmtType === "SELECT" || isDmlStatement;
|
|
7149
|
+
const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || isDmlStatement;
|
|
7049
7150
|
if (!supported) {
|
|
7050
7151
|
process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
|
|
7051
7152
|
`);
|
|
@@ -7093,7 +7194,7 @@ async function run() {
|
|
|
7093
7194
|
const appIds = sql ? extractAppIds(sql) : [];
|
|
7094
7195
|
const defaultApp = args.app ?? envInt("KSQL_APP") ?? profile.app ?? null;
|
|
7095
7196
|
if (appIds.length === 0 && defaultApp !== null) appIds.push(defaultApp);
|
|
7096
|
-
const allowNoFromSelect = isNoFromSelectStatement(parsedStmt);
|
|
7197
|
+
const allowNoFromSelect = isNoFromSelectStatement(parsedStmt) || stmtType === "SHOW_APPS";
|
|
7097
7198
|
if (appIds.length === 0 && !allowNoFromSelect && !args.dryRun && args.diagRecordId === null) {
|
|
7098
7199
|
process.stderr.write("ArgumentError: no APPxxx found in SQL and --app is not set.\n");
|
|
7099
7200
|
return 2;
|
|
@@ -7104,7 +7205,7 @@ async function run() {
|
|
|
7104
7205
|
return 2;
|
|
7105
7206
|
}
|
|
7106
7207
|
if (!allowDml) {
|
|
7107
|
-
process.stderr.write("ArgumentError: DML is disabled. Use --allow-dml to enable UPDATE/DELETE/INSERT/UPSERT.\n");
|
|
7208
|
+
process.stderr.write("ArgumentError: DML is disabled. Use --allow-dml to enable UPDATE/DELETE/INSERT/UPSERT/REORDER.\n");
|
|
7108
7209
|
return 2;
|
|
7109
7210
|
}
|
|
7110
7211
|
if ((stmtType === "UPDATE" || stmtType === "DELETE") && !hasWhere && !allowWithoutWhere) {
|
|
@@ -7380,7 +7481,7 @@ query=${label}`);
|
|
|
7380
7481
|
});
|
|
7381
7482
|
if (result.type !== "SELECT") {
|
|
7382
7483
|
const output2 = buildMutationOutput(result, format, noHeader, pretty);
|
|
7383
|
-
if (outputPath) (0,
|
|
7484
|
+
if (outputPath) (0, import_fs2.writeFileSync)(outputPath, `${output2}
|
|
7384
7485
|
`, "utf-8");
|
|
7385
7486
|
else if (output2) process.stdout.write(`${output2}
|
|
7386
7487
|
`);
|
|
@@ -7389,7 +7490,7 @@ query=${label}`);
|
|
|
7389
7490
|
return 0;
|
|
7390
7491
|
}
|
|
7391
7492
|
const output = buildOutput(result, format, noHeader, pretty, displayOptions);
|
|
7392
|
-
if (outputPath) (0,
|
|
7493
|
+
if (outputPath) (0, import_fs2.writeFileSync)(outputPath, `${output}
|
|
7393
7494
|
`, "utf-8");
|
|
7394
7495
|
else if (output) process.stdout.write(`${output}
|
|
7395
7496
|
`);
|