Skip to content

fix(wal): reject non-string keys and values on the write path - #345

Open
adiraj66132 wants to merge 1 commit into
danieldeer:mainfrom
adiraj66132:fix/wal-reject-non-string-on-write
Open

adiraj66132 wants to merge 1 commit into
danieldeer:mainfrom
adiraj66132:fix/wal-reject-non-string-on-write

Conversation

@adiraj66132

@adiraj66132 adiraj66132 commented Oct 22, 2026 •

Copy link
Copy Markdown

The bug

#344 made the WAL reader reject entries whose key or value is not a string. The writer was left alone, so this still goes through:

seriousdb.set(5, "a")        # returns fine, fsynced to .sdb.wal
seriousdb.set("keep", "b")   # returns fine, fsynced to .sdb.wal

On the next start, replay reaches the first line, calls it corrupt, and truncates the file right there. That deletes the bad entry and keep with it, and both writes had already returned success. One bad write silently drops every write after it, permanently, because the truncation happens on disk.

I hit this while reviewing #344 on the current main, and confirmed it with a small script: three inserts, restart, empty dict, WAL truncated to 0 bytes.

The fix

Check where entries are built, not where they are parsed.

  • SetEntry / DeleteEntry validate in __post_init__ and raise TypeError.
  • from_dict drops its own copies of the check: it gets the same TypeError from the constructor, so the check lives in one place instead of three.
  • insert / delete now raise TypeError before touching memory or the log, the same all-or-nothing behaviour they already have when the append fails.
  • Logs already poisoned by an older version still replay the same way: TypeError is caught, the log is truncated at that entry.

Rejecting only on replay can never fix a bad line that is already on disk. The write side is where the fix belongs.

Tests

  • test_rejected_write_leaves_the_wal_healthy: a rejected write changes neither memory nor the WAL, and a write after it survives a restart. This one fails on main.
  • test_entries_reject_non_string_key_or_value, plus a delete case in the existing malformed-entry replay test.

pytest: 190 passed (was 187). ty, ruff check, ruff format --check and scripts/docs-reference.py --check are clean.

danieldeer#344 added the check only on replay, so a non-string key or value could
still be written. The next replay called that line corrupt and truncated
the log there, dropping every valid write after it.

Validate in SetEntry/DeleteEntry instead, before anything is written or
applied, so insert/delete raise TypeError and leave memory and the log
untouched.
@adiraj66132
adiraj66132 deleted the fix/wal-reject-non-string-on-write branch October 22, 2026 21:18
@mattsva

mattsva commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Im not at my desk rn so I can't make sure if that true or not.
Why did you closed this?

@adiraj66132

adiraj66132 commented Oct 23, 2026 •

Copy link
Copy Markdown
Author

I closed it because I wasn't sure whether the changes introduced any meaningful improvement over the current state of the project. However, my fix addresses a specific issue in the write path that the previous PR left unresolved: invalid entries can still be written to the WAL, causing replay to truncate the log and silently discard subsequent valid writes. I added validation before modifying memory or writing to the WAL, along with regression tests to verify that valid writes remain intact after a rejected write and restart. I haven't received a review yet, so I'd appreciate it if you could take a look when you're back at your desk.

@mattsva mattsva left a comment

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.

Nit (optional): the key check is repeated in SetEntry and DeleteEntry. Fine for two small classes, but a shared helper would avoid drift if more entry types are added.

The rest would look good.
Why dont you see a use for that? I must missed those cases in the original PR, but that just means that I am stupid, not that this is wrong.

LGTM

Comment thread src/seriousdb/cache.py
------
TypeError
If `key` or `value` is not a string.
ServiceUnavailableError

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.

Thats good

Comment thread src/seriousdb/wal.py
raise TypeError("value must be a string")

return SetEntry(key=key, value=value)
return SetEntry(key=data["key"], value=data["value"])

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.

Thats a lot simpler, I do like it

Comment thread src/seriousdb/wal.py
key: str
value: str

def __post_init__(self) -> None:

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.

great

Comment thread tests/test_wal.py
]


def test_entries_reject_non_string_key_or_value():

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 guess that enough

@mattsva mattsva left a comment

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.

Do you want it to be merged?

@adiraj66132
adiraj66132 restored the fix/wal-reject-non-string-on-write branch October 24, 2026 16:18
@adiraj66132 adiraj66132 reopened this Oct 24, 2026
@adiraj66132

Copy link
Copy Markdown
Author

Yes, I'd like it to be merged. Thanks for reviewing it and for the feedback! I'll address the optional suggestion about sharing the key validation logic between SetEntry and DeleteEntry if you think it's worth doing before merging.

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