Skip to main content

projectatlas_db/
legacy_relations.rs

1//! Complete, bounded compatibility reads of the stored symbol-relation rows.
2
3use super::{AtlasStore, DbError, DbResult, SqliteReadProgressGuard, like_query};
4use projectatlas_core::graph::GraphContractError;
5use projectatlas_core::symbols::{ParserKind, RelationKind, SymbolRelation};
6use projectatlas_core::{IndexWorkControl, IndexWorkStage};
7use rusqlite::limits::Limit;
8use rusqlite::{Connection, ErrorCode, Row, params};
9use serde::Serialize;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::{Duration, Instant};
13
14/// Validated resource envelope for one complete legacy relation array.
15#[derive(Clone, Copy, Debug)]
16pub struct LegacyRelationReadBudget {
17    /// Maximum complete eligible rows.
18    rows: usize,
19    /// Maximum cooperative time spent inside this reader.
20    deadline: Duration,
21    /// Maximum decoded source and retained candidate bytes.
22    intermediate_bytes: u64,
23}
24
25impl LegacyRelationReadBudget {
26    /// Maximum UTF-8 query bytes before adding the two LIKE wildcards.
27    pub const MAX_QUERY_BYTES: usize = 1_024;
28    /// Absolute compatibility-array row ceiling.
29    pub const MAX_ROWS: usize = 200;
30    /// Absolute decoded/intermediate ceiling.
31    pub const MAX_INTERMEDIATE_BYTES: u64 = 1_024 * 1_024;
32    /// Shared instruction ceiling across candidate discovery and hydration.
33    pub const MAX_INSTRUCTIONS: u64 = 100_000;
34    /// Maximum cooperative query duration.
35    pub const MAX_DEADLINE: Duration = Duration::from_millis(1_000);
36
37    /// Validate a complete-array resource envelope.
38    ///
39    /// # Arguments
40    ///
41    /// `rows` bounds the complete result; `deadline` and `intermediate_bytes`
42    /// may tighten the absolute query ceilings.
43    ///
44    /// # Returns
45    ///
46    /// A validated read budget.
47    ///
48    /// # Errors
49    ///
50    /// Rejects zero or oversized bounds before database work.
51    pub fn new(rows: usize, deadline: Duration, intermediate_bytes: u64) -> DbResult<Self> {
52        if rows == 0
53            || rows > Self::MAX_ROWS
54            || deadline.is_zero()
55            || deadline > Self::MAX_DEADLINE
56            || intermediate_bytes == 0
57            || intermediate_bytes > Self::MAX_INTERMEDIATE_BYTES
58        {
59            return Err(GraphContractError::InvalidLimits {
60                reason: "legacy relations require 1..200 rows, at most 1000 ms and 1 MiB intermediate bytes; use anchored detailed navigation for larger results",
61            }.into());
62        }
63        Ok(Self {
64            rows,
65            deadline,
66            intermediate_bytes,
67        })
68    }
69}
70
71/// Causal work performed by one complete compatibility read.
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
73pub struct LegacyRelationReadWork {
74    /// Executed SQLite instructions, including all candidate and hydration statements.
75    pub instructions: u64,
76    /// Candidate identifiers observed, including the refusal sentinel.
77    pub candidate_rows: u32,
78    /// Admitted candidate storage, relation structures and raw text bytes.
79    pub decoded_bytes: u64,
80}
81
82/// Complete legacy rows plus bounded owning-reader work.
83#[derive(Debug)]
84pub struct LegacyRelationRead {
85    /// Complete eligible rows in deterministic legacy order.
86    pub relations: Vec<SymbolRelation>,
87    /// Actual candidate, instruction and byte work.
88    pub work: LegacyRelationReadWork,
89}
90
91/// Resource that prevented a complete compatibility result.
92#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
93#[serde(rename_all = "snake_case")]
94pub enum LegacyRelationReadLimit {
95    /// More eligible rows exist than the requested complete-array bound.
96    Rows,
97    /// The shared SQLite instruction ceiling was reached.
98    Instructions,
99    /// The cooperative query deadline was reached.
100    Deadline,
101    /// Candidate or decoded result state cannot fit.
102    IntermediateBytes,
103    /// SQLite refused a wide value before owned decoding.
104    ValueBytes,
105    /// Query text exceeds the admitted UTF-8 pattern bytes.
106    QueryBytes,
107}
108
109/// Scoped progress and native-value limits for one compatibility read.
110struct LegacyReadGuard<'connection> {
111    /// Reuses the storage owner's progress-hook cleanup.
112    progress: SqliteReadProgressGuard<'connection>,
113    /// Native length limit restored after this read.
114    previous_length: i32,
115    /// Native LIKE limit restored after this read.
116    previous_pattern_length: i32,
117    /// Shared instruction counter used by every prepared statement.
118    instructions: Arc<AtomicU64>,
119    /// Query-local deadline, independent of earlier source verification work.
120    deadline: Instant,
121}
122
123impl<'connection> LegacyReadGuard<'connection> {
124    /// Install one instruction-level callback and a bounded SQLite value length.
125    fn new(
126        connection: &'connection Connection,
127        budget: LegacyRelationReadBudget,
128        control: &IndexWorkControl,
129    ) -> DbResult<Self> {
130        control.check(IndexWorkStage::RepositoryTraversal)?;
131        let deadline = Instant::now().checked_add(budget.deadline).ok_or(
132            GraphContractError::InvalidLimits {
133                reason: "legacy deadline is unrepresentable",
134            },
135        )?;
136        let previous_length = connection.limit(Limit::SQLITE_LIMIT_LENGTH)?;
137        let previous_pattern_length = connection.limit(Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)?;
138        let length =
139            i32::try_from(budget.intermediate_bytes).map_err(|source| DbError::InvalidCount {
140                field: "legacy intermediate bytes",
141                value: i64::MAX,
142                source,
143            })?;
144        connection.set_limit(Limit::SQLITE_LIMIT_LENGTH, previous_length.min(length))?;
145        let mut guard = Self {
146            progress: SqliteReadProgressGuard {
147                connection,
148                armed: false,
149            },
150            previous_length,
151            previous_pattern_length,
152            instructions: Arc::new(AtomicU64::new(0)),
153            deadline,
154        };
155        connection.set_limit(
156            Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH,
157            previous_pattern_length.min((LegacyRelationReadBudget::MAX_QUERY_BYTES + 2) as i32),
158        )?;
159        let instructions = Arc::clone(&guard.instructions);
160        let control = control.clone();
161        connection.progress_handler(
162            1,
163            Some(move || {
164                instructions.fetch_add(1, Ordering::Relaxed)
165                    >= LegacyRelationReadBudget::MAX_INSTRUCTIONS - 1
166                    || Instant::now() >= deadline
167                    || control.check(IndexWorkStage::RepositoryTraversal).is_err()
168            }),
169        )?;
170        guard.progress.armed = true;
171        Ok(guard)
172    }
173
174    /// Restore the more restrictive remaining-byte value bound before row hydration.
175    fn admit_value(&self, remaining: u64) -> DbResult<()> {
176        let remaining = i32::try_from(remaining).map_err(|source| DbError::InvalidCount {
177            field: "legacy remaining bytes",
178            value: i64::MAX,
179            source,
180        })?;
181        self.progress.connection.set_limit(
182            Limit::SQLITE_LIMIT_LENGTH,
183            self.previous_length.min(remaining),
184        )?;
185        Ok(())
186    }
187
188    /// Observe all query instructions and preserve caller cancellation/deadline errors.
189    fn check(&self, control: &IndexWorkControl, work: &mut LegacyRelationReadWork) -> DbResult<()> {
190        work.instructions = self.instructions.load(Ordering::Relaxed);
191        control.check(IndexWorkStage::RepositoryTraversal)?;
192        if work.instructions >= LegacyRelationReadBudget::MAX_INSTRUCTIONS {
193            return Err(read_limit(LegacyRelationReadLimit::Instructions, *work));
194        }
195        if Instant::now() >= self.deadline {
196            return Err(read_limit(LegacyRelationReadLimit::Deadline, *work));
197        }
198        Ok(())
199    }
200}
201
202impl Drop for LegacyReadGuard<'_> {
203    fn drop(&mut self) {
204        drop(self.progress.connection.set_limit(
205            Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH,
206            self.previous_pattern_length,
207        ));
208        // This connection's prior nonnegative native limit remains valid.
209        drop(
210            self.progress
211                .connection
212                .set_limit(Limit::SQLITE_LIMIT_LENGTH, self.previous_length),
213        );
214    }
215}
216
217/// Construct a typed complete-array refusal without exposing stored text.
218fn read_limit(limit: LegacyRelationReadLimit, work: LegacyRelationReadWork) -> DbError {
219    DbError::LegacyRelationReadLimit { limit, work }
220}
221
222/// Admit logical candidate/result storage before retaining or copying it.
223fn admit_bytes(
224    work: &mut LegacyRelationReadWork,
225    bytes: u64,
226    budget: LegacyRelationReadBudget,
227) -> DbResult<()> {
228    let observed = work.decoded_bytes.saturating_add(bytes);
229    if observed > budget.intermediate_bytes {
230        return Err(read_limit(
231            LegacyRelationReadLimit::IntermediateBytes,
232            *work,
233        ));
234    }
235    work.decoded_bytes = observed;
236    Ok(())
237}
238
239impl AtlasStore {
240    /// Read a complete small legacy relation array with bounded SQLite and decode work.
241    ///
242    /// # Arguments
243    ///
244    /// `file` and `query` preserve the low-level accessor's exact-file and SQL LIKE
245    /// filtering. `budget` bounds the complete result; `control` retains caller
246    /// cancellation and earlier deadlines.
247    ///
248    /// # Returns
249    ///
250    /// All eligible rows in path/line/source/target/identity order and causal work.
251    /// No partial successful array is returned.
252    ///
253    /// # Errors
254    ///
255    /// Returns typed resource refusal, caller work failure, invalid stored values
256    /// or SQLite errors. Scoped progress hooks and value limits are restored.
257    pub fn load_symbol_relations_bounded(
258        &self,
259        file: Option<&str>,
260        query: Option<&str>,
261        budget: LegacyRelationReadBudget,
262        control: &IndexWorkControl,
263    ) -> DbResult<LegacyRelationRead> {
264        let mut work = LegacyRelationReadWork::default();
265        if query.is_some_and(|query| query.len() > LegacyRelationReadBudget::MAX_QUERY_BYTES) {
266            return Err(read_limit(LegacyRelationReadLimit::QueryBytes, work));
267        }
268        let snapshot = if self.connection.is_autocommit() {
269            Some(self.connection.unchecked_transaction()?)
270        } else {
271            None
272        };
273        if file.is_some_and(|value| value.len() as u64 > budget.intermediate_bytes)
274            || query.is_some_and(|value| {
275                value.len() as u64 > budget.intermediate_bytes.saturating_sub(2)
276            })
277        {
278            return Err(read_limit(LegacyRelationReadLimit::IntermediateBytes, work));
279        }
280        let pattern = query.map(like_query);
281        admit_bytes(
282            &mut work,
283            pattern.as_ref().map_or(0, |value| value.len() as u64),
284            budget,
285        )?;
286        let guard = LegacyReadGuard::new(&self.connection, budget, control)?;
287        let operation = (|| {
288            // Completeness needs only a sentinel, so candidate discovery never sorts wide keys.
289            let candidates = match (file, query) {
290                (Some(file), Some(_query)) => {
291                    let mut statement = self.connection.prepare("SELECT id FROM symbol_relations WHERE path = ?1 AND (source_name LIKE ?2 OR target_name LIKE ?2 OR context LIKE ?2) LIMIT ?3")?;
292                    collect_candidates(
293                        &mut statement,
294                        params![file, pattern.as_deref(), (budget.rows + 1) as i64],
295                        budget,
296                        &mut work,
297                    )?
298                }
299                (Some(file), None) => {
300                    let mut statement = self
301                        .connection
302                        .prepare("SELECT id FROM symbol_relations WHERE path = ?1 LIMIT ?2")?;
303                    collect_candidates(
304                        &mut statement,
305                        params![file, (budget.rows + 1) as i64],
306                        budget,
307                        &mut work,
308                    )?
309                }
310                (None, Some(_query)) => {
311                    let mut statement = self.connection.prepare("SELECT id FROM symbol_relations WHERE source_name LIKE ?1 OR target_name LIKE ?1 OR context LIKE ?1 OR path LIKE ?1 LIMIT ?2")?;
312                    collect_candidates(
313                        &mut statement,
314                        params![pattern.as_deref(), (budget.rows + 1) as i64],
315                        budget,
316                        &mut work,
317                    )?
318                }
319                (None, None) => {
320                    let mut statement = self
321                        .connection
322                        .prepare("SELECT id FROM symbol_relations LIMIT ?1")?;
323                    collect_candidates(
324                        &mut statement,
325                        params![(budget.rows + 1) as i64],
326                        budget,
327                        &mut work,
328                    )?
329                }
330            };
331            let mut statement = self.connection.prepare("SELECT path, source_name, target_name, kind, line, context, parser FROM symbol_relations WHERE id = ?1")?;
332            admit_bytes(
333                &mut work,
334                (size_of::<(i64, SymbolRelation)>() + size_of::<SymbolRelation>()) as u64
335                    * candidates.len() as u64,
336                budget,
337            )?;
338            let mut relations = Vec::with_capacity(candidates.len());
339            for id in candidates {
340                guard.check(control, &mut work)?;
341                guard.admit_value(budget.intermediate_bytes.saturating_sub(work.decoded_bytes))?;
342                let relation =
343                    statement.query_row([id], |row| bounded_relation(row, budget, &mut work))??;
344                relations.push((id, relation));
345            }
346            relations.sort_unstable_by(|(left_id, left), (right_id, right)| {
347                (
348                    &left.path,
349                    left.line,
350                    &left.source_name,
351                    &left.target_name,
352                    left_id,
353                )
354                    .cmp(&(
355                        &right.path,
356                        right.line,
357                        &right.source_name,
358                        &right.target_name,
359                        right_id,
360                    ))
361            });
362            Ok(relations
363                .into_iter()
364                .map(|(_, relation)| relation)
365                .collect())
366        })();
367        guard.check(control, &mut work)?;
368        drop(guard);
369        match operation {
370            Ok(relations) => {
371                if let Some(snapshot) = snapshot {
372                    snapshot.commit()?;
373                }
374                Ok(LegacyRelationRead { relations, work })
375            }
376            Err(DbError::Sqlite(error)) if error.sqlite_error_code() == Some(ErrorCode::TooBig) => {
377                Err(read_limit(LegacyRelationReadLimit::ValueBytes, work))
378            }
379            Err(DbError::LegacyRelationReadLimit { limit, .. }) => Err(read_limit(limit, work)),
380            Err(error) => Err(error),
381        }
382    }
383}
384
385/// Retain only row identifiers and refuse as soon as a completeness sentinel exists.
386fn collect_candidates<P: rusqlite::Params>(
387    statement: &mut rusqlite::Statement<'_>,
388    params: P,
389    budget: LegacyRelationReadBudget,
390    work: &mut LegacyRelationReadWork,
391) -> DbResult<Vec<i64>> {
392    admit_bytes(work, (size_of::<i64>() * budget.rows) as u64, budget)?;
393    let mut rows = statement.query(params)?;
394    let mut candidates = Vec::with_capacity(budget.rows);
395    while let Some(row) = rows.next()? {
396        work.candidate_rows += 1;
397        if candidates.len() == budget.rows {
398            return Err(read_limit(LegacyRelationReadLimit::Rows, *work));
399        }
400        candidates.push(row.get(0)?);
401    }
402    Ok(candidates)
403}
404
405/// Check raw text widths before any owned relation decoding.
406fn bounded_relation(
407    row: &Row<'_>,
408    budget: LegacyRelationReadBudget,
409    work: &mut LegacyRelationReadWork,
410) -> rusqlite::Result<DbResult<SymbolRelation>> {
411    let mut bytes = 0_u64;
412    for index in [0, 1, 2, 3, 5, 6] {
413        bytes = bytes.saturating_add(row.get_ref(index)?.as_bytes()?.len() as u64);
414    }
415    if let Err(error) = admit_bytes(work, bytes, budget) {
416        return Ok(Err(error));
417    }
418    let kind_value: String = row.get(3)?;
419    let Some(kind) = RelationKind::from_db(&kind_value) else {
420        return Ok(Err(DbError::InvalidEnum {
421            field: "symbol relation kind",
422            value: kind_value,
423        }));
424    };
425    let raw_line = row.get::<_, i64>(4)?;
426    let line = match usize::try_from(raw_line) {
427        Ok(line) => line,
428        Err(source) => {
429            return Ok(Err(DbError::InvalidCount {
430                field: "symbol relation line",
431                value: raw_line,
432                source,
433            }));
434        }
435    };
436    Ok(Ok(SymbolRelation {
437        path: row.get(0)?,
438        source_name: row.get(1)?,
439        target_name: row.get(2)?,
440        kind,
441        line,
442        context: row.get(5)?,
443        parser: ParserKind::from_db(row.get_ref(6)?.as_str()?),
444    }))
445}
446
447#[cfg(test)]
448mod tests {
449    use super::*;
450    use projectatlas_core::symbols::SymbolGraph;
451    use projectatlas_core::{IndexCancellation, IndexWorkFailure};
452    use std::error::Error;
453    use std::io;
454
455    fn require(condition: bool, message: &str) -> Result<(), Box<dyn Error>> {
456        if condition {
457            Ok(())
458        } else {
459            Err(io::Error::other(message).into())
460        }
461    }
462
463    fn control() -> IndexWorkControl {
464        IndexWorkControl::new(IndexCancellation::new(), None)
465    }
466
467    fn budget(rows: usize) -> DbResult<LegacyRelationReadBudget> {
468        LegacyRelationReadBudget::new(
469            rows,
470            LegacyRelationReadBudget::MAX_DEADLINE,
471            LegacyRelationReadBudget::MAX_INTERMEDIATE_BYTES,
472        )
473    }
474
475    fn graph(count: usize) -> SymbolGraph {
476        SymbolGraph {
477            path: "lib.rs".to_string(),
478            language: Some("rust".to_string()),
479            parser: ParserKind::TreeSitter,
480            symbols: Vec::new(),
481            relations: (0..count)
482                .rev()
483                .map(|index| SymbolRelation {
484                    path: "lib.rs".to_string(),
485                    source_name: format!("source_{index:05}"),
486                    target_name: "target".to_string(),
487                    kind: RelationKind::Calls,
488                    line: index / 2 + 1,
489                    context: format!("target({index});"),
490                    parser: ParserKind::TreeSitter,
491                })
492                .collect(),
493        }
494    }
495
496    fn limits(connection: &Connection) -> rusqlite::Result<(i32, i32)> {
497        Ok((
498            connection.limit(Limit::SQLITE_LIMIT_LENGTH)?,
499            connection.limit(Limit::SQLITE_LIMIT_LIKE_PATTERN_LENGTH)?,
500        ))
501    }
502
503    #[test]
504    fn complete_arrays_keep_filters_order_and_native_query_plans() -> Result<(), Box<dyn Error>> {
505        let mut store = AtlasStore::in_memory()?;
506        store.replace_symbol_graph(&graph(50))?;
507        let original_limits = limits(&store.connection)?;
508        for (file, query) in [
509            (None, None),
510            (Some("lib.rs"), None),
511            (None, Some("source_0001_")),
512            (Some("lib.rs"), Some("source_0001_")),
513        ] {
514            let old = store.load_symbol_relations(file, query, 200)?;
515            let read = store.load_symbol_relations_bounded(file, query, budget(50)?, &control())?;
516            require(
517                read.relations == old,
518                "complete legacy fields or order changed",
519            )?;
520            require(
521                read.work.instructions > 0 && read.work.instructions < 100_000,
522                "candidate and hydration instructions not counted",
523            )?;
524            require(
525                read.work.decoded_bytes > 0 && read.work.decoded_bytes <= 1_024 * 1_024,
526                "retained bytes not bounded",
527            )?;
528            require(
529                limits(&store.connection)? == original_limits,
530                "native limits leaked",
531            )?;
532        }
533        for (sql, indexed) in [
534            (
535                "EXPLAIN QUERY PLAN SELECT id FROM symbol_relations WHERE path = 'lib.rs' LIMIT 51",
536                true,
537            ),
538            (
539                "EXPLAIN QUERY PLAN SELECT id FROM symbol_relations WHERE source_name LIKE '%absent%' LIMIT 51",
540                false,
541            ),
542        ] {
543            let plans = store
544                .connection
545                .prepare(sql)?
546                .query_map([], |row| row.get::<_, String>(3))?
547                .collect::<rusqlite::Result<Vec<_>>>()?;
548            require(
549                !plans.iter().any(|plan| plan.contains("TEMP B-TREE")),
550                "candidate query sorts wide keys",
551            )?;
552            if indexed {
553                require(
554                    plans.iter().any(|plan| plan.contains("INDEX")),
555                    "file lookup lost its index",
556                )?;
557            }
558        }
559        let mut tied = graph(2);
560        for (index, relation) in tied.relations.iter_mut().enumerate() {
561            relation.source_name = "same".to_string();
562            relation.line = 1;
563            relation.context = format!("ordered_{index}");
564        }
565        store.replace_symbol_graph(&tied)?;
566        store.begin_index_read_snapshot()?;
567        let read = store.load_symbol_relations_bounded(None, None, budget(2)?, &control())?;
568        require(
569            read.relations
570                .iter()
571                .map(|row| row.context.as_str())
572                .collect::<Vec<_>>()
573                == ["ordered_0", "ordered_1"],
574            "equal sort keys lost the stable row-identity order",
575        )?;
576        require(
577            !store.connection.is_autocommit(),
578            "reader committed the caller's snapshot",
579        )?;
580        store.finish_index_read_snapshot()?;
581        Ok(())
582    }
583
584    #[test]
585    fn high_degree_refuses_without_hydration_and_sparse_queries_stop_by_vm_work()
586    -> Result<(), Box<dyn Error>> {
587        let mut store = AtlasStore::in_memory()?;
588        store.replace_symbol_graph(&graph(10_001))?;
589        let original_limits = limits(&store.connection)?;
590        let broad = store.load_symbol_relations_bounded(None, None, budget(10)?, &control());
591        require(
592            matches!(
593                broad,
594                Err(DbError::LegacyRelationReadLimit {
595                    limit: LegacyRelationReadLimit::Rows,
596                    work: LegacyRelationReadWork {
597                        candidate_rows: 11,
598                        decoded_bytes: 80,
599                        ..
600                    },
601                })
602            ),
603            "large result did not stop at ID-only sentinel",
604        )?;
605        let sparse =
606            store.load_symbol_relations_bounded(None, Some("absent"), budget(10)?, &control());
607        require(
608            matches!(
609                sparse,
610                Err(DbError::LegacyRelationReadLimit {
611                    limit: LegacyRelationReadLimit::Instructions,
612                    work: LegacyRelationReadWork {
613                        instructions: 100_000,
614                        candidate_rows: 0,
615                        decoded_bytes: 88
616                    },
617                })
618            ),
619            "sparse scan escaped shared VM bound",
620        )?;
621        require(
622            limits(&store.connection)? == original_limits,
623            "failed-read native limits leaked",
624        )?;
625        store.replace_symbol_graph(&graph(1))?;
626        let retry = store.load_symbol_relations_bounded(
627            Some("lib.rs"),
628            Some("source_00000"),
629            budget(1)?,
630            &control(),
631        )?;
632        require(
633            retry.relations.len() == 1,
634            "read did not recover after VM interruption",
635        )?;
636        Ok(())
637    }
638
639    #[test]
640    fn wide_values_pattern_caps_and_cancellation_restore_connection_state()
641    -> Result<(), Box<dyn Error>> {
642        let mut store = AtlasStore::in_memory()?;
643        let mut source = graph(1);
644        source.relations[0].context = "x".repeat(1_024 * 1_024 + 1);
645        store.replace_symbol_graph(&source)?;
646        let original_limits = limits(&store.connection)?;
647        require(
648            matches!(
649                store.load_symbol_relations_bounded(None, None, budget(1)?, &control()),
650                Err(DbError::LegacyRelationReadLimit {
651                    limit: LegacyRelationReadLimit::ValueBytes,
652                    ..
653                })
654            ),
655            "wide stored value was not refused by native admission",
656        )?;
657        source.relations[0].context = "short".to_string();
658        store.replace_symbol_graph(&source)?;
659        for query in ["x".repeat(1_025), "é".repeat(513)] {
660            require(
661                matches!(
662                    store.load_symbol_relations_bounded(None, Some(&query), budget(1)?, &control()),
663                    Err(DbError::LegacyRelationReadLimit {
664                        limit: LegacyRelationReadLimit::QueryBytes,
665                        work: LegacyRelationReadWork {
666                            instructions: 0,
667                            ..
668                        }
669                    })
670                ),
671                "oversized pattern reached SQLite",
672            )?;
673        }
674        for query in ["%".repeat(1_024), "é".repeat(512)] {
675            let result =
676                store.load_symbol_relations_bounded(None, Some(&query), budget(1)?, &control())?;
677            require(
678                result.work.instructions > 0,
679                "maximum-byte pattern was not admitted",
680            )?;
681        }
682        let cancellation = control();
683        {
684            let guard = LegacyReadGuard::new(&store.connection, budget(1)?, &cancellation)?;
685            cancellation.cancel();
686            require(
687                store
688                    .connection
689                    .query_row("SELECT count(*) FROM symbol_relations", [], |row| {
690                        row.get::<_, i64>(0)
691                    })
692                    .is_err(),
693                "installed progress hook did not observe cancellation",
694            )?;
695            require(
696                matches!(
697                    guard.check(&cancellation, &mut LegacyRelationReadWork::default()),
698                    Err(DbError::IndexWork(IndexWorkFailure::Cancelled { .. }))
699                ),
700                "cancellation lost its typed owner",
701            )?;
702        }
703        require(
704            limits(&store.connection)? == original_limits,
705            "native limits leaked after failure",
706        )?;
707        require(
708            store
709                .load_symbol_relations_bounded(None, None, budget(1)?, &control())?
710                .relations
711                .len()
712                == 1,
713            "next read inherited cancellation or limits",
714        )?;
715        let tiny = LegacyRelationReadBudget::new(1, Duration::from_nanos(1), 1_024 * 1_024)?;
716        require(
717            matches!(
718                store.load_symbol_relations_bounded(None, None, tiny, &control()),
719                Err(DbError::LegacyRelationReadLimit {
720                    limit: LegacyRelationReadLimit::Deadline,
721                    ..
722                })
723            ),
724            "local deadline was not observed",
725        )?;
726        require(
727            limits(&store.connection)? == original_limits,
728            "deadline leaked native limits",
729        )?;
730        Ok(())
731    }
732}