@rex0220/kintone-sql-tools 1.1.1 → 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 +125 -21
  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 = {};
@@ -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 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(
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 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) => {
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 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;
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 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`
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 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;
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 mainAlias = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
5154
- const mainQ = mainParams.query ? `${mainParams.query} (\u6B8B\u308A\u306F JS \u8A55\u4FA1)` : "(\u5168\u4EF6\u53D6\u5F97)";
5155
- 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})`);
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 joinAlias = join2.table.alias ? ` AS ${join2.table.alias}` : "";
5262
+ const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
5161
5263
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
5162
- lines.push(` ${joinType}: APP${join2.table.appId}${joinAlias} (${join2.table.appId})`);
5163
- 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}`);
5164
5268
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
5165
5269
  }
5166
5270
  }
@@ -7045,7 +7149,7 @@ async function run() {
7045
7149
  isDmlStatement = isDmlType(stmtType);
7046
7150
  hasWhere = hasWhereClause(stmt);
7047
7151
  insertValuesCount = getInsertValuesCount(stmt);
7048
- const supported = stmtType === "SELECT" || isDmlStatement;
7152
+ const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || isDmlStatement;
7049
7153
  if (!supported) {
7050
7154
  process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
7051
7155
  `);
@@ -7093,7 +7197,7 @@ async function run() {
7093
7197
  const appIds = sql ? extractAppIds(sql) : [];
7094
7198
  const defaultApp = args.app ?? envInt("KSQL_APP") ?? profile.app ?? null;
7095
7199
  if (appIds.length === 0 && defaultApp !== null) appIds.push(defaultApp);
7096
- const allowNoFromSelect = isNoFromSelectStatement(parsedStmt);
7200
+ const allowNoFromSelect = isNoFromSelectStatement(parsedStmt) || stmtType === "SHOW_APPS";
7097
7201
  if (appIds.length === 0 && !allowNoFromSelect && !args.dryRun && args.diagRecordId === null) {
7098
7202
  process.stderr.write("ArgumentError: no APPxxx found in SQL and --app is not set.\n");
7099
7203
  return 2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex0220/kintone-sql-tools",
3
- "version": "1.1.1",
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.1.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/",