diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index f6640ab..148f280 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -33,13 +33,11 @@ jobs:
fail-fast: false
matrix:
exasol_db_version: [
- 2025.2.1,
- 2025.1.10,
- 7.1.30
+ 2026.1.0
]
env: {
- DEFAULT_EXASOL_DB_VERSION: 2025.2.1
+ DEFAULT_EXASOL_DB_VERSION: 2026.1.0
}
steps:
- name: Free Disk Space
diff --git a/.project-keeper.yml b/.project-keeper.yml
index 196c776..eeca476 100644
--- a/.project-keeper.yml
+++ b/.project-keeper.yml
@@ -8,9 +8,7 @@ sources:
build:
runnerOs: ubuntu-24.04
exasolDbVersions:
- - "2025.2.1"
- - "2025.1.10"
- - "7.1.30"
+ - "2026.1.0"
workflows:
- name: ci-build.yml
stepCustomizations:
diff --git a/README.md b/README.md
index 4082aff..f14372f 100644
--- a/README.md
+++ b/README.md
@@ -19,12 +19,40 @@ This project contains the API required to build [User Defined Functions](https:/
User Defined Functions extend Exasol with functions and scripts that can be called from within SQL statements.
+
+
## Information for API Users
+### Dependency
+
+This library is already included in the Exasol database. Therefore, you should include it with the scope `provided` in Maven or `compileOnly` in Gradle. This ensures that the library is available during compilation but is not bundled with your UDF jar, avoiding conflicts with the version pre-installed in the database.
+
+Replace the version numbers below with the latest version.
+
+#### Maven
+
+```xml
+
+ com.exasol
+ udf-api-java
+ 1.0.10
+ provided
+
+```
+
+#### Gradle
+
+```groovy
+dependencies {
+ compileOnly 'com.exasol:udf-api-java:1.0.10'
+}
+```
+
* [API documentation as JavaDoc](https://exasol.github.io/udf-api-java)
* [Java tutorials](https://github.com/exasol/exasol-java-tutorial) with examples of how to build, test and run Java UDFs
## Additional Information
* [Changelog](doc/changes/changelog.md)
-* [Dependencies](dependencies.md)
\ No newline at end of file
+* [Dependencies](dependencies.md)
+* [Test strategy](doc/test_strategy.md)
diff --git a/doc/changes/changes_1.0.10.md b/doc/changes/changes_1.0.10.md
index 93c642b..da0568c 100644
--- a/doc/changes/changes_1.0.10.md
+++ b/doc/changes/changes_1.0.10.md
@@ -1,18 +1,27 @@
-# Exasol UDF API for Java 1.0.10, released 2026-??-??
+# Exasol UDF API for Java 1.0.10, released 2026-06-16
-Code name:
+Code name: Test Strategy Revisited
## Summary
-## Features
+In this release we experimented with injecting the JaCoCo code coverage agent into the UDF when running the integration test, so that we can measure the test coverage.
-* ISSUE_NUMBER: description
+In the end we decided against the approach, because however we turned it, the coverage we would measure would not represent the actual API coverage. But the good news is that our attempts resulted in cleaner test code. If you want to learn more, check out the [test strategy](../test_strategy.md).
+
+We also updated a number of dependencies.
+
+## Bugfixes
+
+* #40: Report integration test coverage
## Dependency Updates
### Test Dependency Updates
+* Updated `com.exasol:exasol-testcontainers:7.2.3` to `7.3.0`
+* Updated `com.exasol:test-db-builder-java:4.0.0` to `4.0.1`
* Added `org.jacoco:org.jacoco.agent:0.8.14`
+* Updated `org.slf4j:slf4j-jdk14:2.0.17` to `2.0.18`
### Plugin Dependency Updates
diff --git a/doc/test_strategy.md b/doc/test_strategy.md
new file mode 100644
index 0000000..1208a65
--- /dev/null
+++ b/doc/test_strategy.md
@@ -0,0 +1,65 @@
+# Test Strategy
+
+This document explains the test setup of the [Exasol UDF API for Java](../README.md), the options considered for
+measuring integration test coverage, and the decision to not use code coverage as a quality metric for this project.
+
+## Project Setup
+
+This project publishes the Java API that UDF authors compile against when writing Java user defined functions for the Exasol database. In production, the UDF API JAR is provided by the Exasol installation and loaded by the script language container. UDF authors compile against this artifact, but they do not include it in their own UDF JARs.
+
+The integration tests therefore deliberately mirror this deployment model:
+
+* The Maven build creates a test JAR containing probe UDF classes from `src/test/java`.
+* The test JAR is uploaded to BucketFS in an Exasol test container.
+* The integration tests create Java UDF scripts that load this test JAR with a `%jar` directive.
+* The probe UDF classes were compiled against this project, but at runtime they interact with the API classes and
+ implementation provided by Exasol.
+
+This setup is primarily a compatibility test. It verifies that code compiled against this project can run in Exasol and call the relevant UDF API methods.
+
+## Coverage Problem
+
+The project is not a typical Java library with executable production logic. Most of the public surface is made of interfaces such as `ExaIterator` and `ExaMetadata`. Interface method declarations do not contain executable instructions that JaCoCo can cover. The executable classes in this repository are mostly exception classes and a small enum.
+
+When the JaCoCo agent is injected into the UDF JVM, it can observe code executed inside the Exasol script language container. However, the code executed there is not this project's API artifact. It is Exasol's provided runtime API and implementation plus the test probe classes. A local JaCoCo report such as `target/site/jacoco/index.html` can therefore show execution data from the UDF JVM while still reporting no meaningful coverage for this project.
+
+## Options Considered
+
+### Inject JaCoCo Into the UDF JVM
+
+This was implemented and verified to collect execution data from the UDF-side JVM.
+
+Rejected as a coverage metric for this project because the executed API implementation belongs to the Exasol runtime, not to this repository. The report does not answer the question "Which code in this project was covered by the integration tests?"
+
+### Include This Project's API JAR in the UDF
+
+This would make JaCoCo observe this repository's classes in the UDF runtime.
+
+Rejected because it would no longer test the real deployment model. The Exasol installation provides the UDF API at runtime, and forcing this project's API JAR into the UDF could hide incompatibilities with the database-provided API.
+
+The resulting coverage would be easier to measure but less relevant.
+
+### Generate Proxy or Wrapper Code Around the API
+
+A proxy could wrap `ExaIterator`, `ExaMetadata`, and related interfaces, record which methods were called, and delegate
+to Exasol's runtime objects.
+
+Rejected as code coverage because it would measure the proxy and the probe UDFs, not this project's API. Such a proxy could be useful as an API exercise matrix, but it would be a custom compatibility metric rather than JaCoCo coverage.
+
+It would also add test-only code that has to mirror the whole API surface.
+
+### Add Unit Tests for the Executable Classes
+
+Unit tests could cover exception constructors, message formatting, and enum initialization.
+
+Rejected as a substitute for integration coverage because this would only cover incidental executable bytecode. It would not validate the important compatibility property: UDF code compiled against this API must run against Exasol's provided runtime API.
+
+## Decision
+
+We do not attempt to measure meaningful code coverage for this project with integration tests.
+
+The integration tests remain valuable and should be kept as compatibility and smoke tests. They check the behavior that matters for this artifact: Java UDF code compiled against this project can be loaded and executed by Exasol.
+
+Coverage reports for this project should not be used as a release quality gate. If a build or quality system requires a coverage value, this repository should be excluded from that requirement or the exception should point to this document.
+
+If more insight is needed in the future, the preferred direction is an explicit API exercise matrix that reports which UDF API methods are invoked by smoke tests. That metric must be documented as compatibility coverage, not as JaCoCo code coverage.
diff --git a/pom.xml b/pom.xml
index 531e847..26ed262 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,11 +6,16 @@
Exasol UDF API for Java
Interface between User Defined Functions (UDFs) written in Java and the Exasol database.
https://github.com/exasol/udf-api-java/
+
+ 11
+
+ true
+
com.exasol
exasol-testcontainers
- 7.2.3
+ 7.3.0
test
@@ -22,7 +27,7 @@
com.exasol
test-db-builder-java
- 4.0.0
+ 4.0.1
test
@@ -58,12 +63,25 @@
org.slf4j
slf4j-jdk14
- 2.0.17
+ 2.0.18
test
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ true
+
+ -Xlint:all,-path
+ -Werror
+
+
+
com.exasol
project-keeper-maven-plugin
@@ -83,6 +101,8 @@
+
+
test-jar
diff --git a/src/main/java/com/exasol/ExaConnectionAccessException.java b/src/main/java/com/exasol/ExaConnectionAccessException.java
index 963f095..ed7659c 100644
--- a/src/main/java/com/exasol/ExaConnectionAccessException.java
+++ b/src/main/java/com/exasol/ExaConnectionAccessException.java
@@ -6,7 +6,10 @@
*/
public class ExaConnectionAccessException extends Exception {
private static final long serialVersionUID = 1L;
- /** @serial Message without connection exception prefix. */
+ /**
+ * Message without connection exception prefix.
+ * @serial
+ * */
private final String message;
/**
diff --git a/src/test/java/com/exasol/test/JavaUdfIT.java b/src/test/java/com/exasol/test/JavaUdfIT.java
index fad513e..c6a5841 100644
--- a/src/test/java/com/exasol/test/JavaUdfIT.java
+++ b/src/test/java/com/exasol/test/JavaUdfIT.java
@@ -7,9 +7,17 @@
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.sql.*;
+import java.util.Locale;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
+import com.exasol.containers.ExasolContainer;
+import com.exasol.dbbuilder.dialects.exasol.ExasolObjectConfiguration;
+import com.exasol.dbbuilder.dialects.exasol.ExasolSchema;
+import com.exasol.dbbuilder.dialects.exasol.udf.UdfScript;
+import com.exasol.test.testobject.GetSizeUdf;
+import com.exasol.test.testobject.GetTimestampUdf;
+import com.exasol.test.testobject.MetadataMethodExerciser;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -18,8 +26,6 @@
import com.exasol.bucketfs.Bucket;
import com.exasol.bucketfs.BucketAccessException;
-import com.exasol.containers.ExasolContainer;
-import com.exasol.dbbuilder.dialects.Schema;
import com.exasol.dbbuilder.dialects.exasol.ExasolObjectFactory;
import com.exasol.matcher.ResultSetStructureMatcher;
import com.exasol.mavenprojectversiongetter.MavenProjectVersionGetter;
@@ -49,15 +55,16 @@ class JavaUdfIT {
private static final String PROJECT_VERSION = MavenProjectVersionGetter.getCurrentProjectVersion();
private static final String UDF_UNDER_TEST_JAR = "udf-api-java-" + PROJECT_VERSION + "-tests.jar";
private static final Path UDF_UNDER_TEST_JAR_PATH = Path.of("target", UDF_UNDER_TEST_JAR);
- private static final String JAR_INCLUDE_DIRECTIVE = "%jar /buckets/bfsdefault/default/" + UDF_UNDER_TEST_JAR;
+ private static final String UDF_BUCKETFS_PATH = " /buckets/bfsdefault/default/" + UDF_UNDER_TEST_JAR;
private static Connection connection;
- private static Schema schema;
+ private static ExasolSchema schema;
@BeforeAll
static void beforeAll() throws BucketAccessException, FileNotFoundException {
connection = EXASOL.createConnection();
- final ExasolObjectFactory factory = new ExasolObjectFactory(connection);
+ final ExasolObjectFactory factory = new ExasolObjectFactory(connection,
+ ExasolObjectConfiguration.builder().build());
schema = factory.createSchema("CONTEXT_SCHEMA");
copyUdfUnderTestToDefaultBucket();
}
@@ -81,44 +88,33 @@ static void afterAll() throws SQLException {
}
}
- @CsvSource({ //
- "getDatabaseName, DB1", //
- "getDatabaseVersion, \\d+\\.\\d+\\.\\d+", //
- "getNodeCount, 1", //
- "getOutputType, RETURN", //
- "getScopeUser, SYS", //
- "getScriptCode, %jar(?:\\R|.)*class(?:\\R|.)*", //
- "getScriptSchema, CONTEXT_SCHEMA", //
- "getScriptName, CONTEXT_METHOD_GETSCRIPTNAME", //
+ @CsvSource({
+ "getDatabaseName, DB1",
+ "getDatabaseVersion, \\d+\\.\\d+\\.\\d+",
+ "getNodeCount, 1",
+ "getOutputType, RETURN",
+ "getScopeUser, SYS",
+ "getScriptCode, %scriptclass(?:\\R|.)*%jar(?:\\R|.)*",
+ "getScriptSchema, CONTEXT_SCHEMA",
+ "getScriptName, CONTEXT_METHOD_GETSCRIPTNAME",
"getScriptLanguage, Java \\d+\\.\\d+.\\d+" })
@ParameterizedTest
void testGetDatabaseContextInformation(final String methodName, final String expectedResult) {
- final String fullyQualifiedScriptName = createContextMethodTestScript(schema, methodName);
- final String value = executeScalarScriptWithStringReturn(fullyQualifiedScriptName, methodName);
- assertThat("Result of method " + methodName + "()", value, matchesPattern(expectedResult));
- }
-
- private String createContextMethodTestScript(final Schema schema, final String methodName) {
- final String scriptName = "CONTEXT_METHOD_" + methodName.toUpperCase();
- final String fullyQualifiedScriptName = getFullyQualifiedScriptName(schema, scriptName);
- executeStatement("CREATE JAVA SCALAR SCRIPT " + fullyQualifiedScriptName //
- + "(method_name VARCHAR(100)) RETURNS VARCHAR(2000) AS\n" //
- + " " + JAR_INCLUDE_DIRECTIVE + ";\n" //
- + " %scriptclass com.exasol.test.testobject.MetadataMethodExerciser;\n" //
- + "\n/\n");
- return fullyQualifiedScriptName;
- }
-
- private static String getFullyQualifiedScriptName(final Schema schema, final String scriptName) {
- return "\"" + schema.getName() + "\".\"" + scriptName + "\"";
+ try(final UdfScript script = createContextMethodTestScript(methodName)) {
+ final String value = executeScalarScriptWithStringReturn(script.getFullyQualifiedName(), methodName);
+ assertThat("Result of method " + methodName + "()", value, matchesPattern(expectedResult));
+ }
}
- private static void executeStatement(final String sql) {
- try (final Statement statement = connection.createStatement()) {
- statement.execute(sql);
- } catch (final SQLException exception) {
- throw new AssertionError("Unable to execute statement:\n" + sql + "\n", exception);
- }
+ private UdfScript createContextMethodTestScript(final String methodName) {
+ final String scriptName = "CONTEXT_METHOD_" + methodName.toUpperCase(Locale.ENGLISH);
+ return schema.createUdfBuilder(scriptName)
+ .parameter("method_name", "VARCHAR(100)")
+ .inputType(UdfScript.InputType.SCALAR)
+ .language(UdfScript.Language.JAVA)
+ .bucketFsContent(MetadataMethodExerciser.class.getName(), UDF_BUCKETFS_PATH)
+ .returns("VARCHAR(2000)")
+ .build();
}
private String executeScalarScriptWithStringReturn(final String fullyQualifiedScriptName, final String methodName) {
@@ -136,16 +132,17 @@ private String executeScalarScriptWithStringReturn(final String fullyQualifiedSc
void testGetTimestampFromSetScript() {
final String date = "2001-02-03";
final String time = "04:05:06.007";
- final String scriptName = "GET_TIMESTAMP_SCRIPT";
- final String fullyQualifiedScriptName = getFullyQualifiedScriptName(schema, scriptName);
- executeStatement("CREATE JAVA SET SCRIPT " + fullyQualifiedScriptName //
- + "(V TIMESTAMP) RETURNS VARCHAR(2000) AS\n" //
- + " " + JAR_INCLUDE_DIRECTIVE + ";\n" //
- + " %scriptclass com.exasol.test.testobject.GetTimestampUdf;\n" //
- + "/\n");
- assertQueryResult("SELECT " + fullyQualifiedScriptName + "(T.V)" + //
- "FROM VALUES (TO_TIMESTAMP('" + date + "T" + time + "Z', 'YYYY-MM-DDTHH24:MI:SS.FF3Z')) AS T(V)", //
- table().row(date + " " + time));
+ try(UdfScript script = schema.createUdfBuilder("GET_TIMESTAMP_SCRIPT")
+ .parameter("V", "TIMESTAMP")
+ .language(UdfScript.Language.JAVA)
+ .inputType(UdfScript.InputType.SET)
+ .bucketFsContent(GetTimestampUdf.class.getName(), UDF_BUCKETFS_PATH)
+ .returns("VARCHAR(2000)")
+ .build()) {
+ assertQueryResult("SELECT " + script.getFullyQualifiedName() + "(T.V)" +
+ " FROM VALUES (TO_TIMESTAMP('" + date + "T" + time + "Z', 'YYYY-MM-DDTHH24:MI:SS.FF3Z')) AS T(V)",
+ table().row(date + " " + time));
+ }
}
private static void assertQueryResult(final String sql, final ResultSetStructureMatcher.Builder rowMatcher) {
@@ -159,24 +156,29 @@ private static void assertQueryResult(final String sql, final ResultSetStructure
@Test
void testGetSizeFromScalarScript() {
- final String scriptName = "SIZE_IN_SCALAR_CONTEXT";
- final String fullyQualifiedScriptName = getFullyQualifiedScriptName(schema, scriptName);
- executeStatement("CREATE JAVA SCALAR SCRIPT " + fullyQualifiedScriptName + "() RETURNS INTEGER AS\n" //
- + " " + JAR_INCLUDE_DIRECTIVE + ";\n" //
- + " %scriptclass com.exasol.test.testobject.GetSizeUdf;\n" //
- + "/\n");
- assertQueryResult("SELECT " + fullyQualifiedScriptName + "()", table().row(1L));
+ try(final UdfScript script = schema.createUdfBuilder("SIZE_IN_SCALAR_CONTEXT")
+ .language(UdfScript.Language.JAVA)
+ .inputType(UdfScript.InputType.SCALAR)
+ .bucketFsContent(GetSizeUdf.class.getName(), UDF_BUCKETFS_PATH)
+ .returns("INTEGER")
+ .build()) {
+ assertQueryResult("SELECT " + script.getFullyQualifiedName() + "()", table().row(1L));
+ }
}
@Test
void testGetSizeFromSetScript() {
final String scriptName = "SIZE_IN_SET_CONTEXT";
- final String fullyQualifiedScriptName = getFullyQualifiedScriptName(schema, scriptName);
- executeStatement("CREATE JAVA SET SCRIPT " + fullyQualifiedScriptName + "(COL CHAR(1)) RETURNS INTEGER AS\n" //
- + " " + JAR_INCLUDE_DIRECTIVE + ";\n" //
- + " %scriptclass com.exasol.test.testobject.GetSizeUdf;\n" //
- + "/\n");
- assertQueryResult("SELECT " + fullyQualifiedScriptName + "(v) FROM VALUES ('a'), ('b'), ('c') AS v(v)", //
- table().row(3L));
+ try(final UdfScript script = schema.createUdfBuilder(scriptName)
+ .parameter("COL", "CHAR(1)")
+ .language(UdfScript.Language.JAVA)
+ .inputType(UdfScript.InputType.SET)
+ .bucketFsContent(GetSizeUdf.class.getName(), UDF_BUCKETFS_PATH)
+ .returns("INTEGER")
+ .build()
+ ) {
+ assertQueryResult("SELECT " + script.getFullyQualifiedName() + "(v) FROM VALUES ('a'), ('b'), ('c') AS v(v)",
+ table().row(3L));
+ }
}
}