@rex0220/kintone-sql-tools 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist-cli/ksql.js +131 -22
  2. package/package.json +2 -2
package/dist-cli/ksql.js CHANGED
@@ -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 = {};
@@ -3652,7 +3708,12 @@ function project(rows, columns, scalarCache) {
3652
3708
  }
3653
3709
  case "STRFUNC_COL": {
3654
3710
  const key = col.alias ?? stringFuncDefaultKey(col.expr);
3655
- out[key] = row[key] ?? evalStringFunc(col.expr, row);
3711
+ if (hasAggregateInStringFuncExpr2(col.expr)) {
3712
+ const srcKey = stringFuncDefaultKey(col.expr);
3713
+ out[key] = row[col.alias ?? srcKey] ?? row[srcKey] ?? evalStringFunc(col.expr, row);
3714
+ } else {
3715
+ out[key] = evalStringFunc(col.expr, row);
3716
+ }
3656
3717
  if (rowIdx === 0) orderedKeys.push(key);
3657
3718
  break;
3658
3719
  }
@@ -3993,7 +4054,21 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
3993
4054
  const parallel = options.fetchParallel ?? 1;
3994
4055
  await resolveSubqueries(stmt.where, client, options, cacheContext);
3995
4056
  await resolveSubqueries(stmt.having, client, options, cacheContext);
3996
- const mainRecords = await fetchTableRecordsForFullScan(
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(
3997
4072
  stmt,
3998
4073
  stmt.from,
3999
4074
  client,
@@ -4001,11 +4076,39 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
4001
4076
  parallel,
4002
4077
  true,
4003
4078
  options.onLimitReached ?? "error",
4004
- warnings
4079
+ warnings,
4080
+ mainPushDown
4005
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;
4006
4106
  const tables = /* @__PURE__ */ new Map();
4007
4107
  tables.set(stmt.from.alias, mainRecords);
4008
- const joinFetches = stmt.joins.map(async (join2) => {
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) => {
4009
4112
  const optimized = await tryFetchJoinRecordsBySourceKeys(
4010
4113
  stmt,
4011
4114
  join2,
@@ -4014,7 +4117,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
4014
4117
  maxRecords,
4015
4118
  parallel,
4016
4119
  options.onLimitReached ?? "error",
4017
- warnings
4120
+ warnings,
4121
+ null
4018
4122
  );
4019
4123
  const joinRecords = optimized ?? await fetchTableRecordsForFullScan(
4020
4124
  stmt,
@@ -4024,11 +4128,11 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
4024
4128
  parallel,
4025
4129
  false,
4026
4130
  options.onLimitReached ?? "error",
4027
- warnings
4131
+ warnings,
4132
+ null
4028
4133
  );
4029
4134
  tables.set(join2.table.alias, joinRecords);
4030
- });
4031
- await Promise.all(joinFetches);
4135
+ }));
4032
4136
  const scalarCache = await resolveScalarColumns(stmt.columns, client, options, cacheContext);
4033
4137
  const optionOrders = await buildOptionOrdersForSelect(stmt, client, cacheContext);
4034
4138
  const sortKinds = await buildSortKindsForSelect(stmt, client, cacheContext);
@@ -4241,13 +4345,15 @@ function processRowToKintoneRecord(row) {
4241
4345
  Object.entries(row).map(([k, v]) => [k, { value: v ?? "" }])
4242
4346
  );
4243
4347
  }
4244
- 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) {
4245
4349
  const fields = selectToFetchAllFields(stmt, table);
4246
4350
  const onTruncate = (max) => {
4247
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`);
4248
4352
  };
4249
4353
  if (!table.subtableCode) {
4250
- const query = isMainTable ? selectToFetchAllParams(stmt, table.appId).query : "";
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;
4251
4357
  const resolved = await fetchRecordsForSharedPlan(client.getRecords, table.appId, query, fields, {
4252
4358
  parallel,
4253
4359
  maxRecords,
@@ -4282,7 +4388,7 @@ function splitChunks(items, size) {
4282
4388
  var JOIN_IN_CHUNK_SIZE = 50;
4283
4389
  var JOIN_IN_MAX_CHUNKS = 6;
4284
4390
  var JOIN_IN_MAX_KEYS = JOIN_IN_CHUNK_SIZE * JOIN_IN_MAX_CHUNKS;
4285
- 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) {
4286
4392
  if (join2.type !== "INNER") return null;
4287
4393
  if (!join2.table.alias) return null;
4288
4394
  if (join2.table.subtableCode) return null;
@@ -4315,7 +4421,7 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
4315
4421
  if (values.length === 0) return [];
4316
4422
  if (values.length > JOIN_IN_MAX_KEYS) {
4317
4423
  warnings.add(
4318
- `JOIN\u30AD\u30FC\u304C ${values.length} \u4EF6\u306E\u305F\u3081 IN \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`
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`
4319
4425
  );
4320
4426
  return null;
4321
4427
  }
