Skip to content

Commit 94b3703

Browse files
authored
refactor(jdbc): extract SqliteRealText from LibsqlResultSet (#216)
Fixes #198 Pure move, zero behavior change. Extracts the SQLite REAL text formatter from `LibsqlResultSet` into a package-private `SqliteRealText` with a package-private `static String format(double)`. **Base note:** this PR targets `test/i195-pin-jdbc-behaviors`, the parent stack link (wave 0 characterization tests, 9 new tests). It is not based on `main`. ## Diff stat ``` .../magnusp/libsql/jdbc/LibsqlResultSet.java | 264 +------------------- .../github/magnusp/libsql/jdbc/SqliteRealText.java | 272 +++++++++++++++++++++ 2 files changed, 274 insertions(+), 262 deletions(-) ``` ## Proof the moved lines are unchanged Old `LibsqlResultSet.java` lines 187–444 (258 lines) vs `SqliteRealText.java` lines 14–271 — the only differing line is the entry-method signature, which the issue requires renaming and widening to package-private: ``` $ git show origin/test/i195-pin-jdbc-behaviors:.../LibsqlResultSet.java | sed -n '187,444p' > /tmp/blk-old.txt $ sed -n '14,271p' .../SqliteRealText.java > /tmp/blk-new.txt $ diff /tmp/blk-old.txt /tmp/blk-new.txt 32c32 < private static String sqliteRealText(double value) { --- > static String format(double value) { ``` Everything else — the two explanatory comments, the four power tables (`FP_POWERS_OF_TEN`, `FP_POWER_SCALES`, `FP_POWER_SCALE_LOW`, plus `FP_POWER_SCALE_HIGH`), `roundTripsTo`, `fpPowerOfTenToTwo`, `fpPowerOfTwoToTen`, `fpPowerOfTen`, `fpNormalizeHigh`, `fpMultiplyWide`, `fpMultiplyHigh`, and `fpUnsigned` — is identical text, at identical indentation, with method names, constants, comments, and arithmetic byte for byte. No `BigDecimal.toPlainString`, `Double.toString`, `String.format`, or `%g`. The only other edit in `LibsqlResultSet` is removing the now-unused `java.math.BigInteger` import (its only use lived in the moved code; `BigDecimal` is still used). Call sites repointed: - `getString` (was 181, now 180): `case Value.Float f -> SqliteRealText.format(f.value());` - `getBytes` (was 641, now 381): `case Value.Float f -> SqliteRealText.format(f.value()).getBytes(...);` Behavior preserved: negative zero renders `0.0`, exponents render signed and zero padded to two digits, NaN renders `NaN`, and fixed notation applies when the decimal exponent is between -4 and 16. ## Line counts | File | Before | After | |---|---|---| | `LibsqlResultSet.java` | 1498 | **1238** (under the 1250 gate) | | `SqliteRealText.java` | — | 272 | ## Lanes ``` $ ./mvnw -pl libsql-jdbc -am test [INFO] Tests run: 383, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS ``` ``` $ ./mvnw spotless:apply [INFO] BUILD SUCCESS $ ./mvnw spotless:check [INFO] BUILD SUCCESS ``` ``` $ ./mvnw -pl libsql-conformance -am verify [INFO] Tests run: 272, Failures: 0, Errors: 0, Skipped: 7 [INFO] BUILD SUCCESS ``` No test file, dependency, or `pom.xml` change. No public surface change. Nothing else in `LibsqlResultSet` moved or renamed.
1 parent fe91f99 commit 94b3703

2 files changed

Lines changed: 274 additions & 262 deletions

File tree

‎libsql-jdbc/src/main/java/com/github/magnusp/libsql/jdbc/LibsqlResultSet.java‎

Lines changed: 2 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.io.StringReader;
1313
import java.io.Writer;
1414
import java.math.BigDecimal;
15-
import java.math.BigInteger;
1615
import java.net.URL;
1716
import java.nio.charset.StandardCharsets;
1817
import java.sql.Array;
@@ -178,271 +177,12 @@ public String getString(int columnIndex) throws SQLException {
178177
return switch (val) {
179178
case Value.Null n -> null;
180179
case Value.Integer i -> Long.toString(i.value());
181-
case Value.Float f -> sqliteRealText(f.value());
180+
case Value.Float f -> SqliteRealText.format(f.value());
182181
case Value.Text t -> t.value();
183182
case Value.Blob b -> new String(b.bytes(), java.nio.charset.StandardCharsets.UTF_8);
184183
};
185184
}
186185

187-
// Port of the fixed-point conversion SQLite 3.53.4 uses for printf("%!.17g"), which is the
188-
// text sqlite-jdbc 3.53.4.0 returns for REAL. The power-of-ten tables come from the same
189-
// source (util.c and tool/mkfptab.c).
190-
private static final long[] FP_POWERS_OF_TEN = {
191-
0x8000000000000000L, 0xa000000000000000L, 0xc800000000000000L, 0xfa00000000000000L,
192-
0x9c40000000000000L, 0xc350000000000000L, 0xf424000000000000L, 0x9896800000000000L,
193-
0xbebc200000000000L, 0xee6b280000000000L, 0x9502f90000000000L, 0xba43b74000000000L,
194-
0xe8d4a51000000000L, 0x9184e72a00000000L, 0xb5e620f480000000L, 0xe35fa931a0000000L,
195-
0x8e1bc9bf04000000L, 0xb1a2bc2ec5000000L, 0xde0b6b3a76400000L, 0x8ac7230489e80000L,
196-
0xad78ebc5ac620000L, 0xd8d726b7177a8000L, 0x878678326eac9000L, 0xa968163f0a57b400L,
197-
0xd3c21bcecceda100L, 0x84595161401484a0L, 0xa56fa5b99019a5c8L,
198-
};
199-
200-
private static final long[] FP_POWER_SCALES = {
201-
0x8049a4ac0c5811aeL, 0xcf42894a5dce35eaL, 0xa76c582338ed2621L, 0x873e4f75e2224e68L,
202-
0xda7f5bf590966848L, 0xb080392cc4349decL, 0x8e938662882af53eL, 0xe65829b3046b0afaL,
203-
0xba121a4650e4ddebL, 0x964e858c91ba2655L, 0xf2d56790ab41c2a2L, 0xc428d05aa4751e4cL,
204-
0x9e74d1b791e07e48L, 0xccccccccccccccccL, 0xcecb8f27f4200f3aL, 0xa70c3c40a64e6c51L,
205-
0x86f0ac99b4e8dafdL, 0xda01ee641a708de9L, 0xb01ae745b101e9e4L, 0x8e41ade9fbebc27dL,
206-
0xe5d3ef282a242e81L, 0xb9a74a0637ce2ee1L, 0x95f83d0a1fb69cd9L, 0xf24a01a73cf2dccfL,
207-
0xc3b8358109e84f07L, 0x9e19db92b4e31ba9L,
208-
};
209-
210-
private static final int[] FP_POWER_SCALE_LOW = {
211-
0x205b896d, 0x52064cad, 0xaf2af2b8, 0x5a7744a7, 0xaf39a475, 0xbd8d794e,
212-
0x547eb47b, 0x0cb4a5a3, 0x92f34d62, 0x3a6a07f9, 0xfae27299, 0xaa97e14c,
213-
0x775ea265, 0xcccccccc, 0x00000000, 0x999090b6, 0x69a028bb, 0xe80e6f48,
214-
0x5ec05dd0, 0x14588f14, 0x8f1668c9, 0x6d953e2c, 0x4abdaf10, 0xbc633b39,
215-
0x0a862f81, 0x6c07a2c2,
216-
};
217-
218-
private static String sqliteRealText(double value) {
219-
if (Double.isNaN(value)) {
220-
return "NaN";
221-
}
222-
if (Double.isInfinite(value)) {
223-
return value < 0 ? "-Inf" : "Inf";
224-
}
225-
boolean negative = value < 0.0;
226-
double magnitude = negative ? -value : value;
227-
if (magnitude == 0.0) {
228-
return "0.0";
229-
}
230-
231-
long bits = Double.doubleToRawLongBits(magnitude);
232-
int exponent = (int) ((bits >>> 52) & 0x7ff);
233-
long mantissa = bits & 0x000fffffffffffffL;
234-
if (exponent == 0) {
235-
int shift = Long.numberOfLeadingZeros(mantissa);
236-
mantissa <<= shift;
237-
exponent = -1074 - shift;
238-
} else {
239-
mantissa = (mantissa << 11) | (1L << 63);
240-
exponent -= 1086;
241-
}
242-
243-
int power = 17 - fpPowerOfTwoToTen(exponent + 63);
244-
long powerOfTen = fpPowerOfTen(power);
245-
long high = fpMultiplyHigh(mantissa, powerOfTen);
246-
int shift = -(exponent + fpPowerOfTenToTwo(power) + 2);
247-
high >>>= shift;
248-
long digitsValue = (high + ((high << 1) & 2)) >>> 1;
249-
int powerOfTenExponent = -power;
250-
251-
String digits = Long.toUnsignedString(digitsValue);
252-
int digitCount = digits.length();
253-
int decimalPoint = digitCount + powerOfTenExponent;
254-
255-
if (digitCount > 17) {
256-
int roundAt = 17;
257-
if (digits.charAt(15) == '9' && digits.charAt(14) == '9') {
258-
int lastKept = 14;
259-
while (lastKept > 0 && digits.charAt(lastKept - 1) == '9') {
260-
lastKept--;
261-
}
262-
BigInteger candidate = lastKept == 0
263-
? BigInteger.ONE
264-
: new BigInteger(digits.substring(0, lastKept)).add(BigInteger.ONE);
265-
if (roundTripsTo(magnitude, candidate, powerOfTenExponent + digitCount - lastKept)) {
266-
roundAt = lastKept + 1;
267-
}
268-
} else if (decimalPoint >= digitCount
269-
|| (digits.charAt(15) == '0'
270-
&& digits.charAt(14) == '0'
271-
&& digits.charAt(13) == '0')) {
272-
int lastKept = 13;
273-
while (lastKept > 0 && digits.charAt(lastKept - 1) == '0') {
274-
lastKept--;
275-
}
276-
BigInteger candidate = new BigInteger(digits.substring(0, lastKept));
277-
if (roundTripsTo(magnitude, candidate, powerOfTenExponent + digitCount - lastKept)) {
278-
roundAt = lastKept + 1;
279-
}
280-
}
281-
282-
char[] rounded = new char[digitCount + 1];
283-
digits.getChars(0, digitCount, rounded, 1);
284-
int offset = 1;
285-
int count = roundAt;
286-
if (rounded[offset + roundAt] >= '5') {
287-
int index = offset + roundAt - 1;
288-
while (true) {
289-
rounded[index]++;
290-
if (rounded[index] <= '9') {
291-
break;
292-
}
293-
rounded[index] = '0';
294-
if (index == offset) {
295-
offset--;
296-
rounded[offset] = '1';
297-
count++;
298-
decimalPoint++;
299-
break;
300-
}
301-
index--;
302-
}
303-
}
304-
digits = new String(rounded, offset, count);
305-
}
306-
307-
int end = digits.length();
308-
while (end > 1 && digits.charAt(end - 1) == '0') {
309-
end--;
310-
}
311-
digits = digits.substring(0, end);
312-
313-
int decimalExponent = decimalPoint - 1;
314-
StringBuilder out = new StringBuilder();
315-
if (negative) {
316-
out.append('-');
317-
}
318-
if (decimalExponent < -4 || decimalExponent > 16) {
319-
out.append(digits.charAt(0)).append('.');
320-
int fractionEnd = digits.length();
321-
while (fractionEnd > 1 && digits.charAt(fractionEnd - 1) == '0') {
322-
fractionEnd--;
323-
}
324-
if (fractionEnd > 1) {
325-
out.append(digits, 1, fractionEnd);
326-
} else {
327-
out.append('0');
328-
}
329-
out.append('e');
330-
out.append(decimalExponent < 0 ? '-' : '+');
331-
int absoluteExponent = Math.abs(decimalExponent);
332-
if (absoluteExponent >= 100) {
333-
out.append((char) ('0' + absoluteExponent / 100));
334-
}
335-
out.append((char) ('0' + (absoluteExponent / 10) % 10));
336-
out.append((char) ('0' + absoluteExponent % 10));
337-
} else {
338-
int integerPlaces = decimalPoint - 1;
339-
int fractionPrecision = 16 - decimalExponent;
340-
StringBuilder fraction = new StringBuilder();
341-
if (integerPlaces < 0) {
342-
out.append('0');
343-
int leadingZeros = Math.min(-1 - integerPlaces, fractionPrecision);
344-
fraction.append("0".repeat(leadingZeros));
345-
fractionPrecision -= leadingZeros;
346-
fraction.append(digits, 0, Math.min(digits.length(), fractionPrecision));
347-
} else {
348-
int integerDigits = Math.min(integerPlaces + 1, digits.length());
349-
out.append(digits, 0, integerDigits);
350-
out.append("0".repeat(Math.max(0, integerPlaces + 1 - integerDigits)));
351-
int significantDigits = Math.min(digits.length() - integerDigits, fractionPrecision);
352-
if (significantDigits > 0) {
353-
fraction.append(digits, integerDigits, integerDigits + significantDigits);
354-
}
355-
}
356-
int fractionEnd = fraction.length();
357-
while (fractionEnd > 0 && fraction.charAt(fractionEnd - 1) == '0') {
358-
fractionEnd--;
359-
}
360-
out.append('.');
361-
if (fractionEnd == 0) {
362-
out.append('0');
363-
} else {
364-
out.append(fraction, 0, fractionEnd);
365-
}
366-
}
367-
return out.toString();
368-
}
369-
370-
private static boolean roundTripsTo(double magnitude, BigInteger digits, int powerOfTen) {
371-
double candidate = new BigDecimal(digits).scaleByPowerOfTen(powerOfTen).doubleValue();
372-
return magnitude == candidate;
373-
}
374-
375-
private static int fpPowerOfTenToTwo(int power) {
376-
return (power * 108853) >> 15;
377-
}
378-
379-
private static int fpPowerOfTwoToTen(int power) {
380-
return (power * 78913) >> 18;
381-
}
382-
383-
private static long fpPowerOfTen(int power) {
384-
int[] nextBits = new int[1];
385-
if (power < 0) {
386-
if (power == -1) {
387-
nextBits[0] = FP_POWER_SCALE_LOW[13];
388-
return FP_POWER_SCALES[13];
389-
}
390-
int group = power / 27;
391-
int index = power % 27;
392-
if (index != 0) {
393-
group--;
394-
index += 27;
395-
}
396-
long scale = FP_POWER_SCALES[group + 13];
397-
if (index == 0) {
398-
nextBits[0] = FP_POWER_SCALE_LOW[group + 13];
399-
return scale;
400-
}
401-
return fpNormalizeHigh(
402-
fpMultiplyWide(
403-
scale, FP_POWER_SCALE_LOW[group + 13], FP_POWERS_OF_TEN[index], nextBits),
404-
nextBits);
405-
}
406-
if (power < 27) {
407-
nextBits[0] = 0;
408-
return FP_POWERS_OF_TEN[power];
409-
}
410-
int group = power / 27;
411-
int index = power % 27;
412-
long scale = FP_POWER_SCALES[group + 13];
413-
if (index == 0) {
414-
nextBits[0] = FP_POWER_SCALE_LOW[group + 13];
415-
return scale;
416-
}
417-
return fpNormalizeHigh(
418-
fpMultiplyWide(scale, FP_POWER_SCALE_LOW[group + 13], FP_POWERS_OF_TEN[index], nextBits),
419-
nextBits);
420-
}
421-
422-
private static long fpNormalizeHigh(long high, int[] nextBits) {
423-
if (high >= 0) {
424-
high = (high << 1) | ((nextBits[0] >>> 31) & 1);
425-
nextBits[0] = (nextBits[0] << 1) | 1;
426-
}
427-
return high;
428-
}
429-
430-
private static long fpMultiplyWide(long high, int low, long multiplier, int[] resultLow) {
431-
BigInteger left = fpUnsigned(high).shiftLeft(32).or(BigInteger.valueOf(low & 0xffffffffL));
432-
BigInteger product = left.multiply(fpUnsigned(multiplier));
433-
resultLow[0] = product.shiftRight(64).intValue();
434-
return product.shiftRight(96).longValue();
435-
}
436-
437-
private static long fpMultiplyHigh(long left, long right) {
438-
return fpUnsigned(left).multiply(fpUnsigned(right)).shiftRight(64).longValue();
439-
}
440-
441-
private static BigInteger fpUnsigned(long value) {
442-
BigInteger unsigned = BigInteger.valueOf(value);
443-
return value < 0 ? unsigned.add(BigInteger.ONE.shiftLeft(64)) : unsigned;
444-
}
445-
446186
/**
447187
* Converts every storage class through {@link #getInt(int)}, matching sqlite-jdbc. NULL and values
448188
* whose integer coercion is zero are false: non-numeric text such as {@code "true"}, REAL values
@@ -638,7 +378,7 @@ public byte[] getBytes(int columnIndex) throws SQLException {
638378
case Value.Blob b -> b.bytes();
639379
case Value.Text t -> t.value().getBytes(java.nio.charset.StandardCharsets.UTF_8);
640380
case Value.Integer i -> Long.toString(i.value()).getBytes(java.nio.charset.StandardCharsets.UTF_8);
641-
case Value.Float f -> sqliteRealText(f.value()).getBytes(java.nio.charset.StandardCharsets.UTF_8);
381+
case Value.Float f -> SqliteRealText.format(f.value()).getBytes(java.nio.charset.StandardCharsets.UTF_8);
642382
};
643383
}
644384

0 commit comments

Comments
 (0)