From 441ce5abe259081016484719c3f62e93799f6bf2 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 6 Oct 2026 21:51:20 +0000 Subject: [PATCH 1/4] feat(ENGKNOW-3999): throwif -m and -d to include offending row -m replaces the default "Gor throw on: " message with a literal (quoted) string. -d adds the header and the row that triggered the throw to the error via GorDataException's header/row support. In nor context the internal ChromNOR/PosNOR columns are left out. Options combine with each other and with -retriable; default behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) --- documentation/src/command/THROWIF.rst | 24 ++++- .../gorsat/Analysis/ThrowIfAnalysis.scala | 21 ++++- .../main/scala/gorsat/Commands/ThrowIf.scala | 8 +- .../src/test/java/gorsat/UTestThrowIf.java | 88 +++++++++++++++++++ .../Analysis/UTestThrowIfAnalysis.scala | 36 ++++++++ 5 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 gortools/src/test/java/gorsat/UTestThrowIf.java diff --git a/documentation/src/command/THROWIF.rst b/documentation/src/command/THROWIF.rst index e9ab04513..1f690ca2f 100644 --- a/documentation/src/command/THROWIF.rst +++ b/documentation/src/command/THROWIF.rst @@ -9,6 +9,9 @@ THROWIF ======= The **THROWIF** command is a very simple tool which throws an exception if the contained statement is true. +By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message and ``-d`` to +include the header and the content of the row that triggered the exception in the error. + Usage ===== @@ -21,4 +24,23 @@ Options +--------------------+---------------------------------------------------------------------------------------+ | ``-retriable`` | If set an retriable exception is thrown. | -+--------------------+---------------------------------------------------------------------------------------+ \ No newline at end of file ++--------------------+---------------------------------------------------------------------------------------+ +| ``-m `` | Use ```` as the error message instead of ``Gor throw on: ``. | +| | Quote the message if it contains spaces. | ++--------------------+---------------------------------------------------------------------------------------+ +| ``-d`` | Include the header and the row that triggered the exception in the error. | ++--------------------+---------------------------------------------------------------------------------------+ + +Examples +======== +Fail with a custom message that names the offending row: + +.. code-block:: gor + + gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' -d status != 'mapped' + +The error message is:: + + liftover failed + Header: chrom pos status + Row: chr1 1 unmapped \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala index 35368513c..9a69471d8 100644 --- a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala +++ b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala @@ -27,11 +27,20 @@ import org.gorpipe.exceptions.GorDataException import org.gorpipe.gor.model.Row import org.gorpipe.gor.session.GorContext -case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: String, header: String, isRetriable: Boolean = false) +/** + * Throws a GorDataException on the first row where filterSrc evaluates to true. + * + * @param message error message to use instead of the default "Gor throw on: ", null for the default + * @param includeRow if true the header and the offending row are added to the error + */ +case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: String, header: String, + isRetriable: Boolean = false, message: String = null, includeRow: Boolean = false) extends Analysis with Filtering { filter.setContext(context, executeNor) + private val errorMessage = if (message == null) s"Gor throw on: $filterSrc" else message + override def isTypeInformationNeeded: Boolean = true override def isTypeInformationMaintained: Boolean = true @@ -52,7 +61,15 @@ case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: override def process(r: Row): Unit = { if (filter.evalBooleanFunction(r)) { - val ex = new GorDataException(s"Gor throw on: $filterSrc", -1) + val ex = if (includeRow) { + // Nor rows carry the internal ChromNOR/PosNOR columns, show only the user columns + val (errorHeader, errorRow) = + if (executeNor) (header.split('\t').drop(2).mkString("\t"), r.otherCols()) + else (header, r.toString) + new GorDataException(errorMessage, -1, errorHeader, errorRow) + } else { + new GorDataException(errorMessage, -1) + } if (isRetriable) { ex.fullRetry() } diff --git a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala index bad08ee41..b29e80d22 100644 --- a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala +++ b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala @@ -23,18 +23,20 @@ package gorsat.Commands import gorsat.Analysis.ThrowIfAnalysis -import gorsat.Commands.CommandParseUtilities.hasOption +import gorsat.Commands.CommandParseUtilities.{hasOption, replaceSingleQuotes, stringValueOfOption} import org.gorpipe.gor.session.GorContext class ThrowIf extends CommandInfo("THROWIF", - CommandArguments("-retriable", "", 1, -1, ignoreIllegalArguments = true), + CommandArguments("-retriable -d", "-m", 1, -1, ignoreIllegalArguments = true), CommandOptions(gorCommand = true, norCommand = true)) { override def processArguments(context: GorContext, argString: String, iargs: Array[String], args: Array[String], executeNor: Boolean, forcedInputHeader: String) : CommandParsingResult = { - val pipeStep = ThrowIfAnalysis(context, executeNor, iargs.mkString(" "), forcedInputHeader, hasOption(args, "-retriable")) + val message = if (hasOption(args, "-m")) replaceSingleQuotes(stringValueOfOption(args, "-m")) else null + val pipeStep = ThrowIfAnalysis(context, executeNor, iargs.mkString(" "), forcedInputHeader, + hasOption(args, "-retriable"), message, hasOption(args, "-d")) CommandParsingResult(pipeStep, forcedInputHeader) } } \ No newline at end of file diff --git a/gortools/src/test/java/gorsat/UTestThrowIf.java b/gortools/src/test/java/gorsat/UTestThrowIf.java new file mode 100644 index 000000000..b635e2092 --- /dev/null +++ b/gortools/src/test/java/gorsat/UTestThrowIf.java @@ -0,0 +1,88 @@ +/* + * BEGIN_COPYRIGHT + * + * Copyright (C) 2011-2013 deCODE genetics Inc. + * Copyright (C) 2013-2019 WuXi NextCode Inc. + * All Rights Reserved. + * + * GORpipe is free software: you can redistribute it and/or modify + * it under the terms of the AFFERO GNU General Public License as published by + * the Free Software Foundation. + * + * GORpipe is distributed "AS-IS" AND WITHOUT ANY WARRANTY OF ANY KIND, + * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * NON-INFRINGEMENT, OR FITNESS FOR A PARTICULAR PURPOSE. See + * the AFFERO GNU General Public License for the complete license terms. + * + * You should have received a copy of the AFFERO GNU General Public License + * along with GORpipe. If not, see + * + * END_COPYRIGHT + */ + +package gorsat; + +import org.gorpipe.exceptions.GorDataException; +import org.junit.Assert; +import org.junit.Test; + +public class UTestThrowIf { + + private static GorDataException runExpectingThrow(String query) { + try { + TestUtils.runGorPipe(query); + } catch (GorDataException e) { + return e; + } + Assert.fail("Expected GorDataException for: " + query); + return null; + } + + @Test + public void defaultMessageUnchanged() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif status != 'mapped'"); + Assert.assertEquals("Gor throw on: status != 'mapped'", e.getMessage()); + Assert.assertEquals("", e.getRow()); + Assert.assertFalse(e.isFullRetry()); + } + + @Test + public void customMessage() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); + Assert.assertEquals("", e.getRow()); + } + + @Test + public void customMessageWithQuotes() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m \"it's 'not' mapped\" status != 'mapped'"); + Assert.assertEquals("it's 'not' mapped", e.getMessage()); + } + + @Test + public void detailIncludesOnlyOffendingRow() { + GorDataException e = runExpectingThrow("gorrows -p chr1:1-4 | calc status if(pos=2,'unmapped','mapped') | throwif -d status != 'mapped'"); + Assert.assertEquals("Gor throw on: status != 'mapped'\nHeader: chrom\tpos\tstatus\nRow: chr1\t2\tunmapped", e.getMessage()); + Assert.assertEquals("chr1\t2\tunmapped", e.getRow()); + } + + @Test + public void customMessageAndDetail() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' -d status != 'mapped'"); + Assert.assertEquals("liftover failed\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped", e.getMessage()); + } + + @Test + public void customMessageAndDetailWithRetriable() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -retriable -d -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped", e.getMessage()); + Assert.assertTrue(e.isFullRetry()); + } + + @Test + public void norContext() { + GorDataException e = runExpectingThrow("norrows 3 | calc status if(rownum=1,'unmapped','mapped') | throwif -m 'liftover failed' -d status != 'mapped'"); + Assert.assertEquals("liftover failed\nHeader: RowNum\tstatus\nRow: 1\tunmapped", e.getMessage()); + Assert.assertEquals("1\tunmapped", e.getRow()); + } +} diff --git a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala index 4153618af..f72510fd7 100644 --- a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala +++ b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala @@ -73,4 +73,40 @@ class UTestThrowIfAnalysis extends AnyFlatSpec { assert(thrown.getMessage == "Gor throw on: A=='ABC'") assert(thrown.isFullRetry() == true) } + + it should "use custom message when message is given" in { + val sink = AnalysisSink() + val header = "chrom\tpos\tA" + val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, message = "custom failure") | sink + pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) + + val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) + assert(thrown.getMessage == "custom failure") + assert(thrown.getRow == "") + } + + it should "include header and offending row when detail is set" in { + val sink = AnalysisSink() + val header = "chrom\tpos\tA" + val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, includeRow = true) | sink + pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) + + pipe.process(RowObj("chr1\t1\tXYZ")) + val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t2\tABC"))) + assert(thrown.getMessage == "Gor throw on: A=='ABC'\nHeader: chrom\tpos\tA\nRow: chr1\t2\tABC") + assert(thrown.getRow == "chr1\t2\tABC") + assert(sink.rows.size == 1) + } + + it should "combine custom message, detail and retriable" in { + val sink = AnalysisSink() + val header = "chrom\tpos\tA" + val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, isRetriable = true, + message = "custom failure", includeRow = true) | sink + pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) + + val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) + assert(thrown.getMessage == "custom failure\nHeader: chrom\tpos\tA\nRow: chr1\t1\tABC") + assert(thrown.isFullRetry()) + } } From 946a06cd8bca5c2fb5421fde6c0ff09c2b576119 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 6 Oct 2026 22:14:41 +0000 Subject: [PATCH 2/4] refactor(ENGKNOW-3999): drop throwif -d option, keep -m only -d formatted the header and row into the message while ExceptionUtilities also appends getHeader()/getRow(), so they were printed twice. The option is not needed; remove it. Use stringValueOfOptionWithDefault for -m. Co-Authored-By: Claude Opus 5.5 (1M context) --- documentation/src/command/THROWIF.rst | 16 ++++--------- .../gorsat/Analysis/ThrowIfAnalysis.scala | 15 +++--------- .../main/scala/gorsat/Commands/ThrowIf.scala | 8 +++---- .../src/test/java/gorsat/UTestThrowIf.java | 24 ++++--------------- .../Analysis/UTestThrowIfAnalysis.scala | 19 +++------------ 5 files changed, 19 insertions(+), 63 deletions(-) diff --git a/documentation/src/command/THROWIF.rst b/documentation/src/command/THROWIF.rst index 1f690ca2f..a87f5a54c 100644 --- a/documentation/src/command/THROWIF.rst +++ b/documentation/src/command/THROWIF.rst @@ -9,8 +9,7 @@ THROWIF ======= The **THROWIF** command is a very simple tool which throws an exception if the contained statement is true. -By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message and ``-d`` to -include the header and the content of the row that triggered the exception in the error. +By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message. Usage ===== @@ -27,20 +26,13 @@ Options +--------------------+---------------------------------------------------------------------------------------+ | ``-m `` | Use ```` as the error message instead of ``Gor throw on: ``. | | | Quote the message if it contains spaces. | -+--------------------+---------------------------------------------------------------------------------------+ -| ``-d`` | Include the header and the row that triggered the exception in the error. | -+--------------------+---------------------------------------------------------------------------------------+ Examples ======== -Fail with a custom message that names the offending row: +Fail with a custom message: .. code-block:: gor - gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' -d status != 'mapped' - -The error message is:: + gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' status != 'mapped' - liftover failed - Header: chrom pos status - Row: chr1 1 unmapped \ No newline at end of file +The error message is ``liftover failed``. \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala index 9a69471d8..e609d7fea 100644 --- a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala +++ b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala @@ -30,11 +30,10 @@ import org.gorpipe.gor.session.GorContext /** * Throws a GorDataException on the first row where filterSrc evaluates to true. * - * @param message error message to use instead of the default "Gor throw on: ", null for the default - * @param includeRow if true the header and the offending row are added to the error + * @param message error message to use instead of the default "Gor throw on: ", null for the default */ case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: String, header: String, - isRetriable: Boolean = false, message: String = null, includeRow: Boolean = false) + isRetriable: Boolean = false, message: String = null) extends Analysis with Filtering { filter.setContext(context, executeNor) @@ -61,15 +60,7 @@ case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: override def process(r: Row): Unit = { if (filter.evalBooleanFunction(r)) { - val ex = if (includeRow) { - // Nor rows carry the internal ChromNOR/PosNOR columns, show only the user columns - val (errorHeader, errorRow) = - if (executeNor) (header.split('\t').drop(2).mkString("\t"), r.otherCols()) - else (header, r.toString) - new GorDataException(errorMessage, -1, errorHeader, errorRow) - } else { - new GorDataException(errorMessage, -1) - } + val ex = new GorDataException(errorMessage, -1) if (isRetriable) { ex.fullRetry() } diff --git a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala index b29e80d22..153307bf5 100644 --- a/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala +++ b/gortools/src/main/scala/gorsat/Commands/ThrowIf.scala @@ -23,20 +23,20 @@ package gorsat.Commands import gorsat.Analysis.ThrowIfAnalysis -import gorsat.Commands.CommandParseUtilities.{hasOption, replaceSingleQuotes, stringValueOfOption} +import gorsat.Commands.CommandParseUtilities.{hasOption, replaceSingleQuotes, stringValueOfOptionWithDefault} import org.gorpipe.gor.session.GorContext class ThrowIf extends CommandInfo("THROWIF", - CommandArguments("-retriable -d", "-m", 1, -1, ignoreIllegalArguments = true), + CommandArguments("-retriable", "-m", 1, -1, ignoreIllegalArguments = true), CommandOptions(gorCommand = true, norCommand = true)) { override def processArguments(context: GorContext, argString: String, iargs: Array[String], args: Array[String], executeNor: Boolean, forcedInputHeader: String) : CommandParsingResult = { - val message = if (hasOption(args, "-m")) replaceSingleQuotes(stringValueOfOption(args, "-m")) else null + val message = replaceSingleQuotes(stringValueOfOptionWithDefault(args, "-m", null)) val pipeStep = ThrowIfAnalysis(context, executeNor, iargs.mkString(" "), forcedInputHeader, - hasOption(args, "-retriable"), message, hasOption(args, "-d")) + hasOption(args, "-retriable"), message) CommandParsingResult(pipeStep, forcedInputHeader) } } \ No newline at end of file diff --git a/gortools/src/test/java/gorsat/UTestThrowIf.java b/gortools/src/test/java/gorsat/UTestThrowIf.java index b635e2092..da8455f89 100644 --- a/gortools/src/test/java/gorsat/UTestThrowIf.java +++ b/gortools/src/test/java/gorsat/UTestThrowIf.java @@ -60,29 +60,15 @@ public void customMessageWithQuotes() { } @Test - public void detailIncludesOnlyOffendingRow() { - GorDataException e = runExpectingThrow("gorrows -p chr1:1-4 | calc status if(pos=2,'unmapped','mapped') | throwif -d status != 'mapped'"); - Assert.assertEquals("Gor throw on: status != 'mapped'\nHeader: chrom\tpos\tstatus\nRow: chr1\t2\tunmapped", e.getMessage()); - Assert.assertEquals("chr1\t2\tunmapped", e.getRow()); - } - - @Test - public void customMessageAndDetail() { - GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' -d status != 'mapped'"); - Assert.assertEquals("liftover failed\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped", e.getMessage()); - } - - @Test - public void customMessageAndDetailWithRetriable() { - GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -retriable -d -m 'liftover failed' status != 'mapped'"); - Assert.assertEquals("liftover failed\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped", e.getMessage()); + public void customMessageWithRetriable() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -retriable -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); Assert.assertTrue(e.isFullRetry()); } @Test public void norContext() { - GorDataException e = runExpectingThrow("norrows 3 | calc status if(rownum=1,'unmapped','mapped') | throwif -m 'liftover failed' -d status != 'mapped'"); - Assert.assertEquals("liftover failed\nHeader: RowNum\tstatus\nRow: 1\tunmapped", e.getMessage()); - Assert.assertEquals("1\tunmapped", e.getRow()); + GorDataException e = runExpectingThrow("norrows 3 | calc status if(rownum=1,'unmapped','mapped') | throwif -m 'liftover failed' status != 'mapped'"); + Assert.assertEquals("liftover failed", e.getMessage()); } } diff --git a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala index f72510fd7..b80911d0a 100644 --- a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala +++ b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala @@ -85,28 +85,15 @@ class UTestThrowIfAnalysis extends AnyFlatSpec { assert(thrown.getRow == "") } - it should "include header and offending row when detail is set" in { - val sink = AnalysisSink() - val header = "chrom\tpos\tA" - val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, includeRow = true) | sink - pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) - - pipe.process(RowObj("chr1\t1\tXYZ")) - val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t2\tABC"))) - assert(thrown.getMessage == "Gor throw on: A=='ABC'\nHeader: chrom\tpos\tA\nRow: chr1\t2\tABC") - assert(thrown.getRow == "chr1\t2\tABC") - assert(sink.rows.size == 1) - } - - it should "combine custom message, detail and retriable" in { + it should "combine custom message and retriable" in { val sink = AnalysisSink() val header = "chrom\tpos\tA" val pipe = ThrowIfAnalysis(context, executeNor = false, "A=='ABC'", header, isRetriable = true, - message = "custom failure", includeRow = true) | sink + message = "custom failure") | sink pipe.setRowHeader(RowHeader(header, List("S", "I", "S").toArray)) val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) - assert(thrown.getMessage == "custom failure\nHeader: chrom\tpos\tA\nRow: chr1\t1\tABC") + assert(thrown.getMessage == "custom failure") assert(thrown.isFullRetry()) } } From ec9f5f64bea4dd70012069b250d7b3efcf03283b Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 6 Oct 2026 22:22:44 +0000 Subject: [PATCH 3/4] feat(ENGKNOW-3999): throwif always attaches header and offending row The GorDataException now carries the header and the row that triggered the throw, built with doFormat=false so getMessage() stays the plain message and ExceptionUtilities renders Header/Row once. In nor context the internal ChromNOR/PosNOR columns are left out. Co-Authored-By: Claude Opus 5.5 (1M context) --- documentation/src/command/THROWIF.rst | 10 ++++++-- .../gorsat/Analysis/ThrowIfAnalysis.scala | 10 ++++++-- .../src/test/java/gorsat/UTestThrowIf.java | 25 +++++++++++++++++-- .../Analysis/UTestThrowIfAnalysis.scala | 3 ++- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/documentation/src/command/THROWIF.rst b/documentation/src/command/THROWIF.rst index a87f5a54c..a8a249a9a 100644 --- a/documentation/src/command/THROWIF.rst +++ b/documentation/src/command/THROWIF.rst @@ -9,7 +9,8 @@ THROWIF ======= The **THROWIF** command is a very simple tool which throws an exception if the contained statement is true. -By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message. +By default the error message is ``Gor throw on: ``. Use ``-m`` to give a custom message. The error also +includes the header and the row that triggered the exception. Usage ===== @@ -35,4 +36,9 @@ Fail with a custom message: gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' status != 'mapped' -The error message is ``liftover failed``. \ No newline at end of file +The error is:: + + liftover failed + + Header: chrom pos status + Row: chr1 1 unmapped \ No newline at end of file diff --git a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala index e609d7fea..47d2891aa 100644 --- a/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala +++ b/gortools/src/main/scala/gorsat/Analysis/ThrowIfAnalysis.scala @@ -28,7 +28,8 @@ import org.gorpipe.gor.model.Row import org.gorpipe.gor.session.GorContext /** - * Throws a GorDataException on the first row where filterSrc evaluates to true. + * Throws a GorDataException on the first row where filterSrc evaluates to true. The exception carries the + * header and the offending row. * * @param message error message to use instead of the default "Gor throw on: ", null for the default */ @@ -60,7 +61,12 @@ case class ThrowIfAnalysis(context: GorContext, executeNor: Boolean, filterSrc: override def process(r: Row): Unit = { if (filter.evalBooleanFunction(r)) { - val ex = new GorDataException(errorMessage, -1) + // Nor rows carry the internal ChromNOR/PosNOR columns, show only the user columns + val (errorHeader, errorRow) = + if (executeNor) (header.split('\t').drop(2).mkString("\t"), r.otherCols()) + else (header, r.toString) + // doFormat=false: header and row are rendered from the exception fields, keep them out of the message + val ex = new GorDataException(errorMessage, -1, errorHeader, errorRow, null, false) if (isRetriable) { ex.fullRetry() } diff --git a/gortools/src/test/java/gorsat/UTestThrowIf.java b/gortools/src/test/java/gorsat/UTestThrowIf.java index da8455f89..a7d97fbd5 100644 --- a/gortools/src/test/java/gorsat/UTestThrowIf.java +++ b/gortools/src/test/java/gorsat/UTestThrowIf.java @@ -22,6 +22,7 @@ package gorsat; +import org.gorpipe.exceptions.ExceptionUtilities; import org.gorpipe.exceptions.GorDataException; import org.junit.Assert; import org.junit.Test; @@ -38,11 +39,14 @@ private static GorDataException runExpectingThrow(String query) { return null; } + private static int occurrences(String text, String sub) { + return text.split(sub, -1).length - 1; + } + @Test public void defaultMessageUnchanged() { GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif status != 'mapped'"); Assert.assertEquals("Gor throw on: status != 'mapped'", e.getMessage()); - Assert.assertEquals("", e.getRow()); Assert.assertFalse(e.isFullRetry()); } @@ -50,7 +54,22 @@ public void defaultMessageUnchanged() { public void customMessage() { GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' status != 'mapped'"); Assert.assertEquals("liftover failed", e.getMessage()); - Assert.assertEquals("", e.getRow()); + } + + @Test + public void exceptionCarriesOnlyOffendingRow() { + GorDataException e = runExpectingThrow("gorrows -p chr1:1-4 | calc status if(pos=2,'unmapped','mapped') | throwif status != 'mapped'"); + Assert.assertEquals("chrom\tpos\tstatus", e.getHeader()); + Assert.assertEquals("chr1\t2\tunmapped", e.getRow()); + } + + @Test + public void renderedErrorShowsHeaderAndRowOnce() { + GorDataException e = runExpectingThrow("gorrow chr1,1 | calc status 'unmapped' | throwif -m 'liftover failed' status != 'mapped'"); + String rendered = ExceptionUtilities.gorExceptionToString(e); + Assert.assertTrue(rendered, rendered.contains("liftover failed\n\nHeader: chrom\tpos\tstatus\nRow: chr1\t1\tunmapped")); + Assert.assertEquals(rendered, 1, occurrences(rendered, "Header: ")); + Assert.assertEquals(rendered, 1, occurrences(rendered, "Row: ")); } @Test @@ -70,5 +89,7 @@ public void customMessageWithRetriable() { public void norContext() { GorDataException e = runExpectingThrow("norrows 3 | calc status if(rownum=1,'unmapped','mapped') | throwif -m 'liftover failed' status != 'mapped'"); Assert.assertEquals("liftover failed", e.getMessage()); + Assert.assertEquals("RowNum\tstatus", e.getHeader()); + Assert.assertEquals("1\tunmapped", e.getRow()); } } diff --git a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala index b80911d0a..1f30e5730 100644 --- a/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala +++ b/gortools/src/test/scala/gorsat/Analysis/UTestThrowIfAnalysis.scala @@ -82,7 +82,8 @@ class UTestThrowIfAnalysis extends AnyFlatSpec { val thrown = intercept[GorDataException](pipe.process(RowObj("chr1\t1\tABC"))) assert(thrown.getMessage == "custom failure") - assert(thrown.getRow == "") + assert(thrown.getHeader == header) + assert(thrown.getRow == "chr1\t1\tABC") } it should "combine custom message and retriable" in { From 91dcfe7c3cecd68de69d88f2cb561530649e6d85 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 6 Oct 2026 22:43:25 +0000 Subject: [PATCH 4/4] docs(ENGKNOW-3999): fix THROWIF options table border and example output Co-Authored-By: Claude Opus 5.5 (1M context) --- documentation/src/command/THROWIF.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/src/command/THROWIF.rst b/documentation/src/command/THROWIF.rst index a8a249a9a..12ba5d83d 100644 --- a/documentation/src/command/THROWIF.rst +++ b/documentation/src/command/THROWIF.rst @@ -27,6 +27,7 @@ Options +--------------------+---------------------------------------------------------------------------------------+ | ``-m `` | Use ```` as the error message instead of ``Gor throw on: ``. | | | Quote the message if it contains spaces. | ++--------------------+---------------------------------------------------------------------------------------+ Examples ======== @@ -36,8 +37,9 @@ Fail with a custom message: gorrow chr1,1 | calc status 'unmapped' | THROWIF -m 'liftover failed' status != 'mapped' -The error is:: +The error report contains (command source and stack trace omitted):: + ==== Data Error ==== liftover failed Header: chrom pos status