Skip to content

Commit e41cac1

Browse files
authored
feat(jdbc): comply with JDBC 4.3 Appendix B type mapping (#247)
## Why The driver mapped result values by storage class, so the declared type did not control metadata or `getObject`, and `setObject` ignored the target type. JDBC 4.3 Appendix B is the oracle. Seven stacked pull requests implement it. The stack cannot land link by link because the `main` ruleset requires the full build green at every head, and the Appendix B oracle is red until the last link. This PR lands the verified tip as one commit. ## What changed - Add the Appendix B oracle suite and the vendor table, then resolve one JDBC type per declared column for metadata and `getObject`. - Honor the `setObject` target type, add the missing getters and LOB wrappers, and support the offset temporal types. - Mark `ARRAY`, `STRUCT`, `REF`, `JAVA_OBJECT`, and `DISTINCT` N/A and reject unlisted Table B-6 conversions. - Publish the compliance profile, quarantine the four xerial tests that conflict with the spec, and record the policy. ## Scope The seven stacked PRs are #239, #240, #241, #242, #243, #244, and #245. No unrelated changes. `vendor/JDBC4-TYPE-MAPPING.md` is the oracle. ## Tradeoffs - The five unrepresentable types throw instead of emulating them. - The conformance quarantine is method-level. The lost checks are named in `libsql-conformance/README.md`. - An expression column with no `decltype` keeps the storage fallback, documented as a profile exception. ## Blast Radius Metadata and `getObject` now follow the declared type, so a `BIGINT` holding 42 returns `Long`, and an unlisted getter conversion throws. The default build was red from #239 through #245 by design and is green at this tip. ## Verification - `clean verify` at `0861960`: 2,006 tests, 0 failures, 7 skipped, about 63 seconds. - Appendix B unit 1,050 of 1,050, integration 148 of 148, conformance 270 of 270 with 7 skipped. - Clean clone at the same SHA: unit 1,510 of 1,510, integration 148 of 148. - Each link has a verdict and ten live lanes. The verdict comments are on #239 through #245.
1 parent c4357d2 commit e41cac1

28 files changed

Lines changed: 5073 additions & 282 deletions

‎AGENTS.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ DOCKER_CONFIG="$PWD/libsql-conformance/src/test/docker-config" \
134134

135135
Use the absolute path shown. A relative `DOCKER_CONFIG` is resolved against the fork's working directory, not the shell's.
136136

137+
### JDBC 4.3 Appendix B policy
138+
139+
JDBC 4.3 Appendix B is the oracle for type mapping. The source is [`vendor/JDBC4-TYPE-MAPPING.md`](vendor/JDBC4-TYPE-MAPPING.md).
140+
141+
- When Appendix B and sqlite-jdbc disagree, Appendix B wins. A conformance test that encodes the xerial behavior is quarantined in `libsql-conformance`, not copied.
142+
- Keep sqlite-jdbc parity only where Appendix B is silent. Parity never overrides a defined Appendix B conversion.
143+
- `ARRAY`, `STRUCT`, `REF`, `JAVA_OBJECT`, and `DISTINCT` are N/A. The driver throws `SQLFeatureNotSupportedException` and does not emulate them.
144+
- An expression column has no declared type. Its storage-class fallback is a documented exception in [`docs/jdbc-appendix-b-compliance.md`](docs/jdbc-appendix-b-compliance.md), not a supported Appendix B row.
145+
146+
The compliance profile is [`docs/jdbc-appendix-b-compliance.md`](docs/jdbc-appendix-b-compliance.md). Run the commands in [`README.md`](README.md) before changing a type mapping.
147+
137148
---
138149

139150
## 5. Coding Standards & Conventions

‎README.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,41 @@ mise exec -- ./mvnw clean verify
8484
mise exec -- ./mvnw spotless:apply
8585
```
8686

87+
### JDBC 4.3 Appendix B checks
88+
89+
Run the Appendix B unit suite.
90+
91+
```bash
92+
./mvnw -q -pl libsql-jdbc -Dtest='Jdbc4AppendixB*' -Dsurefire.failIfNoSpecifiedTests=false test
93+
```
94+
95+
Run the full `libsql-jdbc` unit suite.
96+
97+
```bash
98+
./mvnw -q -pl libsql-jdbc test
99+
```
100+
101+
Run the Appendix B server suite. The install step puts the current driver in the local repository. Under the nono sandbox, the Docker config path is required.
102+
103+
```bash
104+
./mvnw -q -DskipTests install -pl libsql-hrana-codec,libsql-client,libsql-jdbc
105+
DOCKER_CONFIG="$PWD/libsql-conformance/src/test/docker-config" ./mvnw -pl libsql-itests -Dit.test=Jdbc4AppendixBTypeMappingIT -Dtest=NoSuchTest -Dsurefire.failIfNoSpecifiedTests=false verify
106+
```
107+
108+
Run the xerial conformance suite.
109+
110+
```bash
111+
./mvnw -pl libsql-conformance -am verify
112+
```
113+
114+
Run the full gate.
115+
116+
```bash
117+
DOCKER_CONFIG="$PWD/libsql-conformance/src/test/docker-config" ./mvnw clean verify
118+
```
119+
120+
The compliance profile is in [`docs/jdbc-appendix-b-compliance.md`](docs/jdbc-appendix-b-compliance.md).
121+
87122
## Contributing
88123

89124
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines and development workflow, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community standards.

‎docs/jdbc-appendix-b-compliance.md‎

Lines changed: 245 additions & 0 deletions
Large diffs are not rendered by default.

‎libsql-conformance/README.md‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,32 @@ with:
4747
./mvnw -pl libsql-conformance -am verify
4848
```
4949

50-
Last green result: 272 tests, 7 skipped, with `SavepointTest` included. The
51-
parity queue (#54) closed at 268 tests, 7 skipped.
50+
Last green result: 270 tests, 7 skipped, with `SavepointTest` included. The
51+
count excludes the four Appendix B quarantines below and includes the two
52+
harness tests for the quarantine.
53+
54+
### Appendix B quarantines
55+
56+
JDBC 4.3 Appendix B is the oracle. Four selected xerial test methods assert
57+
behavior that Appendix B replaces. They stay in the vendored tree and still
58+
run in the `xerial-control` profile, but `XerialQuarantine` excludes them from
59+
the libSQL run. The full type profile is in
60+
[`../docs/jdbc-appendix-b-compliance.md`](../docs/jdbc-appendix-b-compliance.md).
61+
62+
| Test | Why the xerial expectation is quarantined |
63+
|---|---|
64+
| `org.sqlite.StatementTest#bytesTest` | xerial allows `getBytes` on a declared `BLOB`. Table B-6 lists `getBytes` only for `BINARY`, `VARBINARY`, and `LONGVARBINARY`. |
65+
| `org.sqlite.RSMetaDataTest#columnTypes` | xerial reports a declared `NCHAR` as `Types.CHAR`. Appendix B defines `NCHAR` as its own JDBC type, so the driver reports `Types.NCHAR`. |
66+
| `org.sqlite.RSMetaDataTest#testGetColumnClassName` | xerial reports a declared `BLOB` as `java.lang.Object`. Table B-3 maps `BLOB` to `java.sql.Blob`. |
67+
| `org.sqlite.PrepStmtTest#getObject` | xerial returns an `Integer` for `getObject` on a declared `BIT`. Table B-3 maps `BIT` to `Boolean`. |
68+
69+
The exclusion is method-level, so each method also loses checks that do not
70+
conflict with Appendix B:
71+
72+
- `StatementTest#bytesTest` also loses its BLOB `setBytes`/`getBytes` round trip.
73+
- `RSMetaDataTest#columnTypes` also checks other column types that do not conflict, including the INTEGER family, text, numeric, boolean, date and timestamp types, plus DECIMAL precision and scale.
74+
- `RSMetaDataTest#testGetColumnClassName` also checks the INTEGER, VARCHAR, and DOUBLE class names.
75+
- `PrepStmtTest#getObject` also checks other `getObject` rows (INTEGER, FLOAT, untyped, VARCHAR and BLOB columns) and the typed getters above them.
5276

5377
### Three-layer contract
5478

‎libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialHarnessTest.java‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.util.concurrent.atomic.AtomicInteger;
1717
import org.junit.jupiter.api.Test;
1818
import org.junit.jupiter.api.io.TempDir;
19+
import org.junit.platform.engine.TestDescriptor;
20+
import org.junit.platform.engine.support.descriptor.MethodSource;
1921

2022
/**
2123
* Exercises the conformance harness itself, without a running sqld container where possible.
@@ -63,6 +65,54 @@ void fileMemoryUriIsNotRetained() {
6365
assertThat(memoryMode.retained()).isFalse();
6466
}
6567

68+
@Test
69+
void quarantineExcludesOnlyTheAppendixBConflicts() {
70+
XerialQuarantine quarantine = new XerialQuarantine();
71+
72+
for (XerialQuarantine.Quarantined quarantined : XerialQuarantine.QUARANTINED) {
73+
assertThat(
74+
quarantine
75+
.apply(
76+
descriptor(
77+
quarantined.className(),
78+
quarantined.methodName()))
79+
.excluded())
80+
.as("%s#%s", quarantined.className(), quarantined.methodName())
81+
.isTrue();
82+
}
83+
84+
assertThat(quarantine.apply(descriptor("org.sqlite.ResultSetTest", "next")).included())
85+
.as("an unconflicted xerial test")
86+
.isTrue();
87+
}
88+
89+
@Test
90+
void quarantineIsInactiveDuringTheXerialControlRun() {
91+
String previous = System.getProperty(XerialControl.ENABLED_PROPERTY);
92+
System.setProperty(XerialControl.ENABLED_PROPERTY, "xerial");
93+
try {
94+
XerialQuarantine quarantine = new XerialQuarantine();
95+
for (XerialQuarantine.Quarantined quarantined : XerialQuarantine.QUARANTINED) {
96+
assertThat(
97+
quarantine
98+
.apply(
99+
descriptor(
100+
quarantined.className(),
101+
quarantined.methodName()))
102+
.included())
103+
.as("control run keeps %s#%s", quarantined.className(),
104+
quarantined.methodName())
105+
.isTrue();
106+
}
107+
} finally {
108+
if (previous == null) {
109+
System.clearProperty(XerialControl.ENABLED_PROPERTY);
110+
} else {
111+
System.setProperty(XerialControl.ENABLED_PROPERTY, previous);
112+
}
113+
}
114+
}
115+
66116
@Test
67117
void queryStringIsPartOfTheFileNamespace() {
68118
XerialConformanceDriver.Target readOnly =
@@ -163,6 +213,19 @@ void stopReportsASwallowedCleanupFailure() {
163213
}
164214
}
165215

216+
private static TestDescriptor descriptor(String className, String methodName) {
217+
MethodSource source = MethodSource.from(className, methodName);
218+
return (TestDescriptor)
219+
Proxy.newProxyInstance(
220+
TestDescriptor.class.getClassLoader(),
221+
new Class<?>[] {TestDescriptor.class},
222+
(proxy, method, args) ->
223+
switch (method.getName()) {
224+
case "getSource" -> java.util.Optional.of(source);
225+
default -> null;
226+
});
227+
}
228+
166229
private static Connection wrap(Connection connection, String namespace) {
167230
return wrap(connection, namespace, ignored -> {});
168231
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.github.magnusp.libsql.conformance;
2+
3+
import java.util.List;
4+
import org.junit.platform.engine.FilterResult;
5+
import org.junit.platform.engine.TestDescriptor;
6+
import org.junit.platform.engine.support.descriptor.MethodSource;
7+
import org.junit.platform.launcher.PostDiscoveryFilter;
8+
9+
/**
10+
* Quarantines the xerial tests whose expectations Appendix B replaces.
11+
*
12+
* <p>These tests assert sqlite-jdbc behavior that JDBC 4.3 Appendix B defines differently. They
13+
* stay in the vendored tree and still run in the {@code xerial-control} profile, where the
14+
* reference driver is the subject. They are excluded only from the libSQL run, and the reason is
15+
* kept here so the exclusion cannot become an unexplained skip.
16+
*/
17+
public final class XerialQuarantine implements PostDiscoveryFilter {
18+
19+
/** One upstream test method whose xerial expectation conflicts with Appendix B. */
20+
record Quarantined(String className, String methodName, String reason) {}
21+
22+
static final List<Quarantined> QUARANTINED =
23+
List.of(
24+
new Quarantined(
25+
"org.sqlite.StatementTest",
26+
"bytesTest",
27+
"xerial allows getBytes on a declared BLOB; Table B-6 lists getBytes"
28+
+ " only for BINARY, VARBINARY and LONGVARBINARY"),
29+
new Quarantined(
30+
"org.sqlite.RSMetaDataTest",
31+
"columnTypes",
32+
"xerial reports a declared NCHAR as Types.CHAR; Appendix B defines"
33+
+ " NCHAR as its own JDBC type, so the driver reports Types.NCHAR"),
34+
new Quarantined(
35+
"org.sqlite.RSMetaDataTest",
36+
"testGetColumnClassName",
37+
"xerial reports a declared BLOB as java.lang.Object; Table B-3 maps"
38+
+ " BLOB to java.sql.Blob"),
39+
new Quarantined(
40+
"org.sqlite.PrepStmtTest",
41+
"getObject",
42+
"xerial returns an Integer for getObject on a declared BIT; Table B-3"
43+
+ " maps BIT to Boolean"));
44+
45+
@Override
46+
public FilterResult apply(TestDescriptor descriptor) {
47+
if (XerialControl.isEnabled()
48+
|| !(descriptor.getSource().orElse(null) instanceof MethodSource source)) {
49+
return FilterResult.included("Appendix B quarantine is inactive for this run");
50+
}
51+
for (Quarantined quarantined : QUARANTINED) {
52+
if (quarantined.className().equals(source.getClassName())
53+
&& quarantined.methodName().equals(source.getMethodName())) {
54+
return FilterResult.excluded(quarantined.reason());
55+
}
56+
}
57+
return FilterResult.included("not quarantined");
58+
}
59+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.github.magnusp.libsql.conformance.XerialQuarantine

0 commit comments

Comments
 (0)