Skip to content

Commit aa536db

Browse files
dfa1claude
andcommitted
refactor(core): ADR 0001 Phase 5 — split Extension write surface
Extension no longer extends ExtensionEncoder. Four *ExtensionEncoder impls in writer/encode/ carry encodeAll; WriteRegistry and VortexWriter updated to load ExtensionEncoder via ServiceLoader. ExtensionStorage made public for cross-module access. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3187532 commit aa536db

17 files changed

Lines changed: 323 additions & 598 deletions

core/src/main/java/io/github/dfa1/vortex/extension/DateExtension.java

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22

33
import io.github.dfa1.vortex.core.DType;
44
import io.github.dfa1.vortex.core.PType;
5-
import io.github.dfa1.vortex.core.VortexException;
65
import io.github.dfa1.vortex.core.array.Array;
7-
import io.github.dfa1.vortex.core.array.NullableData;
86

97
import java.time.LocalDate;
108
import java.util.ArrayList;
11-
import java.util.Collection;
129
import java.util.List;
1310

1411
/// {@code vortex.date} — days since the Unix epoch, signed integer storage.
@@ -77,45 +74,4 @@ public int encode(LocalDate value) {
7774
return Math.toIntExact(value.toEpochDay());
7875
}
7976

80-
/// Encodes a collection of dates into the storage layout the writer accepts
81-
/// for an I32 column (a primitive {@code int[]} in row order).
82-
///
83-
/// @param values dates to encode
84-
/// @return packed {@code int[]} suitable for {@code writer.writeChunk}
85-
/// @throws ArithmeticException if any date is too far from the epoch to fit in I32
86-
public int[] encodeAll(Collection<LocalDate> values) {
87-
int[] out = new int[values.size()];
88-
int i = 0;
89-
for (LocalDate v : values) {
90-
out[i++] = encode(v);
91-
}
92-
return out;
93-
}
94-
95-
@Override
96-
@SuppressWarnings("unchecked")
97-
public Object encodeAll(DType.Extension dtype, Collection<?> values) {
98-
Collection<LocalDate> typed = (Collection<LocalDate>) values;
99-
int n = typed.size();
100-
int[] out = new int[n];
101-
boolean[] validity = new boolean[n];
102-
boolean anyNull = false;
103-
int i = 0;
104-
for (LocalDate v : typed) {
105-
if (v == null) {
106-
anyNull = true;
107-
} else {
108-
out[i] = encode(v);
109-
validity[i] = true;
110-
}
111-
i++;
112-
}
113-
if (!anyNull) {
114-
return out;
115-
}
116-
if (!dtype.nullable()) {
117-
throw new VortexException("null element in non-nullable vortex.date column");
118-
}
119-
return new NullableData(out, validity);
120-
}
12177
}

core/src/main/java/io/github/dfa1/vortex/extension/Extension.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,24 @@
22

33
import io.github.dfa1.vortex.core.DType;
44

