Skip to content

refactor(jdbc): use Locale.ROOT for every case-folding call site - #285

Merged
magnusp merged 2 commits into
mainfrom
refactor/283-locale-root
Oct 31, 2026
Merged

magnusp merged 2 commits into
mainfrom
refactor/283-locale-root

Conversation

@magnusp

@magnusp magnusp commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

What changed

The three remaining Locale.ENGLISH case-folding call sites now use Locale.ROOT. Two pre-existing default-locale sites remain in LibsqlConnection.TransactionMode.from and MetadataSql, and they are tracked in #286. The production diff is three tokens. The java.util.Locale import stays at each site.

Probe result

A scratch Java probe compared String.toUpperCase and String.toLowerCase under Locale.ENGLISH and Locale.ROOT for every Unicode code point from U+0000 to U+10FFFF (surrogates excluded) and for the four call-site corpora: declared type names, foreign key rules, SQLite error codes and messages, and identifiers.

Result: 0 upper-case differences and 0 lower-case differences. The two locales are interchangeable for every input the driver can see, so the change alters no result.

Call sites

File Line Folding
libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/ColumnMetadata.java 123 declared type in columnShape
libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/DeclaredType.java 44 declared type name
libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/ForeignKeyMetadata.java 131 foreign key rule in keyRule

Test added

libsql-jdbc/src/test/java/io/github/magnusp/libsql/jdbc/LocaleFoldingTest.java, 36 tests:

  • A parameterized corpus over declared type names, foreign key rules, SQLite error codes and messages, and identifiers, asserting toUpperCase(Locale.ENGLISH) equals toUpperCase(Locale.ROOT) and the same for lower case. The corpus includes i, ı, İ, and ß.
  • A Turkish default locale pin (Locale.forLanguageTag("tr")) that drives all three call sites through their package-private seams and asserts the Locale.ROOT result: ColumnMetadata.columns(...) reads TYPE_NAME for a numeric pragma row, DeclaredType.of(new Col("c", "numeric")).typeName() is NUMERIC, and ForeignKeyMetadata.fromImported(...) maps cascade and set null rules. The default locale is restored in @AfterEach.

No existing assertion was edited. No public API, exception type, or message changed.

Verification lanes

Command Result
mise exec -- ./mvnw -pl libsql-jdbc -am test Pass, 1631 tests, 0 failures, 0 errors.
mise exec -- ./mvnw spotless:apply then mise exec -- ./mvnw spotless:check Pass.
mise exec -- ./mvnw -pl libsql-conformance -am verify Pass, 271 tests, 0 failures, 6 skipped.

Notes

Acceptance checklist

  • grep -rn "Locale.ENGLISH" libsql-jdbc/src/main returns nothing.
  • ./mvnw -pl libsql-jdbc -am test is green with no existing assertion edited.
  • ./mvnw spotless:check is green.
  • ./mvnw -pl libsql-conformance -am verify is green.
  • Public API, exception types, and messages are unchanged.

Closes #283
Refs #282

@magnusp
magnusp force-pushed the refactor/283-locale-root branch from 11dc29f to f569357 Compare October 31, 2026 17:27
@magnusp

magnusp commented Oct 31, 2026

Copy link
Copy Markdown
Owner Author

PASS+NOTES

Independent verification of the exact head commit, not the PR prose.

Identity

Item Value
Head SHA f5693573e1b02aa4645b7b45ea7a0ffd9e5da3b7
Base branch main
Base SHA 6893f3e8cdcb69171fd512502bf2bdfcc190d1c5
Merge base 6893f3e8cdcb69171fd512502bf2bdfcc190d1c5 (HEAD is a descendant of origin/main)
Stable patch id babccf63f93617faa2d47d18ac77b2dfbaae4c23
Worktree detached at head, clean before and after; nothing committed, pushed, rebased, or modified

What I ran

# Command Result
1 git fetch origin --prune; git rev-parse HEAD; git merge-base --is-ancestor origin/main HEAD HEAD matches; ancestor check exit 0
2 git diff origin/main...HEAD 4 files, +223/−3. Production diff is exactly three tokens (Locale.ENGLISH → Locale.ROOT) at ColumnMetadata.java:123, DeclaredType.java:44, ForeignKeyMetadata.java:131. Plus one new 220-line test file. No public API, exception type, or message change; all three classes are package-private and the edited expressions are internal
3 mise exec -- ./mvnw -pl libsql-jdbc -am test BUILD SUCCESS — 1631 tests, 0 failures, 0 errors, 0 skipped. LocaleFoldingTest: 36 tests
4 mise exec -- ./mvnw spotless:check BUILD SUCCESS — every module clean, 0 files need changes
5 mise exec -- ./mvnw -pl libsql-conformance -am verify BUILD SUCCESS — 271 tests, 0 failures, 0 errors, 6 skipped. Docker was available, so this lane actually ran rather than being blocked
6 mise exec java@corretto-26 -- ./mvnw -pl libsql-jdbc -am -Dtest=LocaleFoldingTest -Dsurefire.failIfNoSpecifiedTests=false test BUILD SUCCESS — 36 tests, 0 failures, on JVM 26.0.2.1 (Corretto 26.0.2.11.1). Note: the POM still targets release 25, so this runs the tests on a 26 JVM with 25 bytecode
7 git diff origin/main...HEAD | git patch-id --stable babccf63f93617faa2d47d18ac77b2dfbaae4c23
8 Inspect LocaleFoldingTest Drives all three real seams: ColumnMetadata.columns(raw, COLUMN_COLUMNS) reading TYPE_NAME, DeclaredType.of(new Col("c","numeric")).typeName(), and ForeignKeyMetadata.fromImported(raw, null, null, "child") reading UPDATE_RULE/DELETE_RULE, each under Locale.forLanguageTag("tr"). @BeforeEach captures the default locale and @AfterEach restores it, so the global locale is restored even when an assertion fails. The suite is sequential (no parallel/fork configuration), so the global Locale.setDefault does not race other tests