@@ -4327,7 +4433,8 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
4327
4433
  const merged = [];
4328
4434
  const seen = /* @__PURE__ */ new Set();
4329
4435
  for (const chunk2 of chunks) {
4330
- const query = `${joinField} in (${chunk2.map(sqlQuote).join(",")})`;
4436
+ const inClause = `${joinField} in (${chunk2.map(sqlQuote).join(",")})`;
4437
+ const query = pushDownCond !== null ? `(${inClause}) and (${whereToKintone(pushDownCond)})` : inClause;
4331
4438
  const resolved = await fetchRecordsForSharedPlan(client.getRecords, join2.table.appId, query, fields, {
4332
4439
  parallel,
4333
4440
  maxRecords,
@@ -5143,19 +5250,21 @@ function buildSelectPlan(stmt, label) {
5143
5250
  lines.push(` kintone query: ${params.query || "(\u306A\u3057)"}`);
5144
5251
  lines.push(` fields: ${params.fields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : params.fields.join(", ")}`);
5145
5252
  } else {
5146
- const mainParams = selectToFetchAllParams(stmt, stmt.from.appId);
5147
5253
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
5148
- const mainAlias = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
5149
- const mainQ = mainParams.query ? `${mainParams.query} (\u6B8B\u308A\u306F JS \u8A55\u4FA1)` : "(\u5168\u4EF6\u53D6\u5F97)";
5150
- lines.push(` app: APP${stmt.from.appId}${mainAlias} (${stmt.from.appId})`);
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})`);
5151
5258
  lines.push(` kintone query: ${mainQ}`);
5152
5259
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
5153
5260
  for (const join2 of stmt.joins) {
5154
5261
  const joinFields = selectToFetchAllFields(stmt, join2.table);
5155
- const joinAlias = join2.table.alias ? ` AS ${join2.table.alias}` : "";
5262
+ const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
5156
5263
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
5157
- lines.push(` ${joinType}: APP${join2.table.appId}${joinAlias} (${join2.table.appId})`);
5158
- lines.push(` kintone query: (\u5168\u4EF6\u53D6\u5F97)`);
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}`);
5159
5268
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
5160
5269
  }
5161
5270
  }
@@ -7040,7 +7149,7 @@ async function run() {
7040
7149
  isDmlStatement = isDmlType(stmtType);
7041
7150
  hasWhere = hasWhereClause(stmt);
7042
7151
  insertValuesCount = getInsertValuesCount(stmt);
7043
- const supported = stmtType === "SELECT" || isDmlStatement;
7152
+ const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || isDmlStatement;
7044
7153
  if (!supported) {
7045
7154
  process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
7046
7155
  `);
@@ -7088,7 +7197,7 @@ async function run() {
7088
7197
  const appIds = sql ? extractAppIds(sql) : [];
7089
7198
  const defaultApp = args.app ?? envInt("KSQL_APP") ?? profile.app ?? null;
7090
7199
  if (appIds.length === 0 && defaultApp !== null) appIds.push(defaultApp);
7091
- const allowNoFromSelect = isNoFromSelectStatement(parsedStmt);
7200
+ const allowNoFromSelect = isNoFromSelectStatement(parsedStmt) || stmtType === "SHOW_APPS";
7092
7201
  if (appIds.length === 0 && !allowNoFromSelect && !args.dryRun && args.diagRecordId === null) {
7093
7202
  process.stderr.write("ArgumentError: no APPxxx found in SQL and --app is not set.\n");
7094
7203
  return 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex0220/kintone-sql-tools",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "kintone SQL plugin and CLI tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "prepack": "npm run build:cli",
26
26
  "start": "run-p develop upload",
27
27
  "develop": "node build.mjs --watch",
28
- "upload": "rex0220-plugin-uploader -f dist/ksql-plugin-v1.1.0.zip --watch"
28
+ "upload": "rex0220-plugin-uploader -f dist/ksql-plugin-v1.1.2.zip --watch"
29
29
  },
30
30
  "files": [
31
31
  "dist-cli/",