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
//! Identifiers: what the crate requires of them, and what it merely offers.
//!
//! # The decision (D-061, defects AD and J)
//!
//! **Concept ids are opaque, with two reserved characters.** They are caller
//! data, they are not required to be ULIDs, and the crate stores them as it
//! receives them. What it does require is that an id contain neither `|` nor
//! `/`, because two of the crate's own encodings use those bytes as delimiters
//! and are ambiguous without them reserved.
//!
//! Both of the options the plan offered failed on inspection, and the way they
//! failed is what produced this one.
//!
//! **"Wire `validate_id` into the write path" would have meant requiring
//! ULIDs.** That is not a tightening of the current contract, it is a different
//! contract: every test in the suite uses ids like `a`, `SRC` and `n000`, and so,
//! presumably, does every caller. Three modules *assumed* ULIDs; nothing ever
//! *required* them, and the assumption was wrong rather than merely unenforced.
//!
//! **"Declare ids fully opaque" would have left two encodings ambiguous.** It is
//! stated as needing only a width-independent cycle check, and that is the
//! smaller half. The other half is `transaction_log.entity_id`, which a link
//! writes as `source|target|type|valid_from`. With `|` legal inside a component,
//! two genuinely different links can produce one key — so the log cannot say
//! which relationship a row belongs to, and the fold partition that closed defect
//! W does not help, because both rows are links.
//!
//! So the constraint is the delimiters and nothing else. It is narrow enough to
//! leave every existing id valid, and it turns three assumptions into one
//! enforced property:
//!
//! - `edge_key` / `trg_links_log_insert` — the `|` join is now unambiguous.
//! - The traversal CTE's cycle check — `/`-delimited and no longer dependent on
//! every id being the same width. See `TraversalBuilder::build_sql`.
//! - Defect W's collision — a concept id can no longer *be* a link key, so the
//! conflation is unreachable rather than merely harmless. The partition fix
//! stays as defence in depth.
//!
//! Rejected: percent-encoding the components instead (moves the cost to every
//! read and makes the log unreadable by eye, to buy two characters back);
//! choosing delimiters no id would plausibly contain (the same bet, made
//! silently — "plausibly" is what an assumption is).
use Ulid;
use crate;
/// Bytes an identifier may not contain, and where each is load-bearing.
///
/// `|` joins a link's entity key in `transaction_log`. Structural, not
/// cosmetic — a collision produces a wrong answer with no error.
///
/// **`/` was reserved here through 0.5.6 and is free again as of 0.6.0
/// (T0.1, D-076).** It existed for exactly one reason: the traversal CTE
/// carried a `path` column of visited ids, delimited by `/`, and D-061 reserved
/// the character so the cycle check could match a whole element rather than a
/// substring once ids became variable-length. T0.1 deleted the path column —
/// the walk dedupes on entry instead — so nothing in the crate delimits with
/// `/` any more, and continuing to refuse it would be a constraint kept for a
/// mechanism that no longer exists.
///
/// Relaxing is safe in the direction that matters: ids previously refused are
/// now accepted, so no stored id becomes invalid and no migration is implied.
/// Adding a character back later would not be safe, which is why this list is
/// worth keeping short.
pub const RESERVED_ID_CHARS: = ;
/// Generate a new Crockford base32 26-character ULID string.
///
/// **Offered, not required.** Callers with no identifier scheme of their own
/// should use this: ULIDs are sortable, collision-resistant, and contain neither
/// reserved character by construction. Callers who have their own ids keep them,
/// subject only to [`validate_id`].
/// Whether `id` is a ULID. Not a requirement — see [`generate_id`].
/// Refuse an identifier the crate's own encodings cannot represent (D-061).
///
/// Called from `ConceptUpsert::normalized` and `EdgeAssertion::normalized`, so
/// a bad id is refused at the boundary with a typed error rather than becoming
/// an ambiguous log key three layers down.
///
/// **The error type is the fix for defect J.** This used to return
/// `DbError::NotFound(id)` for a malformed identifier, which says the opposite
/// of what happened — the id was not looked up and not missing; it was refused.
/// A caller matching on `NotFound` to decide whether to create the thing would
/// have been sent to create it again, with the same id, forever.