diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 00000000..5c792b4c
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1,11 @@
+--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
+--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
+--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
+--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
diff --git a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java
index e02ef48a..a2461dd8 100644
--- a/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java
+++ b/cli/src/main/java/io/github/dfa1/vortex/cli/tui/VortexInspectorTui.java
@@ -680,7 +680,8 @@ record Failed(String message) implements DataState {
private static String formatValue(Array array, int i, DType declared) {
if (declared instanceof DType.Extension ext
&& io.github.dfa1.vortex.extension.ExtensionId.tryFrom(ext.extensionId())
- == io.github.dfa1.vortex.extension.ExtensionId.VORTEX_DATE) {
+ .filter(id -> id == io.github.dfa1.vortex.extension.ExtensionId.VORTEX_DATE)
+ .isPresent()) {
try {
return io.github.dfa1.vortex.extension.DateExtension.INSTANCE.decode(array, i).toString();
} catch (RuntimeException e) {
diff --git a/core/pom.xml b/core/pom.xml
index b4b90110..c6d30032 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -29,6 +29,10 @@
com.github.luben
zstd-jni
+
+ org.jspecify
+ jspecify
+
org.junit.jupiter
diff --git a/core/src/main/java/io/github/dfa1/vortex/extension/Extension.java b/core/src/main/java/io/github/dfa1/vortex/extension/Extension.java
index 58ca04d3..55b3e237 100644
--- a/core/src/main/java/io/github/dfa1/vortex/extension/Extension.java
+++ b/core/src/main/java/io/github/dfa1/vortex/extension/Extension.java
@@ -4,6 +4,7 @@
import io.github.dfa1.vortex.core.VortexException;
import java.util.Collection;
+import java.util.Optional;
/// Contract for a Vortex extension type — pairs the wire-format identity
/// (an [ExtensionId]) with a factory for the matching [DType.Extension]
@@ -37,23 +38,19 @@ default Object encodeAll(DType.Extension dtype, Collection> values) {
throw new VortexException("encode not supported for " + extensionId());
}
- /// Resolves a {@link DType.Extension} to its spec-defined singleton, or
- /// {@code null} when the wire id isn't one of the four spec extensions.
+ /// Resolves a {@link DType.Extension} to its spec-defined singleton.
/// Closes over the closed-set spec impls; third-party extensions go
/// through {@link io.github.dfa1.vortex.encoding.Registry#lookup(ExtensionId)}.
///
/// @param dtype declared extension dtype
- /// @return matching spec extension singleton, or {@code null}
- static Extension findKnown(DType.Extension dtype) {
- ExtensionId id = ExtensionId.tryFrom(dtype.extensionId());
- if (id == null) {
- return null;
- }
- return switch (id) {
+ /// @return matching spec extension singleton, or empty when the wire id
+ /// isn't one of the four spec extensions
+ static Optional findKnown(DType.Extension dtype) {
+ return ExtensionId.tryFrom(dtype.extensionId()).map(id -> switch (id) {
case VORTEX_DATE -> DateExtension.INSTANCE;
case VORTEX_TIME -> TimeExtension.INSTANCE;
case VORTEX_TIMESTAMP -> TimestampExtension.INSTANCE;
case VORTEX_UUID -> UuidExtension.INSTANCE;
- };
+ });
}
}
diff --git a/core/src/main/java/io/github/dfa1/vortex/extension/ExtensionId.java b/core/src/main/java/io/github/dfa1/vortex/extension/ExtensionId.java
index 74f30443..792d738b 100644
--- a/core/src/main/java/io/github/dfa1/vortex/extension/ExtensionId.java
+++ b/core/src/main/java/io/github/dfa1/vortex/extension/ExtensionId.java
@@ -3,6 +3,7 @@
import io.github.dfa1.vortex.core.VortexException;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -48,9 +49,9 @@ public static ExtensionId from(String id) {
/// Non-throwing lookup for a raw extension id string.
///
/// @param id raw extension id string
- /// @return matching constant, or {@code null} if not a known spec extension
- public static ExtensionId tryFrom(String id) {
- return LOOKUP.get(id);
+ /// @return matching constant, or empty if not a known spec extension
+ public static Optional tryFrom(String id) {
+ return Optional.ofNullable(LOOKUP.get(id));
}
/// Returns the canonical wire-format id string.
diff --git a/core/src/main/java/io/github/dfa1/vortex/extension/TimestampExtension.java b/core/src/main/java/io/github/dfa1/vortex/extension/TimestampExtension.java
index ab6b4508..cec36563 100644
--- a/core/src/main/java/io/github/dfa1/vortex/extension/TimestampExtension.java
+++ b/core/src/main/java/io/github/dfa1/vortex/extension/TimestampExtension.java
@@ -49,7 +49,7 @@ public DType.Extension dtype(boolean nullable) {
/// @param zone IANA timezone, or {@code null} for none
/// @param nullable whether the column allows nulls
/// @return matching extension dtype
- public DType.Extension dtype(TimeUnit unit, ZoneId zone, boolean nullable) {
+ public DType.Extension dtype(TimeUnit unit, @org.jspecify.annotations.Nullable ZoneId zone, boolean nullable) {
byte[] tzBytes = zone == null ? new byte[0] : zone.getId().getBytes(StandardCharsets.UTF_8);
ByteBuffer meta = ByteBuffer.allocate(3 + tzBytes.length).order(ByteOrder.LITTLE_ENDIAN);
meta.put(0, (byte) unit.ordinal());
diff --git a/core/src/main/java/io/github/dfa1/vortex/extension/package-info.java b/core/src/main/java/io/github/dfa1/vortex/extension/package-info.java
new file mode 100644
index 00000000..57562580
--- /dev/null
+++ b/core/src/main/java/io/github/dfa1/vortex/extension/package-info.java
@@ -0,0 +1,7 @@
+/// Spec extension types: {@link io.github.dfa1.vortex.extension.DateExtension},
+/// {@link io.github.dfa1.vortex.extension.TimeExtension},
+/// {@link io.github.dfa1.vortex.extension.TimestampExtension},
+/// {@link io.github.dfa1.vortex.extension.UuidExtension}, plus the
+/// {@link io.github.dfa1.vortex.extension.Extension} SPI for third-party impls.
+@org.jspecify.annotations.NullMarked
+package io.github.dfa1.vortex.extension;
diff --git a/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionIdTest.java b/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionIdTest.java
index 05d8f0df..7d618554 100644
--- a/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionIdTest.java
+++ b/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionIdTest.java
@@ -20,14 +20,14 @@ class ExtensionIdTest {
void tryFrom_knownIds_returnEnumConstant(String wire, ExtensionId expected) {
// Given / When / Then — wire string round-trips to the enum constant
// so the LOOKUP map stays in sync with the enum definition
- assertThat(ExtensionId.tryFrom(wire)).isSameAs(expected);
+ assertThat(ExtensionId.tryFrom(wire)).contains(expected);
}
@Test
- void tryFrom_unknownId_returnsNull() {
+ void tryFrom_unknownId_returnsEmpty() {
// Given — open-world extension id; library doesn't recognise it
// When / Then — non-throwing miss so the registry can route to passthrough
- assertThat(ExtensionId.tryFrom("acme.geopoint")).isNull();
+ assertThat(ExtensionId.tryFrom("acme.geopoint")).isEmpty();
}
@Test
diff --git a/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionTestSupport.java b/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionTestSupport.java
index 3fcc8ad5..13cdf086 100644
--- a/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionTestSupport.java
+++ b/core/src/test/java/io/github/dfa1/vortex/extension/ExtensionTestSupport.java
@@ -34,7 +34,7 @@ static ByteBuffer unitByte(byte tag) {
return meta;
}
- static ByteBuffer tzMeta(byte unitTag, String tz) {
+ static ByteBuffer tzMeta(byte unitTag, @org.jspecify.annotations.Nullable String tz) {
byte[] tzBytes = tz == null ? new byte[0] : tz.getBytes(StandardCharsets.UTF_8);
ByteBuffer meta = ByteBuffer.allocate(3 + tzBytes.length).order(ByteOrder.LITTLE_ENDIAN);
meta.put(0, unitTag);
diff --git a/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java b/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java
index 63ef7bdc..c1ccf50c 100644
--- a/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java
+++ b/jdbc/src/main/java/io/github/dfa1/vortex/jdbc/JdbcImporter.java
@@ -216,10 +216,8 @@ private static boolean fillCell(Object buffer, int rowIdx, ResultSet rs, int col
@SuppressWarnings("unchecked")
private static void fillExtensionCell(List
+
+ org.jspecify
+ jspecify
+ ${jspecify.version}
+
com.h2database
@@ -223,6 +232,29 @@
3.15.0
25
+
+
+
+ com.google.errorprone
+ error_prone_core
+ ${errorprone.version}
+
+
+ com.uber.nullaway
+ nullaway
+ ${nullaway.version}
+
+
+
+ -XDcompilePolicy=simple
+ --should-stop=ifError=FLOW
+ -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:JSpecifyMode=true -XepOpt:NullAway:ExcludedClassAnnotations=javax.annotation.processing.Generated,jakarta.annotation.Generated -XepExcludedPaths:.*/(fbs|proto|cli/.*tui|cli/.*server)/.*
+
+
diff --git a/reader/src/main/java/io/github/dfa1/vortex/scan/Chunk.java b/reader/src/main/java/io/github/dfa1/vortex/scan/Chunk.java
index 91ae0e22..81dbeea7 100644
--- a/reader/src/main/java/io/github/dfa1/vortex/scan/Chunk.java
+++ b/reader/src/main/java/io/github/dfa1/vortex/scan/Chunk.java
@@ -117,10 +117,8 @@ public List as(String name, Class domainType) {
if (!(colDtype instanceof DType.Extension ext)) {
throw new VortexException("not an extension column: " + name);
}
- ExtensionId id = ExtensionId.tryFrom(ext.extensionId());
- if (id == null) {
- throw new VortexException("not a spec extension id: " + ext.extensionId());
- }
+ ExtensionId id = ExtensionId.tryFrom(ext.extensionId())
+ .orElseThrow(() -> new VortexException("not a spec extension id: " + ext.extensionId()));
Array storage = column(name);
Object result = switch (id) {
case VORTEX_DATE -> {
diff --git a/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java b/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java
index cdce13d9..2caad580 100644
--- a/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java
+++ b/writer/src/main/java/io/github/dfa1/vortex/writer/VortexWriter.java
@@ -310,10 +310,10 @@ public void writeChunk(Map columns) throws IOException {
// ExtEncoding wraps the storage child below — matches Rust's nested layout
// (ExtEncoding → PrimitiveEncoding) and lets Registry skip its unwrap path.
if (colDtype instanceof DType.Extension extDtype && data instanceof java.util.Collection> coll) {
- io.github.dfa1.vortex.extension.ExtensionId extId =
- io.github.dfa1.vortex.extension.ExtensionId.tryFrom(extDtype.extensionId());
io.github.dfa1.vortex.extension.Extension impl =
- extId == null ? null : defaultRegistry.lookup(extId);
+ io.github.dfa1.vortex.extension.ExtensionId.tryFrom(extDtype.extensionId())
+ .map(defaultRegistry::lookup)
+ .orElse(null);
if (impl != null) {
data = impl.encodeAll(extDtype, coll);
}