Compare commits
3 Commits
pipeline/f
...
seeding-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e476afdb | ||
|
|
d52dba904c | ||
|
|
fbcf438381 |
@@ -109,10 +109,10 @@ gender from the source data is preserved through to the sampled `Name`
|
||||
persona/bio generation later.
|
||||
|
||||
The full multinational dataset is kept as-is (106 countries for forenames,
|
||||
75 for surnames) rather than trimmed to `locations.json`'s current country
|
||||
75 for surnames) rather than trimmed to `cities.json`'s current country
|
||||
list, so it doesn't need re-sourcing if more countries are added later.
|
||||
`SampleName()` (a free helper in `generate_users.cc`) returns no result for
|
||||
a country present in neither file; of the countries in `locations.json`,
|
||||
a country present in neither file; of the countries in `cities.json`,
|
||||
that's currently `KE`, `SE`, `SG`, `TH`, `VN`, and `ZA` — `GenerateUsers`
|
||||
skips cities in those countries the same way brewery generation skips
|
||||
cities whose enrichment lookup fails.
|
||||
@@ -281,7 +281,7 @@ Output sample:
|
||||
a location's code is unlisted, skip `description_local` generation and fall
|
||||
back to English.
|
||||
- **Upstream sanitization:** strip known low-resource language codes from the
|
||||
`locations.json` payload before generation.
|
||||
`cities.json` payload before generation.
|
||||
- **Downstream flagging:** add a `description_local_confidence` column to the
|
||||
SQLite schema so downstream applications can filter or flag potentially
|
||||
hallucinated text by language tier.
|
||||
|
||||
@@ -86,7 +86,7 @@ curl -L \
|
||||
|
||||
### Run
|
||||
|
||||
Run from `build/` so the copied `locations.json` and `prompts/` are available.
|
||||
Run from `build/` so the copied `cities.json` and `prompts/` are available.
|
||||
Each run writes a fresh dated SQLite file such as
|
||||
`biergarten_seed_2026-04-19T15-30-45.123456Z.sqlite` into the working directory.
|
||||
|
||||
@@ -109,7 +109,7 @@ Each run writes a fresh dated SQLite file such as
|
||||
| `--prompt-dir` | Directory containing prompt files (e.g. `BREWERY_GENERATION.md`). Required unless `--mocked` is set. |
|
||||
| `--output, -o` | Directory for generated SQLite artifacts. Default: `output`. |
|
||||
| `--log-path` | Path for application logs. Default: `pipeline.log`. |
|
||||
| `--location-count` | Number of cities to sample from `locations.json` per run. Default: `10`. |
|
||||
| `--location-count` | Number of cities to sample from `cities.json` per run. Default: `10`. |
|
||||
| `--temperature` | Sampling temperature. Default: `1.0`. |
|
||||
| `--top-p` | Nucleus sampling. Default: `0.95`. |
|
||||
| `--top-k` | Top-k sampling. Default: `64`. |
|
||||
@@ -222,12 +222,12 @@ environment variables listed above to match your run.
|
||||
|
||||
| Stage | Implementation |
|
||||
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Load | `ICuratedDataService` (`CuratedJsonDataService`) reads `locations.json`, `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json` (paths supplied via a `CuratedDataFilePaths` DTO at construction) into typed records, caching each after its first load. `--mocked` runs use `MockCuratedDataService`'s fixed in-memory dataset instead. |
|
||||
| Load | `ICuratedDataService` (`CuratedJsonDataService`) reads `cities.json`, `personas.json`, `forenames-by-country.json`, and `surnames-by-country.json` (paths supplied via a `CuratedDataFilePaths` DTO at construction) into typed records, caching each after its first load. `--mocked` runs use `MockCuratedDataService`'s fixed in-memory dataset instead. |
|
||||
| Sample | `BiergartenPipelineOrchestrator::QueryCitiesWithCountries()` samples `--location-count` locations per run (default `10`). |
|
||||
| Enrich | `WikipediaEnrichmentService` fetches brewing and beer-related context. Keeps going when a lookup fails. `--mocked` runs use `MockEnrichmentService` instead and skip Wikipedia entirely. |
|
||||
| Generate Users | `GenerateUsers()` samples a persona and a forename/surname pair per enriched city (skipping countries with no name data), then `MockGenerator` or `LlamaGenerator` produces a username, bio, and activity weight around the sampled name. |
|
||||
| Generate Breweries | `MockGenerator` or `LlamaGenerator` produces brewery names and descriptions in English and the local language. |
|
||||
| Store | `SqliteExportService` writes each successful user and brewery into a fresh dated `.sqlite` database with normalized `locations`, `users`, and `breweries` tables. |
|
||||
| Store | `SqliteExportService` writes each successful user and brewery into a fresh dated `.sqlite` database with normalized `cities`, `users`, and `breweries` tables. |
|
||||
| Log | `spdlog` writes results and warnings to the console. |
|
||||
|
||||
If name sampling, enrichment, or generation fails for a city, that city is
|
||||
@@ -241,12 +241,27 @@ skipped and the pipeline continues. `GenerateUsers()` runs before
|
||||
`CuratedDataFilePaths` DTO (locations/personas/forenames/surnames paths) in
|
||||
its constructor, then parses and validates curated location, persona, and
|
||||
forename/surname JSON, memoizing each result after its first load on a
|
||||
given instance. `MockCuratedDataService` is the in-memory substitute (4
|
||||
fixed locations, 3 personas, and name data for `US`/`DE`/`FR`/`BE`) used in
|
||||
`--mocked` runs.
|
||||
given instance. Each `cities.json` entry's `postal_code.city_regex` and
|
||||
`postal_code.examples` are parsed into `City::postal_regex` and
|
||||
`City::postal_code_examples`. `MockCuratedDataService` is the in-memory
|
||||
substitute (4 fixed locations, 3 personas, and name data for
|
||||
`US`/`DE`/`FR`/`BE`) used in `--mocked` runs, and carries matching
|
||||
`postal_regex`/`postal_code_examples` values for its 4 locations.
|
||||
- `WikipediaEnrichmentService` — queries Wikipedia extracts, caches results,
|
||||
returns empty context on failure. `MockEnrichmentService` is the no-op
|
||||
substitute used in `--mocked` runs.
|
||||
- `IPostalCodeService` — generates a postal code for a `City`, consumed by
|
||||
`GenerateBreweries()` and stored on `BreweryRecord::address` (an `Address`
|
||||
struct, currently just `postal_code`, mirroring the web backend's
|
||||
`BreweryPostLocation`). Only `MockPostalCodeService` exists today, which
|
||||
ignores `postal_regex` and returns `postal_code_examples.front()` — it's
|
||||
wired into the Boost.DI graph unconditionally (no `--mocked` branch yet,
|
||||
since there's no real implementation to switch to). A real implementation
|
||||
still needs a **xeger**-style generator — turning a `postal_regex` pattern
|
||||
into a random matching string — instead of always replaying a fixed
|
||||
example; see [ROADMAP.md §9](./ROADMAP.md#9-postal-code-generation).
|
||||
Street-address generation (`Address::address_line1`) has no fixture data or
|
||||
service yet and remains future work.
|
||||
- `LlamaGenerator` — formats prompts for Gemma 4, validates JSON output for
|
||||
both `GenerateBrewery` and `GenerateUser`, retries malformed responses up
|
||||
to three times with corrective feedback in the retry prompt. The token
|
||||
@@ -279,14 +294,14 @@ valid output on the first pass; the retry path is kept for resilience.
|
||||
`MockGenerator` uses stable hashes for repeatable output in demos and Storybook
|
||||
runs.
|
||||
|
||||
`CuratedJsonDataService` memoizes each of `LoadLocations()`, `LoadPersonas()`,
|
||||
`CuratedJsonDataService` memoizes each of `LoadCities()`, `LoadPersonas()`,
|
||||
`LoadForenamesByCountry()`, and `LoadSurnamesByCountry()` independently the
|
||||
first time each is called, since `BiergartenPipelineOrchestrator` owns a
|
||||
single `ICuratedDataService` instance for the whole run — later calls return
|
||||
the cached result instead of re-parsing.
|
||||
|
||||
`GenerateUsers()` samples a forename/surname pair per city via `SampleName()`,
|
||||
keyed by the city's ISO 3166-1 code. Countries present in `locations.json`
|
||||
keyed by the city's ISO 3166-1 code. Countries present in `cities.json`
|
||||
but absent from either name fixture (currently `KE`, `SE`, `SG`, `TH`, `VN`,
|
||||
`ZA`) are skipped, the same way a failed enrichment or generation call skips
|
||||
a city — see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
|
||||
@@ -303,9 +318,9 @@ a city — see ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section.
|
||||
|
||||
## Generated Output
|
||||
|
||||
Each successful run stores a `BreweryRecord` pair with the source location
|
||||
and a `BreweryResult` payload, and a `UserRecord` pair with the source
|
||||
location and a `UserResult` payload. The same generated records are also
|
||||
Each successful run stores a `BreweryRecord` (source `City`, an `Address`,
|
||||
and a `BreweryResult` payload), and a `UserRecord` pair with the source
|
||||
`City` and a `UserResult` payload. The same generated records are also
|
||||
written to a fresh SQLite export file named with the current UTC timestamp.
|
||||
|
||||
| Field | Meaning |
|
||||
@@ -314,6 +329,7 @@ written to a fresh SQLite export file named with the current UTC timestamp.
|
||||
| `description_en` | Brewery description in English. |
|
||||
| `name_local` | Brewery name in the local language. |
|
||||
| `description_local` | Brewery description in the local language. |
|
||||
| `postal_code` | Postal code generated for the brewery's city (see `IPostalCodeService`, above). |
|
||||
|
||||
| Field | Meaning |
|
||||
| ----------------- | ----------------------------------------------------------------- |
|
||||
@@ -339,6 +355,7 @@ code, latitude, and longitude for each entry.
|
||||
| `local_languages` | Locale-aware copy selection |
|
||||
| `name_en`, `description_en` | Default English display content |
|
||||
| `name_local`, `description_local` | Local-language display content |
|
||||
| `postal_code` | Brewery address matching, mirrors the web backend's `BreweryPostLocation.PostalCode` |
|
||||
|
||||
---
|
||||
|
||||
@@ -407,7 +424,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
`MockCuratedDataService` swaps in for `CuratedJsonDataService`, so no
|
||||
fixture files need to be present on disk.
|
||||
- `--model` when geographically grounded content matters for demos.
|
||||
- Keep `locations.json` structured enough to support discovery and future
|
||||
- Keep `cities.json` structured enough to support discovery and future
|
||||
filtering.
|
||||
- `personas.json`, `forenames-by-country.json`, and
|
||||
`surnames-by-country.json` are curated/vendored fixture data, not
|
||||
@@ -424,7 +441,7 @@ Silicon; CUDA or HIP/ROCm is detected on Linux when the toolkit is present.
|
||||
| -------------------------------------- | -------------------------------------------------- |
|
||||
| `tooling/pipeline/includes/` | Public headers and shared models. |
|
||||
| `tooling/pipeline/src/` | Implementation files. |
|
||||
| `tooling/pipeline/locations.json` | Curated city input copied into the build tree. |
|
||||
| `tooling/pipeline/cities.json` | Curated city input copied into the build tree. |
|
||||
| `tooling/pipeline/personas.json` | Curated user persona archetypes copied into the build tree. |
|
||||
| `tooling/pipeline/forenames-by-country.json` | Vendored (CC0) forename data by ISO 3166-1 country code. |
|
||||
| `tooling/pipeline/surnames-by-country.json` | Vendored (CC0) surname data by ISO 3166-1 country code. |
|
||||
@@ -446,6 +463,10 @@ Paths below are relative to `tooling/pipeline/`.
|
||||
`ICuratedDataService`, and `MockCuratedDataService`, the in-memory
|
||||
`ICuratedDataService` used in `--mocked` runs.
|
||||
- `src/services/enrichment/wikipedia/` — enrichment service and cache.
|
||||
- `includes/services/postal_code/` — `IPostalCodeService` and
|
||||
`MockPostalCodeService` (header-only), consumed by `GenerateBreweries()`.
|
||||
The real xeger-based implementation and its `--mocked`-aware DI wiring are
|
||||
still to come.
|
||||
- `src/services/sqlite/` — SQLite export implementation.
|
||||
- `src/data_generation/llama/` — local inference, prompt loading, output
|
||||
validation.
|
||||
|
||||
@@ -54,7 +54,9 @@ architecture needs.
|
||||
(present per-forename in the source data) is never discarded the way a
|
||||
pre-flattened `{first_name, last_name}` list would lose it; see §2.
|
||||
- [ ] Extend `GeneratedBrewery` with `brewery_id`, `context_completeness`,
|
||||
`metadata` (currently just `{location, brewery}`).
|
||||
`metadata` (currently `{location, address, brewery}` — see the
|
||||
`Location`→`City` rename and `Address` struct added in the
|
||||
`postal_regex`/§9 entries below).
|
||||
- [ ] Add `GeneratedBeer`, `GeneratedCheckin`, `GeneratedRating`,
|
||||
`GeneratedFollow` aggregate structs.
|
||||
- [x] Add `UserRecord` (`location`, `user : UserResult`, `email`,
|
||||
@@ -70,6 +72,30 @@ architecture needs.
|
||||
holding `generated_users_` in memory. Still missing vs. the planned
|
||||
`GeneratedUser`: `user_id`, `password`, and `metadata`.
|
||||
- [x] Add `UserPersona` (`name`, `description`, `style_affinities`).
|
||||
- [x] Add `postal_regex` (`std::vector<std::string>`) and
|
||||
`postal_code_examples` (`std::vector<std::string>`) to `Location`
|
||||
(now `City` — see below), sourced from each `cities.json` entry's
|
||||
`postal_code.city_regex` / `postal_code.examples`. Not part of the
|
||||
originally planned `Location` shape in `diagrams/planned/class.puml` —
|
||||
added ahead of postal code generation work; see §9.
|
||||
- [x] Renamed `Location` → `City` throughout the pipeline (struct, all
|
||||
parameter/return types, `LocationsList`→`CityList`,
|
||||
`LoadLocations()`→`LoadCities()`) so the domain vocabulary matches the
|
||||
web backend's `City`/`CityId`. Field/variable names that merely hold a
|
||||
`City` instance (e.g. `EnrichedCity::location`, `BreweryRecord::location`)
|
||||
were left as-is — `City` itself has a `city` field (the city name
|
||||
string), so renaming the container field too would read as
|
||||
`enriched_city.city.city`. The SQLite export layer's own vocabulary
|
||||
(`locations` table, `location_id` column, `location_cache_`, etc.) was
|
||||
renamed to `cities`/`city_id` to match, since it's this pipeline's own
|
||||
throwaway export, not the backend's SQL Server schema (see §5).
|
||||
- [x] Added a new `Address` struct (`postal_code` only, for now) and a
|
||||
`BreweryRecord::address` member, mirroring the web backend's
|
||||
`BreweryPostLocation` (`AddressLine1`, `AddressLine2`, `PostalCode`,
|
||||
`CityId`). Only `postal_code` is populated — `address_line1`/
|
||||
`address_line2` generation has no fixture data or service yet. Users
|
||||
do not get an `Address`: the backend's `UserAccount` has no address
|
||||
fields, only `BreweryPostLocation` does.
|
||||
|
||||
## 2. Data Preloading
|
||||
|
||||
@@ -78,7 +104,7 @@ architecture needs.
|
||||
- [x] Extract an interface; have `CuratedJsonDataService` implement it.
|
||||
Landed as `ICuratedDataService`
|
||||
(`services/curated_data/curated_data_service.h`), not `DataPreloader`
|
||||
— `LoadLocations()`, `LoadPersonas()`, `LoadForenamesByCountry()`, and
|
||||
— `LoadCities()`, `LoadPersonas()`, `LoadForenamesByCountry()`, and
|
||||
`LoadSurnamesByCountry()` are all virtual methods returning `const&`
|
||||
and take no arguments. `CuratedJsonDataService`
|
||||
(`services/curated_data/curated_json_data_service.h`, originally
|
||||
@@ -101,7 +127,7 @@ architecture needs.
|
||||
and is already copied into the Docker image
|
||||
(`runpod/Dockerfile`), but no loader reads it yet, and the native
|
||||
CMake build doesn't copy it into `build/` at all — `CMakeLists.txt`'s
|
||||
"Runtime Assets" step only copies `locations.json` and `prompts/`.
|
||||
"Runtime Assets" step only copies `cities.json` and `prompts/`.
|
||||
- [x] Add `LoadPersonas()` and author `personas.json` (doesn't exist yet).
|
||||
- [x] Add name-by-country loading, parsing
|
||||
`tooling/pipeline/forenames-by-country.json` and
|
||||
@@ -116,10 +142,16 @@ architecture needs.
|
||||
`sigpwned/popular-names-by-country-dataset` (CC0) — see
|
||||
ETHICS-AND-KNOWN-ISSUES.md's Names-by-Country Dataset section for
|
||||
provenance. Deliberately not pre-paired or filtered down to just the
|
||||
countries in `locations.json`: keeping the source shape (including
|
||||
countries in `cities.json`: keeping the source shape (including
|
||||
per-forename gender) intact means the loader can support more
|
||||
countries later, or gender-aware persona/bio generation, without
|
||||
re-sourcing data.
|
||||
- [x] Parse `postal_code.city_regex` and `postal_code.examples` out of each
|
||||
`cities.json` entry's nested `postal_code` object in
|
||||
`CuratedJsonDataService::LoadCities()`, into `City::postal_regex`
|
||||
/ `City::postal_code_examples` (see §1). `MockCuratedDataService`'s
|
||||
4 fixed locations carry matching values pulled from the corresponding
|
||||
`cities.json` entries.
|
||||
|
||||
## 3. Policy / Strategy Layer
|
||||
|
||||
@@ -174,17 +206,20 @@ Entirely new — no `includes/policy/` (or equivalent) directory exists today.
|
||||
`kCreateUsersTableSql` / `kInsertUserSql` in
|
||||
`includes/services/database/sqlite_statement_helpers.h`) are
|
||||
implemented — `GenerateUsers()` exports every successful user
|
||||
alongside its resolved `location_id`.
|
||||
alongside its resolved `city_id`.
|
||||
- [x] `ProcessRecord(const BreweryRecord&)` now also binds
|
||||
`BreweryRecord::address.postal_code` into a `postal_code` column on
|
||||
`breweries` (see §9).
|
||||
- [ ] Extend `IExportService` with `ProcessBeer`, `ProcessCheckin`,
|
||||
`ProcessRating`, `ProcessFollow` (today `ProcessRecord(const
|
||||
BreweryRecord&)` and `ProcessRecord(const UserRecord&)` exist).
|
||||
- [ ] Add `beers`, `checkins`, `ratings`, `follows` tables to
|
||||
`SqliteExportService::InitializeSchema()` (`locations`, `breweries`,
|
||||
and `users` already exist) — see `kCreateLocationsTableSql` /
|
||||
`SqliteExportService::InitializeSchema()` (`cities`, `breweries`,
|
||||
and `users` already exist) — see `kCreateCitiesTableSql` /
|
||||
`kCreateBreweriesTableSql` / `kCreateUsersTableSql` in
|
||||
`includes/services/database/sqlite_statement_helpers.h` for the
|
||||
existing pattern to follow.
|
||||
- [ ] Add a `brewery_cache_` alongside the existing `location_cache_`, and
|
||||
- [ ] Add a `brewery_cache_` alongside the existing `city_cache_`, and
|
||||
move from one open transaction for the whole run to per-phase batched
|
||||
commits (`BEGIN` / `COMMIT & BEGIN` on a batch-size threshold), as
|
||||
shown in the planned activity diagram.
|
||||
@@ -233,6 +268,36 @@ Entirely new — no `includes/policy/` (or equivalent) directory exists today.
|
||||
Assets" `configure_file` step in `CMakeLists.txt` so native builds and
|
||||
the Docker image agree on what's available at runtime.
|
||||
|
||||
## 9. Postal Code Generation
|
||||
|
||||
`tooling/pipeline/includes/services/postal_code/`. Not part of the original
|
||||
`diagrams/planned/class.puml` sketch — added once `Location` (now `City`)
|
||||
gained `postal_regex`/`postal_code_examples` (§1, §2).
|
||||
|
||||
- [x] Add the `IPostalCodeService` interface —
|
||||
`GeneratePostalCode(location : const City&) : std::string`.
|
||||
- [x] Add `MockPostalCodeService`: ignores `location.postal_regex` entirely
|
||||
and returns `location.postal_code_examples.front()`.
|
||||
- [ ] **Implement a real, xeger-based `IPostalCodeService`.** A xeger
|
||||
generator turns a regex pattern (here, one of
|
||||
`City::postal_regex`) into a random string that matches it — the
|
||||
inverse of matching a string against a regex. Without it, every
|
||||
generated postal code is either a hardcoded mock value or absent
|
||||
entirely; a real implementation is required before postal codes can
|
||||
vary per generated brewery instead of always reusing the same
|
||||
curated example.
|
||||
- [x] Wire `IPostalCodeService` into `main.cc`'s Boost.DI injector — bound
|
||||
unconditionally to `MockPostalCodeService` (no `--mocked` branch yet,
|
||||
since a real implementation doesn't exist to switch to; add one once
|
||||
the xeger-based generator above lands, mirroring `IEnrichmentService`)
|
||||
— and into `BiergartenPipelineOrchestrator::GenerateBreweries()`,
|
||||
which calls `GeneratePostalCode(location)` per brewery and stores the
|
||||
result on `BreweryRecord::address` (a new `Address` struct — see §1),
|
||||
exported to the `breweries.postal_code` SQLite column (§5).
|
||||
Postal-code generation is per-brewery, not per-city or per-user: users
|
||||
don't have an address concept in the web backend, only breweries do
|
||||
(`BreweryPostLocation`), so `UserRecord` was deliberately not extended.
|
||||
|
||||
---
|
||||
|
||||
## Suggested build order
|
||||
@@ -250,3 +315,7 @@ populated. Phase 1b may begin.", etc.) imply a dependency order. Roughly:
|
||||
planned diagrams.
|
||||
5. Enrichment cache pre-warming and the city/region query decision (§7) —
|
||||
useful at any point, but most valuable once phases run concurrently.
|
||||
6. Postal code generation (§9) is independent of the phases above — the
|
||||
xeger-based generator just needs `City::postal_regex`, already in
|
||||
place. Wiring the mock into brewery output has already landed (§9); the
|
||||
real generator can be swapped in once built.
|
||||
|
||||
@@ -69,14 +69,14 @@ end note
|
||||
:QueryCitiesWithCountries();
|
||||
|
||||
|#E2EBDC|JsonLoader|
|
||||
:JsonLoader::LoadLocations("locations.json");
|
||||
:JsonLoader::LoadCities("cities.json");
|
||||
:std::ranges::sample(all_locations, --location-count);
|
||||
note right
|
||||
--location-count defaults to 10.
|
||||
end note
|
||||
|
||||
|#EAF0E8|BiergartenPipelineOrchestrator|
|
||||
while (For each sampled Location?) is (Remaining cities)
|
||||
while (For each sampled City?) is (Remaining cities)
|
||||
|#DCE8D8|IEnrichmentService|
|
||||
:GetLocationContext(loc);
|
||||
note right
|
||||
@@ -89,7 +89,7 @@ while (For each sampled Location?) is (Remaining cities)
|
||||
if (Lookup failed?) then (yes)
|
||||
:Log warning, skip city;
|
||||
else (no)
|
||||
:Store EnrichedCity{Location, region_context};
|
||||
:Store EnrichedCity{City, region_context};
|
||||
endif
|
||||
endwhile (Done)
|
||||
|
||||
@@ -146,12 +146,12 @@ while (For each EnrichedCity?) is (Remaining cities)
|
||||
|
||||
|#E0EAE0|SqliteExportService|
|
||||
:ProcessRecord(UserRecord);
|
||||
if (Location in cache?) then (yes)
|
||||
:Reuse location_id;
|
||||
if (City in cache?) then (yes)
|
||||
:Reuse city_id;
|
||||
else (no)
|
||||
:Insert Location & Cache ID;
|
||||
:Insert City & Cache ID;
|
||||
endif
|
||||
:Insert User (FK: location_id);
|
||||
:Insert User (FK: city_id);
|
||||
|
||||
if (Exception caught during insert?) then (yes)
|
||||
|#EAF0E8|BiergartenPipelineOrchestrator|
|
||||
@@ -197,12 +197,12 @@ while (For each EnrichedCity?) is (Remaining cities)
|
||||
if (Generation successful?) then (yes)
|
||||
|#E0EAE0|SqliteExportService|
|
||||
:ProcessRecord(GeneratedBrewery);
|
||||
if (Location in cache?) then (yes)
|
||||
:Reuse location_id;
|
||||
if (City in cache?) then (yes)
|
||||
:Reuse city_id;
|
||||
else (no)
|
||||
:Insert Location & Cache ID;
|
||||
:Insert City & Cache ID;
|
||||
endif
|
||||
:Insert Brewery (FK: location_id);
|
||||
:Insert Brewery (FK: city_id);
|
||||
|
||||
if (Exception caught during insert?) then (yes)
|
||||
|#EAF0E8|BiergartenPipelineOrchestrator|
|
||||
|
||||
@@ -31,11 +31,12 @@ class BiergartenPipelineOrchestrator {
|
||||
- generator_ : std::unique_ptr<DataGenerator>
|
||||
- exporter_ : std::unique_ptr<IExportService>
|
||||
- curated_data_service_ : std::unique_ptr<ICuratedDataService>
|
||||
- postal_code_service_ : std::unique_ptr<IPostalCodeService>
|
||||
- application_options_ : ApplicationOptions
|
||||
- generated_breweries_ : std::vector<BreweryRecord>
|
||||
- generated_users_ : std::vector<UserRecord>
|
||||
+ Run() : bool
|
||||
- QueryCitiesWithCountries() : std::vector<Location>
|
||||
- QueryCitiesWithCountries() : std::vector<City>
|
||||
- GenerateBreweries(cities : std::span<const EnrichedCity>) : void
|
||||
- GenerateUsers(cities : std::span<const EnrichedCity>) : void
|
||||
- LogResults() : void
|
||||
@@ -90,17 +91,17 @@ class LogDispatcher {
|
||||
}
|
||||
|
||||
interface IEnrichmentService <<interface>> {
|
||||
+ GetLocationContext(loc : const Location&) : std::string
|
||||
+ GetLocationContext(loc : const City&) : std::string
|
||||
}
|
||||
|
||||
class MockEnrichmentService {
|
||||
+ GetLocationContext(loc : const Location&) : std::string
|
||||
+ GetLocationContext(loc : const City&) : std::string
|
||||
}
|
||||
|
||||
class WikipediaEnrichmentService {
|
||||
- client_ : std::unique_ptr<WebClient>
|
||||
- extract_cache_ : std::unordered_map<std::string, std::string>
|
||||
+ GetLocationContext(loc : const Location&) : std::string
|
||||
+ GetLocationContext(loc : const City&) : std::string
|
||||
- FetchExtract(query : std::string_view) : std::string
|
||||
}
|
||||
|
||||
@@ -115,15 +116,15 @@ class HttpWebClient {
|
||||
}
|
||||
|
||||
interface DataGenerator <<interface>> {
|
||||
+ GenerateBrewery(location : const Location&, region_context : const std::string&) : BreweryResult
|
||||
+ GenerateBrewery(location : const City&, region_context : const std::string&) : BreweryResult
|
||||
+ GenerateUser(city : const EnrichedCity&, persona : const UserPersona&, name : const Name&) : UserResult
|
||||
}
|
||||
|
||||
class MockGenerator {
|
||||
+ GenerateBrewery(location : const Location&, region_context : const std::string&) : BreweryResult
|
||||
+ GenerateBrewery(location : const City&, region_context : const std::string&) : BreweryResult
|
||||
+ GenerateUser(city : const EnrichedCity&, persona : const UserPersona&, name : const Name&) : UserResult
|
||||
- DeterministicHash(location : const Location&) : size_t
|
||||
- DeterministicHash(location : const Location&, persona : const UserPersona&, name : const Name&) : size_t
|
||||
- DeterministicHash(location : const City&) : size_t
|
||||
- DeterministicHash(location : const City&, persona : const UserPersona&, name : const Name&) : size_t
|
||||
}
|
||||
|
||||
class LlamaGenerator {
|
||||
@@ -158,7 +159,7 @@ class PromptDirectory {
|
||||
}
|
||||
|
||||
interface ICuratedDataService <<interface>> {
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
+ LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
|
||||
@@ -166,7 +167,7 @@ interface ICuratedDataService <<interface>> {
|
||||
|
||||
class JsonLoader {
|
||||
- cache_ : cache
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
+ LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
|
||||
@@ -180,11 +181,11 @@ note right of JsonLoader
|
||||
end note
|
||||
|
||||
class MockCuratedDataService {
|
||||
- locations_ : std::vector<Location>
|
||||
- locations_ : std::vector<City>
|
||||
- personas_ : std::vector<UserPersona>
|
||||
- forenames_by_country_ : std::unordered_map<std::string, forename_list>
|
||||
- surnames_by_country_ : std::unordered_map<std::string, surname_list>
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
+ LoadSurnamesByCountry(filepath : const std::filesystem::path&) : const std::unordered_map<std::string, surname_list>&
|
||||
@@ -196,6 +197,27 @@ note right of MockCuratedDataService
|
||||
filepath arguments are ignored.
|
||||
end note
|
||||
|
||||
interface IPostalCodeService <<interface>> {
|
||||
+ GeneratePostalCode(location : const City&) : std::string
|
||||
}
|
||||
|
||||
class MockPostalCodeService {
|
||||
+ GeneratePostalCode(location : const City&) : std::string
|
||||
}
|
||||
|
||||
note right of MockPostalCodeService
|
||||
Ignores location.postal_regex and returns
|
||||
location.postal_code_examples.front(). Bound
|
||||
unconditionally in main.cc's DI graph (no
|
||||
--mocked branch yet -- no real implementation
|
||||
exists to switch to) and called from
|
||||
GenerateBreweries() per brewery, stored on
|
||||
BreweryRecord::address. A real implementation
|
||||
needs a xeger-style generator to turn
|
||||
postal_regex into a synthesized matching
|
||||
string. See ROADMAP.md §9.
|
||||
end note
|
||||
|
||||
interface IExportService <<interface>> {
|
||||
+ Initialize() : void
|
||||
+ ProcessRecord(brewery : const BreweryRecord&) : uint64_t
|
||||
@@ -208,17 +230,17 @@ class SqliteExportService {
|
||||
- run_timestamp_utc_ : std::string
|
||||
- database_path_ : std::filesystem::path
|
||||
- db_handle_ : SqliteDatabaseHandle
|
||||
- insert_location_stmt_ : SqliteStatementHandle
|
||||
- insert_city_stmt_ : SqliteStatementHandle
|
||||
- insert_brewery_stmt_ : SqliteStatementHandle
|
||||
- insert_user_stmt_ : SqliteStatementHandle
|
||||
- transaction_open_ : bool
|
||||
- location_cache_ : std::unordered_map<std::string, sqlite3_int64>
|
||||
- city_cache_ : std::unordered_map<std::string, sqlite3_int64>
|
||||
+ Initialize() : void
|
||||
+ ProcessRecord(brewery : const BreweryRecord&) : uint64_t
|
||||
+ ProcessRecord(user : const UserRecord&) : uint64_t
|
||||
+ Finalize() : void
|
||||
- InitializeSchema() : void
|
||||
- ResolveLocationId(location : const Location&) : sqlite3_int64
|
||||
- ResolveCityId(location : const City&) : sqlite3_int64
|
||||
}
|
||||
|
||||
interface IDateTimeProvider <<interface>> {
|
||||
@@ -235,6 +257,7 @@ BiergartenPipelineOrchestrator *-- IEnrichmentService : owns
|
||||
BiergartenPipelineOrchestrator *-- DataGenerator : owns
|
||||
BiergartenPipelineOrchestrator *-- IExportService : owns
|
||||
BiergartenPipelineOrchestrator *-- ICuratedDataService : owns
|
||||
BiergartenPipelineOrchestrator *-- IPostalCodeService : owns
|
||||
|
||||
LogEntry *-- LogLevel
|
||||
LogEntry *-- PipelinePhase
|
||||
@@ -262,6 +285,8 @@ IPromptDirectory <|.. PromptDirectory : implements
|
||||
ICuratedDataService <|.. JsonLoader : implements
|
||||
ICuratedDataService <|.. MockCuratedDataService : implements
|
||||
|
||||
IPostalCodeService <|.. MockPostalCodeService : implements
|
||||
|
||||
IExportService <|.. SqliteExportService : implements
|
||||
SqliteExportService *-- IDateTimeProvider : owns
|
||||
IDateTimeProvider <|.. SystemDateTimeProvider : implements
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 94 KiB |
@@ -39,7 +39,7 @@ fork
|
||||
:JsonLoader::LoadBeerStyles("beer-styles.json");
|
||||
:EnrichmentService::PreWarmBeerStyleCache(beer_styles);
|
||||
fork again
|
||||
:JsonLoader::LoadLocations("locations.json");
|
||||
:JsonLoader::LoadCities("cities.json");
|
||||
:EnrichmentService::PreWarmLocationCache(sampled_locations);
|
||||
end fork
|
||||
fork
|
||||
@@ -75,7 +75,7 @@ fork
|
||||
fork again
|
||||
|LLM Worker|
|
||||
while (loc_ch has items?) is (yes)
|
||||
:Receive Location;
|
||||
:Receive City;
|
||||
|
||||
:GetLocationContextFromCache(location);
|
||||
note right
|
||||
@@ -146,13 +146,13 @@ end fork
|
||||
fork
|
||||
|Orchestrator|
|
||||
:Loop: Sample User from user_pool_
|
||||
and pair with Location;
|
||||
:Send BreweryTask(Location, User) -> loc_ch;
|
||||
and pair with City;
|
||||
:Send BreweryTask(City, User) -> loc_ch;
|
||||
:Close loc_ch;
|
||||
fork again
|
||||
|LLM Worker|
|
||||
while (loc_ch has items?) is (yes)
|
||||
:Receive BreweryTask(Location, User);
|
||||
:Receive BreweryTask(City, User);
|
||||
|
||||
:GetLocationContextFromCache(task.location);
|
||||
note right
|
||||
|
||||
@@ -12,17 +12,40 @@ title Biergarten Data Pipeline — Class Diagram
|
||||
|
||||
package "Domain: Models" {
|
||||
|
||||
class Location {
|
||||
class City {
|
||||
+ city : std::string
|
||||
+ state_province : std::string
|
||||
+ iso3166_2 : std::string
|
||||
+ country : std::string
|
||||
+ iso3166_1 : std::string
|
||||
+ postal_regex : std::vector<std::string>
|
||||
+ postal_code_examples : std::vector<std::string>
|
||||
+ local_languages : std::vector<std::string>
|
||||
+ latitude : double
|
||||
+ longitude : double
|
||||
}
|
||||
|
||||
note right of City
|
||||
postal_regex / postal_code_examples are
|
||||
sourced from each cities.json entry's
|
||||
postal_code.city_regex / postal_code.examples
|
||||
(implemented today, ahead of the rest of this
|
||||
diagram -- see ROADMAP.md §1/§2). Renamed from
|
||||
Location to City to match the web backend's
|
||||
City/CityId vocabulary -- see ROADMAP.md §1.
|
||||
end note
|
||||
|
||||
class Address {
|
||||
+ postal_code : std::string
|
||||
}
|
||||
|
||||
note right of Address
|
||||
Mirrors the web backend's BreweryPostLocation.
|
||||
Only postal_code is populated today --
|
||||
address_line1/address_line2 have no fixture
|
||||
data or generator yet. See ROADMAP.md §9.
|
||||
end note
|
||||
|
||||
class LocationContext {
|
||||
+ text : std::string
|
||||
+ completeness : Completeness
|
||||
@@ -36,7 +59,7 @@ package "Domain: Models" {
|
||||
}
|
||||
|
||||
class EnrichedCity {
|
||||
+ location : Location
|
||||
+ location : City
|
||||
+ context : LocationContext
|
||||
}
|
||||
|
||||
@@ -130,7 +153,8 @@ package "Domain: Models" {
|
||||
|
||||
class GeneratedBrewery {
|
||||
+ brewery_id : uint64_t
|
||||
+ location : Location
|
||||
+ location : City
|
||||
+ address : Address
|
||||
+ brewery : BreweryResult
|
||||
+ context_completeness : LocationContext::Completeness
|
||||
+ metadata : GenerationMetadata
|
||||
@@ -139,7 +163,7 @@ package "Domain: Models" {
|
||||
class GeneratedBeer {
|
||||
+ beer_id : uint64_t
|
||||
+ brewery_id : uint64_t
|
||||
+ location : Location
|
||||
+ location : City
|
||||
+ style : BeerStyle
|
||||
+ beer : BeerResult
|
||||
+ metadata : GenerationMetadata
|
||||
@@ -147,7 +171,7 @@ package "Domain: Models" {
|
||||
|
||||
class GeneratedUser {
|
||||
+ user_id : uint64_t
|
||||
+ location : Location
|
||||
+ location : City
|
||||
+ user : UserResult
|
||||
+ email : std::string
|
||||
+ date_of_birth : std::string
|
||||
@@ -227,27 +251,27 @@ package "Domain: Application Configuration" {
|
||||
package "Domain: Policy" {
|
||||
|
||||
interface ContextStrategy <<interface>> {
|
||||
+ QueriesFor(loc : const Location&) : std::vector<std::string>
|
||||
+ QueriesFor(loc : const City&) : std::vector<std::string>
|
||||
+ MaxContextChars() : size_t
|
||||
}
|
||||
|
||||
class BreweryContextStrategy {
|
||||
+ QueriesFor(loc : const Location&) : std::vector<std::string>
|
||||
+ QueriesFor(loc : const City&) : std::vector<std::string>
|
||||
+ MaxContextChars() : size_t
|
||||
}
|
||||
|
||||
class BeerContextStrategy {
|
||||
+ QueriesFor(loc : const Location&) : std::vector<std::string>
|
||||
+ QueriesFor(loc : const City&) : std::vector<std::string>
|
||||
+ MaxContextChars() : size_t
|
||||
}
|
||||
|
||||
interface SamplingStrategy <<interface>> {
|
||||
+ Sample(locations : const std::vector<Location>&) : std::vector<Location>
|
||||
+ Sample(locations : const std::vector<City>&) : std::vector<City>
|
||||
}
|
||||
|
||||
class UniformSamplingStrategy {
|
||||
- sample_size_ : size_t
|
||||
+ Sample(locations : const std::vector<Location>&) : std::vector<Location>
|
||||
+ Sample(locations : const std::vector<City>&) : std::vector<City>
|
||||
}
|
||||
|
||||
interface BeerSelectionStrategy <<interface>> {
|
||||
@@ -369,7 +393,7 @@ package "Infrastructure: Pipeline Channel" {
|
||||
package "Infrastructure: Data Preloading" {
|
||||
|
||||
interface ICuratedDataService <<interface>> {
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadBeerStyles(filepath : const std::filesystem::path&) : const std::vector<BeerStyle>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(forenames_filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
@@ -377,7 +401,7 @@ package "Infrastructure: Data Preloading" {
|
||||
}
|
||||
|
||||
class JsonLoader {
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadBeerStyles(filepath : const std::filesystem::path&) : const std::vector<BeerStyle>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(forenames_filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
@@ -391,7 +415,7 @@ package "Infrastructure: Data Preloading" {
|
||||
end note
|
||||
|
||||
class MockCuratedDataService {
|
||||
+ LoadLocations(filepath : const std::filesystem::path&) : const std::vector<Location>&
|
||||
+ LoadCities(filepath : const std::filesystem::path&) : const std::vector<City>&
|
||||
+ LoadBeerStyles(filepath : const std::filesystem::path&) : const std::vector<BeerStyle>&
|
||||
+ LoadPersonas(filepath : const std::filesystem::path&) : const std::vector<UserPersona>&
|
||||
+ LoadForenamesByCountry(forenames_filepath : const std::filesystem::path&) : const std::unordered_map<std::string, forename_list>&
|
||||
@@ -410,13 +434,13 @@ package "Infrastructure: Data Preloading" {
|
||||
package "Infrastructure: Enrichment" {
|
||||
|
||||
interface EnrichmentService <<interface>> {
|
||||
+ GetLocationContext(loc : const Location&,\n strategy : const ContextStrategy&) : LocationContext
|
||||
+ GetLocationContext(loc : const City&,\n strategy : const ContextStrategy&) : LocationContext
|
||||
}
|
||||
|
||||
class WikipediaService {
|
||||
- client_ : std::unique_ptr<WebClient>
|
||||
- extract_cache_ : std::unordered_map<std::string, std::string>
|
||||
+ GetLocationContext(loc : const Location&,\n strategy : const ContextStrategy&) : LocationContext
|
||||
+ GetLocationContext(loc : const City&,\n strategy : const ContextStrategy&) : LocationContext
|
||||
- FetchExtract(query : std::string_view) : std::string
|
||||
}
|
||||
|
||||
@@ -432,6 +456,39 @@ package "Infrastructure: Enrichment" {
|
||||
|
||||
}
|
||||
|
||||
package "Infrastructure: Postal Code Generation" {
|
||||
|
||||
interface IPostalCodeService <<interface>> {
|
||||
+ GeneratePostalCode(location : const City&) : std::string
|
||||
}
|
||||
|
||||
class MockPostalCodeService {
|
||||
+ GeneratePostalCode(location : const City&) : std::string
|
||||
}
|
||||
|
||||
class XegerPostalCodeService {
|
||||
+ GeneratePostalCode(location : const City&) : std::string
|
||||
}
|
||||
|
||||
note right of MockPostalCodeService
|
||||
Implemented today: ignores postal_regex and
|
||||
returns postal_code_examples.front(). Not yet
|
||||
bound in main.cc's DI graph or used by
|
||||
BiergartenPipelineOrchestrator.
|
||||
end note
|
||||
|
||||
note right of XegerPostalCodeService
|
||||
Not implemented yet. Picks one of
|
||||
location.postal_regex and generates a random
|
||||
string matching it (a "xeger" -- the inverse of
|
||||
regex matching) instead of always replaying a
|
||||
fixed example. See ROADMAP.md §9.
|
||||
end note
|
||||
|
||||
IPostalCodeService <|.. MockPostalCodeService
|
||||
IPostalCodeService <|.. XegerPostalCodeService
|
||||
}
|
||||
|
||||
package "Infrastructure: Prompting" {
|
||||
|
||||
interface IPromptDirectory <<interface>> {
|
||||
@@ -451,8 +508,8 @@ package "Infrastructure: Prompting" {
|
||||
package "Infrastructure: Data Generation" {
|
||||
|
||||
interface DataGenerator <<interface>> {
|
||||
+ GenerateBrewery(location : const Location&,\n context : const LocationContext&) : BreweryResult
|
||||
+ GenerateBeer(brewery_id : uint64_t,\n location : const Location&,\n context : const LocationContext&,\n style : const BeerStyle&) : BeerResult
|
||||
+ GenerateBrewery(location : const City&,\n context : const LocationContext&) : BreweryResult
|
||||
+ GenerateBeer(brewery_id : uint64_t,\n location : const City&,\n context : const LocationContext&,\n style : const BeerStyle&) : BeerResult
|
||||
+ GenerateUser(city : const EnrichedCity&,\n persona : const UserPersona&,\n name : const Name&) : UserResult
|
||||
+ GenerateCheckin(user : const GeneratedUser&,\n brewery : const GeneratedBrewery&,\n timestamp : const std::string&) : CheckinResult
|
||||
+ GenerateRating(user : const GeneratedUser&,\n beer : const GeneratedBeer&,\n checkin_id : uint64_t) : RatingResult
|
||||
@@ -464,7 +521,7 @@ package "Infrastructure: Data Generation" {
|
||||
+ GenerateUser(...) : UserResult
|
||||
+ GenerateCheckin(...) : CheckinResult
|
||||
+ GenerateRating(...) : RatingResult
|
||||
- DeterministicHash(location : const Location&) : size_t
|
||||
- DeterministicHash(location : const City&) : size_t
|
||||
}
|
||||
|
||||
class LlamaGenerator {
|
||||
@@ -566,8 +623,8 @@ class BiergartenPipelineOrchestrator {
|
||||
- follow_pool_ : std::vector<GeneratedFollow>
|
||||
--
|
||||
+ Run() : bool
|
||||
- RunUserPhase(locations : const std::vector<Location>&) : void
|
||||
- RunBreweryAndBeerPhase(locations : const std::vector<Location>&) : void
|
||||
- RunUserPhase(locations : const std::vector<City>&) : void
|
||||
- RunBreweryAndBeerPhase(locations : const std::vector<City>&) : void
|
||||
- RunCheckinPhase() : void
|
||||
- RunRatingPhase() : void
|
||||
- RunFollowPhase() : void
|
||||
@@ -625,16 +682,17 @@ PipelineLogger o-- BoundedChannel : logs to
|
||||
LogWorker o-- BoundedChannel : drains from
|
||||
|
||||
' --- Domain Containment ---
|
||||
EnrichedCity *-- Location
|
||||
EnrichedCity *-- City
|
||||
EnrichedCity *-- LocationContext
|
||||
GeneratedBrewery *-- Location
|
||||
GeneratedBrewery *-- City
|
||||
GeneratedBrewery *-- Address
|
||||
GeneratedBrewery *-- BreweryResult
|
||||
GeneratedBrewery *-- GenerationMetadata
|
||||
GeneratedBeer *-- Location
|
||||
GeneratedBeer *-- City
|
||||
GeneratedBeer *-- BeerStyle
|
||||
GeneratedBeer *-- BeerResult
|
||||
GeneratedBeer *-- GenerationMetadata
|
||||
GeneratedUser *-- Location
|
||||
GeneratedUser *-- City
|
||||
GeneratedUser *-- UserResult
|
||||
GeneratedUser *-- GenerationMetadata
|
||||
GeneratedCheckin *-- CheckinResult
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 257 KiB |
@@ -140,6 +140,8 @@ FetchContent_MakeAvailable(cpp-httplib)
|
||||
add_executable(${PROJECT_NAME}
|
||||
includes/services/enrichment/mock_enrichment.h
|
||||
includes/services/curated_data/mock_curated_data_service.h
|
||||
includes/services/postal_code/postal_code_service.h
|
||||
includes/services/postal_code/mock_postal_code_service.h
|
||||
includes/json_handling/pretty_print.h)
|
||||
|
||||
# --- Entry point ---
|
||||
@@ -264,8 +266,8 @@ target_compile_options(biergarten-pipeline PRIVATE
|
||||
|
||||
# 7. Runtime Assets
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/locations.json
|
||||
${CMAKE_BINARY_DIR}/locations.json
|
||||
${CMAKE_SOURCE_DIR}/cities.json
|
||||
${CMAKE_BINARY_DIR}/cities.json
|
||||
COPYONLY
|
||||
)
|
||||
configure_file(
|
||||
@@ -288,4 +290,3 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
${CMAKE_SOURCE_DIR}/prompts
|
||||
${CMAKE_BINARY_DIR}/prompts
|
||||
)
|
||||
|
||||
|
||||
1718
tooling/pipeline/cities.json
Normal file
@@ -16,8 +16,10 @@
|
||||
// --- data_generation ---
|
||||
#include "data_generation/data_generator.h"
|
||||
#include "data_generation/json_grammars.h"
|
||||
#ifndef BIERGARTEN_MOCK_ONLY
|
||||
#include "data_generation/llama_generator.h"
|
||||
#include "data_generation/llama_generator_helpers.h"
|
||||
#endif
|
||||
#include "data_generation/mock_generator.h"
|
||||
#include "data_generation/prompt_formatting/gemma4_jinja_prompt_formatter.h"
|
||||
#include "data_generation/prompt_formatting/prompt_formatter.h"
|
||||
@@ -31,7 +33,9 @@
|
||||
#include "services/curated_data/curated_json_data_service.h"
|
||||
|
||||
// --- llama backend ---
|
||||
#ifndef BIERGARTEN_MOCK_ONLY
|
||||
#include "llama_backend_state.h"
|
||||
#endif
|
||||
|
||||
// --- services: curated_data ---
|
||||
#include "services/curated_data/curated_data_service.h"
|
||||
@@ -60,6 +64,10 @@
|
||||
#include "services/logging/log_producer.h"
|
||||
#include "services/logging/logger.h"
|
||||
|
||||
// --- services: postal_code ---
|
||||
#include "services/postal_code/mock_postal_code_service.h"
|
||||
#include "services/postal_code/postal_code_service.h"
|
||||
|
||||
// --- services: prompting ---
|
||||
#include "services/prompting/prompt_directory.h"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "services/database/export_service.h"
|
||||
#include "services/enrichment/enrichment_service.h"
|
||||
#include "services/logging/logger.h"
|
||||
|
||||
#include "services/postal_code/postal_code_service.h"
|
||||
/**
|
||||
* @brief Main orchestrator for the Biergarten data generation pipeline.
|
||||
*
|
||||
@@ -34,6 +34,7 @@ class BiergartenPipelineOrchestrator {
|
||||
* @param exporter Database backend for persisting generated records.
|
||||
* @param curated_data_service Loads curated location, persona, and name
|
||||
* data used to seed generation.
|
||||
* @param postal_code_service
|
||||
* @param application_options CLI configuration and paths.
|
||||
*/
|
||||
BiergartenPipelineOrchestrator(
|
||||
@@ -42,6 +43,7 @@ class BiergartenPipelineOrchestrator {
|
||||
std::unique_ptr<DataGenerator> generator,
|
||||
std::unique_ptr<IExportService> exporter,
|
||||
std::unique_ptr<ICuratedDataService> curated_data_service,
|
||||
std::unique_ptr<IPostalCodeService> postal_code_service,
|
||||
const ApplicationOptions& application_options);
|
||||
|
||||
/**
|
||||
@@ -78,6 +80,7 @@ class BiergartenPipelineOrchestrator {
|
||||
*/
|
||||
std::unique_ptr<IExportService> exporter_;
|
||||
std::unique_ptr<ICuratedDataService> curated_data_service_;
|
||||
std::unique_ptr<IPostalCodeService> postal_code_service_;
|
||||
|
||||
ApplicationOptions application_options_;
|
||||
|
||||
@@ -87,7 +90,7 @@ class BiergartenPipelineOrchestrator {
|
||||
* @return Vector of locations randomly sampled per
|
||||
* PipelineOptions::location_count.
|
||||
*/
|
||||
std::vector<Location> QueryCitiesWithCountries();
|
||||
std::vector<City> QueryCitiesWithCountries();
|
||||
|
||||
/**
|
||||
* @brief Generate breweries for enriched cities.
|
||||
|
||||
@@ -24,7 +24,7 @@ class DataGenerator {
|
||||
* @param region_context Additional regional context text.
|
||||
* @return Brewery generation result.
|
||||
*/
|
||||
virtual BreweryResult GenerateBrewery(const Location& location,
|
||||
virtual BreweryResult GenerateBrewery(const City& location,
|
||||
const std::string& region_context) = 0;
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,7 +55,7 @@ class LlamaGenerator final : public DataGenerator {
|
||||
* @param region_context Additional regional context.
|
||||
* @return Generated brewery result.
|
||||
*/
|
||||
BreweryResult GenerateBrewery(const Location& location,
|
||||
BreweryResult GenerateBrewery(const City& location,
|
||||
const std::string& region_context) override;
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ class MockGenerator final : public DataGenerator {
|
||||
* @param region_context Unused for mock generation.
|
||||
* @return Generated brewery result.
|
||||
*/
|
||||
BreweryResult GenerateBrewery(const Location& location,
|
||||
BreweryResult GenerateBrewery(const City& location,
|
||||
const std::string& region_context) override;
|
||||
|
||||
/**
|
||||
@@ -46,7 +46,7 @@ class MockGenerator final : public DataGenerator {
|
||||
* @param location City and country names.
|
||||
* @return Deterministic hash value.
|
||||
*/
|
||||
static size_t DeterministicHash(const Location& location);
|
||||
static size_t DeterministicHash(const City& location);
|
||||
|
||||
/**
|
||||
* @brief Combines city, persona, and name into a stable hash value.
|
||||
@@ -56,7 +56,7 @@ class MockGenerator final : public DataGenerator {
|
||||
* @param name Sampled first/last name.
|
||||
* @return Deterministic hash value.
|
||||
*/
|
||||
static size_t DeterministicHash(const Location& location,
|
||||
static size_t DeterministicHash(const City& location,
|
||||
const UserPersona& persona, const Name& name);
|
||||
|
||||
// Hash stride constants for deterministic distribution across fixed-size
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* enriched data, and complete generation results.
|
||||
*/
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "data_model/models.h"
|
||||
@@ -87,15 +88,45 @@ struct UserResult {
|
||||
* @brief Enriched city data with Wikipedia context.
|
||||
*/
|
||||
struct EnrichedCity {
|
||||
Location location;
|
||||
City location;
|
||||
std::string region_context{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A brewery's address. Owns the `City` it belongs to alongside its
|
||||
* street-level fields. Mirrors the `brewery_addresses` table and the
|
||||
* backend's `BreweryPostLocation` shape.
|
||||
*
|
||||
* Only `city` and `postal_code` are populated today -- street-address
|
||||
* generation has no fixture data or service yet, so the address lines stay
|
||||
* empty.
|
||||
*/
|
||||
struct BreweryAddress {
|
||||
City city{};
|
||||
std::optional<std::string> address_line_1{};
|
||||
std::optional<std::string> address_line_2{};
|
||||
std::string postal_code{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief A user's address. Owns the `City` it belongs to alongside its
|
||||
* street-level fields. Mirrors the `user_addresses` table.
|
||||
*
|
||||
* Only `city` is populated today; the street-level columns exist for future
|
||||
* enrichment.
|
||||
*/
|
||||
struct UserAddress {
|
||||
City city{};
|
||||
std::optional<std::string> address_line_1{};
|
||||
std::optional<std::string> address_line_2{};
|
||||
std::optional<std::string> postal_code{};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Helper struct to store generated brewery data.
|
||||
*/
|
||||
struct BreweryRecord {
|
||||
Location location;
|
||||
BreweryAddress address;
|
||||
BreweryResult brewery;
|
||||
};
|
||||
|
||||
@@ -107,7 +138,7 @@ struct BreweryRecord {
|
||||
* consumer can register real accounts from the pipeline's SQLite export.
|
||||
*/
|
||||
struct UserRecord {
|
||||
Location location;
|
||||
UserAddress address{};
|
||||
UserResult user;
|
||||
std::string email{};
|
||||
std::string date_of_birth{};
|
||||
|
||||
@@ -22,13 +22,13 @@ class ILogger;
|
||||
namespace prog_opts = boost::program_options;
|
||||
|
||||
// ============================================================================
|
||||
// Location Models
|
||||
// City Models
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Canonical location record for city-level generation.
|
||||
*/
|
||||
struct Location {
|
||||
struct City {
|
||||
std::string city{};
|
||||
std::string state_province{};
|
||||
|
||||
@@ -44,20 +44,22 @@ struct Location {
|
||||
*/
|
||||
std::string iso3166_1{};
|
||||
|
||||
/**
|
||||
* @brief A list of regular expressions used for valid postal codes in the
|
||||
* region.
|
||||
*
|
||||
*/
|
||||
std::vector<std::string> postal_regex{};
|
||||
|
||||
/**
|
||||
* @brief Example postal codes for the region.
|
||||
*/
|
||||
std::vector<std::string> postal_code_examples{};
|
||||
|
||||
/**
|
||||
* @brief Local language codes in priority order.
|
||||
*/
|
||||
std::vector<std::string> local_languages{};
|
||||
|
||||
/**
|
||||
* @brief Latitude in decimal degrees.
|
||||
*/
|
||||
double latitude{};
|
||||
|
||||
/**
|
||||
* @brief Longitude in decimal degrees.
|
||||
*/
|
||||
double longitude{};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
using ForenameList = std::vector<ForenameEntry>;
|
||||
using SurnameList = std::vector<std::string>;
|
||||
using LocationsList = std::vector<Location>;
|
||||
using CityList = std::vector<City>;
|
||||
using PersonasList = std::vector<UserPersona>;
|
||||
using ForenamesByCountryMap = std::unordered_map<std::string, ForenameList>;
|
||||
using SurnamesByCountryMap = std::unordered_map<std::string, SurnameList>;
|
||||
@@ -37,7 +37,7 @@ class ICuratedDataService {
|
||||
/**
|
||||
* @brief Loads all curated location records.
|
||||
*/
|
||||
virtual const LocationsList& LoadLocations() = 0;
|
||||
virtual const CityList& LoadCities() = 0;
|
||||
|
||||
/**
|
||||
* @brief Loads all curated persona records.
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* CuratedJsonDataService.
|
||||
*/
|
||||
struct CuratedDataFilePaths {
|
||||
std::filesystem::path locations_path;
|
||||
std::filesystem::path cities_path;
|
||||
std::filesystem::path personas_path;
|
||||
std::filesystem::path forenames_path;
|
||||
std::filesystem::path surnames_path;
|
||||
@@ -27,7 +27,7 @@ struct CuratedDataFilePaths {
|
||||
*/
|
||||
class CuratedJsonDataService final : public ICuratedDataService {
|
||||
struct cache {
|
||||
LocationsList locations;
|
||||
CityList locations;
|
||||
PersonasList personas;
|
||||
ForenamesByCountryMap forenames_by_country;
|
||||
SurnamesByCountryMap surnames_by_country;
|
||||
@@ -43,9 +43,9 @@ class CuratedJsonDataService final : public ICuratedDataService {
|
||||
explicit CuratedJsonDataService(CuratedDataFilePaths filepaths);
|
||||
|
||||
/**
|
||||
* @brief Parses a JSON array file and returns all location records.
|
||||
* @brief Parses a JSON array file and returns all city records.
|
||||
*/
|
||||
const LocationsList& LoadLocations() override;
|
||||
const CityList& LoadCities() override;
|
||||
|
||||
/**
|
||||
* @brief Parses a JSON array file and returns all persona records.
|
||||
|
||||
@@ -20,7 +20,7 @@ class MockCuratedDataService final : public ICuratedDataService {
|
||||
public:
|
||||
MockCuratedDataService();
|
||||
|
||||
const LocationsList& LoadLocations() override;
|
||||
const CityList& LoadCities() override;
|
||||
|
||||
const PersonasList& LoadPersonas() override;
|
||||
|
||||
@@ -29,7 +29,7 @@ class MockCuratedDataService final : public ICuratedDataService {
|
||||
const SurnamesByCountryMap& LoadSurnamesByCountry() override;
|
||||
|
||||
private:
|
||||
LocationsList locations_;
|
||||
CityList locations_;
|
||||
PersonasList personas_;
|
||||
ForenamesByCountryMap forenames_by_country_;
|
||||
SurnamesByCountryMap surnames_by_country_;
|
||||
|
||||
@@ -45,7 +45,7 @@ class SqliteExportService final : public IExportService {
|
||||
void RollbackAndCloseNoThrow() noexcept;
|
||||
|
||||
[[nodiscard]] std::filesystem::path BuildDatabasePath() const;
|
||||
[[nodiscard]] static std::string BuildLocationKey(const Location& location);
|
||||
[[nodiscard]] static std::string BuildCityKey(const City& location);
|
||||
|
||||
/**
|
||||
* @brief Returns the row id for @p location, inserting it first if it has
|
||||
@@ -54,18 +54,20 @@ class SqliteExportService final : public IExportService {
|
||||
* Shared by both ProcessRecord() overloads so breweries and users
|
||||
* referencing the same location resolve to the same row.
|
||||
*/
|
||||
[[nodiscard]] sqlite3_int64 ResolveLocationId(const Location& location);
|
||||
[[nodiscard]] sqlite3_int64 ResolveCityId(const City& location);
|
||||
|
||||
std::unique_ptr<IDateTimeProvider> date_time_provider_;
|
||||
std::filesystem::path output_path_;
|
||||
std::string run_timestamp_utc_;
|
||||
std::filesystem::path database_path_;
|
||||
SqliteDatabaseHandle db_handle_;
|
||||
SqliteStatementHandle insert_location_stmt_;
|
||||
SqliteStatementHandle insert_city_stmt_;
|
||||
SqliteStatementHandle insert_brewery_stmt_;
|
||||
SqliteStatementHandle insert_brewery_address_stmt_;
|
||||
SqliteStatementHandle insert_user_stmt_;
|
||||
SqliteStatementHandle insert_user_address_stmt_;
|
||||
bool transaction_open_ = false;
|
||||
std::unordered_map<std::string, sqlite3_int64> location_cache_;
|
||||
std::unordered_map<std::string, sqlite3_int64> city_cache_;
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_DATABASE_SQLITE_EXPORT_SERVICE_H_
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
namespace sqlite_export_service_internal {
|
||||
|
||||
inline constexpr std::string_view kCreateLocationsTableSql = R"sql(
|
||||
inline constexpr std::string_view kCreateCitiesTableSql = R"sql(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
CREATE TABLE IF NOT EXISTS cities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
city TEXT NOT NULL,
|
||||
state_province TEXT NOT NULL,
|
||||
@@ -27,9 +27,7 @@ CREATE TABLE IF NOT EXISTS locations (
|
||||
country TEXT NOT NULL,
|
||||
iso3166_1 TEXT NOT NULL,
|
||||
local_languages_json TEXT NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
UNIQUE(city, state_province, iso3166_2, country, latitude, longitude)
|
||||
UNIQUE(city, state_province, iso3166_2, country)
|
||||
);
|
||||
|
||||
)sql";
|
||||
@@ -38,23 +36,21 @@ inline constexpr std::string_view kCreateBreweriesTableSql = R"sql(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS breweries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location_id INTEGER NOT NULL,
|
||||
city_id INTEGER NOT NULL,
|
||||
name_en TEXT NOT NULL,
|
||||
description_en TEXT NOT NULL,
|
||||
name_local TEXT NOT NULL,
|
||||
description_local TEXT NOT NULL,
|
||||
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
|
||||
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_breweries_location_id ON breweries(location_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_breweries_city_id ON breweries(city_id);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kCreateUsersTableSql = R"sql(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
location_id INTEGER NOT NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
gender TEXT NOT NULL,
|
||||
@@ -62,30 +58,61 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
bio TEXT NOT NULL,
|
||||
activity_weight REAL NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
date_of_birth TEXT NOT NULL,
|
||||
FOREIGN KEY(location_id) REFERENCES locations(id) ON DELETE CASCADE
|
||||
date_of_birth TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_location_id ON users(location_id);
|
||||
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kInsertLocationSql = R"sql(
|
||||
INSERT INTO locations (
|
||||
inline constexpr std::string_view kCreateBreweryAddressTableSql = R"sql(
|
||||
CREATE TABLE IF NOT EXISTS brewery_addresses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
brewery_id INTEGER NOT NULL,
|
||||
address_line_1 TEXT,
|
||||
address_line_2 TEXT,
|
||||
postal_code TEXT NOT NULL,
|
||||
city_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(brewery_id) REFERENCES breweries(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_brewery_addresses_brewery_id
|
||||
ON brewery_addresses(brewery_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_brewery_addresses_city_id
|
||||
ON brewery_addresses(city_id);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kCreateUserAddressTableSql = R"sql(
|
||||
CREATE TABLE IF NOT EXISTS user_addresses (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
address_line_1 TEXT,
|
||||
address_line_2 TEXT,
|
||||
postal_code TEXT,
|
||||
city_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(city_id) REFERENCES cities(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_addresses_user_id
|
||||
ON user_addresses(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_addresses_city_id
|
||||
ON user_addresses(city_id);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kInsertCitySql = R"sql(
|
||||
INSERT INTO cities (
|
||||
city,
|
||||
state_province,
|
||||
iso3166_2,
|
||||
country,
|
||||
iso3166_1,
|
||||
local_languages_json,
|
||||
latitude,
|
||||
longitude
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
local_languages_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kInsertBrewerySql = R"sql(
|
||||
INSERT INTO breweries (
|
||||
location_id,
|
||||
city_id,
|
||||
name_en,
|
||||
description_en,
|
||||
name_local,
|
||||
@@ -95,7 +122,6 @@ INSERT INTO breweries (
|
||||
|
||||
inline constexpr std::string_view kInsertUserSql = R"sql(
|
||||
INSERT INTO users (
|
||||
location_id,
|
||||
first_name,
|
||||
last_name,
|
||||
gender,
|
||||
@@ -104,24 +130,37 @@ INSERT INTO users (
|
||||
activity_weight,
|
||||
email,
|
||||
date_of_birth
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kInsertBreweryAddressSql = R"sql(
|
||||
INSERT INTO brewery_addresses (
|
||||
brewery_id,
|
||||
postal_code,
|
||||
city_id
|
||||
) VALUES (?, ?, ?);
|
||||
)sql";
|
||||
|
||||
inline constexpr std::string_view kInsertUserAddressSql = R"sql(
|
||||
INSERT INTO user_addresses (
|
||||
user_id,
|
||||
city_id
|
||||
) VALUES (?, ?);
|
||||
)sql";
|
||||
|
||||
// sqlite3_bind_*() parameter indices are 1-based, matching the "?"
|
||||
// placeholder order in the SQL above.
|
||||
enum LocationBindIndex {
|
||||
kLocationCityBindIndex = 1,
|
||||
kLocationStateProvinceBindIndex,
|
||||
kLocationIso31662BindIndex,
|
||||
kLocationCountryBindIndex,
|
||||
kLocationIso31661BindIndex,
|
||||
kLocationLanguagesBindIndex,
|
||||
kLocationLatitudeBindIndex,
|
||||
kLocationLongitudeBindIndex,
|
||||
enum CityBindIndex {
|
||||
kCityNameBindIndex = 1,
|
||||
kCityStateProvinceBindIndex,
|
||||
kCityIso31662BindIndex,
|
||||
kCityCountryBindIndex,
|
||||
kCityIso31661BindIndex,
|
||||
kCityLanguagesBindIndex,
|
||||
};
|
||||
|
||||
enum BreweryBindIndex {
|
||||
kBreweryLocationIdBindIndex = 1,
|
||||
kBreweryCityIdBindIndex = 1,
|
||||
kBreweryEnglishNameBindIndex,
|
||||
kBreweryEnglishDescriptionBindIndex,
|
||||
kBreweryLocalNameBindIndex,
|
||||
@@ -129,8 +168,7 @@ enum BreweryBindIndex {
|
||||
};
|
||||
|
||||
enum UserBindIndex {
|
||||
kUserLocationIdBindIndex = 1,
|
||||
kUserFirstNameBindIndex,
|
||||
kUserFirstNameBindIndex = 1,
|
||||
kUserLastNameBindIndex,
|
||||
kUserGenderBindIndex,
|
||||
kUserUsernameBindIndex,
|
||||
@@ -140,6 +178,17 @@ enum UserBindIndex {
|
||||
kUserDateOfBirthBindIndex,
|
||||
};
|
||||
|
||||
enum BreweryAddressBindIndex {
|
||||
kBreweryAddressBreweryIdBindIndex = 1,
|
||||
kBreweryAddressPostalCodeBindIndex,
|
||||
kBreweryAddressCityIdBindIndex,
|
||||
};
|
||||
|
||||
enum UserAddressBindIndex {
|
||||
kUserAddressUserIdBindIndex = 1,
|
||||
kUserAddressCityIdBindIndex,
|
||||
};
|
||||
|
||||
SqliteStatementHandle PrepareStatement(const SqliteDatabaseHandle& db_handle,
|
||||
std::string_view sql,
|
||||
const char* action);
|
||||
|
||||
@@ -23,7 +23,7 @@ class IEnrichmentService {
|
||||
* @param loc Location to enrich.
|
||||
* @return Context text, or an empty string if unavailable.
|
||||
*/
|
||||
virtual std::string GetLocationContext(const Location& loc) = 0;
|
||||
virtual std::string GetLocationContext(const City& loc) = 0;
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_ENRICHMENT_ENRICHMENT_SERVICE_H_
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
class MockEnrichmentService final : public IEnrichmentService {
|
||||
public:
|
||||
std::string GetLocationContext(const Location& /*loc*/) override {
|
||||
std::string GetLocationContext(const City& /*loc*/) override {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ class WikipediaEnrichmentService final : public IEnrichmentService {
|
||||
/**
|
||||
* @brief Returns the Wikipedia-derived context for a location.
|
||||
*/
|
||||
[[nodiscard]] std::string GetLocationContext(const Location& loc) override;
|
||||
[[nodiscard]] std::string GetLocationContext(const City& loc) override;
|
||||
|
||||
private:
|
||||
std::string FetchExtract(std::string_view query);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_
|
||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_
|
||||
|
||||
/**
|
||||
* @file services/postal_code/mock_postal_code_service.h
|
||||
* @brief Placeholder IPostalCodeService used until xeger-based generation is
|
||||
* implemented.
|
||||
*/
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "data_model/models.h"
|
||||
#include "services/postal_code/postal_code_service.h"
|
||||
|
||||
/**
|
||||
* @brief Postal code service that returns the location's first known
|
||||
* example postal code rather than generating one from its regex.
|
||||
*/
|
||||
class MockPostalCodeService final : public IPostalCodeService {
|
||||
public:
|
||||
std::string GeneratePostalCode(const City& location) override {
|
||||
if (location.postal_code_examples.empty()) {
|
||||
throw std::runtime_error(
|
||||
"MockPostalCodeService: location has no postal_code_examples: " +
|
||||
location.city);
|
||||
}
|
||||
return location.postal_code_examples.front();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_MOCK_POSTAL_CODE_SERVICE_H_
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_
|
||||
#define BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_
|
||||
|
||||
/**
|
||||
* @file services/postal_code/postal_code_service.h
|
||||
* @brief Abstraction for generating a postal code for a location.
|
||||
*/
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "data_model/models.h"
|
||||
|
||||
/**
|
||||
* @brief Interface for services that generate a postal code string matching
|
||||
* a location's postal code format (`City::postal_regex`).
|
||||
*/
|
||||
class IPostalCodeService {
|
||||
public:
|
||||
virtual ~IPostalCodeService() = default;
|
||||
|
||||
/**
|
||||
* @brief Generates a postal code for the given location.
|
||||
*
|
||||
* @param location Location whose postal code format to generate for.
|
||||
* @return A postal code string matching `location.postal_regex`.
|
||||
*/
|
||||
virtual std::string GeneratePostalCode(const City& location) = 0;
|
||||
};
|
||||
|
||||
#endif // BIERGARTEN_PIPELINE_INCLUDES_SERVICES_POSTAL_CODE_POSTAL_CODE_SERVICE_H_
|
||||
@@ -52,7 +52,7 @@ WORKDIR /app/build
|
||||
COPY --from=builder /app/build/biergarten-pipeline ./
|
||||
|
||||
# Copy required config files
|
||||
COPY locations.json /app/build/
|
||||
COPY cities.json /app/build/
|
||||
COPY beer-styles.json /app/build/
|
||||
|
||||
# Copy prompt templates
|
||||
|
||||
@@ -7,16 +7,20 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "services/postal_code/postal_code_service.h"
|
||||
|
||||
BiergartenPipelineOrchestrator::BiergartenPipelineOrchestrator(
|
||||
std::shared_ptr<ILogger> logger,
|
||||
std::unique_ptr<IEnrichmentService> context_service,
|
||||
std::unique_ptr<DataGenerator> generator,
|
||||
std::unique_ptr<IExportService> exporter,
|
||||
std::unique_ptr<ICuratedDataService> curated_data_service,
|
||||
std::unique_ptr<IPostalCodeService> postal_code_service,
|
||||
const ApplicationOptions& application_options)
|
||||
: logger_(std::move(logger)),
|
||||
context_service_(std::move(context_service)),
|
||||
generator_(std::move(generator)),
|
||||
exporter_(std::move(exporter)),
|
||||
curated_data_service_(std::move(curated_data_service)),
|
||||
postal_code_service_(std::move(postal_code_service)),
|
||||
application_options_(application_options) {}
|
||||
@@ -22,12 +22,17 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
|
||||
|
||||
const auto generate_record =
|
||||
[this, &skipped_count](
|
||||
const Location& location,
|
||||
const City& location,
|
||||
const std::string& region_context) -> std::optional<BreweryRecord> {
|
||||
try {
|
||||
const BreweryResult brewery =
|
||||
generator_->GenerateBrewery(location, region_context);
|
||||
return BreweryRecord{.location = location, .brewery = brewery};
|
||||
const std::string postal_code =
|
||||
postal_code_service_->GeneratePostalCode(location);
|
||||
return BreweryRecord{
|
||||
.address =
|
||||
BreweryAddress{.city = location, .postal_code = postal_code},
|
||||
.brewery = brewery};
|
||||
} catch (const std::exception& e) {
|
||||
++skipped_count;
|
||||
|
||||
@@ -47,13 +52,13 @@ void BiergartenPipelineOrchestrator::GenerateBreweries(
|
||||
exporter_->ProcessRecord(record);
|
||||
} catch (const std::exception& export_exception) {
|
||||
++export_failed_count;
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||
.message = std::format("[Pipeline] Generated brewery for '{}' ({}) "
|
||||
"but SQLite export failed: {}",
|
||||
record.location.city, record.location.country,
|
||||
export_exception.what())});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::BreweryAndBeerGeneration,
|
||||
.message = std::format(
|
||||
"[Pipeline] Generated brewery for '{}' ({}) "
|
||||
"but SQLite export failed: {}",
|
||||
record.address.city.city, record.address.city.country,
|
||||
export_exception.what())});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
|
||||
generator_->GenerateUser(city, persona, sampled_name);
|
||||
|
||||
return UserRecord{
|
||||
.location = city.location,
|
||||
.address = UserAddress{.city = city.location},
|
||||
.user = user,
|
||||
.email = BuildEmail(sampled_name, used_email_local_parts),
|
||||
.date_of_birth = GenerateDateOfBirth(rng),
|
||||
@@ -161,13 +161,13 @@ void BiergartenPipelineOrchestrator::GenerateUsers(
|
||||
} catch (const std::exception& export_exception) {
|
||||
++export_failed_count;
|
||||
|
||||
logger_->Log(
|
||||
{.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format("[Pipeline] Generated user for '{}' ({}) but "
|
||||
"SQLite export failed: {}",
|
||||
record.location.city, record.location.country,
|
||||
export_exception.what())});
|
||||
logger_->Log({.level = LogLevel::Warn,
|
||||
.phase = PipelinePhase::UserGeneration,
|
||||
.message = std::format(
|
||||
"[Pipeline] Generated user for '{}' ({}) but "
|
||||
"SQLite export failed: {}",
|
||||
record.address.city.city, record.address.city.country,
|
||||
export_exception.what())});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,19 +14,19 @@
|
||||
void BiergartenPipelineOrchestrator::LogResults() const {
|
||||
boost::json::array brewery_output;
|
||||
|
||||
for (const auto& [location, brewery] : generated_breweries_) {
|
||||
for (const auto& [address, brewery] : generated_breweries_) {
|
||||
const City& location = address.city;
|
||||
brewery_output.push_back(boost::json::object{
|
||||
{"name_en", brewery.name_en},
|
||||
{"description_en", brewery.description_en},
|
||||
{"name_local", brewery.name_local},
|
||||
{"description_local", brewery.description_local},
|
||||
{"postal_code", address.postal_code},
|
||||
{"location", boost::json::object{
|
||||
{"city", location.city},
|
||||
{"country", location.country},
|
||||
{"state_province", location.state_province},
|
||||
{"iso3166_2", location.iso3166_2},
|
||||
{"latitude", location.latitude},
|
||||
{"longitude", location.longitude},
|
||||
}}});
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
#include "services/curated_data/curated_json_data_service.h"
|
||||
#include "services/logging/logger.h"
|
||||
|
||||
std::vector<Location>
|
||||
std::vector<City>
|
||||
BiergartenPipelineOrchestrator::QueryCitiesWithCountries() {
|
||||
logger_->Log({.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::Startup,
|
||||
.message = "=== GEOGRAPHIC DATA OVERVIEW ==="});
|
||||
|
||||
const std::vector<Location>& all_locations =
|
||||
curated_data_service_->LoadLocations();
|
||||
const std::vector<City>& all_locations =
|
||||
curated_data_service_->LoadCities();
|
||||
|
||||
const size_t sample_count = std::min(
|
||||
static_cast<size_t>(application_options_.pipeline.location_count),
|
||||
@@ -31,7 +31,7 @@ BiergartenPipelineOrchestrator::QueryCitiesWithCountries() {
|
||||
static_cast<std::iter_difference_t<decltype(all_locations.cbegin())>>(
|
||||
sample_count);
|
||||
|
||||
std::vector<Location> sampled_locations;
|
||||
std::vector<City> sampled_locations;
|
||||
sampled_locations.reserve(sample_count);
|
||||
|
||||
std::random_device random_generator;
|
||||
|
||||
@@ -14,7 +14,7 @@ bool BiergartenPipelineOrchestrator::Run() {
|
||||
try {
|
||||
exporter_->Initialize();
|
||||
|
||||
std::vector<Location> cities = QueryCitiesWithCountries();
|
||||
std::vector<City> cities = QueryCitiesWithCountries();
|
||||
std::vector<EnrichedCity> enriched;
|
||||
enriched.reserve(cities.size());
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ static std::string FormatLocalLanguageCodes(
|
||||
static constexpr int kBreweryInitialMaxTokens = 2800;
|
||||
|
||||
BreweryResult LlamaGenerator::GenerateBrewery(
|
||||
const Location& location, const std::string& region_context) {
|
||||
const City& location, const std::string& region_context) {
|
||||
/**
|
||||
* Preprocess and truncate region context to manageable size
|
||||
*/
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
#include "data_generation/mock_generator.h"
|
||||
|
||||
size_t MockGenerator::DeterministicHash(const Location& location) {
|
||||
size_t MockGenerator::DeterministicHash(const City& location) {
|
||||
size_t seed = 0;
|
||||
boost::hash_combine(seed, location.city);
|
||||
boost::hash_combine(seed, location.country);
|
||||
return seed;
|
||||
}
|
||||
|
||||
size_t MockGenerator::DeterministicHash(const Location& location,
|
||||
size_t MockGenerator::DeterministicHash(const City& location,
|
||||
const UserPersona& persona,
|
||||
const Name& name) {
|
||||
size_t seed = DeterministicHash(location);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "data_generation/mock_generator.h"
|
||||
|
||||
BreweryResult MockGenerator::GenerateBrewery(
|
||||
const Location& location, const std::string& /*region_context*/) {
|
||||
const City& location, const std::string& /*region_context*/) {
|
||||
const size_t hash = DeterministicHash(location);
|
||||
|
||||
const std::string_view adjective =
|
||||
|
||||
@@ -102,12 +102,14 @@ int main(const int argc, char** argv) {
|
||||
|
||||
return std::make_unique<CuratedJsonDataService>(
|
||||
CuratedDataFilePaths{
|
||||
.locations_path = "locations.json",
|
||||
.cities_path = "cities.json",
|
||||
.personas_path = "personas.json",
|
||||
.forenames_path = "forenames-by-country.json",
|
||||
.surnames_path = "surnames-by-country.json",
|
||||
});
|
||||
}),
|
||||
di::bind<IPostalCodeService>().to<MockPostalCodeService>(),
|
||||
|
||||
di::bind<IPromptFormatter>().to([options, log_producer] {
|
||||
if (options.generator.use_mocked) {
|
||||
{
|
||||
@@ -174,6 +176,14 @@ int main(const int argc, char** argv) {
|
||||
return std::make_unique<MockGenerator>();
|
||||
}
|
||||
|
||||
#ifdef BIERGARTEN_MOCK_ONLY
|
||||
// Unreachable: ParseArguments requires --mocked when
|
||||
// BIERGARTEN_MOCK_ONLY is compiled in, so LlamaGenerator is
|
||||
// never constructed and is not linked into this binary.
|
||||
throw std::runtime_error(
|
||||
"LlamaGenerator is unavailable in a BIERGARTEN_MOCK_ONLY "
|
||||
"build");
|
||||
#else
|
||||
log_producer->Log(
|
||||
{.level = LogLevel::Info,
|
||||
.phase = PipelinePhase::Startup,
|
||||
@@ -187,6 +197,7 @@ int main(const int argc, char** argv) {
|
||||
options, model_path, log_producer,
|
||||
inj.template create<std::unique_ptr<IPromptFormatter>>(),
|
||||
std::move(prompt_directory));
|
||||
#endif
|
||||
}));
|
||||
|
||||
const auto orchestrator =
|
||||
@@ -217,4 +228,4 @@ int main(const int argc, char** argv) {
|
||||
|
||||
return shutdown(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file json_handling/json_loader.cc
|
||||
* @brief Parses curated location JSON input into strongly typed Location
|
||||
* @brief Parses curated location JSON input into strongly typed City
|
||||
* records with strict field validation and descriptive error reporting.
|
||||
*/
|
||||
|
||||
@@ -109,41 +109,68 @@ std::string ReadFirstOfStringArray(const boost::json::object& object,
|
||||
CuratedJsonDataService::CuratedJsonDataService(CuratedDataFilePaths filepaths)
|
||||
: filepaths_(std::move(filepaths)) {}
|
||||
|
||||
const LocationsList& CuratedJsonDataService::LoadLocations() {
|
||||
const CityList& CuratedJsonDataService::LoadCities() {
|
||||
if (!cache_.locations.empty()) {
|
||||
return cache_.locations;
|
||||
}
|
||||
|
||||
const boost::json::value root =
|
||||
ParseJsonFile(filepaths_.locations_path, "locations");
|
||||
ParseJsonFile(filepaths_.cities_path, "cities");
|
||||
|
||||
if (!root.is_array()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid locations JSON: root element must be an array");
|
||||
"Invalid cities JSON: root element must be an array");
|
||||
}
|
||||
|
||||
LocationsList locations;
|
||||
CityList locations;
|
||||
const auto& items = root.as_array();
|
||||
locations.reserve(items.size());
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (!item.is_object()) {
|
||||
throw std::runtime_error(
|
||||
"Invalid locations JSON: each entry must be an object");
|
||||
"Invalid cities JSON: each entry must be an object");
|
||||
}
|
||||
|
||||
const auto& object = item.as_object();
|
||||
locations.push_back(Location{
|
||||
|
||||
const boost::json::value* postal_code = object.if_contains("postal_code");
|
||||
if (postal_code == nullptr || !postal_code->is_object()) {
|
||||
throw std::runtime_error("Missing or invalid object field: postal_code");
|
||||
}
|
||||
|
||||
locations.push_back(City{
|
||||
.city = ReadRequiredString(object, "city"),
|
||||
.state_province = ReadRequiredString(object, "state_province"),
|
||||
.iso3166_2 = ReadRequiredString(object, "iso3166_2"),
|
||||
.country = ReadRequiredString(object, "country"),
|
||||
.iso3166_1 = ReadRequiredString(object, "iso3166_1"),
|
||||
.local_languages = ReadRequiredStringArray(object, "local_languages"),
|
||||
.latitude = ReadRequiredNumber(object, "latitude"),
|
||||
.longitude = ReadRequiredNumber(object, "longitude"),
|
||||
.postal_regex =
|
||||
ReadRequiredStringArray(postal_code->as_object(), "city_regex"),
|
||||
.postal_code_examples =
|
||||
ReadRequiredStringArray(postal_code->as_object(), "examples"),
|
||||
});
|
||||
}
|
||||
|
||||
for (auto location : locations) {
|
||||
std::cout << "Location: " << location.city << ", "
|
||||
<< location.state_province << ", " << location.iso3166_2 << ", "
|
||||
<< location.country << ", " << location.iso3166_1 << "\n";
|
||||
|
||||
for (const auto& regex : location.postal_regex) {
|
||||
std::cout << " Postal regex: " << regex << "\n";
|
||||
}
|
||||
|
||||
for (const auto& example : location.postal_code_examples) {
|
||||
std::cout << " Postal example: " << example << "\n";
|
||||
}
|
||||
|
||||
for (const auto& lang : location.local_languages) {
|
||||
std::cout << " Local language: " << lang << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
cache_.locations = std::move(locations);
|
||||
return cache_.locations;
|
||||
}
|
||||
|
||||
@@ -7,38 +7,38 @@
|
||||
|
||||
MockCuratedDataService::MockCuratedDataService()
|
||||
: locations_{
|
||||
Location{.city = "Portland",
|
||||
.state_province = "Oregon",
|
||||
.iso3166_2 = "US-OR",
|
||||
.country = "United States",
|
||||
.iso3166_1 = "US",
|
||||
.local_languages = {"en"},
|
||||
.latitude = 45.5152,
|
||||
.longitude = -122.6784},
|
||||
Location{.city = "Munich",
|
||||
.state_province = "Bavaria",
|
||||
.iso3166_2 = "DE-BY",
|
||||
.country = "Germany",
|
||||
.iso3166_1 = "DE",
|
||||
.local_languages = {"de"},
|
||||
.latitude = 48.1351,
|
||||
.longitude = 11.5820},
|
||||
Location{.city = "Lyon",
|
||||
.state_province = "Auvergne-Rhone-Alpes",
|
||||
.iso3166_2 = "FR-ARA",
|
||||
.country = "France",
|
||||
.iso3166_1 = "FR",
|
||||
.local_languages = {"fr"},
|
||||
.latitude = 45.7640,
|
||||
.longitude = 4.8357},
|
||||
Location{.city = "Brussels",
|
||||
.state_province = "Brussels-Capital",
|
||||
.iso3166_2 = "BE-BRU",
|
||||
.country = "Belgium",
|
||||
.iso3166_1 = "BE",
|
||||
.local_languages = {"nl", "fr"},
|
||||
.latitude = 50.8503,
|
||||
.longitude = 4.3517},
|
||||
{.city = "Portland",
|
||||
.state_province = "Oregon",
|
||||
.iso3166_2 = "US-OR",
|
||||
.country = "United States",
|
||||
.iso3166_1 = "US",
|
||||
.local_languages = {"en"},
|
||||
.postal_regex = {"^972[0-9]{2}(?:-[0-9]{4})?$"},
|
||||
.postal_code_examples = {"97201", "97294"}},
|
||||
{.city = "Munich",
|
||||
.state_province = "Bavaria",
|
||||
.iso3166_2 = "DE-BY",
|
||||
.country = "Germany",
|
||||
.iso3166_1 = "DE",
|
||||
.local_languages = {"de"},
|
||||
.postal_regex = {"^8[01][0-9]{3}$"},
|
||||
.postal_code_examples = {"80331", "81929"}},
|
||||
{.city = "Lyon",
|
||||
.state_province = "Auvergne-Rhone-Alpes",
|
||||
.iso3166_2 = "FR-ARA",
|
||||
.country = "France",
|
||||
.iso3166_1 = "FR",
|
||||
.local_languages = {"fr"},
|
||||
.postal_regex = {"^6900[1-9]$"},
|
||||
.postal_code_examples = {"69001", "69009"}},
|
||||
{.city = "Brussels",
|
||||
.state_province = "Brussels-Capital",
|
||||
.iso3166_2 = "BE-BRU",
|
||||
.country = "Belgium",
|
||||
.iso3166_1 = "BE",
|
||||
.local_languages = {"nl", "fr"},
|
||||
.postal_regex = {"^1(?:0[0-9]{2}|1[0-9]{2}|20[0-9]|210)$"},
|
||||
.postal_code_examples = {"1000", "1210"}},
|
||||
},
|
||||
personas_{
|
||||
UserPersona{.name = "Hophead Explorer",
|
||||
@@ -83,9 +83,7 @@ MockCuratedDataService::MockCuratedDataService()
|
||||
{"BE", SurnameList{"Peeters", "Janssens"}},
|
||||
} {}
|
||||
|
||||
const LocationsList& MockCuratedDataService::LoadLocations() {
|
||||
return locations_;
|
||||
}
|
||||
const CityList& MockCuratedDataService::LoadCities() { return locations_; }
|
||||
|
||||
const PersonasList& MockCuratedDataService::LoadPersonas() { return personas_; }
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "services/enrichment/wikipedia_service.h"
|
||||
|
||||
std::string WikipediaEnrichmentService::GetLocationContext(
|
||||
const Location& loc) {
|
||||
const City& loc) {
|
||||
using namespace std::literals::chrono_literals;
|
||||
if (!this->client_) {
|
||||
if (logger_) {
|
||||
|
||||
@@ -14,9 +14,11 @@ void SqliteExportService::Finalize() {
|
||||
}
|
||||
|
||||
try {
|
||||
insert_user_address_stmt_.reset();
|
||||
insert_user_stmt_.reset();
|
||||
insert_brewery_address_stmt_.reset();
|
||||
insert_brewery_stmt_.reset();
|
||||
insert_location_stmt_.reset();
|
||||
insert_city_stmt_.reset();
|
||||
if (transaction_open_) {
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, "COMMIT;", "Failed to commit SQLite transaction");
|
||||
@@ -24,7 +26,7 @@ void SqliteExportService::Finalize() {
|
||||
}
|
||||
|
||||
db_handle_.reset();
|
||||
location_cache_.clear();
|
||||
city_cache_.clear();
|
||||
} catch (...) {
|
||||
RollbackAndCloseNoThrow();
|
||||
throw;
|
||||
|
||||
@@ -28,26 +28,39 @@ std::filesystem::path SqliteExportService::BuildDatabasePath() const {
|
||||
|
||||
void SqliteExportService::InitializeSchema() const {
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateLocationsTableSql,
|
||||
"Failed to create SQLite locations table");
|
||||
db_handle_, sqlite_export_service_internal::kCreateCitiesTableSql,
|
||||
"Failed to create SQLite cities table");
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateBreweriesTableSql,
|
||||
"Failed to create SQLite breweries table");
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateUsersTableSql,
|
||||
"Failed to create SQLite users table");
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateBreweryAddressTableSql,
|
||||
"Failed to create SQLite brewery addresses table");
|
||||
sqlite_export_service_internal::ExecSql(
|
||||
db_handle_, sqlite_export_service_internal::kCreateUserAddressTableSql,
|
||||
"Failed to create SQLite user addresses table");
|
||||
}
|
||||
|
||||
void SqliteExportService::PrepareStatements() {
|
||||
insert_location_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertLocationSql,
|
||||
"Failed to prepare SQLite location insert statement");
|
||||
insert_city_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertCitySql,
|
||||
"Failed to prepare SQLite city insert statement");
|
||||
insert_brewery_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertBrewerySql,
|
||||
"Failed to prepare SQLite brewery insert statement");
|
||||
insert_brewery_address_stmt_ =
|
||||
sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertBreweryAddressSql,
|
||||
"Failed to prepare SQLite brewery address insert statement");
|
||||
insert_user_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertUserSql,
|
||||
"Failed to prepare SQLite user insert statement");
|
||||
insert_user_address_stmt_ = sqlite_export_service_internal::PrepareStatement(
|
||||
db_handle_, sqlite_export_service_internal::kInsertUserAddressSql,
|
||||
"Failed to prepare SQLite user address insert statement");
|
||||
}
|
||||
|
||||
void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
|
||||
@@ -60,11 +73,13 @@ void SqliteExportService::RollbackAndCloseNoThrow() noexcept {
|
||||
transaction_open_ = false;
|
||||
}
|
||||
|
||||
insert_user_address_stmt_.reset();
|
||||
insert_user_stmt_.reset();
|
||||
insert_brewery_address_stmt_.reset();
|
||||
insert_brewery_stmt_.reset();
|
||||
insert_location_stmt_.reset();
|
||||
insert_city_stmt_.reset();
|
||||
db_handle_.reset();
|
||||
location_cache_.clear();
|
||||
city_cache_.clear();
|
||||
}
|
||||
|
||||
void SqliteExportService::Initialize() {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/**
|
||||
* @file services/sqlite/process_record.cc
|
||||
* @brief SqliteExportService::ProcessRecord() implementation
|
||||
* and the shared location-resolution helper.
|
||||
* and the shared city-resolution helper.
|
||||
*/
|
||||
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -12,94 +11,74 @@
|
||||
#include "services/database/sqlite_export_service.h"
|
||||
#include "services/database/sqlite_export_service_helpers.h"
|
||||
|
||||
constexpr int kLocationPrecision = 17;
|
||||
|
||||
std::string SqliteExportService::BuildLocationKey(const Location& location) {
|
||||
std::string SqliteExportService::BuildCityKey(const City& location) {
|
||||
std::ostringstream key_stream;
|
||||
key_stream << location.city << '\n'
|
||||
<< location.state_province << '\n'
|
||||
<< location.iso3166_2 << '\n'
|
||||
<< location.country << '\n'
|
||||
<< location.iso3166_1 << '\n'
|
||||
<< std::setprecision(kLocationPrecision) << location.latitude
|
||||
<< '\n'
|
||||
<< std::setprecision(kLocationPrecision) << location.longitude
|
||||
<< '\n'
|
||||
<< sqlite_export_service_internal::SerializeVector(
|
||||
location.local_languages);
|
||||
return key_stream.str();
|
||||
}
|
||||
|
||||
sqlite3_int64 SqliteExportService::ResolveLocationId(const Location& location) {
|
||||
const std::string location_key = BuildLocationKey(location);
|
||||
const auto cached_location = location_cache_.find(location_key);
|
||||
if (cached_location != location_cache_.end()) {
|
||||
return cached_location->second;
|
||||
sqlite3_int64 SqliteExportService::ResolveCityId(const City& location) {
|
||||
const std::string city_key = BuildCityKey(location);
|
||||
const auto cached_city = city_cache_.find(city_key);
|
||||
if (cached_city != city_cache_.end()) {
|
||||
return cached_city->second;
|
||||
}
|
||||
|
||||
const std::string local_languages_json =
|
||||
sqlite_export_service_internal::SerializeVector(location.local_languages);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCityBindIndex,
|
||||
.index = sqlite_export_service_internal::kCityNameBindIndex,
|
||||
.value = location.city,
|
||||
.action = "Failed to bind SQLite location city"});
|
||||
.action = "Failed to bind SQLite city name"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kLocationStateProvinceBindIndex,
|
||||
.index = sqlite_export_service_internal::kCityStateProvinceBindIndex,
|
||||
.value = location.state_province,
|
||||
.action = "Failed to bind SQLite location state/province"});
|
||||
.action = "Failed to bind SQLite city state/province"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31662BindIndex,
|
||||
.index = sqlite_export_service_internal::kCityIso31662BindIndex,
|
||||
.value = location.iso3166_2,
|
||||
.action = "Failed to bind SQLite location ISO 3166-2 code"});
|
||||
.action = "Failed to bind SQLite city ISO 3166-2 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationCountryBindIndex,
|
||||
.index = sqlite_export_service_internal::kCityCountryBindIndex,
|
||||
.value = location.country,
|
||||
.action = "Failed to bind SQLite location country"});
|
||||
.action = "Failed to bind SQLite city country"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationIso31661BindIndex,
|
||||
.index = sqlite_export_service_internal::kCityIso31661BindIndex,
|
||||
.value = location.iso3166_1,
|
||||
.action = "Failed to bind SQLite location ISO 3166-1 code"});
|
||||
.action = "Failed to bind SQLite city ISO 3166-1 code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
insert_city_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::kLocationLanguagesBindIndex,
|
||||
.index = sqlite_export_service_internal::kCityLanguagesBindIndex,
|
||||
.value = local_languages_json,
|
||||
.action = "Failed to bind SQLite location languages"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam{
|
||||
.index = sqlite_export_service_internal::kLocationLatitudeBindIndex,
|
||||
.value = location.latitude,
|
||||
.action = "Failed to bind SQLite location latitude"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_location_stmt_,
|
||||
sqlite_export_service_internal::BoundParam{
|
||||
.index = sqlite_export_service_internal::kLocationLongitudeBindIndex,
|
||||
.value = location.longitude,
|
||||
.action = "Failed to bind SQLite location longitude"});
|
||||
.action = "Failed to bind SQLite city languages"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_location_stmt_,
|
||||
"Failed to insert SQLite location row");
|
||||
db_handle_, insert_city_stmt_, "Failed to insert SQLite city row");
|
||||
|
||||
const sqlite3_int64 location_id =
|
||||
const sqlite3_int64 city_id =
|
||||
sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
location_cache_.emplace(location_key, location_id);
|
||||
sqlite_export_service_internal::ResetStatement(insert_location_stmt_);
|
||||
city_cache_.emplace(city_key, city_id);
|
||||
sqlite_export_service_internal::ResetStatement(insert_city_stmt_);
|
||||
|
||||
return location_id;
|
||||
return city_id;
|
||||
}
|
||||
|
||||
uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
|
||||
@@ -107,14 +86,14 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
|
||||
throw std::runtime_error("SQLite export service is not initialized");
|
||||
}
|
||||
|
||||
const sqlite3_int64 location_id = ResolveLocationId(brewery.location);
|
||||
const sqlite3_int64 city_id = ResolveCityId(brewery.address.city);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kBreweryLocationIdBindIndex,
|
||||
.value = location_id,
|
||||
.action = "Failed to bind SQLite brewery location id"});
|
||||
.index = sqlite_export_service_internal::kBreweryCityIdBindIndex,
|
||||
.value = city_id,
|
||||
.action = "Failed to bind SQLite brewery city id"});
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_stmt_,
|
||||
@@ -149,7 +128,37 @@ uint64_t SqliteExportService::ProcessRecord(const BreweryRecord& brewery) {
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_brewery_stmt_, "Failed to insert SQLite brewery row");
|
||||
|
||||
const sqlite3_int64 brewery_id =
|
||||
sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
sqlite_export_service_internal::ResetStatement(insert_brewery_stmt_);
|
||||
|
||||
return sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_address_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kBreweryAddressBreweryIdBindIndex,
|
||||
.value = brewery_id,
|
||||
.action = "Failed to bind SQLite brewery address brewery id"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_address_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
.index = sqlite_export_service_internal::
|
||||
kBreweryAddressPostalCodeBindIndex,
|
||||
.value = brewery.address.postal_code,
|
||||
.action = "Failed to bind SQLite brewery address postal code"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_brewery_address_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index =
|
||||
sqlite_export_service_internal::kBreweryAddressCityIdBindIndex,
|
||||
.value = city_id,
|
||||
.action = "Failed to bind SQLite brewery address city id"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_brewery_address_stmt_,
|
||||
"Failed to insert SQLite brewery address row");
|
||||
|
||||
sqlite_export_service_internal::ResetStatement(insert_brewery_address_stmt_);
|
||||
|
||||
return brewery_id;
|
||||
}
|
||||
|
||||
@@ -13,14 +13,8 @@ uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
|
||||
throw std::runtime_error("SQLite export service is not initialized");
|
||||
}
|
||||
|
||||
const sqlite3_int64 location_id = ResolveLocationId(user.location);
|
||||
const sqlite3_int64 city_id = ResolveCityId(user.address.city);
|
||||
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kUserLocationIdBindIndex,
|
||||
.value = location_id,
|
||||
.action = "Failed to bind SQLite user location id"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<std::string_view>{
|
||||
@@ -73,7 +67,28 @@ uint64_t SqliteExportService::ProcessRecord(const UserRecord& user) {
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_user_stmt_, "Failed to insert SQLite user row");
|
||||
|
||||
const sqlite3_int64 user_id =
|
||||
sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
sqlite_export_service_internal::ResetStatement(insert_user_stmt_);
|
||||
|
||||
return sqlite_export_service_internal::LastInsertRowId(db_handle_);
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_address_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kUserAddressUserIdBindIndex,
|
||||
.value = user_id,
|
||||
.action = "Failed to bind SQLite user address user id"});
|
||||
sqlite_export_service_internal::Bind(
|
||||
insert_user_address_stmt_,
|
||||
sqlite_export_service_internal::BoundParam<sqlite3_int64>{
|
||||
.index = sqlite_export_service_internal::kUserAddressCityIdBindIndex,
|
||||
.value = city_id,
|
||||
.action = "Failed to bind SQLite user address city id"});
|
||||
|
||||
sqlite_export_service_internal::StepStatement(
|
||||
db_handle_, insert_user_address_stmt_,
|
||||
"Failed to insert SQLite user address row");
|
||||
|
||||
sqlite_export_service_internal::ResetStatement(insert_user_address_stmt_);
|
||||
|
||||
return user_id;
|
||||
}
|
||||
|
||||