mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 10:02:17 -05:00
A tag maps a UID to "the thing to bring up", and only some of those things are
spools -- or rows at all. Locations are the case that settles the shape: a
location in Spoolman is a String(64) on Spool, not a table, so a tag meaning
"show me Shelf A" can never be a foreign key.
Getting this wrong is asymmetrically expensive. Renaming the table, dropping
spool_id's NOT NULL and adding a discriminator all mean altering a populated
table on four databases; adding an endpoint or a target kind later is additive
and free. The migration has shipped to nobody -- it is not on master and
v0.26.1 predates it -- so the schema is settled now and the unshipped revision
is rewritten in place rather than chased with a second one.
spool_tag becomes tag:
uid unchanged, still globally unique -- one physical tag, one thing
target_type which kind of target is in force, rather than leaving readers
to infer it from whichever column is non-null
spool_id nullable now; targets that ARE rows keep real foreign keys, so
filament_id the database still cascades deletes and refuses orphans
target_value targets that are not rows; a new kind of this sort costs no
migration at all, only a new target_type string
Exactly-one-target is enforced in database/tag.py rather than by a CHECK: no
migration in this tree uses one, and MySQL below 8.0.16 parses and silently
ignores them, so a constraint here would be real on three databases out of
four. Single write path beats a constraint that is only sometimes a constraint
-- the same argument, in the same module, as normalize_uid.
filament_id gets a column and a foreign key but no ORM relationship: nothing
writes filament tags, and a selectin collection on Filament would add a query
to every filament listing for rows that cannot exist. ON DELETE CASCADE holds
integrity until the feature that needs the relationship arrives.
No API surface changes. POST /spool/{id}/tag, ?tag=, the spool response's tags
and matched_spool_id are all exactly as they were; the reads are just explicit
now about wanting a spool rather than assuming the tag they found is one.
TagConflictError's spool_id becomes optional for the same reason, which the
client already handled -- it only offers "move it here" when there is an id.
Proven on sqlite, postgres, mariadb and cockroachdb: 526 integration tests each.
37 lines
1.0 KiB
Python
37 lines
1.0 KiB
Python
"""Various exceptions used."""
|
|
|
|
|
|
class ItemNotFoundError(Exception):
|
|
pass
|
|
|
|
|
|
class ItemDeleteError(Exception):
|
|
pass
|
|
|
|
|
|
class ItemCreateError(Exception):
|
|
pass
|
|
|
|
|
|
class SpoolMeasureError(Exception):
|
|
pass
|
|
|
|
|
|
class TagConflictError(Exception):
|
|
"""A tag UID is already linked to something else.
|
|
|
|
Carries the conflicting spool's ID rather than only a message, so the API can hand it
|
|
to the client and the client can offer "move the tag to this spool instead" without a
|
|
second lookup.
|
|
|
|
`spool_id` is None when the UID is held by something that is not a spool -- a tag
|
|
identifies exactly one thing, and not every such thing is a spool (see
|
|
`database.models.Tag`). The message says what in that case, and a client with nothing
|
|
to offer beyond it simply reports it.
|
|
"""
|
|
|
|
def __init__(self, message: str, spool_id: int | None = None) -> None:
|
|
"""Initialize with the message and the ID of the spool already holding the tag."""
|
|
super().__init__(message)
|
|
self.spool_id = spool_id
|