1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
//! Deprecation reporting for configuration keys CQLite has REMOVED (issue #1696).
//!
//! # Why a Rust struct is not the whole story
//!
//! Deleting a decorative field from [`crate::Config`] is a COMPILE error for an
//! embedder writing Rust — the loudest signal available. But `Config` derives
//! `Deserialize`, and serde's default behaviour is to DISCARD unknown fields, so
//! every non-Rust authoring surface gets the opposite: a pre-change document that
//! still says
//!
//! ```json
//! { "storage": { "block_size": 65536 }, "query": { "parallel": { "enabled": true } } }
//! ```
//!
//! deserializes SUCCESSFULLY and is silently ignored. The Python bindings' dict /
//! JSON bridge is exactly that surface (`cqlite.open(path, config={...})`), which
//! made the crate-wide rule stated in #1696
//!
//! > A removed knob must produce a LOUD signal at the layer where it is set,
//! > never silence.
//!
//! false for anyone configuring CQLite through the bindings (#1696 roborev F1).
//!
//! # WHERE THE RULE IS ENFORCED — and where it is NOT (#1696 roborev r2 F3)
//!
//! Stated exactly, because a rule quoted without its scope reads as universal
//! coverage and this one does not have it. The rule holds on:
//!
//! * the CLI's config-file loader (`cqlite_cli::config::removed_keys`) — a named
//! warning on stderr;
//! * the Python bindings' dict/JSON entry points — a `UserWarning` naming each
//! dead path (visible under Python's DEFAULT filters, which is why the category
//! is not `DeprecationWarning`);
//! * Rust FIELD ACCESS — a compile error, and only for callers writing Rust.
//!
//! It does NOT hold on a DIRECT serde deserialization of [`crate::Config`].
//! `Config` derives `Deserialize`, so `serde_json::from_str::<Config>` /
//! `from_value::<Config>` bypass [`crate::Config::from_json_str`] and
//! [`crate::Config::from_json_str_reporting_removed`] entirely, and serde
//! silently DISCARDS the removed keys. An embedder deserializing a `Config`
//! document themselves therefore still gets silence: a reporting constructor is
//! OPTIONAL, and an optional constructor enforces nothing at the boundary it sits
//! beside. That residual is **issue #3520**, pinned by
//! `direct_serde_deserialization_is_the_unreported_surface` in this module's
//! tests. It was scoped out of #1696 rather than fixed: closing it needs a custom
//! `Deserialize` capturing unknown keys across every nested config struct.
//!
//! # Why not `deny_unknown_fields`
//!
//! Because it would HARD-FAIL a Python caller whose config predates the removal,
//! with no migration path, for keys that never did anything. That is the opposite
//! of the parse-and-ignore-PLUS-a-named-warning posture deliberately chosen for
//! the CLI's file surface, and #1696 requires ONE consistent posture crate-wide.
//! So this module is the same mechanism, at the deserialization boundary.
//!
//! # One mechanism, one table per DOCUMENT SCHEMA
//!
//! [`Removed`], [`removed_keys_present`] and [`deprecation_warning`] live here and
//! are shared with `cqlite_cli::config::removed_keys`, so the wording, the
//! matching rule and the shape cannot drift between the two surfaces.
//!
//! The TABLES are necessarily per-schema and must not be crossed: this crate's
//! [`REMOVED_KEYS`] describes a `cqlite_core::Config` document, while the CLI's
//! describes a `cqlite.toml`/`.yaml`/`.json`. Applying this table to a CLI file
//! would be WRONG, not merely redundant — the CLI's `[performance]` section is
//! live (`query_timeout_ms`, `memory_limit_mb`, `cache_size_mb`) while the core's
//! `performance` tree was removed, so a shared table would scold a user for a key
//! that works.
use Write as _;
/// A configuration key removed by an issue, and why it is gone.
/// Every `cqlite_core::Config` key removed by issue #1696.
///
/// # Scope
///
/// This table starts at #1696, which is where the mechanism starts: a removal PR
/// from now on extends it, and one that forgets to is incomplete. Removals that
/// predate the mechanism (e.g. #1619's decorative `compaction.strategy` /
/// `max_sstables` / `size_ratio` / `max_threads` / `background_interval`) are
/// deliberately NOT listed — they were never reported and adding them now would
/// be a separate, testable change rather than part of this one.
pub const REMOVED_KEYS: & = &;
/// Which of `table`'s removed keys a document still names.
///
/// `has_path` answers "does the document contain this dotted path as a mapping
/// key" for one concrete document type, so the FILTER is written once and every
/// surface (this crate's JSON, the CLI's TOML/YAML/JSON) shares it.
/// The operator-facing deprecation warning for a set of still-named removed keys,
/// or `None` when the document names none.
///
/// Returned as a string rather than logged here so the caller picks the sink (the
/// CLI prints to stderr, the bindings raise a Python `UserWarning`) and a test can
/// assert the exact text.
///
/// # THE TEXT REPORTS ONLY WHICH KEYS ARE DEAD — never the fate of the load
///
/// Stated as a rule at the seam, because the alternative was found broken THREE
/// times (#1696 roborev F3, r2 F3, r5 F1). The text used to add an assurance —
/// "they are IGNORED, the configuration still loads" — which is a claim about a
/// LATER stage's outcome. Every fix moved the emission one stage later
/// (after deserialization; then after the bindings' validation) and the defect
/// reappeared at the next stage, because there is ALWAYS a next stage: the CLI
/// deserializes, then maps into `cqlite_core::Config`, then validates
/// semantically (`memory_limit_mb = 1` with `cache_size_mb = 64` fails there),
/// then the caller does whatever it does. No placement can make such a promise
/// safe.
///
/// So this warning says nothing about whether the load succeeds. It names the
/// keys and states that they have no effect — two facts fully known at the point
/// of the scan. A warning that only reports what it knows cannot be wrong about
/// anything else, and callers are consequently free to emit it wherever the raw
/// document is still in hand. DO NOT reintroduce an outcome claim here.
/// Whether a parsed JSON document contains `path` as an object key.
///
/// Walked segment by segment, so `storage.block_size` matches only a
/// `block_size` under `storage` — never a top-level one, nor a
/// `query.block_size`. Only KEY PRESENCE matters: a removed key set to any value
/// at all, `null` included, is still someone believing they configured something.
/// The deprecation warning a JSON `Config` document should produce, or `None`
/// when it names no removed key (or is not parseable JSON at all).
///
/// Unparseable content yields `None` rather than an error: this scan must never
/// be the thing that rejects a document. [`crate::Config::from_json_str`] owns
/// the real parse and the real error, and only reaches this once that parse
/// SUCCEEDED.