|
1 | | -"""``_MAX_COLUMN_COUNT`` is set to dqlite's actual emission ceiling |
2 | | -(255) so a hostile peer cannot inflate intermediate Python-side |
3 | | -allocations by claiming a higher column count than the C server |
4 | | -ever produces. |
| 1 | +"""``_MAX_COLUMN_COUNT`` is set to SQLite's documented column limit |
| 2 | +(``SQLITE_MAX_COLUMN = 2000``) so legitimate wide-table SELECT |
| 3 | +results decode while still rejecting absurd peer emissions. |
5 | 4 |
|
6 | | -The C server's ``stmt.c:10`` defines |
7 | | -``STMT__MAX_COLUMNS = (1 << 8) - 1 = 255``. |
| 5 | +The C server emits ``sqlite3_column_count(stmt)`` as a uint64 |
| 6 | +without cap (``query.c:111-120``); ``stmt.c:10``'s |
| 7 | +``STMT__MAX_COLUMNS = (1 << 8) - 1 = 255`` macro is defined but |
| 8 | +never referenced. SQLite's compile-time default is 2000 (raisable |
| 9 | +to 32767 via ``SQLITE_MAX_COLUMN`` build flag); a wide-table |
| 10 | +SELECT against an analytics / feature-store schema legitimately |
| 11 | +crosses 255 columns. |
| 12 | +
|
| 13 | +The per-name cap (``_MAX_COLUMN_NAME_SIZE = 4096``) and the frame- |
| 14 | +envelope cap (default 64 MiB) already bound memory growth from the |
| 15 | +N × name allocation; this cap is defence-in-depth against |
| 16 | +pathological peer emissions, not the load-bearing memory bound. |
8 | 17 | """ |
9 | 18 |
|
10 | 19 | import pytest |
|
18 | 27 | from dqlitewire.types import encode_uint64 |
19 | 28 |
|
20 | 29 |
|
21 | | -def test_max_column_count_pinned_to_255() -> None: |
22 | | - assert _MAX_COLUMN_COUNT == 255 |
| 30 | +def test_max_column_count_pinned_to_sqlite_default() -> None: |
| 31 | + """SQLite's documented default ``SQLITE_MAX_COLUMN`` is 2000.""" |
| 32 | + assert _MAX_COLUMN_COUNT == 2000 |
23 | 33 |
|
24 | 34 |
|
25 | | -def test_rows_response_rejects_count_above_255() -> None: |
26 | | - body = encode_uint64(256) |
| 35 | +def test_rows_response_rejects_count_above_cap() -> None: |
| 36 | + body = encode_uint64(_MAX_COLUMN_COUNT + 1) |
27 | 37 | with pytest.raises(DecodeError, match="(?i)column count"): |
28 | 38 | RowsResponse.decode_body(body) |
29 | 39 |
|
30 | 40 |
|
31 | | -def test_rows_response_accepts_count_at_255() -> None: |
32 | | - """A 255-column rows response is well-formed and must not be |
| 41 | +def test_rows_response_accepts_count_at_cap() -> None: |
| 42 | + """A 2000-column rows response is well-formed and must not be |
33 | 43 | rejected by the cap; it fails the body-size check instead |
34 | 44 | because we only sent the count, not the column names.""" |
35 | | - body = encode_uint64(255) |
| 45 | + body = encode_uint64(_MAX_COLUMN_COUNT) |
| 46 | + with pytest.raises(DecodeError, match="exceeds maximum possible"): |
| 47 | + RowsResponse.decode_body(body) |
| 48 | + |
| 49 | + |
| 50 | +def test_rows_response_accepts_count_above_old_255_cap() -> None: |
| 51 | + """Pin the regression-vs-old-cap shape: a 1500-column emission |
| 52 | + (legitimate wide table, above the prior 255 cap but below the |
| 53 | + new 2000 cap) must NOT trip the column-count cap. It still |
| 54 | + fails the body-size check below because we only sent the count, |
| 55 | + not the per-column name payload.""" |
| 56 | + body = encode_uint64(1500) |
36 | 57 | with pytest.raises(DecodeError, match="exceeds maximum possible"): |
37 | 58 | RowsResponse.decode_body(body) |
38 | 59 |
|
39 | 60 |
|
40 | | -def test_servers_response_rejects_count_above_255() -> None: |
41 | | - """``ServersResponse`` shares the same column-count cap path |
42 | | - via ``_MAX_NODE_COUNT`` (separate cap) but uses |
43 | | - ``_MAX_COLUMN_COUNT``-style protection on its own count field.""" |
44 | | - # ``ServersResponse`` uses ``_MAX_NODE_COUNT = 10_000``; the |
45 | | - # column cap does not apply directly. Pinning here is a sanity |
46 | | - # check that the cap constant was not accidentally inlined into |
47 | | - # an unrelated field. |
| 61 | +def test_rows_response_rejects_absurd_count() -> None: |
| 62 | + """A pathological emission (``column_count = 2^31``) must still |
| 63 | + be rejected so a hostile peer cannot inflate Python-side |
| 64 | + allocations.""" |
| 65 | + body = encode_uint64(1 << 31) |
| 66 | + with pytest.raises(DecodeError, match="(?i)column count"): |
| 67 | + RowsResponse.decode_body(body) |
| 68 | + |
| 69 | + |
| 70 | +def test_servers_response_uses_separate_cap() -> None: |
| 71 | + """``ServersResponse`` uses ``_MAX_NODE_COUNT = 10_000``; the |
| 72 | + column cap does not apply. Pinning here is a sanity check that |
| 73 | + the cap constant was not accidentally inlined into an unrelated |
| 74 | + field.""" |
48 | 75 | assert _MAX_COLUMN_COUNT < 10_000 |
49 | 76 |
|
50 | 77 |
|
|
0 commit comments