From a19030e4ae5b83ae46e1f45d5937d2c7a16ece06 Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 13:23:52 +0500 Subject: [PATCH 01/10] fix(frontend): improve static error msgs to be user-friendly for unconfigured operators --- .../projection/ProjectionOpDesc.scala | 2 +- .../source/scan/arrow/ArrowSourceOpDesc.scala | 12 +++++-- .../source/scan/csv/CSVScanSourceOpDesc.scala | 11 ++++-- .../csv/ParallelCSVScanSourceOpDesc.scala | 11 ++++-- .../scan/csvOld/CSVOldScanSourceOpDesc.scala | 13 ++++--- .../scan/json/JSONLScanSourceOpDesc.scala | 10 ++++-- .../operator/source/sql/SQLSourceOpDesc.scala | 12 +++---- .../sql/asterixdb/AsterixDBSourceOpDesc.scala | 7 ++-- .../error-frame/error-frame.component.ts | 34 +++++++++++++++++-- 9 files changed, 85 insertions(+), 27 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala index 3af25b98326..d4008486465 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala @@ -56,7 +56,7 @@ class ProjectionOpDesc extends MapOpDesc { .withOutputPorts(operatorInfo.outputPorts) .withDerivePartition(derivePartition()) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => { - require(attributes.nonEmpty, "Attributes must not be empty") + require(attributes.nonEmpty, "Please select at least 1 attribute to project.") val inputSchema = inputSchemas.values.head val outputSchema = if (!isDrop) { diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala index ac43dbe65dd..054148d7f3d 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala @@ -71,7 +71,15 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { */ @Override def inferSchema(): Schema = { - val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() + require(fileResolved(), "No file selected. Please select a valid .arrow file from the 'File' dropdown in the right panel.") + + val file = try { + DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() + } catch { + case _: Exception => + throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .arrow file from the 'File' dropdown.") + } + val allocator = new RootAllocator() Using @@ -82,7 +90,7 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { ArrowUtils.toTexeraSchema(arrowSchema) } .getOrElse { - throw new IOException("Failed to infer schema from Arrow file.") + throw new RuntimeException("Failed to read the .arrow file. Please ensure it is a valid Arrow file.") } } } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala index d32bbb31c65..2b96e3db52b 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala @@ -78,10 +78,15 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.isEmpty || !fileResolved()) { - return null + require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") + + val stream = try { + DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() + } catch { + case _: Exception => + throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") } - val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() val inputReader = new InputStreamReader(stream, fileEncoding.getCharset) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala index 9609b4e2398..85ed447a87b 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala @@ -79,10 +79,15 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.isEmpty || !fileResolved()) { - return null + require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") + + val file = try { + DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() + } catch { + case _: Exception => + throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") } - val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala index cdf9e655cc6..eb9be8dacbf 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala @@ -75,11 +75,16 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.isEmpty || !fileResolved()) { - return null - } + require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") + // infer schema from the first few lines of the file - val file = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() + val file = try { + DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() + } catch { + case _: Exception => + throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") + } implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala index 5ca429f511c..6e7fa634be7 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala @@ -68,10 +68,14 @@ class JSONLScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (!fileResolved()) { - return null + require(fileResolved(), "No file selected. Please select a valid .jsonl file from the 'File' dropdown in the right panel.") + + val stream = try { + DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() + } catch { + case _: Exception => + throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .jsonl file from the 'File' dropdown.") } - val stream = DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() val reader = new BufferedReader(new InputStreamReader(stream, fileEncoding.getCharset)) var fieldNames = Set[String]() diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala index 4cccd0ff2ad..b87140d8911 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala @@ -138,12 +138,12 @@ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor { * @return Schema */ private def querySchema: Schema = { - if ( - this.host == null || this.port == null || this.database == null - || this.table == null || this.username == null || this.password == null - ) { - return null - } + require(host != null && host.trim.nonEmpty, s"Please enter a valid host name for the database in the properties panel.") + require(port != null && port.trim.nonEmpty, s"Please enter a valid port for the database in the properties panel.") + require(database != null && database.trim.nonEmpty, s"Please enter a valid database name in the properties panel.") + require(table != null && table.trim.nonEmpty, s"Please enter a valid table name in the properties panel.") + require(username != null && username.trim.nonEmpty, s"Please enter a valid username in the properties panel.") + require(password != null, s"Please enter a valid password in the properties panel.") updatePort() try { diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala index d12d334e704..0182c6a13e2 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala @@ -139,9 +139,10 @@ class AsterixDBSourceOpDesc extends SQLSourceOpDesc { override def updatePort(): Unit = port = if (port.trim().equals("default")) "19002" else port override def sourceSchema(): Schema = { - if (this.host == null || this.port == null || this.database == null || this.table == null) { - return null - } + require(host != null && host.trim.nonEmpty, "Please enter a valid host name for AsterixDB.") + require(port != null && port.trim.nonEmpty, "Please enter a valid port for AsterixDB.") + require(database != null && database.trim.nonEmpty, "Please enter a valid database name for AsterixDB.") + require(table != null && table.trim.nonEmpty, "Please enter a valid table name for AsterixDB.") updatePort() diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index e90a5288db1..77b81b1d127 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -72,11 +72,41 @@ export class ErrorFrameComponent implements OnInit { errorMessages = errorMessages.filter(err => err.operatorId === this.operatorId); } this.categoryToErrorMapping = errorMessages.reduce((acc, obj) => { - const key = obj.type.name; + let key = obj.type.name; + let message = obj.message; + let details = obj.details; + + if (key === "COMPILATION_ERROR") { + key = "WARNING"; + + // Strip out common Java exception class names and formatting to make it more user-friendly + const exceptionRegex = /^\s*(?:[a-zA-Z0-9_]+\.)+[a-zA-Z0-9_]+Exception:\s*/; + const requirementFailedRegex = /^\s*requirement failed:\s*/; + const genericExceptionRegex = /^\s*Exception:\s*/; + + if (message) { + message = message.replace(exceptionRegex, ""); + message = message.replace(requirementFailedRegex, ""); + message = message.replace(genericExceptionRegex, ""); + } + + if (details) { + details = details.replace(exceptionRegex, ""); + details = details.replace(requirementFailedRegex, ""); + details = details.replace(genericExceptionRegex, ""); + } + } + + const formattedError: WorkflowFatalError = { + ...obj, + message: message, + details: details + }; + if (!acc.has(key)) { acc.set(key, []); } - acc.get(key)!.push(obj); + acc.get(key)!.push(formattedError); return acc; }, new Map()); } From 74c6895a8375f5ee904092c0b150ee86d69d5b8a Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 20:38:32 +0500 Subject: [PATCH 02/10] address copilot PR review comments --- .../source/scan/arrow/ArrowSourceOpDesc.scala | 28 ++++++++++------- .../source/scan/csv/CSVScanSourceOpDesc.scala | 17 +++++++---- .../csv/ParallelCSVScanSourceOpDesc.scala | 17 +++++++---- .../scan/csvOld/CSVOldScanSourceOpDesc.scala | 20 ++++++++----- .../scan/json/JSONLScanSourceOpDesc.scala | 19 +++++++----- .../operator/source/sql/SQLSourceOpDesc.scala | 30 +++++++++++++++---- 6 files changed, 88 insertions(+), 43 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala index 054148d7f3d..9a0722ce8bb 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala @@ -71,15 +71,20 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { */ @Override def inferSchema(): Schema = { - require(fileResolved(), "No file selected. Please select a valid .arrow file from the 'File' dropdown in the right panel.") + require( + fileResolved(), + "No file selected. Please select a valid .arrow file from the 'File' dropdown in the right panel." + ) - val file = try { - DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() - } catch { - case _: Exception => - throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .arrow file from the 'File' dropdown.") + val uri = new URI(fileName.get) + if (uri.getScheme == "file") { + require( + new java.io.File(uri).isFile, + "The selected item is a folder or does not exist. Please select an actual .arrow file from the 'File' dropdown." + ) } - + val file = DocumentFactory.openReadonlyDocument(uri).asFile() + val allocator = new RootAllocator() Using @@ -89,8 +94,11 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema ArrowUtils.toTexeraSchema(arrowSchema) } - .getOrElse { - throw new RuntimeException("Failed to read the .arrow file. Please ensure it is a valid Arrow file.") - } + .recover { case e: Throwable => + throw new RuntimeException( + "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", + e + ) + }.get } } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala index 2b96e3db52b..95d597d4c0c 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala @@ -79,14 +79,19 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc { override def sourceSchema(): Schema = { require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") - require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") + require( + fileResolved(), + "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." + ) - val stream = try { - DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() - } catch { - case _: Exception => - throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") + val uri = new URI(fileName.get) + if (uri.getScheme == "file") { + require( + new java.io.File(uri).isFile, + "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown." + ) } + val stream = DocumentFactory.openReadonlyDocument(uri).asInputStream() val inputReader = new InputStreamReader(stream, fileEncoding.getCharset) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala index 85ed447a87b..3191e9b24fb 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala @@ -80,14 +80,19 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { override def sourceSchema(): Schema = { require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") - require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") + require( + fileResolved(), + "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." + ) - val file = try { - DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() - } catch { - case _: Exception => - throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") + val uri = new URI(fileName.get) + if (uri.getScheme == "file") { + require( + new java.io.File(uri).isFile, + "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown." + ) } + val file = DocumentFactory.openReadonlyDocument(uri).asFile() implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala index eb9be8dacbf..deb4474aadc 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala @@ -76,15 +76,19 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { override def sourceSchema(): Schema = { require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") - require(fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel.") - - // infer schema from the first few lines of the file - val file = try { - DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asFile() - } catch { - case _: Exception => - throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .csv file from the 'File' dropdown.") + require( + fileResolved(), + "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." + ) + + val uri = new URI(fileName.get) + if (uri.getScheme == "file") { + require( + new java.io.File(uri).isFile, + "The selected item is a folder or does not exist. Please select an actual .csv file from the 'File' dropdown." + ) } + val file = DocumentFactory.openReadonlyDocument(uri).asFile() implicit object CustomFormat extends DefaultCSVFormat { override val delimiter: Char = customDelimiter.get.charAt(0) } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala index 6e7fa634be7..faccc76f882 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDesc.scala @@ -68,14 +68,19 @@ class JSONLScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - require(fileResolved(), "No file selected. Please select a valid .jsonl file from the 'File' dropdown in the right panel.") - - val stream = try { - DocumentFactory.openReadonlyDocument(new URI(fileName.get)).asInputStream() - } catch { - case _: Exception => - throw new RuntimeException("The selected item is a folder, not a file. Please select an actual .jsonl file from the 'File' dropdown.") + require( + fileResolved(), + "No file selected. Please select a valid .jsonl file from the 'File' dropdown in the right panel." + ) + + val uri = new URI(fileName.get) + if (uri.getScheme == "file") { + require( + new java.io.File(uri).isFile, + "The selected item is a folder or does not exist. Please select an actual .jsonl file from the 'File' dropdown." + ) } + val stream = DocumentFactory.openReadonlyDocument(uri).asInputStream() val reader = new BufferedReader(new InputStreamReader(stream, fileEncoding.getCharset)) var fieldNames = Set[String]() diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala index b87140d8911..5277cd25c8a 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala @@ -138,12 +138,30 @@ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor { * @return Schema */ private def querySchema: Schema = { - require(host != null && host.trim.nonEmpty, s"Please enter a valid host name for the database in the properties panel.") - require(port != null && port.trim.nonEmpty, s"Please enter a valid port for the database in the properties panel.") - require(database != null && database.trim.nonEmpty, s"Please enter a valid database name in the properties panel.") - require(table != null && table.trim.nonEmpty, s"Please enter a valid table name in the properties panel.") - require(username != null && username.trim.nonEmpty, s"Please enter a valid username in the properties panel.") - require(password != null, s"Please enter a valid password in the properties panel.") + require( + host != null && host.trim.nonEmpty, + s"Please enter a valid host name for the database in the properties panel." + ) + require( + port != null && port.trim.nonEmpty, + s"Please enter a valid port for the database in the properties panel." + ) + require( + database != null && database.trim.nonEmpty, + s"Please enter a valid database name in the properties panel." + ) + require( + table != null && table.trim.nonEmpty, + s"Please enter a valid table name in the properties panel." + ) + require( + username != null && username.trim.nonEmpty, + s"Please enter a valid username in the properties panel." + ) + require( + password != null && password.trim.nonEmpty, + s"Please enter a valid password in the properties panel." + ) updatePort() try { From 5879ca65d05f165921b0c7bdfa5aeb914373a85d Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 20:46:49 +0500 Subject: [PATCH 03/10] address additional copilot PR review comments --- .../amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala | 2 +- .../amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala | 4 +++- .../source/scan/csv/ParallelCSVScanSourceOpDesc.scala | 4 +++- .../texera/amber/operator/source/sql/SQLSourceOpDesc.scala | 2 +- .../result-panel/error-frame/error-frame.component.ts | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala index 9a0722ce8bb..b0fec59dd8c 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala @@ -94,7 +94,7 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema ArrowUtils.toTexeraSchema(arrowSchema) } - .recover { case e: Throwable => + .recover { case scala.util.control.NonFatal(e) => throw new RuntimeException( "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", e diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala index 95d597d4c0c..8f01375b115 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala @@ -78,7 +78,9 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + if (customDelimiter.forall(_.isEmpty)) { + customDelimiter = Option(",") + } require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala index 3191e9b24fb..1044de2d5cd 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala @@ -79,7 +79,9 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + if (customDelimiter.forall(_.isEmpty)) { + customDelimiter = Option(",") + } require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala index 5277cd25c8a..16046aa6ed3 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala @@ -159,7 +159,7 @@ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor { s"Please enter a valid username in the properties panel." ) require( - password != null && password.trim.nonEmpty, + password != null, s"Please enter a valid password in the properties panel." ) diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index 77b81b1d127..0ad6c2836be 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -77,7 +77,7 @@ export class ErrorFrameComponent implements OnInit { let details = obj.details; if (key === "COMPILATION_ERROR") { - key = "WARNING"; + // key = "WARNING"; // Strip out common Java exception class names and formatting to make it more user-friendly const exceptionRegex = /^\s*(?:[a-zA-Z0-9_]+\.)+[a-zA-Z0-9_]+Exception:\s*/; From 35821a2dfa3131dd30f76621fd250a163f3a8a51 Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 20:56:55 +0500 Subject: [PATCH 04/10] address copilot PR review comments (round 3) --- .../operator/projection/ProjectionOpDesc.scala | 2 +- .../source/scan/arrow/ArrowSourceOpDesc.scala | 13 ++++++++----- .../amber/operator/source/sql/SQLSourceOpDesc.scala | 2 +- .../error-frame/error-frame.component.ts | 2 -- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala index d4008486465..fb9258410cb 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/projection/ProjectionOpDesc.scala @@ -56,7 +56,7 @@ class ProjectionOpDesc extends MapOpDesc { .withOutputPorts(operatorInfo.outputPorts) .withDerivePartition(derivePartition()) .withPropagateSchema(SchemaPropagationFunc(inputSchemas => { - require(attributes.nonEmpty, "Please select at least 1 attribute to project.") + require(attributes.nonEmpty, "Please select at least one attribute to project.") val inputSchema = inputSchemas.values.head val outputSchema = if (!isDrop) { diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala index b0fec59dd8c..2563450aa06 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala @@ -94,11 +94,14 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema ArrowUtils.toTexeraSchema(arrowSchema) } - .recover { case scala.util.control.NonFatal(e) => - throw new RuntimeException( - "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", - e + .recoverWith { case scala.util.control.NonFatal(e) => + scala.util.Failure( + new RuntimeException( + "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", + e + ) ) - }.get + } + .get } } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala index 16046aa6ed3..5277cd25c8a 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDesc.scala @@ -159,7 +159,7 @@ abstract class SQLSourceOpDesc extends SourceOperatorDescriptor { s"Please enter a valid username in the properties panel." ) require( - password != null, + password != null && password.trim.nonEmpty, s"Please enter a valid password in the properties panel." ) diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index 0ad6c2836be..99c3f49e4f3 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -77,8 +77,6 @@ export class ErrorFrameComponent implements OnInit { let details = obj.details; if (key === "COMPILATION_ERROR") { - // key = "WARNING"; - // Strip out common Java exception class names and formatting to make it more user-friendly const exceptionRegex = /^\s*(?:[a-zA-Z0-9_]+\.)+[a-zA-Z0-9_]+Exception:\s*/; const requirementFailedRegex = /^\s*requirement failed:\s*/; From 82b5912122b17b572f8bf730f7e815976fc147e9 Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 21:04:32 +0500 Subject: [PATCH 05/10] address copilot PR review comments (round 4) --- .../org/apache/texera/workflow/WorkflowCompilerSpec.scala | 2 +- .../operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala | 4 +++- .../apache/texera/amber/compiler/WorkflowCompilerSpec.scala | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala index 94a8ffce552..486808c62a5 100644 --- a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala +++ b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala @@ -207,6 +207,6 @@ class WorkflowCompilerSpec extends AnyFlatSpec { val ex = intercept[RuntimeException] { new WorkflowCompiler(ctx).compile(pojo(List(orphanCsv), List.empty)) } - assert(ex.getMessage == "no input file name") + assert(ex.getMessage.contains("No file selected")) } } diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala index deb4474aadc..0acf0137664 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala @@ -75,7 +75,9 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - require(customDelimiter.isDefined, "Please specify a delimiter in the properties panel.") + if (customDelimiter.forall(_.isEmpty)) { + customDelimiter = Option(",") + } require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." diff --git a/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala b/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala index ee221728d31..79df9e98ee6 100644 --- a/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala +++ b/workflow-compiling-service/src/test/scala/org/apache/texera/amber/compiler/WorkflowCompilerSpec.scala @@ -210,7 +210,7 @@ class WorkflowCompilerSpec extends AnyFlatSpec { val err = result.operatorIdToError(orphan.operatorIdentifier) assert(err.`type` == COMPILATION_ERROR) assert(err.operatorId == orphan.operatorIdentifier.id) - assert(err.message.contains("no input file name"), s"unexpected message: ${err.message}") + assert(err.message.contains("No file selected"), s"unexpected message: ${err.message}") assert(err.details.nonEmpty, "stack-trace details should be populated for UI display") } From 14282f84a3b4a6dd0d7d84fec318bae014275084 Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 9 Jul 2026 22:57:24 +0500 Subject: [PATCH 06/10] address copilot PR review comments (round 5) --- .../result-panel/error-frame/error-frame.component.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index 99c3f49e4f3..69d14fb92fb 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -78,20 +78,17 @@ export class ErrorFrameComponent implements OnInit { if (key === "COMPILATION_ERROR") { // Strip out common Java exception class names and formatting to make it more user-friendly - const exceptionRegex = /^\s*(?:[a-zA-Z0-9_]+\.)+[a-zA-Z0-9_]+Exception:\s*/; + const exceptionRegex = /^\s*(?:(?:[a-zA-Z0-9_]+\.)*[a-zA-Z0-9_]+(?:Exception|Error)):\s*/; const requirementFailedRegex = /^\s*requirement failed:\s*/; - const genericExceptionRegex = /^\s*Exception:\s*/; if (message) { message = message.replace(exceptionRegex, ""); message = message.replace(requirementFailedRegex, ""); - message = message.replace(genericExceptionRegex, ""); } if (details) { details = details.replace(exceptionRegex, ""); details = details.replace(requirementFailedRegex, ""); - details = details.replace(genericExceptionRegex, ""); } } From 2f94fe490f50f285cae4e3f5d399729af04b41eb Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Fri, 10 Jul 2026 04:55:01 +0500 Subject: [PATCH 07/10] address copilot PR review comments (round 6) --- .../operator/source/scan/csv/CSVScanSourceOpDesc.scala | 6 ++---- .../source/scan/csv/ParallelCSVScanSourceOpDesc.scala | 6 ++---- .../source/scan/csvOld/CSVOldScanSourceOpDesc.scala | 6 ++---- .../result-panel/error-frame/error-frame.component.ts | 5 +++-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala index 8f01375b115..328e3b6c1f9 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/CSVScanSourceOpDesc.scala @@ -78,9 +78,7 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.forall(_.isEmpty)) { - customDelimiter = Option(",") - } + val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0) require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." @@ -98,7 +96,7 @@ class CSVScanSourceOpDesc extends ScanSourceOpDesc { new InputStreamReader(stream, fileEncoding.getCharset) val csvFormat = new CsvFormat() - csvFormat.setDelimiter(customDelimiter.get.charAt(0)) + csvFormat.setDelimiter(delimiterChar) csvFormat.setLineSeparator("\n") val csvSetting = new CsvParserSettings() csvSetting.setMaxCharsPerColumn(-1) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala index 1044de2d5cd..b3d1071b86f 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csv/ParallelCSVScanSourceOpDesc.scala @@ -79,9 +79,7 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.forall(_.isEmpty)) { - customDelimiter = Option(",") - } + val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0) require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." @@ -96,7 +94,7 @@ class ParallelCSVScanSourceOpDesc extends ScanSourceOpDesc { } val file = DocumentFactory.openReadonlyDocument(uri).asFile() implicit object CustomFormat extends DefaultCSVFormat { - override val delimiter: Char = customDelimiter.get.charAt(0) + override val delimiter: Char = delimiterChar } var reader: CSVReader = CSVReader.open(file)(CustomFormat) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala index 0acf0137664..2a18d431ffb 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDesc.scala @@ -75,9 +75,7 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { } override def sourceSchema(): Schema = { - if (customDelimiter.forall(_.isEmpty)) { - customDelimiter = Option(",") - } + val delimiterChar = customDelimiter.filter(_.nonEmpty).getOrElse(",").charAt(0) require( fileResolved(), "No file selected. Please select a valid .csv file from the 'File' dropdown in the right panel." @@ -92,7 +90,7 @@ class CSVOldScanSourceOpDesc extends ScanSourceOpDesc { } val file = DocumentFactory.openReadonlyDocument(uri).asFile() implicit object CustomFormat extends DefaultCSVFormat { - override val delimiter: Char = customDelimiter.get.charAt(0) + override val delimiter: Char = delimiterChar } var reader: CSVReader = CSVReader.open(file, fileEncoding.getCharset.name())(CustomFormat) diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index 69d14fb92fb..8dab99ebf4d 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -72,11 +72,12 @@ export class ErrorFrameComponent implements OnInit { errorMessages = errorMessages.filter(err => err.operatorId === this.operatorId); } this.categoryToErrorMapping = errorMessages.reduce((acc, obj) => { - let key = obj.type.name; + const key = obj.type.name; let message = obj.message; let details = obj.details; - if (key === "COMPILATION_ERROR") { + const shouldFormat = key === "COMPILATION_ERROR" || key === "EXECUTION_FAILURE"; + if (shouldFormat) { // Strip out common Java exception class names and formatting to make it more user-friendly const exceptionRegex = /^\s*(?:(?:[a-zA-Z0-9_]+\.)*[a-zA-Z0-9_]+(?:Exception|Error)):\s*/; const requirementFailedRegex = /^\s*requirement failed:\s*/; From faa965a08a5ef1d4b1a15b3a786feb0a1b44f5cc Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Fri, 10 Jul 2026 05:17:07 +0500 Subject: [PATCH 08/10] fix formatting issues --- .../source/scan/arrow/ArrowSourceOpDesc.scala | 13 +++++++------ .../error-frame/error-frame.component.ts | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala index 2563450aa06..ad1d7a34176 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDesc.scala @@ -94,13 +94,14 @@ class ArrowSourceOpDesc extends ScanSourceOpDesc { val arrowSchema: ArrowSchema = reader.getVectorSchemaRoot.getSchema ArrowUtils.toTexeraSchema(arrowSchema) } - .recoverWith { case scala.util.control.NonFatal(e) => - scala.util.Failure( - new RuntimeException( - "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", - e + .recoverWith { + case scala.util.control.NonFatal(e) => + scala.util.Failure( + new RuntimeException( + "Failed to read the .arrow file. Please ensure it is a valid Arrow file.", + e + ) ) - ) } .get } diff --git a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts index 8dab99ebf4d..41b5cac55a9 100644 --- a/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts +++ b/frontend/src/app/workspace/component/result-panel/error-frame/error-frame.component.ts @@ -86,7 +86,7 @@ export class ErrorFrameComponent implements OnInit { message = message.replace(exceptionRegex, ""); message = message.replace(requirementFailedRegex, ""); } - + if (details) { details = details.replace(exceptionRegex, ""); details = details.replace(requirementFailedRegex, ""); @@ -96,7 +96,7 @@ export class ErrorFrameComponent implements OnInit { const formattedError: WorkflowFatalError = { ...obj, message: message, - details: details + details: details, }; if (!acc.has(key)) { From 81bf4a3e290bb9e19ea599816f53df1df21ccaa3 Mon Sep 17 00:00:00 2001 From: probe Date: Thu, 9 Jul 2026 17:50:01 -0700 Subject: [PATCH 09/10] fix(engine): unbreak CI for user-friendly static error messages - reformat the over-100-column require() in AsterixDBSourceOpDesc so scalafmtCheckAll passes - align the scan-source missing-fileName error in both LogicalPlan copies (amber + workflow-compiling-service) with the new 'No file selected...' wording that the updated specs expect - WorkflowCompiler: keep the first error recorded per operator so the root-cause resolution failure (e.g. FileNotFoundException with the bad path) is not overwritten by follow-on schema-propagation failures on the same operator - update workflow-operator DescSpecs that pinned the old contracts: sourceSchema() returning null before configuration (CSVOld/JSONL/MySQL/PostgreSQL/AsterixDB/SQL) and ArrowSourceOpDesc.inferSchema throwing a bare IOException --- .../apache/texera/workflow/LogicalPlan.scala | 6 +++++- .../workflow/WorkflowCompilerSpec.scala | 2 +- .../sql/asterixdb/AsterixDBSourceOpDesc.scala | 5 ++++- .../scan/arrow/ArrowSourceOpDescSpec.scala | 6 +++--- .../csvOld/CSVOldScanSourceOpDescSpec.scala | 5 +++-- .../scan/json/JSONLScanSourceOpDescSpec.scala | 5 +++-- .../source/sql/SQLSourceOpDescSpec.scala | 5 +++-- .../asterixdb/AsterixDBSourceOpDescSpec.scala | 5 +++-- .../sql/mysql/MySQLSourceOpDescSpec.scala | 5 +++-- .../PostgreSQLSourceOpDescSpec.scala | 5 +++-- .../amber/compiler/WorkflowCompiler.scala | 21 ++++++++++++------- .../amber/compiler/model/LogicalPlan.scala | 6 +++++- 12 files changed, 49 insertions(+), 27 deletions(-) diff --git a/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala b/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala index 974d17f40a4..6e759439a02 100644 --- a/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala +++ b/amber/src/main/scala/org/apache/texera/workflow/LogicalPlan.scala @@ -99,7 +99,11 @@ case class LogicalPlan( case operator @ (scanOp: ScanSourceOpDesc) => Try { // Resolve file path for ScanSourceOpDesc - val fileName = scanOp.fileName.getOrElse(throw new RuntimeException("no input file name")) + val fileName = scanOp.fileName.getOrElse( + throw new RuntimeException( + "No file selected. Please select a file from the 'File' dropdown in the right panel." + ) + ) val fileUri = FileResolver.resolve(fileName) // Convert to URI // Set the URI in the ScanSourceOpDesc diff --git a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala index 486808c62a5..3ee61d3f5cb 100644 --- a/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala +++ b/amber/src/test/scala/org/apache/texera/workflow/WorkflowCompilerSpec.scala @@ -198,7 +198,7 @@ class WorkflowCompilerSpec extends AnyFlatSpec { "WorkflowCompiler in strict mode (no errorList)" should "throw when a scan source has no fileName set" in { // CSVScanSourceOpDesc defaults fileName to None; `resolveScanSourceOpFileName(None)` - // hits `scanOp.fileName.getOrElse(throw new RuntimeException("no input file name"))` + // hits the "No file selected" RuntimeException thrown from `scanOp.fileName.getOrElse` // and surfaces that exception out of `compile` because the compiler passes // `None` for the errorList (i.e. fail-fast on the execution path). val orphanCsv = new CSVScanSourceOpDesc() diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala index 0182c6a13e2..0ef06d740d4 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDesc.scala @@ -141,7 +141,10 @@ class AsterixDBSourceOpDesc extends SQLSourceOpDesc { override def sourceSchema(): Schema = { require(host != null && host.trim.nonEmpty, "Please enter a valid host name for AsterixDB.") require(port != null && port.trim.nonEmpty, "Please enter a valid port for AsterixDB.") - require(database != null && database.trim.nonEmpty, "Please enter a valid database name for AsterixDB.") + require( + database != null && database.trim.nonEmpty, + "Please enter a valid database name for AsterixDB." + ) require(table != null && table.trim.nonEmpty, "Please enter a valid table name for AsterixDB.") updatePort() diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala index 6bd54c2c0c9..54dd677913e 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/arrow/ArrowSourceOpDescSpec.scala @@ -153,13 +153,13 @@ class ArrowSourceOpDescSpec extends AnyFlatSpec with Matchers { d.inferSchema() shouldBe schema } - it should "throw an IOException when the file is not a valid Arrow file" in { + it should "throw a friendly error when the file is not a valid Arrow file" in { val bogus = File.createTempFile("not-arrow-", ".arrow") bogus.deleteOnExit() Files.write(bogus.toPath, "this is not arrow".getBytes) val d = new ArrowSourceOpDesc d.fileName = Some(bogus.toURI.toString) - val ex = intercept[java.io.IOException](d.inferSchema()) - ex.getMessage shouldBe "Failed to infer schema from Arrow file." + val ex = intercept[RuntimeException](d.inferSchema()) + ex.getMessage shouldBe "Failed to read the .arrow file. Please ensure it is a valid Arrow file." } } diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala index e0d7efabf00..b72476641b0 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/csvOld/CSVOldScanSourceOpDescSpec.scala @@ -54,8 +54,9 @@ class CSVOldScanSourceOpDescSpec extends AnyFlatSpec with Matchers { d.fileTypeName shouldBe Some("CSVOld") } - "CSVOldScanSourceOpDesc.sourceSchema" should "be null before a file is resolved" in { - (new CSVOldScanSourceOpDesc).sourceSchema() shouldBe null + "CSVOldScanSourceOpDesc.sourceSchema" should "prompt for a file before one is resolved" in { + val ex = intercept[IllegalArgumentException]((new CSVOldScanSourceOpDesc).sourceSchema()) + ex.getMessage should include("No file selected") } "CSVOldScanSourceOpDesc.getPhysicalOp" should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala index 7f34281b19e..c0d8cf2e93c 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/scan/json/JSONLScanSourceOpDescSpec.scala @@ -53,8 +53,9 @@ class JSONLScanSourceOpDescSpec extends AnyFlatSpec with Matchers { d.fileTypeName shouldBe Some("JSONL") } - "JSONLScanSourceOpDesc.sourceSchema" should "be null before a file is resolved" in { - (new JSONLScanSourceOpDesc).sourceSchema() shouldBe null + "JSONLScanSourceOpDesc.sourceSchema" should "prompt for a file before one is resolved" in { + val ex = intercept[IllegalArgumentException]((new JSONLScanSourceOpDesc).sourceSchema()) + ex.getMessage should include("No file selected") } "JSONLScanSourceOpDesc.getPhysicalOp" should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala index 5f715bb2da9..077f1380788 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/SQLSourceOpDescSpec.scala @@ -89,10 +89,11 @@ class SQLSourceOpDescSpec extends AnyFlatSpec with Matchers with MockFactory { configure(new TestSQLSourceOpDesc(conn)).sourceSchema().getAttribute("c").getType } - "SQLSourceOpDesc.sourceSchema" should "return null until every connection field is set" in { + "SQLSourceOpDesc.sourceSchema" should "prompt for the missing connection field" in { val desc = configure(new TestSQLSourceOpDesc(mock[Connection])) desc.table = null - assert(desc.sourceSchema() == null) + val ex = intercept[IllegalArgumentException](desc.sourceSchema()) + ex.getMessage should include("table") } it should "map each JDBC data type to its Texera attribute type" in { diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala index 84f3e6a6c9e..2f8a9352476 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/asterixdb/AsterixDBSourceOpDescSpec.scala @@ -56,8 +56,9 @@ class AsterixDBSourceOpDescSpec extends AnyFlatSpec with Matchers { d.interval shouldBe 0L } - "AsterixDBSourceOpDesc.sourceSchema" should "be null before a connection is configured" in { - (new AsterixDBSourceOpDesc).sourceSchema() shouldBe null + "AsterixDBSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in { + val ex = intercept[IllegalArgumentException]((new AsterixDBSourceOpDesc).sourceSchema()) + ex.getMessage should include("host") } "AsterixDBSourceOpDesc.getPhysicalOp" should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala index cde2c636800..618cebc22f6 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLSourceOpDescSpec.scala @@ -60,8 +60,9 @@ class MySQLSourceOpDescSpec extends AnyFlatSpec with Matchers { d.interval shouldBe 0L } - "MySQLSourceOpDesc.sourceSchema" should "be null before a connection is configured" in { - (new MySQLSourceOpDesc).sourceSchema() shouldBe null + "MySQLSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in { + val ex = intercept[IllegalArgumentException]((new MySQLSourceOpDesc).sourceSchema()) + ex.getMessage should include("host") } "MySQLSourceOpDesc.getPhysicalOp" should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala index df158479cf3..d32f087d7e9 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLSourceOpDescSpec.scala @@ -55,8 +55,9 @@ class PostgreSQLSourceOpDescSpec extends AnyFlatSpec with Matchers { d.interval shouldBe 0L } - "PostgreSQLSourceOpDesc.sourceSchema" should "be null before a connection is configured" in { - (new PostgreSQLSourceOpDesc).sourceSchema() shouldBe null + "PostgreSQLSourceOpDesc.sourceSchema" should "prompt for connection details before a connection is configured" in { + val ex = intercept[IllegalArgumentException]((new PostgreSQLSourceOpDesc).sourceSchema()) + ex.getMessage should include("host") } "PostgreSQLSourceOpDesc.getPhysicalOp" should diff --git a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala index 25166e7ac52..666ddb70a88 100644 --- a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala +++ b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/WorkflowCompiler.scala @@ -64,17 +64,22 @@ object WorkflowCompiler { errorList: List[(OperatorIdentity, Throwable)] ): Map[OperatorIdentity, WorkflowFatalError] = { val opIdToError = mutable.Map[OperatorIdentity, WorkflowFatalError]() - errorList.map { + errorList.foreach { case (opId, err) => // map each error to WorkflowFatalError, and report them in the log logger.error(s"Error occurred in logical plan compilation for opId: $opId", err) - opIdToError += (opId -> WorkflowFatalError( - COMPILATION_ERROR, - Timestamp(Instant.now), - err.toString, - getStackTraceWithAllCauses(err), - opId.id - )) + // keep only the first error per operator: it is the root cause (e.g. a file + // resolution failure), while later stages re-fail on the same operator with + // less specific messages (e.g. schema propagation seeing an unresolved file) + if (!opIdToError.contains(opId)) { + opIdToError += (opId -> WorkflowFatalError( + COMPILATION_ERROR, + Timestamp(Instant.now), + err.toString, + getStackTraceWithAllCauses(err), + opId.id + )) + } } opIdToError.toMap } diff --git a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala index eecb435cc87..d7fb25679bd 100644 --- a/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala +++ b/workflow-compiling-service/src/main/scala/org/apache/texera/amber/compiler/model/LogicalPlan.scala @@ -117,7 +117,11 @@ case class LogicalPlan( case operator @ (scanOp: ScanSourceOpDesc) => Try { // Resolve file path for ScanSourceOpDesc - val fileName = scanOp.fileName.getOrElse(throw new RuntimeException("no input file name")) + val fileName = scanOp.fileName.getOrElse( + throw new RuntimeException( + "No file selected. Please select a file from the 'File' dropdown in the right panel." + ) + ) val fileUri = FileResolver.resolve(fileName) // Convert to URI // Set the URI in the ScanSourceOpDesc From d897aff882491b65fe5cc216dbd6c240602c0322 Mon Sep 17 00:00:00 2001 From: roshiiiiz Date: Thu, 23 Jul 2026 05:38:28 +0500 Subject: [PATCH 10/10] fix(operator): add 100MB memory limit to binary file scan to prevent OOM - Intercepts binary file reads when they exceed 100MB to prevent Java heap IllegalArgumentException and Garbage Collection death spirals. - Throws a clean, user-friendly RuntimeException directing users to use the large binary attribute type instead for massive files. - Ensures the worker thread safely aborts without crashing the JVM or freezing the application. --- .../source/scan/file/FileScanUtils.scala | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala index a7f81b4869c..7fd4938fed1 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/scan/file/FileScanUtils.scala @@ -21,7 +21,7 @@ package org.apache.texera.amber.operator.source.scan.file import org.apache.commons.compress.archivers.ArchiveStreamFactory import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream -import org.apache.commons.io.IOUtils.toByteArray + import org.apache.texera.amber.core.storage.DocumentFactory import org.apache.texera.amber.core.tuple.AttributeTypeUtils.parseField import org.apache.texera.amber.core.tuple.LargeBinary @@ -39,6 +39,33 @@ import scala.collection.mutable import scala.jdk.CollectionConverters.IteratorHasAsScala private[file] object FileScanUtils { + + private val MAX_SAFE_SIZE = 100L * 1024L * 1024L // 100 MB + + private def safeToByteArray(entry: InputStream, attributeType: FileAttributeType): Array[Byte] = { + val out = new ByteArrayOutputStream() + val buffer = new Array[Byte](8192) + var bytesRead = entry.read(buffer) + var totalBytes = 0L + + while (bytesRead != -1) { + totalBytes += bytesRead + if (totalBytes > MAX_SAFE_SIZE) { + val largeBinaryHint = attributeType match { + case FileAttributeType.BINARY => "Please use 'large binary' attribute type instead." + case FileAttributeType.SINGLE_STRING => + "Please split the file or use a chunked reading method." + case _ => "File is too large to fit in memory." + } + throw new RuntimeException( + s"File exceeds maximum safe memory size of 100MB for '${attributeType.getName}' type. $largeBinaryHint" + ) + } + out.write(buffer, 0, bytesRead) + bytesRead = entry.read(buffer) + } + out.toByteArray + } def createTuplesFromFile( fileName: String, displayFileName: String, @@ -90,7 +117,7 @@ private[file] object FileScanUtils { } fields += (attributeType match { case FileAttributeType.SINGLE_STRING => - new String(toByteArray(entry), fileEncoding.getCharset) + new String(safeToByteArray(entry, attributeType), fileEncoding.getCharset) case FileAttributeType.LARGE_BINARY => val largeBinary = new LargeBinary() val out = new LargeBinaryOutputStream(largeBinary) @@ -105,7 +132,7 @@ private[file] object FileScanUtils { out.close() } largeBinary - case _ => parseField(toByteArray(entry), attributeType.getType) + case _ => parseField(safeToByteArray(entry, attributeType), attributeType.getType) }) TupleLike(fields.toSeq: _*) }