|
59 | 59 | import com.google.cloud.spanner.CommitResponse;
|
60 | 60 | import com.google.cloud.spanner.CommitStats;
|
61 | 61 | import com.google.cloud.spanner.Dialect;
|
| 62 | +import com.google.cloud.spanner.ErrorCode; |
62 | 63 | import com.google.cloud.spanner.Options.RpcPriority;
|
| 64 | +import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; |
63 | 65 | import com.google.cloud.spanner.ResultSet;
|
64 | 66 | import com.google.cloud.spanner.ResultSets;
|
| 67 | +import com.google.cloud.spanner.SpannerExceptionFactory; |
| 68 | +import com.google.cloud.spanner.Statement; |
65 | 69 | import com.google.cloud.spanner.Struct;
|
66 | 70 | import com.google.cloud.spanner.TimestampBound;
|
67 | 71 | import com.google.cloud.spanner.Type;
|
|
71 | 75 | import com.google.common.base.Preconditions;
|
72 | 76 | import com.google.common.collect.ImmutableMap;
|
73 | 77 | import com.google.protobuf.Duration;
|
| 78 | +import com.google.spanner.v1.PlanNode; |
| 79 | +import com.google.spanner.v1.QueryPlan; |
74 | 80 | import com.google.spanner.v1.RequestOptions;
|
75 | 81 | import com.google.spanner.v1.RequestOptions.Priority;
|
| 82 | +import java.util.ArrayList; |
76 | 83 | import java.util.Collections;
|
77 | 84 | import java.util.Map;
|
78 | 85 | import java.util.concurrent.TimeUnit;
|
|
83 | 90 | * calls are then forwarded into a {@link Connection}.
|
84 | 91 | */
|
85 | 92 | class ConnectionStatementExecutorImpl implements ConnectionStatementExecutor {
|
| 93 | + |
86 | 94 | static final class StatementTimeoutGetter implements DurationValueGetter {
|
87 | 95 | private final Connection connection;
|
88 | 96 |
|
@@ -442,4 +450,196 @@ public StatementResult statementShowRPCPriority() {
|
442 | 450 | public StatementResult statementShowTransactionIsolationLevel() {
|
443 | 451 | return resultSet("transaction_isolation", "serializable", SHOW_TRANSACTION_ISOLATION_LEVEL);
|
444 | 452 | }
|
| 453 | + |
| 454 | + private String processQueryPlan(PlanNode planNode) { |
| 455 | + StringBuilder planNodeDescription = new StringBuilder(" : { "); |
| 456 | + com.google.protobuf.Struct metadata = planNode.getMetadata(); |
| 457 | + |
| 458 | + for (String key : metadata.getFieldsMap().keySet()) { |
| 459 | + planNodeDescription |
| 460 | + .append(key) |
| 461 | + .append(" : ") |
| 462 | + .append(metadata.getFieldsMap().get(key).getStringValue()) |
| 463 | + .append(" , "); |
| 464 | + } |
| 465 | + String substring = planNodeDescription.substring(0, planNodeDescription.length() - 3); |
| 466 | + planNodeDescription.setLength(0); |
| 467 | + planNodeDescription.append(substring).append(" }"); |
| 468 | + |
| 469 | + return planNodeDescription.toString(); |
| 470 | + } |
| 471 | + |
| 472 | + private String processExecutionStats(PlanNode planNode) { |
| 473 | + StringBuilder executionStats = new StringBuilder(""); |
| 474 | + for (String key : planNode.getExecutionStats().getFieldsMap().keySet()) { |
| 475 | + executionStats.append(key).append(" : { "); |
| 476 | + com.google.protobuf.Struct value = |
| 477 | + planNode.getExecutionStats().getFieldsMap().get(key).getStructValue(); |
| 478 | + for (String newKey : value.getFieldsMap().keySet()) { |
| 479 | + String newValue = value.getFieldsMap().get(newKey).getStringValue(); |
| 480 | + executionStats.append(newKey).append(" : ").append(newValue).append(" , "); |
| 481 | + } |
| 482 | + String substring = executionStats.substring(0, executionStats.length() - 3); |
| 483 | + executionStats.setLength(0); |
| 484 | + executionStats.append(substring).append(" } , "); |
| 485 | + } |
| 486 | + String substring = executionStats.substring(0, executionStats.length() - 3); |
| 487 | + executionStats.setLength(0); |
| 488 | + executionStats.append(substring); |
| 489 | + return executionStats.toString(); |
| 490 | + } |
| 491 | + |
| 492 | + private StatementResult getStatementResultFromQueryPlan(QueryPlan queryPlan, boolean isAnalyze) { |
| 493 | + ArrayList<Struct> list = new ArrayList<>(); |
| 494 | + for (PlanNode planNode : queryPlan.getPlanNodesList()) { |
| 495 | + String planNodeDescription = planNode.getDisplayName(); |
| 496 | + String executionStats = ""; |
| 497 | + |
| 498 | + if (!planNode.getMetadata().toString().equalsIgnoreCase("")) { |
| 499 | + planNodeDescription += processQueryPlan(planNode); |
| 500 | + } |
| 501 | + |
| 502 | + if (!planNode.getShortRepresentation().toString().equalsIgnoreCase("")) { |
| 503 | + planNodeDescription += " : " + planNode.getShortRepresentation().getDescription(); |
| 504 | + } |
| 505 | + |
| 506 | + if (isAnalyze && !planNode.getExecutionStats().toString().equals("")) { |
| 507 | + executionStats = processExecutionStats(planNode); |
| 508 | + } |
| 509 | + Struct.Builder builder = Struct.newBuilder().set("QUERY PLAN").to(planNodeDescription); |
| 510 | + |
| 511 | + if (isAnalyze) { |
| 512 | + builder.set("EXECUTION STATS").to(executionStats); |
| 513 | + } |
| 514 | + list.add(builder.build()); |
| 515 | + } |
| 516 | + |
| 517 | + ResultSet resultSet; |
| 518 | + if (isAnalyze) { |
| 519 | + resultSet = |
| 520 | + ResultSets.forRows( |
| 521 | + Type.struct( |
| 522 | + StructField.of("QUERY PLAN", Type.string()), |
| 523 | + StructField.of("EXECUTION STATS", Type.string())), |
| 524 | + list); |
| 525 | + } else { |
| 526 | + resultSet = |
| 527 | + ResultSets.forRows(Type.struct(StructField.of("QUERY PLAN", Type.string())), list); |
| 528 | + } |
| 529 | + return StatementResultImpl.of(resultSet); |
| 530 | + } |
| 531 | + |
| 532 | + private StatementResult executeStatement(String sql, QueryAnalyzeMode queryAnalyzeMode) { |
| 533 | + Statement statement = Statement.newBuilder(sql).build(); |
| 534 | + try (ResultSet resultSet = getConnection().analyzeQuery(statement, queryAnalyzeMode)) { |
| 535 | + while (resultSet.next()) { |
| 536 | + // ResultSet.next() should return false in order to access the ResultSet.Stats |
| 537 | + } |
| 538 | + |
| 539 | + if (resultSet.getStats() != null && resultSet.getStats().getQueryPlan() != null) { |
| 540 | + return getStatementResultFromQueryPlan( |
| 541 | + resultSet.getStats().getQueryPlan(), queryAnalyzeMode.equals(QueryAnalyzeMode.PROFILE)); |
| 542 | + } |
| 543 | + } |
| 544 | + throw SpannerExceptionFactory.newSpannerException( |
| 545 | + ErrorCode.FAILED_PRECONDITION, String.format("Couldn't fetch stats for %s", sql)); |
| 546 | + } |
| 547 | + |
| 548 | + // This method removes parenthesis from the sql string assuming it is ending with the closing |
| 549 | + // parenthesis |
| 550 | + private String removeParenthesisAndTrim(String sql) { |
| 551 | + sql = sql.trim(); |
| 552 | + if (sql.charAt(0) == '(') { |
| 553 | + sql = sql.substring(1, sql.length() - 1); |
| 554 | + } |
| 555 | + return sql.trim(); |
| 556 | + } |
| 557 | + |
| 558 | + /* |
| 559 | + * This method executes the given SQL string in either PLAN or PROFILE mode and returns |
| 560 | + * the query plan and execution stats (if necessary). |
| 561 | + * |
| 562 | + * The only additional option that is supported is ANALYZE. The method will throw a SpannerException |
| 563 | + * if it is invoked with a statement that includes any other options. |
| 564 | + * |
| 565 | + * If the SQL string has ANALYZE option, it will be executed in PROFILE mode and will return a resultset |
| 566 | + * with two String columns namely QUERY PLAN and EXECUTION STATS. |
| 567 | + * |
| 568 | + * If the sql string doesn't have any option, it will be executed in PLAN mode and will return a resultset |
| 569 | + * with one string column namely QUERY PLAN. |
| 570 | + */ |
| 571 | + @Override |
| 572 | + public StatementResult statementExplain(String sql) { |
| 573 | + if (sql == null) { |
| 574 | + throw SpannerExceptionFactory.newSpannerException( |
| 575 | + ErrorCode.INVALID_ARGUMENT, String.format("Invalid String with Explain")); |
| 576 | + } |
| 577 | + |
| 578 | + if (sql.charAt(0) == '(') { |
| 579 | + int index = sql.indexOf(')'); |
| 580 | + if (index == -1) { |
| 581 | + throw SpannerExceptionFactory.newSpannerException( |
| 582 | + ErrorCode.INVALID_ARGUMENT, |
| 583 | + String.format("Missing closing parenthesis in the query: %s", sql)); |
| 584 | + } |
| 585 | + String options[] = sql.substring(1, index).split("\\s*,\\s*"); |
| 586 | + boolean isAnalyze = false, startAfterIndex = false; |
| 587 | + for (String option : options) { |
| 588 | + String optionExpression[] = option.trim().split("\\s+"); |
| 589 | + if (optionExpression.length >= 3) { |
| 590 | + isAnalyze = false; |
| 591 | + break; |
| 592 | + } else if (ClientSideStatementExplainExecutor.EXPLAIN_OPTIONS.contains( |
| 593 | + optionExpression[0].toLowerCase())) { |
| 594 | + throw SpannerExceptionFactory.newSpannerException( |
| 595 | + ErrorCode.UNIMPLEMENTED, |
| 596 | + String.format("%s is not implemented yet", optionExpression[0])); |
| 597 | + } else if (optionExpression[0].equalsIgnoreCase("analyse") |
| 598 | + || optionExpression[0].equalsIgnoreCase("analyze")) { |
| 599 | + isAnalyze = true; |
| 600 | + } else { |
| 601 | + isAnalyze = false; |
| 602 | + break; |
| 603 | + } |
| 604 | + |
| 605 | + if (optionExpression.length == 2) { |
| 606 | + if (optionExpression[1].equalsIgnoreCase("false") |
| 607 | + || optionExpression[1].equalsIgnoreCase("0") |
| 608 | + || optionExpression[1].equalsIgnoreCase("off")) { |
| 609 | + isAnalyze = false; |
| 610 | + startAfterIndex = true; |
| 611 | + } else if (!(optionExpression[1].equalsIgnoreCase("true") |
| 612 | + || optionExpression[1].equalsIgnoreCase("1") |
| 613 | + || optionExpression[1].equalsIgnoreCase("on"))) { |
| 614 | + isAnalyze = false; |
| 615 | + break; |
| 616 | + } |
| 617 | + } |
| 618 | + } |
| 619 | + if (isAnalyze) { |
| 620 | + String newSql = removeParenthesisAndTrim(sql.substring(index + 1)); |
| 621 | + return executeStatement(newSql, QueryAnalyzeMode.PROFILE); |
| 622 | + } else if (startAfterIndex) { |
| 623 | + String newSql = removeParenthesisAndTrim(sql.substring(index + 1)); |
| 624 | + return executeStatement(newSql, QueryAnalyzeMode.PLAN); |
| 625 | + } else { |
| 626 | + return executeStatement(removeParenthesisAndTrim(sql), QueryAnalyzeMode.PLAN); |
| 627 | + } |
| 628 | + } else { |
| 629 | + String[] arr = sql.split("\\s+", 2); |
| 630 | + if (arr.length >= 2) { |
| 631 | + String option = arr[0].toLowerCase(); |
| 632 | + String statementToBeExplained = arr[1]; |
| 633 | + |
| 634 | + if (ClientSideStatementExplainExecutor.EXPLAIN_OPTIONS.contains(option)) { |
| 635 | + throw SpannerExceptionFactory.newSpannerException( |
| 636 | + ErrorCode.UNIMPLEMENTED, String.format("%s is not implemented yet", option)); |
| 637 | + } else if (option.equals("analyze") || option.equals("analyse")) { |
| 638 | + return executeStatement( |
| 639 | + removeParenthesisAndTrim(statementToBeExplained), QueryAnalyzeMode.PROFILE); |
| 640 | + } |
| 641 | + } |
| 642 | + return executeStatement(sql, QueryAnalyzeMode.PLAN); |
| 643 | + } |
| 644 | + } |
445 | 645 | }
|
0 commit comments