77import core .framework .db .Repository ;
88import core .framework .db .Transaction ;
99import core .framework .db .UncheckedSQLException ;
10+ import core .framework .internal .db .inspector .MySQLQueryAnalyzer ;
11+ import core .framework .internal .db .inspector .PostgreSQLQueryAnalyzer ;
12+ import core .framework .internal .db .inspector .QueryInspector ;
1013import core .framework .internal .log .ActionLog ;
1114import core .framework .internal .log .LogManager ;
15+ import core .framework .internal .log .TrackResult ;
1216import core .framework .internal .resource .Pool ;
13- import core .framework .util .ASCII ;
1417import core .framework .util .StopWatch ;
1518import org .jspecify .annotations .Nullable ;
1619import org .slf4j .Logger ;
@@ -57,6 +60,7 @@ public final class DatabaseImpl implements Database {
5760 private @ Nullable Properties driverProperties ;
5861 private Duration timeout ;
5962 private Driver driver ;
63+ private QueryInspector inspector ;
6064
6165 public DatabaseImpl (String name ) {
6266 initializeRowMappers ();
@@ -130,17 +134,12 @@ Properties driverProperties(String url) {
130134 properties .setProperty ("logger" , "Slf4JLogger" );
131135 properties .setProperty ("cachePrepStmts" , "true" );
132136
133- int index = url .indexOf ('?' );
134- // mysql with ssl has overhead, usually we ensure security on arch level, e.g. gcloud sql proxy or firewall rule
135- // with gcloud/azure iam / clear_text_password plugin, ssl is required
136- // refer to https://cloud.google.com/sql/docs/mysql/authentication
137+ // with gcloud/azure iam / clear_text_password plugin, ssl is required, refer to https://cloud.google.com/sql/docs/mysql/authentication
137138 if (authProvider != null ) {
138- properties .setProperty ("sslMode" , "PREFERRED" );
139139 properties .setProperty (CloudAuthProvider .Provider .CLOUD_AUTH , "true" );
140- } else if (index == -1 || url .indexOf ("sslMode=" , index + 1 ) == -1 ) {
141- properties .setProperty ("sslMode" , "DISABLED" );
142140 }
143141 // refer to https://dev.mysql.com/doc/connector-j/en/connector-j-reference-charsets.html
142+ int index = url .indexOf ('?' );
144143 if (index == -1 || url .indexOf ("characterEncoding=" , index + 1 ) == -1 )
145144 properties .setProperty ("characterEncoding" , "utf-8" );
146145 } else if (url .startsWith ("jdbc:postgresql:" )) {
@@ -175,12 +174,15 @@ public void url(String url) {
175174 private Driver driver (String url ) {
176175 if (url .startsWith ("jdbc:mysql:" )) {
177176 operation .dialect = Dialect .MYSQL ;
177+ inspector = new QueryInspector (new MySQLQueryAnalyzer (operation ));
178178 return createDriver ("com.mysql.cj.jdbc.Driver" );
179179 } else if (url .startsWith ("jdbc:postgresql:" )) {
180180 operation .dialect = Dialect .POSTGRESQL ;
181+ inspector = new QueryInspector (new PostgreSQLQueryAnalyzer (operation ));
181182 return createDriver ("org.postgresql.Driver" );
182183 } else if (url .startsWith ("jdbc:hsqldb:" )) {
183184 operation .dialect = Dialect .MYSQL ; // unit test use mysql dialect
185+ inspector = new QueryInspector (null );
184186 return createDriver ("org.hsqldb.jdbc.JDBCDriver" );
185187 } else {
186188 throw new Error ("not supported database, url=" + url );
@@ -224,7 +226,8 @@ public Transaction beginTransaction() {
224226 @ Override
225227 public <T > List <T > select (String sql , Class <T > viewClass , Object ... params ) {
226228 var watch = new StopWatch ();
227- validateSQL (sql );
229+ inspector .inspect (sql , params );
230+
228231 int returnedRows = 0 ;
229232 try {
230233 List <T > results = operation .select (sql , rowMapper (viewClass ), params );
@@ -233,14 +236,19 @@ public <T> List<T> select(String sql, Class<T> viewClass, Object... params) {
233236 } finally {
234237 long elapsed = watch .elapsed ();
235238 logger .debug ("select, sql={}, params={}, returnedRows={}, elapsed={}" , sql , new SQLParams (operation .enumMapper , params ), returnedRows , elapsed );
236- track (elapsed , returnedRows , 0 , 1 ); // check after sql debug log, to make log easier to read
239+ boolean slow = track (elapsed , returnedRows , 0 , 1 ); // check after sql debug log, to make log easier to read
240+ if (slow ) {
241+ String plan = inspector .explain (sql , params );
242+ logger .debug ("plan=\n {}" , plan );
243+ }
237244 }
238245 }
239246
240247 @ Override
241248 public <T > Optional <T > selectOne (String sql , Class <T > viewClass , Object ... params ) {
242249 var watch = new StopWatch ();
243- validateSQL (sql );
250+ inspector .inspect (sql , params );
251+
244252 int returnedRows = 0 ;
245253 try {
246254 Optional <T > result = operation .selectOne (sql , rowMapper (viewClass ), params );
@@ -249,14 +257,19 @@ public <T> Optional<T> selectOne(String sql, Class<T> viewClass, Object... param
249257 } finally {
250258 long elapsed = watch .elapsed ();
251259 logger .debug ("selectOne, sql={}, params={}, returnedRows={}, elapsed={}" , sql , new SQLParams (operation .enumMapper , params ), returnedRows , elapsed );
252- track (elapsed , returnedRows , 0 , 1 );
260+ boolean slow = track (elapsed , returnedRows , 0 , 1 );
261+ if (slow ) {
262+ String plan = inspector .explain (sql , params );
263+ logger .debug ("plan=\n {}" , plan );
264+ }
253265 }
254266 }
255267
256268 @ Override
257269 public int execute (String sql , Object ... params ) {
258270 var watch = new StopWatch ();
259- validateSQL (sql );
271+ inspector .inspect (sql , params );
272+
260273 int affectedRows = 0 ;
261274 try {
262275 affectedRows = operation .update (sql , params );
@@ -271,8 +284,9 @@ public int execute(String sql, Object... params) {
271284 @ Override
272285 public int [] batchExecute (String sql , List <Object []> params ) {
273286 var watch = new StopWatch ();
274- validateSQL (sql );
275287 if (params .isEmpty ()) throw new Error ("params must not be empty" );
288+ inspector .inspect (sql , params .getFirst ());
289+
276290 int affectedRows = 0 ;
277291 try {
278292 int [] results = operation .batchUpdate (sql , params );
@@ -307,39 +321,14 @@ private <T> void registerViewClass(Class<T> viewClass) {
307321 rowMappers .put (viewClass , mapper );
308322 }
309323
310- void track (long elapsed , int readRows , int writeRows , int queries ) {
324+ // return if slow
325+ boolean track (long elapsed , int readRows , int writeRows , int queries ) {
311326 ActionLog actionLog = LogManager .currentActionLog ();
312327 if (actionLog != null ) {
313328 actionLog .stats .compute ("db_queries" , (k , oldValue ) -> (oldValue == null ) ? queries : oldValue + queries );
314- actionLog .track ("db" , elapsed , readRows , writeRows , 0 , 0 );
315- }
316- }
317-
318- void validateSQL (String sql ) {
319- if (sql .startsWith ("CREATE " )) return ; // ignore DDL
320-
321- // validate asterisk
322- // execute() could have select part, e.g. insert into select
323- int index = sql .indexOf ('*' );
324- while (index > -1 ) { // check whether it's wildcard or multiply operator
325- int length = sql .length ();
326- char ch = 0 ;
327- index ++;
328- for (; index < length ; index ++) {
329- ch = sql .charAt (index );
330- if (ch != ' ' ) break ; // seek to next non-whitespace
331- }
332- if (ch == ','
333- || index == length // sql ends with *
334- || index + 4 <= length && ASCII .toUpperCase (ch ) == 'F' && "FROM" .equals (ASCII .toUpperCase (sql .substring (index , index + 4 ))))
335- throw new Error ("sql must not contain wildcard(*), please only select columns needed, sql=" + sql );
336- index = sql .indexOf ('*' , index + 1 );
329+ TrackResult result = actionLog .track ("db" , elapsed , readRows , writeRows , 0 , 0 );
330+ return result .slow ();
337331 }
338-
339- // validate string value
340- // by this way, it also disallows functions with string values, e.g. IFNULL(column, 'value'), but it usually can be prevented by different design,
341- // and we prefer to simplify db usage if possible, and shift complexity to application layer
342- if (sql .indexOf ('\'' ) != -1 )
343- throw new Error ("sql must not contain single quote('), please use prepared statement and question mark(?), sql=" + sql );
332+ return false ;
344333 }
345334}
0 commit comments