Skip to content

db: ordered migrations, user_items table, and shared inventory helpers - #32

Open
Seltraeh wants to merge 5 commits into
decompfrontier:devfrom
Seltraeh:split/03-schema-foundation
Open

db: ordered migrations, user_items table, and shared inventory helpers#32
Seltraeh wants to merge 5 commits into
decompfrontier:devfrom
Seltraeh:split/03-schema-foundation

Conversation

@Seltraeh

@Seltraeh Seltraeh commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

db: ordered migrations, user_items table, and shared inventory helpers

Branch: split/03-schema-foundation
Base: dev
Merge position: 03 of 13

Part of the PR #28 split. Each PR branches from dev and contains only its
own changes, so this diff is exactly one subsystem. The set is designed to be
merged in numeric order; merging all 13 reproduces PR #28 byte for byte
(verified against tree 79a4e065).

Later PRs in the series touch Handlers.hpp, GmeControllerHandlers.cpp and
UserInfo.cpp too, so once earlier ones land this branch may need a rebase.
Those conflicts are always additions on both sides — keep both. Maintainer
edits are enabled, so feel free to push the rebase directly to this branch.

Three files, but the highest-risk change in the whole series. Everything downstream depends on these table shapes.

What's included

Migration ordering. Migrations lived in an unordered_map and therefore ran in hash order. They are now a vector, running in declaration order. Because a vector does not dedup, a startup guard aborts on a duplicate migration name, preserving the uniqueness the map gave for free.

02072026_ExtendUserUnitsForUnitOps — 26 additive columns on user_units: per-unit stats, two sphere equipment slots, and the favourite flag. Consolidates three earlier fork migrations onto the upstream table shape.

05072026_CreateUserItemsTable — one row per item stack, UNIQUE(user_id, item_id), with instance_id as the warehouse row id the client references.

Structural tables for the later PRs — town, campaign, scenario and summon-ticket tables land here too, so MigrationManager.cpp is touched once rather than by five branches. All are CREATE TABLE IF NOT EXISTS, structural only, never seeded. Say the word if you'd rather each subsystem carried its own and I'll redistribute.

Common.hpp helpers

  • addUserItem() — upserts a stack, incrementing item_num when the species is already owned.
  • returnEquippedSpheres() — reads sphere slots off units about to be deleted and credits them back to the warehouse. Must be called before the DELETE in any handler that consumes units.

Known debt, flagging deliberately

The 26 user_units columns duplicate upstream's unit_lvl / base_rec / bb_*. Upstream columns remain the source of truth for upstream handlers; these serve handlers not yet on PacketInterface. The intent is to consolidate them away as each handler moves. If you'd rather that port happened before any of this merges, that's a reasonable call and I'd like to know now rather than later.

Verification

Fresh-DB boot runs every migration in declared order; second boot skips recorded ones. Duplicate-name guard tested by temporarily doubling an entry.

Migrations previously lived in an unordered_map and therefore ran in hash
order. They are now a vector so they run in declaration order, with a startup
guard that aborts on a duplicate migration name to preserve the uniqueness the
map gave for free.

Adds 02072026_ExtendUserUnitsForUnitOps: 26 additive columns on user_units
covering per-unit stats, the two sphere equipment slots and the favourite
flag. This consolidates three earlier fork migrations onto the upstream table
shape. Upstream columns (unit_lvl, base_rec, bb_*) remain the source of truth
for upstream handlers; these serve handlers not yet moved to PacketInterface
and are intended to be consolidated away as each one moves.

Adds 05072026_CreateUserItemsTable: one row per item stack, keyed
UNIQUE(user_id, item_id), with instance_id as the warehouse row id the client
references.

Common.hpp gains two helpers. addUserItem() upserts a stack, incrementing
item_num when the species is already owned. returnEquippedSpheres() reads the
sphere slots off units that are about to be deleted and credits them back to
the warehouse; it must be called before the DELETE in any handler that
consumes units, or the spheres are destroyed silently.
// shape. Upstream columns (unit_lvl/base_rec/ext_rec/bb_*) remain the
// source of truth for upstream handlers; these serve the not-yet-ported
// quests handlers and are consolidated away as each moves to
// PacketInterface.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont understand why this exists, a lot of it seems like duplicates of user units.

Comment thread gimuserver/db/PacketInterfaceSchemas.hpp Outdated
.update = true,
.insert = true,
}),
// Extras the read/display path was missing — mapped to the quests mirror

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i actually dont agree with adding everything here - it introduces bloat

for example, do we really need to add fe_bp? or leader_skill_id?

for each of these fields, we should only add them under 2 conditiions (this applies to every single other field as well)

  1. we understand fully what it does in the client
  2. the client needs it either for game functionailty or it crashes

Comment thread gimuserver/gme/common/Common.hpp Outdated
Comment thread gimuserver/gme/common/Common.hpp Outdated
}

