3535import org .eclipse .rdf4j .benchmark .common .plan .QueryPlanSnapshot ;
3636import org .eclipse .rdf4j .benchmark .rio .util .ThemeDataSetGenerator .Theme ;
3737import org .eclipse .rdf4j .common .annotation .Experimental ;
38+ import org .eclipse .rdf4j .query .TupleQuery ;
3839import org .eclipse .rdf4j .queryrender .sparql .TupleExprIRRenderer ;
3940import org .eclipse .rdf4j .repository .sail .SailRepositoryConnection ;
4041
@@ -171,14 +172,16 @@ private void runSingleQueryCapture(QueryPlanSnapshotCliOptions options,
171172 QueryExecutionVerification executionVerification ;
172173 try (SailRepositoryConnection connection = storeRuntime .repository .getConnection ()) {
173174 if (options .persist ) {
174- snapshotPath = capture .captureAndWrite (context , () -> connection .prepareTupleQuery (queryText ));
175+ snapshotPath = capture .captureAndWrite (context ,
176+ () -> prepareTupleQuery (connection , queryText , options .queryTimeoutSeconds ));
175177 currentSnapshot = capture .readSnapshot (snapshotPath );
176178 output .println ("Snapshot written: " + snapshotPath .toAbsolutePath ());
177179 } else {
178- currentSnapshot = capture .capture (context , () -> connection .prepareTupleQuery (queryText ));
180+ currentSnapshot = capture .capture (context ,
181+ () -> prepareTupleQuery (connection , queryText , options .queryTimeoutSeconds ));
179182 output .println ("Snapshot captured in-memory only (--persist=false)." );
180183 }
181- executionVerification = verifyRepeatedExecution (connection , queryText );
184+ executionVerification = verifyRepeatedExecution (connection , queryText , options . queryTimeoutSeconds );
182185 }
183186
184187 printResultsSection (options , queryId , queryText );
@@ -222,14 +225,17 @@ private void runAllThemeQueriesCapture(QueryPlanSnapshotCliOptions options,
222225 QueryExecutionVerification executionVerification ;
223226 try (SailRepositoryConnection connection = storeRuntime .repository .getConnection ()) {
224227 if (options .persist ) {
225- snapshotPath = capture .captureAndWrite (context , () -> connection .prepareTupleQuery (queryText ));
228+ snapshotPath = capture .captureAndWrite (context ,
229+ () -> prepareTupleQuery (connection , queryText , perQueryOptions .queryTimeoutSeconds ));
226230 currentSnapshot = capture .readSnapshot (snapshotPath );
227231 output .println ("Snapshot written: " + snapshotPath .toAbsolutePath ());
228232 } else {
229- currentSnapshot = capture .capture (context , () -> connection .prepareTupleQuery (queryText ));
233+ currentSnapshot = capture .capture (context ,
234+ () -> prepareTupleQuery (connection , queryText , perQueryOptions .queryTimeoutSeconds ));
230235 output .println ("Snapshot captured in-memory only (--persist=false)." );
231236 }
232- executionVerification = verifyRepeatedExecution (connection , queryText );
237+ executionVerification = verifyRepeatedExecution (connection , queryText ,
238+ perQueryOptions .queryTimeoutSeconds );
233239 }
234240
235241 output .println ();
@@ -589,6 +595,9 @@ private void promptAllRunOptions(QueryPlanSnapshotCliOptions options) throws IOE
589595 options .diffMode = promptDiffMode (options .diffMode );
590596 }
591597 options .queryId = promptOptionalValue ("Query id (blank keeps default/auto)" , options .queryId );
598+ options .queryTimeoutSeconds = promptOptionalNonNegativeInteger (
599+ "Query timeout seconds (blank keeps current, 0 disables timeout)" ,
600+ options .queryTimeoutSeconds );
592601 if (options .outputDirectory == null ) {
593602 options .outputDirectory = promptOptionalPath ("Output directory (blank uses default)" );
594603 }
@@ -641,6 +650,20 @@ private Path promptOptionalPath(String message) throws IOException {
641650 return Path .of (raw .trim ());
642651 }
643652
653+ private Integer promptOptionalNonNegativeInteger (String message , Integer currentValue ) throws IOException {
654+ while (true ) {
655+ String raw = prompt (message );
656+ if (raw .isBlank ()) {
657+ return currentValue ;
658+ }
659+ try {
660+ return QueryPlanSnapshotCliOptions .parseNonNegativeInteger (raw , message );
661+ } catch (IllegalArgumentException e ) {
662+ output .println (e .getMessage ());
663+ }
664+ }
665+ }
666+
644667 private void promptForAssignments (String heading , Map <String , String > target ) throws IOException {
645668 output .println (heading );
646669 while (true ) {
@@ -807,6 +830,9 @@ private static QueryPlanCaptureContext createContext(QueryPlanSnapshotCliOptions
807830 .addMetadata (QueryPlanCapture .metadataFromSystemProperties ())
808831 .featureFlagCollector (featureFlags )
809832 .tupleExprRenderer (QueryPlanSnapshotCli ::renderTupleExprWithIr );
833+ if (options .queryTimeoutSeconds != null ) {
834+ contextBuilder .addMetadata ("queryTimeoutSeconds" , options .queryTimeoutSeconds .toString ());
835+ }
810836
811837 if (options .queryIndex != null && benchmarkQuery != null ) {
812838 contextBuilder .addMetadata ("queryIndex" , Integer .toString (options .queryIndex ))
@@ -823,7 +849,8 @@ private static FeatureFlagCollector createFeatureFlagCollector(QueryPlanSnapshot
823849 .addValue ("cli.store" , options .store .id )
824850 .addValue ("cli.theme" , options .theme .name ())
825851 .addValue ("cli.querySource" , querySource )
826- .addValue ("cli.persist" , Boolean .toString (options .persist ));
852+ .addValue ("cli.persist" , Boolean .toString (options .persist ))
853+ .addValue ("cli.queryTimeoutSeconds" , formatQueryTimeoutSeconds (options .queryTimeoutSeconds ));
827854 if (options .queryIndex != null ) {
828855 featureFlags .addValue ("cli.queryIndex" , options .queryIndex .toString ());
829856 }
@@ -903,6 +930,22 @@ private static String normalizedOrNull(String value) {
903930 return normalized .isEmpty () ? null : normalized ;
904931 }
905932
933+ private static String formatQueryTimeoutSeconds (Integer queryTimeoutSeconds ) {
934+ if (queryTimeoutSeconds == null || queryTimeoutSeconds == 0 ) {
935+ return "<none>" ;
936+ }
937+ return queryTimeoutSeconds .toString ();
938+ }
939+
940+ private static TupleQuery prepareTupleQuery (SailRepositoryConnection connection , String queryText ,
941+ Integer queryTimeoutSeconds ) {
942+ TupleQuery tupleQuery = connection .prepareTupleQuery (queryText );
943+ if (queryTimeoutSeconds != null && queryTimeoutSeconds > 0 ) {
944+ tupleQuery .setMaxExecutionTime (queryTimeoutSeconds );
945+ }
946+ return tupleQuery ;
947+ }
948+
906949 private void printPrettyExplanations (QueryPlanSnapshot snapshot ) {
907950 Map <String , QueryPlanExplanation > explanations = snapshot .getExplanations ();
908951 if (explanations == null || explanations .isEmpty ()) {
@@ -925,7 +968,8 @@ private void printResultsSection(QueryPlanSnapshotCliOptions options, String que
925968 output .println ("=== Results ===" );
926969 output .println ("Original query:" );
927970 output .println (queryText .trim ());
928- output .println ("Store=" + options .store .id + ", Theme=" + options .theme + ", QueryId=" + queryId );
971+ output .println ("Store=" + options .store .id + ", Theme=" + options .theme + ", QueryId=" + queryId
972+ + ", QueryTimeoutSeconds=" + formatQueryTimeoutSeconds (options .queryTimeoutSeconds ));
929973 }
930974
931975 private void printExplanation (String levelKey , QueryPlanExplanation explanation ) {
@@ -952,7 +996,8 @@ private void printExplanation(String levelKey, QueryPlanExplanation explanation)
952996 }
953997 }
954998
955- private QueryExecutionVerification verifyRepeatedExecution (SailRepositoryConnection connection , String queryText ) {
999+ private QueryExecutionVerification verifyRepeatedExecution (SailRepositoryConnection connection , String queryText ,
1000+ Integer queryTimeoutSeconds ) {
9561001 long elapsedNanos = 0 ;
9571002 long stableResultCount = Long .MIN_VALUE ;
9581003 int runs = 0 ;
@@ -971,7 +1016,10 @@ private QueryExecutionVerification verifyRepeatedExecution(SailRepositoryConnect
9711016 }
9721017
9731018 long startedAt = System .nanoTime ();
974- long currentResultCount = connection .prepareTupleQuery (queryText ).evaluate ().stream ().count ();
1019+ long currentResultCount = prepareTupleQuery (connection , queryText , queryTimeoutSeconds )
1020+ .evaluate ()
1021+ .stream ()
1022+ .count ();
9751023 long runNanos = Math .max (1L , System .nanoTime () - startedAt );
9761024 elapsedNanos += runNanos ;
9771025 runs ++;
0 commit comments