5-
/// Contract for a Vortex extension type — pairs the wire-format identity
6-
/// (an [ExtensionId]) with a factory for the matching [DType.Extension]
7-
/// dtype. Behaviour-specific decode methods live on each concrete
8-
/// implementation, not on this interface, so read callers get typed
9-
/// return values without casting through {@code Object}.
5+
/// Read-side contract for a Vortex extension type — pairs the wire-format
6+
/// identity (an [ExtensionId]) with a factory for the matching [DType.Extension]
7+
/// dtype. Behaviour-specific decode methods live on each concrete implementation,
8+
/// not on this interface, so read callers get typed return values without casting
9+
/// through {@code Object}.
1010
///
11-
/// <p>Extends {@link ExtensionEncoder} so existing bifunctional implementations
12-
/// satisfy both contracts unchanged; ADR 0001 Phase 5 progressively peels the
13-
/// write-side {@code encodeAll} surface into standalone {@link ExtensionEncoder}
14-
/// implementations living in the writer module.
15-
public interface Extension extends ExtensionEncoder {
11+
/// <p>The write-side surface ({@code encodeAll}) lives on
12+
/// {@link ExtensionEncoder} implementations in the {@code writer} module.
13+
public interface Extension {
14+
15+
/// @return the spec identity of this extension
16+
ExtensionId extensionId();
17+
18+
/// @param nullable whether the column allows nulls
19+
/// @return matching {@link DType.Extension} (storage dtype + default metadata)
20+
DType.Extension dtype(boolean nullable);
1621

1722
/// Resolves a {@link DType.Extension} to its spec-defined singleton.
18-
/// Closes over the closed-set spec impls; third-party extensions go
19-
/// through {@link io.github.dfa1.vortex.encoding.Registry#lookup(ExtensionId)}.
2023
///
2124
/// @param dtype declared extension dtype
2225
/// @return matching spec extension singleton, or empty when the wire id

core/src/main/java/io/github/dfa1/vortex/extension/ExtensionStorage.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import java.nio.ByteBuffer;
1414
import java.time.Instant;
1515

16-
/// Shared decode helpers for the {@link Extension} implementations in this package.
17-
final class ExtensionStorage {
16+
/// Low-level storage helpers shared by extension decode and encode paths.
17+
public final class ExtensionStorage {
1818

1919
private ExtensionStorage() {
2020
}
@@ -27,7 +27,7 @@ private ExtensionStorage() {
2727
/// @param i row index
2828
/// @return cell value as long
2929
/// @throws VortexException if storage isn't an integer primitive or the cell is null
30-
static long epochInteger(Array storage, long i) {
30+
public static long epochInteger(Array storage, long i) {
3131
return switch (storage) {
3232
case ByteArray a -> a.getByte(i);
3333
case ShortArray a -> a.getShort(i);
@@ -50,7 +50,7 @@ static long epochInteger(Array storage, long i) {
5050
/// @param ext declared extension dtype
5151
/// @return decoded time unit
5252
/// @throws VortexException if the metadata is missing
53-
static TimeUnit readUnit(DType.Extension ext) {
53+
public static TimeUnit readUnit(DType.Extension ext) {
5454
ByteBuffer meta = ext.metadata();
5555
if (meta == null || !meta.hasRemaining()) {
5656
throw new VortexException("missing TimeUnit metadata byte for " + ext.extensionId());
@@ -64,7 +64,7 @@ static TimeUnit readUnit(DType.Extension ext) {
6464
/// @param unit time resolution; must not be {@link TimeUnit#Days}
6565
/// @return matching {@link Instant}
6666
/// @throws VortexException if {@code unit} is {@link TimeUnit#Days}
67-
static Instant instantFromRaw(long raw, TimeUnit unit) {
67+
public static Instant instantFromRaw(long raw, TimeUnit unit) {
6868
return switch (unit) {
6969
case Seconds -> Instant.ofEpochSecond(raw);
7070
case Milliseconds -> Instant.ofEpochMilli(raw);
@@ -86,7 +86,7 @@ static Instant instantFromRaw(long raw, TimeUnit unit) {
8686
///
8787
/// @param i row index to check
8888
/// @param length array length
89-
static void checkBounds(long i, long length) {
89+
public static void checkBounds(long i, long length) {
9090
if (i < 0 || i >= length) {
9191
throw new IndexOutOfBoundsException("index " + i + " out of bounds for length " + length);
9292
}

core/src/main/java/io/github/dfa1/vortex/extension/TimeExtension.java

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
import io.github.dfa1.vortex.core.PType;
55
import io.github.dfa1.vortex.core.VortexException;
66
import io.github.dfa1.vortex.core.array.Array;
7-
import io.github.dfa1.vortex.core.array.NullableData;
87
import io.github.dfa1.vortex.encoding.TimeUnit;
98

109
import java.nio.ByteBuffer;
1110
import java.time.LocalTime;
1211
import java.util.ArrayList;
13-
import java.util.Collection;
1412
import java.util.List;
1513

1614
/// {@code vortex.time} — sub-day count in the {@link TimeUnit} recorded in the metadata byte.
@@ -110,83 +108,4 @@ public long encode(LocalTime value, TimeUnit unit) {
110108
return value.toNanoOfDay() / divisor;
111109
}
112110

113-
/// Encodes a collection of times into the storage layout matching the unit:
114-
/// {@code int[]} for {@link TimeUnit#Seconds}/{@link TimeUnit#Milliseconds},
115-
/// {@code long[]} for {@link TimeUnit#Microseconds}/{@link TimeUnit#Nanoseconds}.
116-
/// Return type is {@code Object} so the writer can switch on the array type.
117-
///
118-
/// @param values times to encode
119-
/// @param unit resolution; controls storage width
120-
/// @return {@code int[]} or {@code long[]} suitable for {@code writer.writeChunk}
121-
/// @throws VortexException if {@code unit} is {@link TimeUnit#Days}
122-
public Object encodeAll(Collection<LocalTime> values, TimeUnit unit) {
123-
return switch (unit) {
124-
case Seconds, Milliseconds -> {
125-
int[] out = new int[values.size()];
126-
int i = 0;
127-
for (LocalTime v : values) {
128-
out[i++] = Math.toIntExact(encode(v, unit));
129-
}
130-
yield out;
131-
}
132-
case Microseconds, Nanoseconds -> {
133-
long[] out = new long[values.size()];
134-
int i = 0;
135-
for (LocalTime v : values) {
136-
out[i++] = encode(v, unit);
137-
}
138-
yield out;
139-
}
140-
case Days -> throw new VortexException("Time.encodeAll: Days unit not valid for vortex.time");
141-
};
142-
}
143-
144-
@Override
145-
@SuppressWarnings("unchecked")
146-
public Object encodeAll(DType.Extension dtype, Collection<?> values) {
147-
TimeUnit unit = ExtensionStorage.readUnit(dtype);
148-
Collection<LocalTime> typed = (Collection<LocalTime>) values;
149-
int n = typed.size();
150-
boolean[] validity = new boolean[n];
151-
boolean anyNull = false;
152-
Object out;
153-
int i = 0;
154-
switch (unit) {
155-
case Seconds, Milliseconds -> {
156-
int[] arr = new int[n];
157-
for (LocalTime v : typed) {
158-
if (v == null) {
159-
anyNull = true;
160-
} else {
161-
arr[i] = Math.toIntExact(encode(v, unit));
162-
validity[i] = true;
163-
}
164-
i++;
165-
}
166-
out = arr;
167-
}
168-
case Microseconds, Nanoseconds -> {
169-
long[] arr = new long[n];
170-
for (LocalTime v : typed) {
171-
if (v == null) {
172-
anyNull = true;
173-
} else {
174-
arr[i] = encode(v, unit);
175-
validity[i] = true;
176-
}
177-
i++;
178-
}
179-
out = arr;
180-
}
181-
case Days -> throw new VortexException("Time.encodeAll: Days unit not valid for vortex.time");
182-
default -> throw new VortexException("unknown TimeUnit: " + unit);
183-
}
184-
if (!anyNull) {
185-
return out;
186-
}
187-
if (!dtype.nullable()) {
188-
throw new VortexException("null element in non-nullable vortex.time column");
189-
}
190-
return new NullableData(out, validity);
191-
}
192111
}

core/src/main/java/io/github/dfa1/vortex/extension/TimestampExtension.java

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
import io.github.dfa1.vortex.core.DType;
44
import io.github.dfa1.vortex.core.PType;
5-
import io.github.dfa1.vortex.core.VortexException;
65
import io.github.dfa1.vortex.core.array.Array;
7-
import io.github.dfa1.vortex.core.array.NullableData;
86
import io.github.dfa1.vortex.encoding.TimeUnit;
97

108
import java.nio.ByteBuffer;
@@ -14,7 +12,6 @@
1412
import java.time.ZoneId;
1513
import java.time.ZonedDateTime;
1614
import java.util.ArrayList;
17-
import java.util.Collection;
1815
import java.util.List;
1916
import java.util.Optional;
2017

@@ -160,46 +157,4 @@ public long encode(Instant value, TimeUnit unit) {
160157
};
161158
}
162159

163-
/// Encodes a collection of instants into a packed {@code long[]} matching
164-
/// the I64 storage layout the writer accepts.
165-
///
166-
/// @param values instants to encode
167-
/// @param unit resolution
168-
/// @return packed {@code long[]} suitable for {@code writer.writeChunk}
169-
public long[] encodeAll(Collection<Instant> values, TimeUnit unit) {
170-
long[] out = new long[values.size()];
171-
int i = 0;
172-
for (Instant v : values) {
173-
out[i++] = encode(v, unit);
174-
}
175-
return out;
176-
}
177-
178-
@Override
179-
@SuppressWarnings("unchecked")
180-
public Object encodeAll(DType.Extension dtype, Collection<?> values) {
181-
TimeUnit unit = ExtensionStorage.readUnit(dtype);
182-
Collection<Instant> typed = (Collection<Instant>) values;
183-
int n = typed.size();
184-
long[] out = new long[n];
185-
boolean[] validity = new boolean[n];
186-
boolean anyNull = false;
187-
int i = 0;
188-
for (Instant v : typed) {
189-
if (v == null) {
190-
anyNull = true;
191-
} else {
192-
out[i] = encode(v, unit);
193-
validity[i] = true;
194-
}
195-
i++;
196-
}
197-
if (!anyNull) {
198-
return out;
199-
}
200-
if (!dtype.nullable()) {
201-
throw new VortexException("null element in non-nullable vortex.timestamp column");
202-
}
203-
return new NullableData(out, validity);
204-
}
205160
}

core/src/main/java/io/github/dfa1/vortex/extension/UuidExtension.java

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import io.github.dfa1.vortex.core.array.FixedSizeListArray;
99

1010
import java.util.ArrayList;
11-
import java.util.Collection;
1211
import java.util.List;
1312
import java.util.UUID;
1413

@@ -110,59 +109,4 @@ public byte[] encode(UUID value) {
110109
return out;
111110
}
112111

113-
/// Encodes a collection of UUIDs into a flat {@code byte[]} of size {@code 16 * n},
114-
/// matching the {@code FixedSizeList(U8, 16)} storage layout.
115-
///
116-
/// @param values UUIDs to encode
117-
/// @return packed bytes; the writer slices it into 16-byte rows
118-
public byte[] encodeAll(Collection<UUID> values) {
119-
byte[] out = new byte[16 * values.size()];
120-
int off = 0;
121-
for (UUID v : values) {
122-
long msb = v.getMostSignificantBits();
123-
long lsb = v.getLeastSignificantBits();
124-
for (int k = 0; k < 8; k++) {
125-
out[off + k] = (byte) ((msb >> (56 - 8 * k)) & 0xff);
126-
out[off + 8 + k] = (byte) ((lsb >> (56 - 8 * k)) & 0xff);
127-
}
128-
off += 16;
129-
}
130-
return out;
131-
}
132-
133-
@Override
134-
@SuppressWarnings("unchecked")
135-
public Object encodeAll(DType.Extension dtype, Collection<?> values) {
136-
Collection<UUID> typed = (Collection<UUID>) values;
137-
int n = typed.size();
138-
byte[] flat = new byte[16 * n];
139-
boolean[] validity = new boolean[n];
140-
boolean anyNull = false;
141-
int row = 0;
142-
for (UUID v : typed) {
143-
if (v == null) {
144-
anyNull = true;
145-
} else {
146-
long msb = v.getMostSignificantBits();
147-
long lsb = v.getLeastSignificantBits();
148-
int off = row * 16;
149-
for (int k = 0; k < 8; k++) {
150-
flat[off + k] = (byte) ((msb >> (56 - 8 * k)) & 0xff);
151-
flat[off + 8 + k] = (byte) ((lsb >> (56 - 8 * k)) & 0xff);
152-
}
153-
validity[row] = true;
154-
}
155-
row++;
156-
}
157-
io.github.dfa1.vortex.encoding.FixedSizeListData storage =
158-
new io.github.dfa1.vortex.encoding.FixedSizeListData(flat, n);
159-
if (!anyNull) {
160-
return storage;
161-
}
162-
if (!dtype.nullable()) {
163-
throw new io.github.dfa1.vortex.core.VortexException(
164-
"null element in non-nullable vortex.uuid column");
165-
}
166-
return new io.github.dfa1.vortex.core.array.NullableData(storage, validity);
167-
}
168112
}

0 commit comments

Comments
 (0)