/*!
* Returns any spheres equipped on soon-to-be-consumed units to the owner's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont understand the mechanism here - does the client delete the sphere if we fuse the unit or sell the unit?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we have to free up the sphere attached to a unit prior to it's destruction by any means.

Addresses the review comments on this PR ("dont use co_await
database->execSqlCoro(, use the existing API"). Both flagged call sites used
SQL the typed interface could not express, which is why they bypassed it, so
this extends the interface instead of routing around it.

  * DatabaseInterface::upsert(db, table, cells, conflict, accumulate)
    INSERT ... ON CONFLICT(...) DO UPDATE. Columns in `accumulate` are added
    to (col = col + excluded.col), the rest replaced. Plain insert only ever
    ignored conflicts, so addUserItem's "add to the stack I already own" had
    no expressible form.
  * db::LookupIn(name, values) -> WHERE col IN ($1, $2, ...), one placeholder
    per value; an empty list throws rather than letting a caller splice ids
    into SQL. read/update/remove now share a single buildWhere() that handles
    equality and IN with correct placeholder numbering.

addUserItem and returnEquippedSpheres now go through the interface.

Verified past compiling: the emitted statements were run against a copy of
deploy/gme.sqlite. Two grants of 3 then 4 leave item_num = 7, confirming the
conflict clause accumulates rather than replaces.

Two deliberate leftovers:

  * addDefaultDecks still uses execSqlCoro. It is a recursive CTE that
    generates ten deck rows in one INSERT...SELECT; the interface is
    row-oriented and cannot express it. Converting it would mean ten
    round trips to satisfy a rule, which is worse. Flagging rather than
    hiding it.
  * returnEquippedSpheres still takes the comma-joined id string its callers
    build for their own DELETEs and splits it back into bound values. That
    round trip disappears when those DELETEs move onto the interface.
Addresses both schema comments on this PR -- "a lot of it seems like
duplicates of user units" on the 02072026 migration, and "why the rename?" on
PacketInterfaceSchemas mapping ext_rec to "ext_heal". Same root cause.

The client names the same stat differently in different packets: UserUnitInfo
says base_rec / ext_rec / unit_lvl, while FriendInfo, ReinforcementInfo,
FixedReinforcementInfo and UnitReinforceEntry say base_heal / ext_heal /
add_heal / unit_lv. Both spellings are the client's, carried from IDA (see
net/friends.kdl, whose field docs cite vtable offsets). A column was added per
SPELLING, so one unit could hold two recovery values free to disagree.

Now one column per concept. rec/lvl wins because 08032025_CreateUserUnitsTable
established it. PacketInterfaceFor<T> maps either packet vocabulary onto the
one column, which is what that indirection is for -- so a field<> whose packet
and column names differ is correct here, not a smell. Packet field names are
untouched; they are the client's, not ours to normalise.

06082026_ConsolidateUserUnitStatColumns merges duplicate values into the
canonical column, DROPs unit_lv/base_heal/ext_heal and RENAMEs add_heal ->
add_rec, limit_over_heal -> limit_over_rec. 02072026 is deliberately left as
written, with a comment pointing forward: applied migrations are recorded by
name, so editing it would change nothing for existing databases while
diverging fresh ones.

Merge rule: the duplicate wins only where the canonical column is still at its
default, since quests-branch handlers wrote duplicates while upstream handlers
wrote canonicals. Verified against a real database before writing it -- 0 rows
held conflicting values and all 83 carried their value in a column being
dropped, so the merge was lossless. After migrating: 44 columns -> 41, all 83
rows kept their level.

Call sites were rewritten only inside quoted strings, since these identifiers
appear as both a SQL column and a generated packet field, sometimes on one
line (rd.base_heal = br["base_heal"]). A blanket find/replace would have
silently broken four generated structs.

The matching handler changes land on the branches that own those files:
UnitEvo/UnitMix on split/05-units, CampaignBattleStart on split/10-campaign.
Applies the review's own test -- add a field only when we understand what it
does AND the client demonstrably needs it -- to the two fields the review
named.

Both fail it. fe_bp and fe_max_usable_bp were INSERTed as the literal
constants 100 and 200, read straight back into the packet, and never computed
from or consumed by any handler; Frontier Evolution is not implemented. The
KDL fields stay, so the client still receives both keys (defaulting to 0);
only the per-user persistence goes. Re-add them alongside the subsystem that
gives them meaning, when the stored values will mean something.

06082026_DropUnusedFeBpColumns removes the columns. 02072026 keeps its ADD
lines so databases that already ran it and fresh ones converge, same pattern
as the consolidation migration above it.

Also drops a duplicate base_rec from the addUserUnit column list, left over
from the rec/heal consolidation. SQLite tolerates a repeated column in an
INSERT list and both values were the same bind, so nothing was written
incorrectly -- the list just no longer matched intent.

Verified on a real database: user_units 41 -> 39 columns, all 83 rows intact,
server boots.

Not dropped despite looking constant in a dev save: eqip_item_* (spheres --
returnEquippedSpheres depends on them), add_*/ext_*/limit_over_* (enhancement
state, merely unexercised), user_id. A single-user database makes unused
instance state look derivable, so column variance is a prompt to check a
field's meaning rather than evidence on its own.

The matching handler changes are on split/05-units.
Completes the field-bloat comment, which named fe_bp and leader_skill_id.

leader_skill_id is species data -- every copy of a unit has the same leader
skill -- so it belongs to UnitMst keyed by unit_id, not to a per-user row.
UnitEvo already sourced it from targetMst; UnitMix was the only reader of the
stored copy and now takes it from the MST too (it already held the handle for
element/exp_pattern/max_lv, so no extra lookup).

The packet field stays, so the client still receives the key. Only the
redundant copy goes, removing one more place the database can drift from the
MST.

Verified on a real database: user_units 39 -> 38 columns, 83 rows intact,
server boots. UserUnitInfo persistence is now 34 of 47 packet fields, down
from 37.

The matching UnitMix/UnitEvo changes are on split/05-units.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants