Skip to content

fix(db): guard string_to_datetime against non-string input#201

Draft
clewdotcc wants to merge 1 commit into
dylanljones:masterfrom
clewdotcc:fix/string-to-datetime-typeguard
Draft

fix(db): guard string_to_datetime against non-string input#201
clewdotcc wants to merge 1 commit into
dylanljones:masterfrom
clewdotcc:fix/string-to-datetime-typeguard

Conversation

@clewdotcc
Copy link
Copy Markdown

Problem

pyrekordbox.masterdb.models.string_to_datetime (also exposed as pyrekordbox.db6.tables.string_to_datetime via the back-compat re-export) calls value.endswith("Z") without type-guarding value. Rekordbox 6 has been observed in the wild to store non-string values (specifically integer 0) in DateTime columns — for example DjmdAlbum.created_at / updated_at.

When this happens, the function raises AttributeError: 'int' object has no attribute 'endswith' at SQLAlchemy result-set materialization time. This is not a recoverable per-row error — a single corrupt row crashes the entire query: session.query(DjmdAlbum).all(), .first(), etc. all fail.

Reproduction

>>> from pyrekordbox.masterdb.models import string_to_datetime
>>> string_to_datetime(0)
Traceback (most recent call last):
  ...
AttributeError: 'int' object has no attribute 'endswith'

Fix

Type-guard the function: return None for non-string input. This treats malformed DateTime columns as "no date" rather than crashing the consuming query.

This is consistent with the existing DateTime.process_result_value behavior, which already declares -> Optional[datetime] and returns None when the raw column value is falsy. The return-type annotation of string_to_datetime is widened from datetime to Optional[datetime] to honestly describe the new None return path; the only in-repo caller (DateTime.process_result_value) already handles Optional[datetime].

Before / After

# Before
>>> string_to_datetime(0)
AttributeError: 'int' object has no attribute 'endswith'

# After
>>> string_to_datetime(0)  # returns None — treated as missing value

Behavior on valid string input is unchanged. Behavior on empty string "" is also unchanged (still raises ValueError from datetime.fromisoformat(""), same as before) — in practice the DateTime TypeDecorator's truthy guard at process_result_value already short-circuits this path, so the empty-string semantic is irrelevant in production but is preserved here for strict backward-compat.

Tests

Added test_string_to_datetime_returns_none_for_non_string_input in tests/test_masterdb.py, parametrized over 0, None, and a larger integer. The test fails on master (raises AttributeError) and passes with this patch.

Scope

Limited to pyrekordbox/masterdb/models.py. Side-find: pyrekordbox/devicelib_plus/models.py has a byte-identical string_to_datetime with the same vulnerability. I kept it out of this PR to keep the review focused, but happy to extend the PR or open a parallel one — whichever you prefer.

Backward compatibility

  • Valid ISO string inputs: behavior unchanged.
  • Empty string: behavior unchanged (still raises ValueError).
  • Non-string inputs (int, None, ...): previously crashed, now return None.
  • Return type widens from datetime to Optional[datetime]. The only in-repo caller already handles Optional[datetime]; external callers that rely on a non-None return for non-string inputs were already encountering AttributeError, so this is a strict improvement.

Rekordbox 6 has been observed to store non-string values (e.g. integer
0) in DateTime columns such as DjmdAlbum.created_at and updated_at.
Without a type-guard, the unconditional `value.endswith("Z")` call
crashes with `AttributeError: 'int' object has no attribute 'endswith'`
at ORM result-set materialization time, breaking any query that
touches the affected row (e.g. session.query(DjmdAlbum).all()).

Type-guard the function: return None for non-string input. This is
consistent with DateTime.process_result_value, which already returns
Optional[datetime] when the raw column value is falsy — so downstream
callers of the TypeDecorator already handle a None datetime.

Before:

    >>> from pyrekordbox.masterdb.models import string_to_datetime
    >>> string_to_datetime(0)
    AttributeError: 'int' object has no attribute 'endswith'

After:

    >>> string_to_datetime(0)
    >>> # None — treated as missing value
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.

1 participant