Skip to content

Commit 841aaf8

Browse files
committed
NEW: QueryPlanLogger for DB2
1 parent 6d5f740 commit 841aaf8

File tree

5 files changed

+370
-0
lines changed

5 files changed

+370
-0
lines changed

ebean-api/src/main/java/io/ebean/DatabaseBuilder.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2079,6 +2079,11 @@ default DatabaseBuilder queryPlanEnable(boolean queryPlanEnable) {
20792079
@Deprecated
20802080
DatabaseBuilder setQueryPlanEnable(boolean queryPlanEnable);
20812081

2082+
/**
2083+
* Set platform specific query plan options.
2084+
*/
2085+
public void setQueryPlanOptions(String queryPlanOptions);
2086+
20822087
/**
20832088
* Set the query plan collection threshold in microseconds.
20842089
* <p>
@@ -3061,6 +3066,11 @@ interface Settings extends DatabaseBuilder {
30613066
*/
30623067
boolean isQueryPlanEnable();
30633068

3069+
/**
3070+
* Returns platform specific query plan options.
3071+
*/
3072+
public String getQueryPlanOptions();
3073+
30643074
/**
30653075
* Return the query plan collection threshold in microseconds.
30663076
*/

ebean-api/src/main/java/io/ebean/config/DatabaseConfig.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ public class DatabaseConfig implements DatabaseBuilder.Settings {
511511
*/
512512
private boolean queryPlanEnable;
513513

514+
/**
515+
* Additional platform specific options for query-plan generation.
516+
*/
517+
private String queryPlanOptions;
518+
514519
/**
515520
* The default threshold in micros for collecting query plans.
516521
*/
@@ -2146,6 +2151,7 @@ protected void loadSettings(PropertiesWrapper p) {
21462151
queryPlanTTLSeconds = p.getInt("queryPlanTTLSeconds", queryPlanTTLSeconds);
21472152
slowQueryMillis = p.getLong("slowQueryMillis", slowQueryMillis);
21482153
queryPlanEnable = p.getBoolean("queryPlan.enable", queryPlanEnable);
2154+
queryPlanOptions = p.get("queryPlan.options", queryPlanOptions);
21492155
queryPlanThresholdMicros = p.getLong("queryPlan.thresholdMicros", queryPlanThresholdMicros);
21502156
queryPlanCapture = p.getBoolean("queryPlan.capture", queryPlanCapture);
21512157
queryPlanCapturePeriodSecs = p.getLong("queryPlan.capturePeriodSecs", queryPlanCapturePeriodSecs);
@@ -2461,6 +2467,25 @@ public DatabaseConfig setQueryPlanEnable(boolean queryPlanEnable) {
24612467
return this;
24622468
}
24632469

2470+
/**
2471+
* Returns platform specific query plan options.
2472+
*/
2473+
@Override
2474+
public String getQueryPlanOptions() {
2475+
return queryPlanOptions;
2476+
}
2477+
2478+
/**
2479+
* Set platform specific query plan options.
2480+
*/
2481+
@Override
2482+
public void setQueryPlanOptions(String queryPlanOptions) {
2483+
this.queryPlanOptions = queryPlanOptions;
2484+
}
2485+
2486+
/**
2487+
* Return the query plan collection threshold in microseconds.
2488+
*/
24642489
@Override
24652490
public long getQueryPlanThresholdMicros() {
24662491
return queryPlanThresholdMicros;

ebean-core/src/main/java/io/ebeaninternal/server/core/InternalConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,8 @@ QueryPlanLogger queryPlanLogger(Platform platform) {
595595
return new QueryPlanLoggerSqlServer();
596596
case ORACLE:
597597
return new QueryPlanLoggerOracle();
598+
case DB2:
599+
return new QueryPlanLoggerDb2(config.getQueryPlanOptions());
598600
case POSTGRES:
599601
return new QueryPlanLoggerExplain("explain (analyze, buffers) ");
600602
case YUGABYTE:
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
package io.ebeaninternal.server.query;
3+
4+
import io.ebean.util.IOUtils;
5+
import io.ebean.util.StringHelper;
6+
import io.ebeaninternal.api.CoreLog;
7+
import io.ebeaninternal.api.SpiDbQueryPlan;
8+
import io.ebeaninternal.api.SpiQueryPlan;
9+
import io.ebeaninternal.server.bind.capture.BindCapture;
10+
11+
import java.io.BufferedReader;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.sql.Connection;
15+
import java.sql.PreparedStatement;
16+
import java.sql.ResultSet;
17+
import java.sql.SQLException;
18+
import java.sql.Statement;
19+
import java.util.Map;
20+
import java.util.Random;
21+
22+
import static java.lang.System.Logger.Level.WARNING;
23+
24+
/**
25+
* A QueryPlanLogger for DB2.
26+
* <p>
27+
* To use query plan capturing, you have to install the explain tables with
28+
* <code>SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA )</code>.
29+
* To do this in a repeatable script, you may use this statement:
30+
*
31+
* <pre>
32+
* BEGIN
33+
* IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = CURRENT SCHEMA AND TABNAME = 'EXPLAIN_STREAM') THEN
34+
* call SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', CURRENT SCHEMA );
35+
* END IF;
36+
* END
37+
* </pre>
38+
*
39+
* @author Roland Praml, FOCONIS AG
40+
*/
41+
public final class QueryPlanLoggerDb2 extends QueryPlanLogger {
42+
43+
private Random rnd = new Random();
44+
45+
private final String schema;
46+
47+
private final boolean create;
48+
49+
private static final String GET_PLAN_TEMPLATE = readReasource("QueryPlanLoggerDb2.sql");
50+
51+
private static final String CREATE_TEMPLATE = "BEGIN\n"
52+
+ "IF NOT EXISTS (SELECT * FROM SYSCAT.TABLES WHERE TABSCHEMA = ${SCHEMA} AND TABNAME = 'EXPLAIN_STREAM') THEN\n"
53+
+ " CALL SYSPROC.SYSINSTALLOBJECTS( 'EXPLAIN', 'C' , '', ${SCHEMA} );\n"
54+
+ "END IF;\n"
55+
+ "END";
56+
57+
public QueryPlanLoggerDb2(String opts) {
58+
Map<String, String> map = StringHelper.delimitedToMap(opts, ";", "=");
59+
create = !"false" .equals(map.get("create")); // default is create
60+
String schema = map.get("schema"); // should be null or SYSTOOLS
61+
if (schema == null || schema.isEmpty()) {
62+
this.schema = null;
63+
} else {
64+
this.schema = schema.toUpperCase();
65+
}
66+
}
67+
68+
private static String readReasource(String resName) {
69+
try (InputStream stream = QueryPlanLoggerDb2.class.getResourceAsStream(resName)) {
70+
if (stream == null) {
71+
throw new IllegalStateException("Could not find resource " + resName);
72+
}
73+
BufferedReader reader = IOUtils.newReader(stream);
74+
StringBuilder sb = new StringBuilder();
75+
reader.lines().forEach(line -> sb.append(line).append('\n'));
76+
return sb.toString();
77+
} catch (IOException e) {
78+
throw new IllegalStateException("Could not read resource " + resName, e);
79+
}
80+
}
81+
82+
@Override
83+
public SpiDbQueryPlan collectPlan(Connection conn, SpiQueryPlan plan, BindCapture bind) {
84+
try (Statement stmt = conn.createStatement()) {
85+
if (create) {
86+
// create explain tables if neccessary
87+
if (schema == null) {
88+
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "CURRENT USER"));
89+
} else {
90+
stmt.execute(CREATE_TEMPLATE.replace("${SCHEMA}", "'" + schema + "'"));
91+
}
92+
conn.commit();
93+
}
94+
95+
try {
96+
int queryNo = rnd.nextInt(Integer.MAX_VALUE);
97+
98+
String sql = "EXPLAIN PLAN SET QUERYNO = " + queryNo + " FOR " + plan.sql();
99+
try (PreparedStatement explainStmt = conn.prepareStatement(sql)) {
100+
bind.prepare(explainStmt, conn);
101+
explainStmt.execute();
102+
}
103+
104+
sql = schema == null
105+
? GET_PLAN_TEMPLATE.replace("${SCHEMA}", conn.getMetaData().getUserName().toUpperCase())
106+
: GET_PLAN_TEMPLATE.replace("${SCHEMA}", schema);
107+
108+
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
109+
pstmt.setInt(1, queryNo);
110+
try (ResultSet rset = pstmt.executeQuery()) {
111+
return readQueryPlan(plan, bind, rset);
112+
}
113+
}
114+
} finally {
115+
conn.rollback(); // do not keep query plans in DB
116+
}
117+
} catch (SQLException e) {
118+
CoreLog.log.log(WARNING, "Could not log query plan", e);
119+
return null;
120+
}
121+
}
122+
}

0 commit comments

Comments
 (0)