Skip to main content

tilewright_pack/read/
rows.rs

1// Copyright 2026 Seraphina Studios
2// SPDX-License-Identifier: MIT
3
4//! Single reads and listings of planes, zones, anchors and definitions, and
5//! of the layers and chunk positions a plane has. Like [`Pack::chunk`], each
6//! checks the shape and bounds of what it loads, and agreement between a
7//! row and its own document; whether the targets a row names exist is
8//! checked by [`Pack::verify`].
9
10use super::{
11    bounded_identifier, bounded_text, maximum_document_bytes, stairwell_row_within,
12    stored_document, text, utf8_column,
13};
14use crate::{Error, ErrorCode, Pack, is_identifier};
15use rusqlite::{Row, ToSql, params, types};
16use std::fmt::Write as _;
17use tilewright_jsonc::{Document, MAX_SAFE_INTEGER, Value};
18use tilewright_source::{CHUNK_SIZE, Stairwell};
19
20/// A plane row and its stored document.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct Plane {
23    pub id: String,
24    /// `shared`, `instanced` or `per_player`.
25    pub kind: String,
26    pub chunk_size: u16,
27    /// The plane's source document as canonical JSON.
28    pub document: String,
29}
30
31/// A zone row and its stored entry.
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct Zone {
34    pub plane: String,
35    pub id: String,
36    /// The parent zone's ID, or `None` for a root zone.
37    pub parent: Option<String>,
38    /// The zone's source entry as canonical JSON.
39    pub document: String,
40}
41
42/// A named position on a plane.
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub struct Anchor {
45    pub plane: String,
46    pub id: String,
47    pub layer: i32,
48    pub x: i64,
49    pub y: i64,
50}
51
52/// A definition row and its stored document.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct Definition {
55    /// The definition type, such as `proto`.
56    pub kind: String,
57    pub id: String,
58    /// The definition's source document as canonical JSON.
59    pub document: String,
60}
61
62/// SQL for `column` when it is an integer, else NULL.
63pub(super) fn integer(column: &str) -> String {
64    format!("CASE WHEN typeof({column}) = 'integer' THEN {column} END")
65}
66
67/// The identifier in column `index` of a row selected with
68/// [`bounded_identifier`]; `code` is the rule a bad one breaks.
69pub(super) fn identifier(
70    row: &Row<'_>,
71    index: usize,
72    what: &str,
73    code: ErrorCode,
74) -> Result<String, Error> {
75    utf8_column(row, index, code)?
76        .filter(|value| is_identifier(value))
77        .ok_or_else(|| Error::Rejected(code, format!("a {what} key is not an identifier")))
78}
79
80/// The 32-bit layer in column `index` of a row selected with [`integer`].
81pub(super) fn layer(
82    row: &Row<'_>,
83    index: usize,
84    what: &str,
85    code: ErrorCode,
86) -> Result<i32, Error> {
87    let value: Option<i64> = row.get(index)?;
88    value
89        .and_then(|layer| i32::try_from(layer).ok())
90        .ok_or_else(|| Error::Rejected(code, format!("a {what} layer is not a 32-bit integer")))
91}
92
93/// The integer in column `index` of a row selected with [`integer`], within
94/// the source format's integer range.
95pub(super) fn position(
96    row: &Row<'_>,
97    index: usize,
98    what: &str,
99    code: ErrorCode,
100) -> Result<i64, Error> {
101    let value: Option<i64> = row.get(index)?;
102    value
103        .filter(|value| (-MAX_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(value))
104        .ok_or_else(|| {
105            Error::Rejected(
106                code,
107                format!("a {what} position is not an integer within ±{MAX_SAFE_INTEGER}"),
108            )
109        })
110}
111
112/// The chunk coordinate in column `index` of a row selected with
113/// [`integer`]: one whose chunk, `CHUNK_SIZE` tiles across as format 1
114/// requires, keeps every tile within the format's integer range.
115pub(super) fn chunk_coordinate(row: &Row<'_>, index: usize, code: ErrorCode) -> Result<i64, Error> {
116    let size = i64::from(CHUNK_SIZE);
117    let range = -(MAX_SAFE_INTEGER / size)..=(MAX_SAFE_INTEGER - (size - 1)) / size;
118    let value: Option<i64> = row.get(index)?;
119    value.filter(|value| range.contains(value)).ok_or_else(|| {
120        Error::Rejected(
121            code,
122            format!(
123                "a chunk coordinate is not an integer from {} to {}",
124                range.start(),
125                range.end()
126            ),
127        )
128    })
129}
130
131/// The error for a stored document that disagrees with its row.
132pub(super) fn disagrees(what: &str, code: ErrorCode) -> Error {
133    Error::Rejected(
134        code,
135        format!("the document of {what} disagrees with its row"),
136    )
137}
138
139/// The columns of a plane row: `id, kind, chunk_size, document`.
140pub(super) fn plane_columns() -> String {
141    format!(
142        "{}, {}, {}, {}",
143        bounded_identifier("id"),
144        bounded_identifier("kind"),
145        integer("chunk_size"),
146        bounded_text("document")
147    )
148}
149
150/// A row of `planes` selected with [`plane_columns`], and its stored
151/// document. As verification checks, the kind is one format 1 defines, the
152/// chunk size is [`CHUNK_SIZE`], and the document agrees with the row on its
153/// format, ID, kind and chunk size, and has a string `default_zone`, if any;
154/// whether that zone exists is checked by verification.
155pub(super) fn plane_row(row: &Row<'_>) -> Result<(String, Document), Error> {
156    let id = identifier(row, 0, "plane", ErrorCode::Plane)?;
157    let kind = utf8_column(row, 1, ErrorCode::Plane)?;
158    let chunk_size: Option<i64> = row.get(2)?;
159    let stored = stored_document(
160        utf8_column(row, 3, ErrorCode::Plane)?,
161        "a plane document",
162        ErrorCode::Plane,
163    )?;
164    if !kind
165        .as_deref()
166        .is_some_and(|kind| ["shared", "instanced", "per_player"].contains(&kind))
167        || chunk_size != Some(i64::from(CHUNK_SIZE))
168    {
169        return Err(Error::Rejected(
170            ErrorCode::Plane,
171            format!("plane {id} has an invalid kind or a chunk size other than {CHUNK_SIZE}"),
172        ));
173    }
174    if text(&stored, "format") != Some("tilewright.plane/1")
175        || text(&stored, "id") != Some(&id)
176        || text(&stored, "kind") != kind.as_deref()
177        || stored.root().get("chunk_size").and_then(Value::as_i64) != chunk_size
178        || stored
179            .root()
180            .get("default_zone")
181            .is_some_and(|zone| zone.as_str().is_none())
182    {
183        return Err(disagrees(&format!("plane {id}"), ErrorCode::Plane));
184    }
185    Ok((id, stored))
186}
187
188/// A [`Plane`] from a row selected with [`plane_columns`].
189fn plane(row: &Row<'_>) -> Result<Plane, Error> {
190    let (id, stored) = plane_row(row)?;
191    Ok(Plane {
192        id,
193        kind: text(&stored, "kind").unwrap_or_default().to_owned(),
194        chunk_size: CHUNK_SIZE,
195        document: stored.source().to_owned(),
196    })
197}
198
199/// The columns of a zone row: `plane, id, parent IS NULL, parent, document`.
200pub(super) fn zone_columns() -> String {
201    format!(
202        "{}, {}, parent IS NULL, {}, {}",
203        bounded_identifier("plane"),
204        bounded_identifier("id"),
205        bounded_identifier("parent"),
206        bounded_text("document")
207    )
208}
209
210/// A row of `zones` selected with [`zone_columns`], and its stored document,
211/// which agrees with the row on its ID and parent, as verification checks.
212pub(super) fn zone_row(row: &Row<'_>) -> Result<(String, String, Document), Error> {
213    let (plane, id) = (
214        identifier(row, 0, "zone", ErrorCode::Zone)?,
215        identifier(row, 1, "zone", ErrorCode::Zone)?,
216    );
217    let parent = if row.get::<_, bool>(2)? {
218        None
219    } else {
220        Some(identifier(row, 3, "zone parent", ErrorCode::Zone)?)
221    };
222    let stored = stored_document(
223        utf8_column(row, 4, ErrorCode::Zone)?,
224        "a zone document",
225        ErrorCode::Zone,
226    )?;
227    if text(&stored, "id") != Some(&id)
228        || stored.root().get("parent").map(Value::as_str) != parent.as_deref().map(Some)
229    {
230        return Err(disagrees(&format!("zone {plane}/{id}"), ErrorCode::Zone));
231    }
232    Ok((plane, id, stored))
233}
234
235/// A [`Zone`] from a row selected with [`zone_columns`].
236fn zone(row: &Row<'_>) -> Result<Zone, Error> {
237    let (plane, id, stored) = zone_row(row)?;
238    Ok(Zone {
239        plane,
240        id,
241        parent: text(&stored, "parent").map(str::to_owned),
242        document: stored.source().to_owned(),
243    })
244}
245
246/// The columns of an anchor row: `plane, id, layer, x, y`.
247pub(super) fn anchor_columns() -> String {
248    format!(
249        "{}, {}, {}, {}, {}",
250        bounded_identifier("plane"),
251        bounded_identifier("id"),
252        integer("layer"),
253        integer("x"),
254        integer("y")
255    )
256}
257
258/// An anchor row selected with [`anchor_columns`]: identifier keys, a
259/// 32-bit layer and a position within the source format's integer range.
260pub(super) fn anchor_row(row: &Row<'_>) -> Result<Anchor, Error> {
261    let code = ErrorCode::Anchor;
262    Ok(Anchor {
263        plane: identifier(row, 0, "anchor", code)?,
264        id: identifier(row, 1, "anchor", code)?,
265        layer: layer(row, 2, "anchor", code)?,
266        x: position(row, 3, "anchor", code)?,
267        y: position(row, 4, "anchor", code)?,
268    })
269}
270
271/// The columns of a definition row: `type, id, document`.
272pub(super) fn definition_columns() -> String {
273    format!(
274        "{}, {}, {}",
275        bounded_identifier("type"),
276        bounded_identifier("id"),
277        bounded_text("document")
278    )
279}
280
281/// A row of `definitions` selected with [`definition_columns`], whose
282/// document agrees with it on its format, type and ID.
283pub(super) fn definition_row(row: &Row<'_>) -> Result<(String, String, Document), Error> {
284    let (kind, id) = (
285        identifier(row, 0, "definition", ErrorCode::Definition)?,
286        identifier(row, 1, "definition", ErrorCode::Definition)?,
287    );
288    let stored = stored_document(
289        utf8_column(row, 2, ErrorCode::Definition)?,
290        "a definition document",
291        ErrorCode::Definition,
292    )?;
293    if text(&stored, "format") != Some("tilewright.def/1")
294        || text(&stored, "type") != Some(&kind)
295        || text(&stored, "id") != Some(&id)
296    {
297        return Err(disagrees(
298            &format!("definition {kind}/{id}"),
299            ErrorCode::Definition,
300        ));
301    }
302    Ok((kind, id, stored))
303}
304
305/// A [`Definition`] from a row selected with [`definition_columns`].
306fn definition(row: &Row<'_>) -> Result<Definition, Error> {
307    let (kind, id, stored) = definition_row(row)?;
308    Ok(Definition {
309        kind,
310        id,
311        document: stored.source().to_owned(),
312    })
313}
314
315/// Which rows of a listing to visit: those after the row whose key is
316/// `after`, in the listing's key order, at most `limit` of them, and no
317/// more once their text reaches `bytes`. A listing reads a page with one
318/// query that starts at `after` in the table's primary key, so a caller can
319/// walk a table a page at a time, holding one page, and resume from the
320/// last key it saw.
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
322pub struct Page<K> {
323    /// The key of the last row already seen, or `None` to start at the
324    /// first row.
325    pub after: Option<K>,
326    /// The most rows to visit.
327    pub limit: usize,
328    /// The text to visit: the page stops after the row that brings the
329    /// bytes of its rows' keys and documents to this, so it always visits a
330    /// row when there is one, and holds less than this and one row.
331    pub bytes: usize,
332}
333
334impl<K> Page<K> {
335    /// Every row, from the first.
336    #[must_use]
337    pub const fn all() -> Self {
338        Self {
339            after: None,
340            limit: usize::MAX,
341            bytes: usize::MAX,
342        }
343    }
344}
345
346/// A listing's query: `SELECT ... FROM ...` and its conditions, with its
347/// parameters in the order they bind.
348struct Listing {
349    sql: String,
350    conditions: Vec<String>,
351    values: Vec<types::Value>,
352}
353
354impl Listing {
355    /// Selects `columns` from `table`. Columns that load a document bound
356    /// it by `?1`, the largest stored document, so `documents` binds it.
357    fn new(columns: &str, table: &str, documents: bool) -> Self {
358        Self {
359            sql: format!("SELECT {columns} FROM {table}"),
360            conditions: Vec::new(),
361            values: if documents {
362                vec![types::Value::Integer(maximum_document_bytes())]
363            } else {
364                Vec::new()
365            },
366        }
367    }
368
369    /// The next parameter, `?N`, bound to `value`.
370    fn bind(&mut self, value: impl Into<types::Value>) -> String {
371        self.values.push(value.into());
372        format!("?{}", self.values.len())
373    }
374
375    /// Adds a condition that takes the next parameters.
376    fn condition(&mut self, condition: impl FnOnce(&mut Self) -> String) -> &mut Self {
377        let condition = condition(self);
378        self.conditions.push(condition);
379        self
380    }
381
382    /// The query, ending in `order`, its `ORDER BY` clause and any
383    /// grouping before it, and limited to `limit` rows; a limit past
384    /// SQLite's is none.
385    fn finish(mut self, order: &str, limit: usize) -> (String, Vec<types::Value>) {
386        let limit = self.bind(i64::try_from(limit).unwrap_or(-1));
387        let mut sql = self.sql;
388        if !self.conditions.is_empty() {
389            sql.push_str(" WHERE ");
390            sql.push_str(&self.conditions.join(" AND "));
391        }
392        let _ = write!(sql, " {order} LIMIT {limit}");
393        (sql, self.values)
394    }
395}
396
397impl Pack {
398    /// The first row of `sql`, bound to `parameters`, read with `read`.
399    fn row_of<T>(
400        &self,
401        sql: &str,
402        parameters: &[&dyn ToSql],
403        read: fn(&Row<'_>) -> Result<T, Error>,
404    ) -> Result<Option<T>, Error> {
405        let mut statement = self.connection.prepare(sql)?;
406        let mut rows = statement.query(parameters)?;
407        rows.next()?.map(read).transpose()
408    }
409
410    /// Calls `visit` with each row of a listing, read with `read`, until the
411    /// sizes `keyed` gives the rows reach `bytes`. Keys, which `keyed` also
412    /// gives, must rise strictly from `after`, as a table's primary key
413    /// does; a page that does not move forward is an invalid table,
414    /// `code`'s error, so walking a table page by page always ends.
415    #[expect(
416        clippy::too_many_arguments,
417        reason = "each listing gives its query, page, row reader, key and code"
418    )]
419    fn walk<T, K: Ord>(
420        &self,
421        (sql, values): (String, Vec<types::Value>),
422        after: Option<K>,
423        bytes: usize,
424        read: impl Fn(&Row<'_>) -> Result<T, Error>,
425        keyed: impl Fn(&T) -> (K, usize),
426        code: ErrorCode,
427        visit: &mut dyn FnMut(T) -> Result<(), Error>,
428    ) -> Result<(), Error> {
429        let mut statement = self.connection.prepare(&sql)?;
430        let mut rows = statement.query(rusqlite::params_from_iter(values))?;
431        let mut last = after;
432        let mut total: usize = 0;
433        while let Some(row) = rows.next()? {
434            let value = read(row)?;
435            let (next, size) = keyed(&value);
436            if last.as_ref().is_some_and(|last| *last >= next) {
437                return Err(Error::Rejected(
438                    code,
439                    "a table's rows are not in the order of its primary key".into(),
440                ));
441            }
442            last = Some(next);
443            visit(value)?;
444            total = total.saturating_add(size);
445            if total >= bytes {
446                break;
447            }
448        }
449        Ok(())
450    }
451
452    /// The stored profile as canonical JSON, checked as a full
453    /// verification checks it; see [`Pack::layer_height`].
454    ///
455    /// # Errors
456    /// Returns an error if SQLite fails or the stored profile is invalid.
457    pub fn profile(&self) -> Result<String, Error> {
458        self.profile_layers()?;
459        self.meta("profile")?
460            .ok_or_else(|| Error::Rejected(ErrorCode::Meta, "the pack has no profile".into()))
461    }
462
463    /// A plane, read from its row and stored document.
464    ///
465    /// # Errors
466    /// Returns an error if SQLite fails or the stored plane is invalid.
467    pub fn plane(&self, id: &str) -> Result<Option<Plane>, Error> {
468        self.row_of(
469            &format!("SELECT {} FROM planes WHERE id = ?2", plane_columns()),
470            params![maximum_document_bytes(), id],
471            plane,
472        )
473    }
474
475    /// Calls `visit` with the planes of `page`, in ID order. One stored
476    /// document is held at a time.
477    ///
478    /// # Errors
479    /// Returns an error if SQLite fails, a stored plane is invalid, or
480    /// `visit` fails.
481    pub fn visit_planes(
482        &self,
483        page: Page<&str>,
484        visit: &mut dyn FnMut(Plane) -> Result<(), Error>,
485    ) -> Result<(), Error> {
486        self.walk(
487            planes_listing(page),
488            page.after.map(str::to_owned),
489            page.bytes,
490            plane,
491            |plane| {
492                let size = plane.id.len() + plane.kind.len() + plane.document.len();
493                (plane.id.clone(), size)
494            },
495            ErrorCode::Plane,
496            visit,
497        )
498    }
499
500    /// A zone of a plane. Whether the plane and parent exist is checked by
501    /// [`Pack::verify`].
502    ///
503    /// # Errors
504    /// Returns an error if SQLite fails or the stored zone is invalid.
505    pub fn zone(&self, plane: &str, id: &str) -> Result<Option<Zone>, Error> {
506        self.row_of(
507            &format!(
508                "SELECT {} FROM zones WHERE plane = ?2 AND id = ?3",
509                zone_columns()
510            ),
511            params![maximum_document_bytes(), plane, id],
512            zone,
513        )
514    }
515
516    /// Calls `visit` with the zones of a plane in `page`, in ID order.
517    ///
518    /// # Errors
519    /// Returns an error if SQLite fails, a stored zone is invalid, or
520    /// `visit` fails.
521    pub fn visit_zones(
522        &self,
523        plane: &str,
524        page: Page<&str>,
525        visit: &mut dyn FnMut(Zone) -> Result<(), Error>,
526    ) -> Result<(), Error> {
527        self.walk(
528            in_plane(&zone_columns(), "zones", true, plane, page),
529            page.after.map(str::to_owned),
530            page.bytes,
531            zone,
532            |zone| {
533                let parent = zone.parent.as_ref().map_or(0, String::len);
534                let size = zone.plane.len() + zone.id.len() + parent + zone.document.len();
535                (zone.id.clone(), size)
536            },
537            ErrorCode::Zone,
538            visit,
539        )
540    }
541
542    /// An anchor of a plane. Whether the plane exists is checked by
543    /// [`Pack::verify`].
544    ///
545    /// # Errors
546    /// Returns an error if SQLite fails or the stored anchor is invalid.
547    pub fn anchor(&self, plane: &str, id: &str) -> Result<Option<Anchor>, Error> {
548        self.row_of(
549            &format!(
550                "SELECT {} FROM anchors WHERE plane = ?1 AND id = ?2",
551                anchor_columns()
552            ),
553            params![plane, id],
554            anchor_row,
555        )
556    }
557
558    /// Calls `visit` with the anchors of a plane in `page`, in ID order.
559    ///
560    /// # Errors
561    /// Returns an error if SQLite fails, a stored anchor is invalid, or
562    /// `visit` fails.
563    pub fn visit_anchors(
564        &self,
565        plane: &str,
566        page: Page<&str>,
567        visit: &mut dyn FnMut(Anchor) -> Result<(), Error>,
568    ) -> Result<(), Error> {
569        self.walk(
570            in_plane(&anchor_columns(), "anchors", false, plane, page),
571            page.after.map(str::to_owned),
572            page.bytes,
573            anchor_row,
574            |anchor| (anchor.id.clone(), anchor.plane.len() + anchor.id.len()),
575            ErrorCode::Anchor,
576            visit,
577        )
578    }
579
580    /// Calls `visit` with the stairwells of a plane in `page`, in ID order,
581    /// read from their stored documents as [`Pack::stairwell`] reads one.
582    /// One stored document is held at a time.
583    ///
584    /// # Errors
585    /// Returns an error if SQLite fails, a stored stairwell is invalid, or
586    /// `visit` fails.
587    pub fn visit_stairwells(
588        &self,
589        plane: &str,
590        page: Page<&str>,
591        visit: &mut dyn FnMut(Stairwell) -> Result<(), Error>,
592    ) -> Result<(), Error> {
593        self.visit_stairwells_within(plane, page, u64::MAX, visit)
594    }
595
596    /// [`Pack::visit_stairwells`], with a budget of `maximum_steps` steps
597    /// for each stairwell; see [`Pack::stairwell_within`].
598    ///
599    /// # Errors
600    /// Returns an error if SQLite fails, a stored stairwell is invalid or
601    /// lists more steps than the budget, or `visit` fails.
602    pub fn visit_stairwells_within(
603        &self,
604        plane: &str,
605        page: Page<&str>,
606        maximum_steps: u64,
607        visit: &mut dyn FnMut(Stairwell) -> Result<(), Error>,
608    ) -> Result<(), Error> {
609        let columns = format!("{}, {}", bounded_identifier("id"), bounded_text("document"));
610        self.walk(
611            in_plane(&columns, "stairwells", true, plane, page),
612            page.after.map(str::to_owned),
613            page.bytes,
614            |row| stairwell_row_within(row, maximum_steps),
615            |stairwell| {
616                let steps: usize = stairwell.steps.iter().map(|step| step.proto.len()).sum();
617                let custom = stairwell.custom.as_ref().map_or(0, String::len);
618                let size = stairwell.id.len() + steps + custom + stairwell.canonical.len();
619                (stairwell.id.clone(), size)
620            },
621            ErrorCode::Stairwell,
622            visit,
623        )
624    }
625
626    /// A definition, by type and ID.
627    ///
628    /// # Errors
629    /// Returns an error if SQLite fails or the stored definition is invalid.
630    pub fn definition(&self, kind: &str, id: &str) -> Result<Option<Definition>, Error> {
631        self.row_of(
632            &format!(
633                "SELECT {} FROM definitions WHERE type = ?2 AND id = ?3",
634                definition_columns()
635            ),
636            params![maximum_document_bytes(), kind, id],
637            definition,
638        )
639    }
640
641    /// Calls `visit` with the definitions of `page`, by type and then ID:
642    /// those of type `kind`, or every definition when `kind` is `None`.
643    /// A page's key is a definition's type and ID. One stored document is
644    /// held at a time.
645    ///
646    /// # Errors
647    /// Returns an error if SQLite fails, a stored definition is invalid, or
648    /// `visit` fails.
649    pub fn visit_definitions(
650        &self,
651        kind: Option<&str>,
652        page: Page<(&str, &str)>,
653        visit: &mut dyn FnMut(Definition) -> Result<(), Error>,
654    ) -> Result<(), Error> {
655        let Some(query) = definitions_listing(kind, page) else {
656            return Ok(());
657        };
658        self.walk(
659            query,
660            page.after
661                .map(|(kind, id)| (kind.to_owned(), id.to_owned())),
662            page.bytes,
663            definition,
664            |definition| {
665                let size = definition.kind.len() + definition.id.len() + definition.document.len();
666                ((definition.kind.clone(), definition.id.clone()), size)
667            },
668            ErrorCode::Definition,
669            visit,
670        )
671    }
672
673    /// Calls `visit` with the layers of a plane that have chunks, in
674    /// `page`, in order.
675    ///
676    /// # Errors
677    /// Returns an error if SQLite fails, a stored layer is not a 32-bit
678    /// integer, or `visit` fails.
679    pub fn visit_layers(
680        &self,
681        plane: &str,
682        page: Page<i32>,
683        visit: &mut dyn FnMut(i32) -> Result<(), Error>,
684    ) -> Result<(), Error> {
685        self.walk(
686            layers_listing(plane, page),
687            page.after,
688            page.bytes,
689            |row| layer(row, 0, "chunk", ErrorCode::Chunk),
690            |layer| (*layer, 0),
691            ErrorCode::Chunk,
692            visit,
693        )
694    }
695
696    /// Calls `visit` with the chunk coordinates `(cx, cy)` of the chunks of
697    /// a plane's layer in `page`, by `cx` and then `cy`. Each keeps every
698    /// tile of its chunk within the source format's integer range.
699    ///
700    /// # Errors
701    /// Returns an error if SQLite fails, a stored position is out of range,
702    /// or `visit` fails.
703    pub fn visit_chunk_keys(
704        &self,
705        plane: &str,
706        layer: i32,
707        page: Page<(i64, i64)>,
708        visit: &mut dyn FnMut(i64, i64) -> Result<(), Error>,
709    ) -> Result<(), Error> {
710        self.walk(
711            chunk_keys_listing(plane, layer, page),
712            page.after,
713            page.bytes,
714            |row| {
715                Ok((
716                    chunk_coordinate(row, 0, ErrorCode::Chunk)?,
717                    chunk_coordinate(row, 1, ErrorCode::Chunk)?,
718                ))
719            },
720            |key| (*key, 0),
721            ErrorCode::Chunk,
722            &mut |(cx, cy)| visit(cx, cy),
723        )
724    }
725}
726
727// Each listing's query seeks its table's primary key to the row after the
728// page's key, so a walk of a table page by page reads each row once.
729
730/// The listing of planes in `page`.
731fn planes_listing(page: Page<&str>) -> (String, Vec<types::Value>) {
732    let mut listing = Listing::new(&plane_columns(), "planes", true);
733    if let Some(after) = page.after {
734        listing.condition(|listing| format!("id > {}", listing.bind(after.to_owned())));
735    }
736    listing.finish("ORDER BY id", page.limit)
737}
738
739/// The listing of a plane's rows of `table`, keyed by `id`, in `page`.
740fn in_plane(
741    columns: &str,
742    table: &str,
743    documents: bool,
744    plane: &str,
745    page: Page<&str>,
746) -> (String, Vec<types::Value>) {
747    let mut listing = Listing::new(columns, table, documents);
748    listing.condition(|listing| format!("plane = {}", listing.bind(plane.to_owned())));
749    if let Some(after) = page.after {
750        listing.condition(|listing| format!("id > {}", listing.bind(after.to_owned())));
751    }
752    listing.finish("ORDER BY id", page.limit)
753}
754
755/// The listing of the definitions of `page`, of type `kind` or of every
756/// type, or `None` when no row can follow the page's key. With a type, the
757/// key's type decides which IDs follow, so the query bounds only the ID: a
758/// bound on `(type, id)` as well as `type = ?` would make SQLite seek the
759/// type and filter its rows from the first, for every page.
760fn definitions_listing(
761    kind: Option<&str>,
762    page: Page<(&str, &str)>,
763) -> Option<(String, Vec<types::Value>)> {
764    let mut listing = Listing::new(&definition_columns(), "definitions", true);
765    match (kind, page.after) {
766        (Some(kind), after) => {
767            listing.condition(|listing| format!("type = {}", listing.bind(kind.to_owned())));
768            if let Some((after_kind, after_id)) = after {
769                match after_kind.cmp(kind) {
770                    // Every definition of the type follows the key.
771                    std::cmp::Ordering::Less => {}
772                    std::cmp::Ordering::Equal => {
773                        listing.condition(|listing| {
774                            format!("id > {}", listing.bind(after_id.to_owned()))
775                        });
776                    }
777                    std::cmp::Ordering::Greater => return None,
778                }
779            }
780        }
781        (None, Some((after_kind, after_id))) => {
782            listing.condition(|listing| {
783                format!(
784                    "(type, id) > ({}, {})",
785                    listing.bind(after_kind.to_owned()),
786                    listing.bind(after_id.to_owned())
787                )
788            });
789        }
790        (None, None) => {}
791    }
792    Some(listing.finish("ORDER BY type, id", page.limit))
793}
794
795/// The listing of the layers of a plane that have chunks, in `page`.
796fn layers_listing(plane: &str, page: Page<i32>) -> (String, Vec<types::Value>) {
797    let mut listing = Listing::new(&integer("layer"), "chunks", false);
798    listing.condition(|listing| format!("plane = {}", listing.bind(plane.to_owned())));
799    if let Some(after) = page.after {
800        listing.condition(|listing| format!("layer > {}", listing.bind(after)));
801    }
802    // Grouping by the stored layer lets SQLite read the primary key in
803    // order instead of collecting the layers first.
804    listing.finish("GROUP BY layer ORDER BY layer", page.limit)
805}
806
807/// The listing of the chunk keys of a plane's layer, in `page`.
808fn chunk_keys_listing(
809    plane: &str,
810    layer: i32,
811    page: Page<(i64, i64)>,
812) -> (String, Vec<types::Value>) {
813    let mut listing = Listing::new(
814        &format!("{}, {}", integer("cx"), integer("cy")),
815        "chunks",
816        false,
817    );
818    listing.condition(|listing| format!("plane = {}", listing.bind(plane.to_owned())));
819    listing.condition(|listing| format!("layer = {}", listing.bind(layer)));
820    if let Some((cx, cy)) = page.after {
821        listing.condition(|listing| {
822            format!("(cx, cy) > ({}, {})", listing.bind(cx), listing.bind(cy))
823        });
824    }
825    listing.finish("ORDER BY cx, cy", page.limit)
826}
827
828#[cfg(test)]
829mod tests {
830    use super::{
831        Listing, Page, chunk_keys_listing, definitions_listing, in_plane, layers_listing, plane,
832        plane_columns, planes_listing, types,
833    };
834    use crate::{Error, ErrorCode, Pack, VerifyLimits};
835
836    fn pack() -> Pack {
837        Pack::open_with(&crate::tests::valid_pack(), &VerifyLimits::default()).unwrap()
838    }
839
840    /// The rows of a listing read all at once, and read one page of one row
841    /// at a time from the last row's key, which must be the same. `list`
842    /// reads the page after a key, of at most a number of rows.
843    fn paged<T: PartialEq + std::fmt::Debug, K>(
844        list: impl Fn(Option<&K>, usize, &mut dyn FnMut(T) -> Result<(), Error>) -> Result<(), Error>,
845        key: impl Fn(&T) -> K,
846    ) -> Vec<T> {
847        let read = |after: Option<&K>, limit| {
848            let mut rows = Vec::new();
849            list(after, limit, &mut |row| {
850                rows.push(row);
851                Ok(())
852            })
853            .unwrap();
854            rows
855        };
856        let all = read(None, usize::MAX);
857        let mut one_by_one: Vec<T> = Vec::new();
858        loop {
859            let after = one_by_one.last().map(&key);
860            let mut page = read(after.as_ref(), 1);
861            assert!(page.len() <= 1);
862            let Some(row) = page.pop() else { break };
863            one_by_one.push(row);
864        }
865        assert_eq!(one_by_one, all);
866        all
867    }
868
869    fn text(after: Option<&String>) -> Option<&str> {
870        after.map(String::as_str)
871    }
872
873    fn pair(after: Option<&(String, String)>) -> Option<(&str, &str)> {
874        after.map(|(kind, id)| (kind.as_str(), id.as_str()))
875    }
876
877    #[test]
878    fn plane_listings_page_by_their_keys() {
879        let pack = pack();
880        let planes = paged(
881            |after, limit, visit| {
882                pack.visit_planes(
883                    Page {
884                        after: text(after),
885                        limit,
886                        bytes: usize::MAX,
887                    },
888                    visit,
889                )
890            },
891            |plane| plane.id.clone(),
892        );
893        assert_eq!(planes.len(), 2);
894        let zones = paged(
895            |after, limit, visit| {
896                pack.visit_zones(
897                    "overworld",
898                    Page {
899                        after: text(after),
900                        limit,
901                        bytes: usize::MAX,
902                    },
903                    visit,
904                )
905            },
906            |zone| zone.id.clone(),
907        );
908        assert_eq!(zones.len(), 3);
909        let anchors = paged(
910            |after, limit, visit| {
911                pack.visit_anchors(
912                    "overworld",
913                    Page {
914                        after: text(after),
915                        limit,
916                        bytes: usize::MAX,
917                    },
918                    visit,
919                )
920            },
921            |anchor| anchor.id.clone(),
922        );
923        assert_eq!(anchors.len(), 2);
924        let stairwells = paged(
925            |after, limit, visit| {
926                pack.visit_stairwells(
927                    "overworld",
928                    Page {
929                        after: text(after),
930                        limit,
931                        bytes: usize::MAX,
932                    },
933                    visit,
934                )
935            },
936            |stairwell| stairwell.id.clone(),
937        );
938        assert_eq!(stairwells.len(), 3);
939    }
940
941    #[test]
942    fn definitions_layers_and_chunk_keys_page_by_their_keys() {
943        let pack = pack();
944        let definitions = paged(
945            |after, limit, visit| {
946                pack.visit_definitions(
947                    None,
948                    Page {
949                        after: pair(after),
950                        limit,
951                        bytes: usize::MAX,
952                    },
953                    visit,
954                )
955            },
956            |definition| (definition.kind.clone(), definition.id.clone()),
957        );
958        assert_eq!(definitions.len(), 12);
959        let protos = paged(
960            |after, limit, visit| {
961                pack.visit_definitions(
962                    Some("proto"),
963                    Page {
964                        after: pair(after),
965                        limit,
966                        bytes: usize::MAX,
967                    },
968                    visit,
969                )
970            },
971            |definition| (definition.kind.clone(), definition.id.clone()),
972        );
973        assert!(!protos.is_empty() && protos.iter().all(|found| found.kind == "proto"));
974        let layers = paged(
975            |after, limit, visit| {
976                pack.visit_layers(
977                    "overworld",
978                    Page {
979                        after: after.copied(),
980                        limit,
981                        bytes: usize::MAX,
982                    },
983                    visit,
984                )
985            },
986            |layer| *layer,
987        );
988        assert_eq!(layers, [-1, 0, 1]);
989        let keys = paged(
990            |after, limit, visit| {
991                pack.visit_chunk_keys(
992                    "overworld",
993                    0,
994                    Page {
995                        after: after.copied(),
996                        limit,
997                        bytes: usize::MAX,
998                    },
999                    &mut |cx, cy| visit((cx, cy)),
1000                )
1001            },
1002            |key| *key,
1003        );
1004        assert_eq!(keys, [(-1, 0), (0, 0)]);
1005        // A page past the last row, and an empty page, visit nothing.
1006        for page in [
1007            Page {
1008                after: Some("~"),
1009                limit: 5,
1010                bytes: usize::MAX,
1011            },
1012            Page {
1013                after: None,
1014                limit: 0,
1015                bytes: usize::MAX,
1016            },
1017        ] {
1018            pack.visit_planes(page, &mut |_| panic!("{page:?} is empty"))
1019                .unwrap();
1020        }
1021    }
1022
1023    /// A page of up to 64 rows after `key`.
1024    fn after<K>(key: K) -> Page<K> {
1025        Page {
1026            after: Some(key),
1027            limit: 64,
1028            bytes: usize::MAX,
1029        }
1030    }
1031
1032    /// The steps of the query plan of `query`.
1033    fn plan(pack: &Pack, (sql, values): (String, Vec<types::Value>)) -> Vec<String> {
1034        let mut statement = pack
1035            .connection
1036            .prepare(&format!("EXPLAIN QUERY PLAN {sql}"))
1037            .unwrap();
1038        statement
1039            .query_map(rusqlite::params_from_iter(values), |row| row.get(3))
1040            .unwrap()
1041            .collect::<Result<_, _>>()
1042            .unwrap()
1043    }
1044
1045    #[test]
1046    fn every_listing_seeks_past_its_page_key() {
1047        let pack = pack();
1048        let stairwell_columns = format!(
1049            "{}, {}",
1050            super::bounded_identifier("id"),
1051            super::bounded_text("document")
1052        );
1053        for (query, seek) in [
1054            (
1055                planes_listing(after("a")),
1056                "SEARCH planes USING PRIMARY KEY (id>?)",
1057            ),
1058            (
1059                in_plane(
1060                    &super::zone_columns(),
1061                    "zones",
1062                    true,
1063                    "overworld",
1064                    after("a"),
1065                ),
1066                "SEARCH zones USING PRIMARY KEY (plane=? AND id>?)",
1067            ),
1068            (
1069                in_plane(
1070                    &super::anchor_columns(),
1071                    "anchors",
1072                    false,
1073                    "overworld",
1074                    after("a"),
1075                ),
1076                "SEARCH anchors USING PRIMARY KEY (plane=? AND id>?)",
1077            ),
1078            (
1079                in_plane(
1080                    &stairwell_columns,
1081                    "stairwells",
1082                    true,
1083                    "overworld",
1084                    after("a"),
1085                ),
1086                "SEARCH stairwells USING PRIMARY KEY (plane=? AND id>?)",
1087            ),
1088            (
1089                definitions_listing(None, after(("proto", "a"))).unwrap(),
1090                "SEARCH definitions USING PRIMARY KEY ((type,id)>(?,?))",
1091            ),
1092            (
1093                definitions_listing(Some("proto"), after(("proto", "a"))).unwrap(),
1094                "SEARCH definitions USING PRIMARY KEY (type=? AND id>?)",
1095            ),
1096            (
1097                layers_listing("overworld", after(0)),
1098                "SEARCH chunks USING PRIMARY KEY (plane=? AND layer>?)",
1099            ),
1100            (
1101                chunk_keys_listing("overworld", 0, after((0, 0))),
1102                "SEARCH chunks USING PRIMARY KEY (plane=? AND layer=? AND (cx,cy)>(?,?))",
1103            ),
1104        ] {
1105            assert_eq!(plan(&pack, query), [seek]);
1106        }
1107    }
1108
1109    #[test]
1110    fn typed_definition_pages_follow_a_key_of_any_type() {
1111        let pack = pack();
1112        let read = |after: Option<(&str, &str)>| {
1113            let mut ids = Vec::new();
1114            let page = Page {
1115                after,
1116                limit: usize::MAX,
1117                bytes: usize::MAX,
1118            };
1119            pack.visit_definitions(Some("proto"), page, &mut |definition| {
1120                ids.push(definition.id);
1121                Ok(())
1122            })
1123            .unwrap();
1124            ids
1125        };
1126        let all = read(None);
1127        assert!(all.len() > 1);
1128        // A key of a type before the listing's is followed by all of its
1129        // rows; a key of a type after it, by none.
1130        assert_eq!(read(Some(("a", "zzz"))), all);
1131        assert!(read(Some(("zzz", "a"))).is_empty());
1132        assert_eq!(read(Some(("proto", all[0].as_str()))), all[1..]);
1133    }
1134
1135    #[test]
1136    fn pages_stop_at_their_byte_budget() {
1137        let pack = pack();
1138        let read = |after: Option<&(String, String)>, bytes| {
1139            let mut rows = Vec::new();
1140            let page = Page {
1141                after: pair(after),
1142                limit: usize::MAX,
1143                bytes,
1144            };
1145            pack.visit_definitions(None, page, &mut |row| {
1146                rows.push(row);
1147                Ok(())
1148            })
1149            .unwrap();
1150            rows
1151        };
1152        let all = read(None, usize::MAX);
1153        // A budget of one byte reads one row a page, and the rows still
1154        // all come, in order.
1155        let mut walked: Vec<super::Definition> = Vec::new();
1156        loop {
1157            let after = walked
1158                .last()
1159                .map(|last| (last.kind.clone(), last.id.clone()));
1160            let page = read(after.as_ref(), 1);
1161            assert!(page.len() <= 1);
1162            if page.is_empty() {
1163                break;
1164            }
1165            walked.extend(page);
1166        }
1167        assert_eq!(walked, all);
1168        // A page stops after the row whose text reaches the budget.
1169        let size = |row: &super::Definition| row.kind.len() + row.id.len() + row.document.len();
1170        let two = size(&all[0]) + size(&all[1]);
1171        assert_eq!(read(None, two).len(), 2);
1172        assert_eq!(read(None, two + 1).len(), 3);
1173    }
1174
1175    #[test]
1176    fn listings_that_do_not_move_forward_fail() {
1177        let pack = pack();
1178        // Rows that come back out of key order, as a damaged table's could,
1179        // are an error rather than a page that could repeat.
1180        let mut listing = Listing::new(&plane_columns(), "planes", true);
1181        listing.conditions.clear();
1182        let descending = listing.finish("ORDER BY id DESC", usize::MAX);
1183        let error = pack
1184            .walk(
1185                descending,
1186                None,
1187                usize::MAX,
1188                plane,
1189                |plane| (plane.id.clone(), 0),
1190                ErrorCode::Plane,
1191                &mut |_| Ok(()),
1192            )
1193            .unwrap_err();
1194        assert_eq!(error.code(), ErrorCode::Plane);
1195        // So is a row at or before the key a page starts after.
1196        let listing = Listing::new(&plane_columns(), "planes", true);
1197        let error = pack
1198            .walk(
1199                listing.finish("ORDER BY id", usize::MAX),
1200                Some("vault".to_owned()),
1201                usize::MAX,
1202                plane,
1203                |plane| (plane.id.clone(), 0),
1204                ErrorCode::Plane,
1205                &mut |_| Ok(()),
1206            )
1207            .unwrap_err();
1208        assert_eq!(error.code(), ErrorCode::Plane);
1209    }
1210}