From fddd3d36b4cf2cea1320e67d01034125bb8ad9e9 Mon Sep 17 00:00:00 2001 From: Luke Merrett Date: Fri, 31 Jul 2026 07:51:54 +0100 Subject: [PATCH 1/2] Reject negative n in generate_n_keys_between A negative n slipped past the n == 0 / n == 1 checks: with one bound None the range() loop simply didn't run and the function silently returned a single key; with both bounds set, mid = floor(n / 2) went negative and the function recursed until RecursionError. Both the silent wrong result and the 1000-frame traceback are worse than enforcing the documented 'n >= 0' precondition with a clear FIError. The JS reference (rocicorp/fractional-indexing) has the same behaviour and only documents the precondition; this is a deliberate deviation, like validating a defaulted int_digits. Co-Authored-By: Claude Fable 5 --- fractional_indexing.py | 6 +++++- tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/fractional_indexing.py b/fractional_indexing.py index f05aef5..0021e80 100644 --- a/fractional_indexing.py +++ b/fractional_indexing.py @@ -411,7 +411,7 @@ def generate_n_keys_between( ) -> List[str]: """ same preconditions as generate_key_between(). - n >= 0. + n must be >= 0 (raises FIError otherwise). Returns an array of n distinct keys in sorted order. If a and b are both null, returns [a0, a1, ...] If one or the other is null, returns consecutive "integer" @@ -419,6 +419,10 @@ def generate_n_keys_between( """ digits, int_digits = _resolve_alphabets(digits, int_digits) + if n < 0: + # Without this guard a negative n silently returns a single key when + # one bound is None, and recurses without bound when both are set. + raise FIError(f'n must be >= 0: {n}') if n == 0: return [] if n == 1: diff --git a/tests.py b/tests.py index e6d9a2d..8d6893c 100644 --- a/tests.py +++ b/tests.py @@ -247,6 +247,20 @@ def test_equal_bounds_rejected() -> None: generate_key_between('a0', 'a0') +# A negative n must raise, not silently return a single key (one bound None) +# or recurse without bound (both bounds set). +@pytest.mark.parametrize(['a', 'b'], [ + (None, None), + ('a0', None), + (None, 'a1'), + ('a0', 'a5'), +]) +def test_negative_n_rejected(a: Optional[str], b: Optional[str]) -> None: + with pytest.raises(FIError) as e: + generate_n_keys_between(a, b, -1) + assert e.value.args[0] == 'n must be >= 0: -1' + + def test_readme_examples_single_key(): # Insert at the beginning first = generate_key_between(None, None) From daff3ada1f7667d4165e9bc2157864552dff3af8 Mon Sep 17 00:00:00 2001 From: Luke Merrett Date: Fri, 31 Jul 2026 07:52:04 +0100 Subject: [PATCH 2/2] Add 1.0.1 changelog entry Co-Authored-By: Claude Fable 5 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 3f009a0..eec033d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,12 @@ This is a Python port of the original JavaScript implementation by [@rocicorp](h ## Changelog +### 1.0.1 + +- `generate_n_keys_between()` now raises `FIError` for a negative `n`, instead of silently returning a single key + (one bound `None`) or hitting `RecursionError` (both bounds set). Deviation from the JS reference, which only + documents the `n >= 0` precondition. + ### 0.2.0 Brings the library to parity with [rocicorp/fractional-indexing](https://github.com/rocicorp/fractional-indexing)