I also mutation-tested the new pin in a throwaway copy outside the worktree (the reviewed worktree was never touched): removing the locale argument at all three production sites produces two failures —

columnMetadataFoldsUnderATurkishDefaultLocale   expected: "NUMERIC" but was: "NUMERİC"
declaredTypeFoldsUnderATurkishDefaultLocale     expected: "NUMERIC" but was: "NUMERİC"

So the ColumnMetadata and DeclaredType pins are real behavior pins.

Notes

1. The foreign-key pin is not a behavior pin. In the same mutation run the FK test still passed. keyRule is exercised with cascade and set null, and neither string contains i or ı, so the Turkish default locale folds them identically to Locale.ROOT. A regression to default-locale folding at ForeignKeyMetadata.java:131 would go undetected. Adding restrict or no action to that case would close it: under a Turkish default those fold to RESTRİCT / NO ACTİON and would land in the default -> importedKeyNoAction branch. Suggest strengthening this case before or after merge.

2. Two case-folding sites still use the default locale, so the PR body's "22 case-folding sites, all Locale.ROOT" is not accurate. The PR and #283 scope is the three Locale.ENGLISH sites, and those are correctly changed. But these remain:

  • LibsqlConnection.java:99 — valueOf(mode.toUpperCase()) in TransactionMode.from. Reachable from the public LibsqlConnection.setTransactionMode(String).
  • MetadataSql.java:36 — types[i].toUpperCase() when building the getTables type filter.

Proven against the built classes with a Turkish default locale:

deferred   -> DEFERRED
immediate  -> THROWS IllegalArgumentException: Unknown transaction mode: immediate
exclusive  -> THROWS IllegalArgumentException: Unknown transaction mode: exclusive

This is pre-existing and outside the three-site scope of #283, so it does not block this PR, but it is the same class of bug the PR claims to have eliminated. Recommend a follow-up issue for both sites and a correction to the "one locale policy" claim in the description.

3. Test count in the PR body is stale. The body says 1610 tests; the head commit measures 1631, which is the 36 new tests plus the existing suite. Not a defect, just a description that no longer matches the head.

4. englishAndRootFoldIdentically is documentation, not a pin. Locale.ENGLISH and Locale.ROOT produce identical case mappings for every string, so that parameterized assertion cannot fail. It is harmless, but it should not be counted as evidence of the locale policy.

Verdict

The three-token change is correct, behavior-preserving, and green in every lane I could run, including the conformance suite and a Java 26 JVM. No public API, exception type, or message changed. The gaps are test-strength and description accuracy, not defects in the change. PASS+NOTES. No approval and no merge from me.

@magnusp
magnusp merged commit 576d21c into main Oct 31, 2026
8 of 9 checks passed
@magnusp
magnusp deleted the refactor/283-locale-root branch October 31, 2026 17:37
magnusp added a commit that referenced this pull request Oct 31, 2026
)

## Description

`TransactionMode.from` and `MetadataSql.tables` called `toUpperCase()`
without a locale. Under a Turkish default locale, `"immediate"` folded
to `"İMMEDİATE"`, so `TransactionMode.from("immediate")` threw
`IllegalArgumentException`, and the `VIEW` table-type filter folded to
`"VİEW"`, so `getTables(..., new String[] {"VIEW"})` matched nothing.

Both sites now use `Locale.ROOT`, matching the policy the rest of the
driver adopted in #283.

### Call sites

| File | Line | Folding |
|---|---:|---|
|
`libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/LibsqlConnection.java`
| 99 | transaction mode in `TransactionMode.from` |
|
`libsql-jdbc/src/main/java/io/github/magnusp/libsql/jdbc/MetadataSql.java`
| 36 | table type in `tables` |

### Tests

- `transactionModeFoldsUnderATurkishDefaultLocale` sets
`Locale.forLanguageTag("tr")` and asserts
`TransactionMode.from("immediate")` is `IMMEDIATE` and
`from("exclusive")` is `EXCLUSIVE`.
- `tableTypeFilterFoldsUnderATurkishDefaultLocale` builds the predicate
with `{"table", "view"}` under the same default and asserts `AND
TABLE_TYPE IN ('TABLE','VIEW')`.
- The existing foreign-key locale pin now uses `restrict` instead of
`set null`, so it is mutation-sensitive too (`set null` and `cascade`
contain no Turkish-sensitive letter). The `set null` mapping stays
covered by
`LibsqlDatabaseMetaDataTest.getImportedKeysMapsForeignKeyRowsAndNames`.

Both new tests fail before the fix and pass after it. Before the fix,
the transaction-mode test fails with `No enum constant ...İMMEDİATE`,
and the table-type test fails on the `VİEW` literal. Reverting the
`ForeignKeyMetadata` call site to the default locale makes the
strengthened FK pin fail.

No public API, exception type, or message changed. `Locale.ROOT` and
`Locale.ENGLISH` fold identically for these inputs, as probed in #285.

## Type of Change

- [x] Bug fix (non-breaking change fixing an issue)

## Checklist

- [x] My code follows the code style of this project (`./mvnw
spotless:check`).
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] All unit tests pass (`./mvnw -pl libsql-jdbc -am test`: 1622
tests, 0 failures). The container lane runs in CI.
- [x] I have updated documentation where appropriate (none needed).

Fixes #286
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(jdbc): use Locale.ROOT for every case-folding call site

1 participant