chore(release): prepare 0.2.0 (QQQ 4.1, Forge sync fix) #29

Merged
james.maes merged 3 commits from chore/release-0.2.0-20261106 into main 2026-11-06 15:32:35 +00:00
7 changed files with 145 additions and 18 deletions

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
# Maven
target/
.flattened-pom.xml
*.jar
*.war
*.ear
+33
View File
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.0] - 2026-11-06
Published as `com.kingsrook.qbits:qbit-geo-data:0.2.0` on the QRun-IO Gitea Maven
registry. Requires Java 21 and QQQ 4.1 (built against `qqq-bom-pom` 4.1.0-RC.21).
This is the first release that works on QQQ 4.1; 0.1.0 was built against QQQ 0.35.0.
### Changed
- Build against the published QQQ 4.1 core (`qqq-bom-pom` 4.1.0-RC.21) instead of 0.35.0.
- `GeoDataQBitProducer.VERSION` is `0.2.0`.
- Import the project's `junit-bom` 5.10.0 ahead of the QQQ BOM so every JUnit artifact
converges on 5.10.0 and Surefire discovers the tests.
- CI runs on Gitea through the shared `KofTwentyTwo/ci-workflows` Maven workflow
(`main` verifies; a `vX.Y.Z` tag publishes to the Gitea registry).
### Fixed
- `Country`, `StateProvince` and `City` ask for their tables (`produceTableMetaData`)
and expose the setters `QRecordEntity` needs to discover fields, so the producer
creates `<prefix>_country`, `<prefix>_stateProvince` and `<prefix>_city` on core 4.1
instead of zero tables. The producer tolerates a table without fields.
- `GeoDataSyncStep` resolves the bundled `countryAlpha2` / `stateCode` source codes to
the persisted `countryId` / `stateProvinceId` before matching, so states and cities
are persisted (0.1.0 reported success after writing 250 countries and zero states or
cities because those source columns do not exist in the schema).
- Cities match on `stateProvinceId`, `name`, `latitude` and `longitude` at the schema's
seven-decimal precision; city names are not unique within a subdivision.
- Numeric comparisons use `BigDecimal` value equality, so a second run reports zero
inserts, updates or deactivations.
- An inactive existing row whose source entry returns is reactivated.
- Insert and update results are checked; a row rejected by the backend fails the sync
with a `QException` instead of reporting success.
## [0.1.0] - 2025-12-28
### Added
- Multi-module structure (qbit-geo-data-core + tools)
- Country, StateProvince, City entities with sync process
+19 -2
View File
@@ -7,16 +7,33 @@ Geographic reference data QBit providing countries, states/provinces, and cities
## Quick Start
The current release is **0.2.0**. It requires Java 21 and QQQ 4.1 (built against
`qqq-bom-pom` 4.1.0-RC.21). See [CHANGELOG.md](CHANGELOG.md).
Add the dependency to your `pom.xml`:
```xml
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-geo-data-core</artifactId>
<version>${qbit-geo-data.version}</version>
<artifactId>qbit-geo-data</artifactId>
<version>0.2.0</version>
</dependency>
```
Releases are published to the QRun-IO Gitea Maven registry, which requires
authentication for reads as well as writes. Add the repository and a matching
`<server>` entry (with your own Gitea username and token) in your private Maven
`settings.xml`:
```xml
<repository>
<id>qrun-io</id>
<url>https://git.kof22.com/api/packages/QRun-IO/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
```
## What You Get
This QBit provides three entities with pre-populated reference data:
+30 -1
View File
@@ -50,7 +50,7 @@
</modules>
<properties>
<revision>0.2.0-SNAPSHOT</revision>
<revision>0.2.0</revision>
<qqq.version>4.1.0-RC.21</qqq.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
@@ -92,6 +92,35 @@
</dependencyManagement>
<build>
<plugins>
<!-- Resolve the CI-friendly ${revision} in the installed and deployed POMs so -->
<!-- consumers of the published artifacts can resolve the parent chain. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.7.3</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten-clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
@@ -28,7 +28,7 @@ public class GeoDataQBitProducer implements QBitProducer
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-geo-data";
public static final String VERSION = "0.1.0";
public static final String VERSION = "0.2.0";
private GeoDataQBitConfig config;
@@ -4,13 +4,15 @@
** Syncs all three entity types (Country, StateProvince, City) from bundled
** JSON files into database tables. Performs natural key upsert:
** - Country: by alpha2Code
** - StateProvince: by countryAlpha2 + code
** - City: by countryAlpha2 + stateCode + name
** - StateProvince: by countryId + code (countryAlpha2 resolved to the persisted country)
** - City: by stateProvinceId + name + latitude + longitude (codes resolved to the persisted state)
*******************************************************************************/
package com.kingsrook.qbits.geodata.sync;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
@@ -62,9 +64,39 @@ public class GeoDataSyncStep implements BackendStep
// Sync in order: countries first (no dependencies), then states, then //
// cities (which may reference states) //
//////////////////////////////////////////////////////////////////////////
int countriesInserted = syncTable(prefix + "_country", "/data/countries.json", List.of("alpha2Code"));
int statesInserted = syncTable(prefix + "_stateProvince", "/data/states.json", List.of("countryAlpha2", "code"));
int citiesInserted = syncTable(prefix + "_city", "/data/cities.json", List.of("countryAlpha2", "stateCode", "name"));
int countriesInserted = syncTable(prefix + "_country", loadJsonData("/data/countries.json"), List.of("alpha2Code"));
Map<String, QRecord> countries = queryExisting(prefix + "_country", List.of("alpha2Code"));
List<QRecord> states = loadJsonData("/data/states.json");
for(QRecord state : states)
{
QRecord country = countries.get(state.getValueString("countryAlpha2"));
if(country == null)
{
throw new QException("Geo Data state references an unavailable country");
}
state.setValue("countryId", country.getValue("id"));
state.getValues().remove("countryAlpha2");
}
int statesInserted = syncTable(prefix + "_stateProvince", states, List.of("countryId", "code"));
Map<String, QRecord> persistedStates = queryExisting(prefix + "_stateProvince", List.of("countryId", "code"));
List<QRecord> cities = loadJsonData("/data/cities.json");
for(QRecord city : cities)
{
QRecord country = countries.get(city.getValueString("countryAlpha2"));
QRecord state = country == null ? null : persistedStates.get(country.getValue("id") + "|" + city.getValueString("stateCode"));
if(state == null)
{
throw new QException("Geo Data city references an unavailable state");
}
city.setValue("stateProvinceId", state.getValue("id"));
for(String coordinate : List.of("latitude", "longitude"))
{
city.setValue(coordinate, new BigDecimal(city.getValue(coordinate).toString()).setScale(7, RoundingMode.HALF_UP));
}
city.getValues().remove("countryAlpha2");
city.getValues().remove("stateCode");
}
int citiesInserted = syncTable(prefix + "_city", cities, List.of("stateProvinceId", "name", "latitude", "longitude"));
LOG.info("Geo data sync complete",
logPair("prefix", prefix),
@@ -83,17 +115,15 @@ public class GeoDataSyncStep implements BackendStep
** Sync a single table from JSON resource.
** Returns count of records processed.
*******************************************************************************/
private int syncTable(String tableName, String resourcePath, List<String> naturalKeyFields) throws QException
private int syncTable(String tableName, List<QRecord> sourceRecords, List<String> naturalKeyFields) throws QException
{
LOG.info("Syncing table",
logPair("table", tableName),
logPair("resource", resourcePath),
logPair("naturalKey", naturalKeyFields));
/////////////////////////////////////////////////////////////////////////
// 1. Load source data from classpath JSON //
/////////////////////////////////////////////////////////////////////////
List<QRecord> sourceRecords = loadJsonData(resourcePath);
LOG.info("Loaded source data", logPair("count", sourceRecords.size()));
/////////////////////////////////////////////////////////////////////////
@@ -119,7 +149,7 @@ public class GeoDataSyncStep implements BackendStep
source.setValue("isActive", true);
toInsert.add(source);
}
else if(hasChanges(source, existing))
else if(hasChanges(source, existing) || !Boolean.TRUE.equals(existing.getValueBoolean("isActive")))
{
source.setValue("id", existing.getValue("id"));
source.setValue("isActive", true);
@@ -182,7 +212,7 @@ public class GeoDataSyncStep implements BackendStep
key.append("|");
}
Object value = record.getValue(field);
key.append(value != null ? value.toString() : "");
key.append(value instanceof Number ? new BigDecimal(value.toString()).stripTrailingZeros().toPlainString() : value != null ? value.toString() : "");
}
return key.toString();
}
@@ -265,7 +295,14 @@ public class GeoDataSyncStep implements BackendStep
}
Object sourceValue = source.getValue(fieldName);
Object existingValue = existing.getValue(fieldName);
if(!Objects.equals(sourceValue, existingValue))
if(sourceValue instanceof Number && existingValue instanceof Number)
{
if(new BigDecimal(sourceValue.toString()).compareTo(new BigDecimal(existingValue.toString())) != 0)
{
return true;
}
}
else if(!Objects.equals(sourceValue, existingValue))
{
return true;
}
@@ -283,7 +320,7 @@ public class GeoDataSyncStep implements BackendStep
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
insertInput.setRecords(records);
new InsertAction().execute(insertInput);
requireSuccess(tableName, records.size(), new InsertAction().execute(insertInput).getRecords());
}
@@ -296,6 +333,16 @@ public class GeoDataSyncStep implements BackendStep
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(tableName);
updateInput.setRecords(records);
new UpdateAction().execute(updateInput);
requireSuccess(tableName, records.size(), new UpdateAction().execute(updateInput).getRecords());
}
/*******************************************************************************
** Do not report a successful sync when a native action rejected rows.
*******************************************************************************/
private void requireSuccess(String tableName, int expected, List<QRecord> records) throws QException
{
if(records == null || records.size() != expected || records.stream().anyMatch(record -> !record.getErrors().isEmpty()))
{
throw new QException("Geo Data persistence failed for table " + tableName);
}
}
}
@@ -165,6 +165,6 @@ class GeoDataQBitProducerTest
{
assertThat(GeoDataQBitProducer.GROUP_ID).isEqualTo("com.kingsrook.qbits");
assertThat(GeoDataQBitProducer.ARTIFACT_ID).isEqualTo("qbit-geo-data");
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.1.0");
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.2.0");
}
}