Skip to main content

agent_block_core/knl/
query.rs

1//! Reading the log with SQL: the request, and the one place SQL text is
2//! touched.
3//!
4//! The kernel does not own a query language.  The log lives in a SQLite
5//! table whose columns are published ([`super::sqlite_store::events_schema`]),
6//! and a caller reads it by writing SQL — no builder, no specification
7//! object, no typed row.  What this module owns is the small amount of work
8//! that has to happen *around* that SQL so it stays a read of this kernel's
9//! log rather than a way into the database:
10//!
11//! 1. the statement is **one** statement and it **reads** — it starts with
12//!    `SELECT` or `WITH`, and nothing follows the `;` if there is one, so
13//!    `INSERT` / `PRAGMA` / `ATTACH` / `a; b` never reach the connection
14//!    ([`plan`]).  The connection a query runs on is read-only, and the
15//!    prepared statement is asked whether it writes before it runs: three
16//!    answers to one question, because the cost of being wrong is a caller
17//!    writing through a view;
18//! 2. values are **bound, never interpolated**.  Nothing a caller passes as a
19//!    parameter is spliced into the text.  The rewrite below inserts
20//!    *placeholders* — never a value — which is what keeps "the kernel does
21//!    not build SQL out of data" true;
22//! 3. every parameter is resolved *here*, into one list of values in the
23//!    order the statement will bind them: `$stream` is the session's own
24//!    stream, `$sessions` is the set the caller asked to read across, and a
25//!    `?` or a `:name` is answered out of what the caller passed.
26//!
27//! # One pass, and it ends in `?`
28//!
29//! This module hands the backend a statement whose every parameter is a bare
30//! `?` and a `Vec<Value>` in the same order ([`QueryPlan::values`]).
31//! Resolution used to be split — the text rewritten here, the values matched
32//! to SQLite's own parameter names in the store — and that split is what one
33//! positional list removes: there is one walk over the statement, and the
34//! value pushed for a token is the value that token gets.
35//!
36//! **The backend can bind by name now, and the rewrite still stays.**  A
37//! parameter set is one thing or the other — positional, or named — and a
38//! statement here is routinely both: `$stream` and the `$sessions` expansion
39//! are the kernel's slots, the `?` and `:name` in the same text are the
40//! caller's, and there is no one set that binds them together.  Naming the
41//! kernel's slots and requiring the caller to name theirs would be a different
42//! contract, not a simpler one — a caller could then collide with `$stream`,
43//! and `$sessions` is not a slot at all but a list whose length only this walk
44//! knows.  Resolving everything to `?` here is what makes both halves one
45//! ordered list, so this is not a workaround for a binder that could not do
46//! names.
47//!
48//! The rule for the walk is the whole of the contract:
49//!
50//! * a parameter is found while walking the statement *as SQL*, so an
51//!   occurrence inside a string literal, a quoted identifier (`"…"`, `[…]`,
52//!   `` `…` ``) or a comment is left alone, as is a longer name that merely
53//!   starts with a reserved one (`$sessions2`);
54//! * `$sessions` becomes `(?, ?, …)`, one placeholder per id in the set, and
55//!   the ids are pushed in order.  `WHERE stream IN $sessions` therefore
56//!   compiles as `WHERE stream IN (?, ?)` for a set of two;
57//! * `$stream` becomes `?` and pushes the session's own stream;
58//! * a bare `?` becomes `?` and takes the next value of
59//!   [`QueryParams::Positional`], in the order they were given;
60//! * `:name` / `@name` / `$name` becomes `?` and takes
61//!   [`QueryParams::Named`]`[name]` — the prefix character is SQLite's, so
62//!   `:kind` is answered by `kind` (the full spelling is accepted too);
63//! * `?NNN` is refused: a numbered parameter says where its value goes, and
64//!   this walk is the thing that decides that.  Number your parameters by
65//!   position or name them;
66//! * every other byte of the statement is handed to SQLite exactly as the
67//!   caller wrote it.
68//!
69//! A parameter nobody answered, and a value nobody asked for, are both
70//! errors: a silent NULL is how a query quietly stops meaning what it says.
71//! `$stream` and `$sessions` are reserved — a parameter a caller names in
72//! that shape is the kernel's, not theirs.
73
74use std::ops::Range;
75use std::time::Duration;
76
77use serde_json::{Map, Value};
78
79use super::{KnlError, KnlResult};
80
81/// How long a query may run before it is interrupted, unless `opts` says
82/// otherwise.
83pub const DEFAULT_TIMEOUT_MS: u64 = 5_000;
84
85/// How many rows a query returns before the rest are cut off, unless `opts`
86/// says otherwise.  A cut is reported ([`QueryRows::truncated`]) rather than
87/// left for the caller to infer from a suspiciously round count.
88pub const DEFAULT_LIMIT: usize = 1_000;
89
90/// The reserved parameter that is the session's own stream.
91///
92/// Matched on the whole name, prefix included: `$stream` is the kernel's,
93/// while `:stream` / `@stream` are ordinary names a caller may use for
94/// anything.
95pub const STREAM_PARAM: &str = "$stream";
96
97/// The reserved token that expands to the set of streams being read.
98pub const SESSIONS_TOKEN: &str = "$sessions";
99
100/// The statements a query may be.
101///
102/// A closed list, checked on the text before SQLite ever sees it.  It is not
103/// the only guard — the connection is read-only and the prepared statement is
104/// asked whether it writes — but it is the one that can say *why* in the
105/// caller's terms.
106const READ_KEYWORDS: [&str; 2] = ["SELECT", "WITH"];
107
108/// The values a caller bound to its own parameters.
109///
110/// Positional for `?`, named for `:name` / `@name` / `$name`, and the two are
111/// not mixed: a statement is written one way or the other.
112#[derive(Debug, Clone, PartialEq, Default)]
113pub enum QueryParams {
114    /// No parameters of the caller's own.
115    #[default]
116    None,
117    /// Values for the anonymous `?` parameters, in the order they appear.
118    Positional(Vec<Value>),
119    /// Values by name.  The key is the name *without* its prefix character,
120    /// so `{ kind = "note" }` answers `:kind`, `@kind` and `$kind` alike.
121    Named(Map<String, Value>),
122}
123
124/// What a caller asked for beyond the SQL itself.
125#[derive(Debug, Clone, PartialEq)]
126pub struct QueryOpts {
127    /// The streams `$sessions` expands to.  `None` is the session's own
128    /// stream and nothing else; an empty list is refused, because a set that
129    /// selects nothing is a mistake rather than a request.
130    pub sessions: Option<Vec<String>>,
131    /// How long the query may run.  Must be positive: a zero timeout would
132    /// be a query that is interrupted before it starts, and "no timeout at
133    /// all" is not on offer.
134    pub timeout_ms: u64,
135    /// How many rows to return before reporting a cut.
136    pub limit: usize,
137}
138
139impl Default for QueryOpts {
140    fn default() -> Self {
141        Self {
142            sessions: None,
143            timeout_ms: DEFAULT_TIMEOUT_MS,
144            limit: DEFAULT_LIMIT,
145        }
146    }
147}
148
149/// A validated query, ready for a backend to prepare and bind.
150///
151/// The SQL here is the caller's with every parameter rewritten to a bare `?`,
152/// and [`QueryPlan::values`] is what those placeholders bind to, in order —
153/// the only difference between this text and what was passed in.
154#[derive(Debug, Clone, PartialEq)]
155pub struct QueryPlan {
156    /// The statement to prepare.  Every parameter in it is a bare `?`.
157    pub sql: String,
158    /// What the `?` placeholders bind to, in the order they appear.
159    pub values: Vec<Value>,
160    /// What `$stream` resolved to: the session's own stream.
161    pub stream: String,
162    /// What `$sessions` expanded to, in order.
163    pub sessions: Vec<String>,
164    /// The caller's own values, as they were given.
165    pub params: QueryParams,
166    /// The deadline for the whole query.
167    pub timeout: Duration,
168    /// The row cap.
169    pub limit: usize,
170}
171
172/// The rows a query returned, and whether there were more.
173#[derive(Debug, Clone, PartialEq, Default)]
174pub struct QueryRows {
175    /// One map per row: column name to value.  A `NULL` column is *absent*
176    /// rather than present-and-null, so it reads as `nil` on the Lua side.
177    pub rows: Vec<Map<String, Value>>,
178    /// Whether the query had more rows than [`QueryOpts::limit`] allowed.
179    pub truncated: bool,
180}
181
182/// Validate `sql`, rewrite every parameter to `?`, and settle what each of
183/// them binds to.
184///
185/// `stream` is the session's own stream: what `$stream` resolves to, and the
186/// default set `$sessions` expands to.
187pub fn plan(
188    sql: &str,
189    params: QueryParams,
190    opts: &QueryOpts,
191    stream: &str,
192) -> KnlResult<QueryPlan> {
193    let sessions = match opts.sessions.as_ref() {
194        None => vec![stream.to_string()],
195        Some(list) if list.is_empty() => {
196            return Err(KnlError::Validation(
197                "opts.sessions is empty; a set that selects no stream is not a request \
198                 (omit it to read this session's own)"
199                    .to_string(),
200            ));
201        }
202        Some(list) => list.clone(),
203    };
204    if opts.timeout_ms == 0 {
205        return Err(KnlError::Validation(
206            "opts.timeout_ms must be a positive whole number of milliseconds".to_string(),
207        ));
208    }
209
210    let scanned = scan(sql)?;
211    if !READ_KEYWORDS.contains(&scanned.keyword.as_str()) {
212        return Err(KnlError::Validation(format!(
213            "a query reads: it must start with SELECT or WITH, got {:?}",
214            scanned.keyword
215        )));
216    }
217
218    let (rewritten, values) = resolve(sql, &scanned.params, &params, stream, &sessions)?;
219
220    Ok(QueryPlan {
221        sql: rewritten,
222        values,
223        stream: stream.to_string(),
224        sessions,
225        params,
226        timeout: Duration::from_millis(opts.timeout_ms),
227        limit: opts.limit,
228    })
229}
230
231/// What answers one parameter token of a caller's statement.
232#[derive(Debug, Clone, PartialEq, Eq)]
233enum Param {
234    /// `$stream`: the session's own stream.
235    Stream,
236    /// `$sessions`: the whole set, as one parenthesised list.
237    Sessions,
238    /// A bare `?`: the next of [`QueryParams::Positional`].
239    Positional,
240    /// `?NNN`: refused, because the number says where the value goes and
241    /// that is what this walk decides.
242    Numbered,
243    /// `:name` / `@name` / `$name`, prefix included.
244    Named(String),
245}
246
247/// One parameter token, and where it sits in the caller's text.
248struct Token {
249    /// The byte range the token occupies.
250    at: Range<usize>,
251    /// What is bound in its place.
252    param: Param,
253}
254
255/// What the walk over a statement found.
256struct Scanned {
257    /// The first word, upper-cased — the statement's kind.
258    keyword: String,
259    /// Every parameter token, in the order they appear.
260    params: Vec<Token>,
261}
262
263/// Rewrite `sql` so every parameter is a bare `?`, and collect what those
264/// placeholders bind to, in order.
265///
266/// The one place a value and a placeholder are matched up.  Everything
267/// outside a token's range is copied byte for byte, so the only edit this
268/// makes is a parameter becoming a `?` — never a value going into the text.
269fn resolve(
270    sql: &str,
271    tokens: &[Token],
272    params: &QueryParams,
273    stream: &str,
274    sessions: &[String],
275) -> KnlResult<(String, Vec<Value>)> {
276    const NO_VALUES: &[Value] = &[];
277    let given: &[Value] = match params {
278        QueryParams::Positional(values) => values,
279        _ => NO_VALUES,
280    };
281
282    let mut out = String::with_capacity(sql.len());
283    let mut values: Vec<Value> = Vec::with_capacity(tokens.len());
284    let mut cursor = 0;
285    let mut taken = 0;
286
287    for token in tokens {
288        out.push_str(&sql[cursor..token.at.start]);
289        cursor = token.at.end;
290        match &token.param {
291            Param::Stream => {
292                out.push('?');
293                values.push(Value::from(stream));
294            }
295            Param::Sessions => {
296                out.push('(');
297                for (index, id) in sessions.iter().enumerate() {
298                    if index > 0 {
299                        out.push_str(", ");
300                    }
301                    out.push('?');
302                    values.push(Value::from(id.as_str()));
303                }
304                out.push(')');
305            }
306            Param::Positional => {
307                let value = given.get(taken).ok_or_else(|| {
308                    KnlError::Validation(format!(
309                        "the query has more `?` parameters than the {} value(s) given",
310                        given.len()
311                    ))
312                })?;
313                taken += 1;
314                out.push('?');
315                values.push(scalar(value)?.clone());
316            }
317            Param::Numbered => {
318                return Err(KnlError::Validation(format!(
319                    "{:?} is a numbered parameter, and the kernel assigns the positions: \
320                     number your parameters by position (a bare `?`) or name them",
321                    &sql[token.at.clone()]
322                )));
323            }
324            Param::Named(name) => {
325                let QueryParams::Named(named) = params else {
326                    return Err(KnlError::Validation(format!(
327                        "the query names the parameter {name:?}, so params must be a table of \
328                         names to values"
329                    )));
330                };
331                // The prefix character is SQLite's, not the caller's: `:kind`
332                // is answered by `kind`.  The full spelling is accepted too,
333                // for a caller that writes what it sees.
334                let value = named
335                    .get(&name[1..])
336                    .or_else(|| named.get(name.as_str()))
337                    .ok_or_else(|| {
338                        KnlError::Validation(format!(
339                            "no value was given for the parameter {name:?}"
340                        ))
341                    })?;
342                out.push('?');
343                values.push(scalar(value)?.clone());
344            }
345        }
346    }
347    out.push_str(&sql[cursor..]);
348
349    if given.len() > taken {
350        return Err(KnlError::Validation(format!(
351            "{} value(s) were given for {taken} `?` parameter(s)",
352            given.len()
353        )));
354    }
355    Ok((out, values))
356}
357
358/// A caller's value, if it is one SQLite has.
359///
360/// The four types a JSON value maps onto without inventing anything: null,
361/// boolean, number, string.  A composite — an array or an object — is not a
362/// SQLite value, and encoding one as its JSON text would be the kernel
363/// guessing what the caller meant, so it is refused here, where the refusal
364/// can still say which parameter it was about.
365fn scalar(value: &Value) -> KnlResult<&Value> {
366    match value {
367        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => Ok(value),
368        other => Err(KnlError::Validation(format!(
369            "a {} is not a SQLite value",
370            type_name_of(other)
371        ))),
372    }
373}
374
375/// What kind of JSON value this is, for a refusal message.
376fn type_name_of(value: &Value) -> &'static str {
377    match value {
378        Value::Null => "null",
379        Value::Bool(_) => "boolean",
380        Value::Number(_) => "number",
381        Value::String(_) => "string",
382        Value::Array(_) => "list",
383        Value::Object(_) => "table",
384    }
385}
386
387/// Walk `sql` as SQL: find its first word, find every parameter token, and
388/// refuse a second statement.
389///
390/// It is a scanner and not a parser.  All it has to tell apart is code from
391/// the three things that look like code and are not — string literals,
392/// quoted identifiers and comments — because a `;` or a `$sessions` inside
393/// one of those is text, not syntax.  Everything else is SQLite's to
394/// understand.
395fn scan(sql: &str) -> KnlResult<Scanned> {
396    let bytes = sql.as_bytes();
397    let mut at = 0;
398    let mut keyword: Option<String> = None;
399    let mut params: Vec<Token> = Vec::new();
400    // Set by a `;`.  Anything but whitespace and comments after it is a
401    // second statement, which is the shape a caller would smuggle a write in
402    // as — and rusqlite's `prepare` compiles only the first statement, so an
403    // unnoticed tail would be silently dropped rather than run.  Either way
404    // the answer is to refuse it.
405    let mut ended = false;
406
407    while at < bytes.len() {
408        let byte = bytes[at];
409        if byte.is_ascii_whitespace() {
410            at += 1;
411            continue;
412        }
413        if byte == b'-' && bytes.get(at + 1) == Some(&b'-') {
414            at += 2;
415            while at < bytes.len() && bytes[at] != b'\n' {
416                at += 1;
417            }
418            continue;
419        }
420        if byte == b'/' && bytes.get(at + 1) == Some(&b'*') {
421            at += 2;
422            while at < bytes.len() && !(bytes[at] == b'*' && bytes.get(at + 1) == Some(&b'/')) {
423                at += 1;
424            }
425            at = usize::min(at + 2, bytes.len());
426            continue;
427        }
428
429        if ended {
430            return Err(KnlError::Validation(
431                "a query is one statement: there is SQL after the `;`".to_string(),
432            ));
433        }
434
435        match byte {
436            b';' => {
437                ended = true;
438                at += 1;
439            }
440            b'\'' | b'"' | b'`' | b'[' => at = skip_quoted(bytes, at),
441            b'?' => {
442                // `?NNN` is digits and nothing else, which is SQLite's own
443                // rule: anything after a `?` that is not a digit is a
444                // separate token and the `?` stands alone.
445                let mut end = at + 1;
446                while end < bytes.len() && bytes[end].is_ascii_digit() {
447                    end += 1;
448                }
449                let param = if end == at + 1 {
450                    Param::Positional
451                } else {
452                    Param::Numbered
453                };
454                params.push(Token { at: at..end, param });
455                at = end;
456            }
457            b'$' | b':' | b'@' => {
458                let end = ident_end(bytes, at + 1);
459                // A lone prefix character names nothing — SQLite would refuse
460                // it when it compiles, which is the right place for a syntax
461                // error to be reported from.
462                if end > at + 1 {
463                    let name = &sql[at..end];
464                    let param = match name {
465                        STREAM_PARAM => Param::Stream,
466                        SESSIONS_TOKEN => Param::Sessions,
467                        other => Param::Named(other.to_string()),
468                    };
469                    params.push(Token { at: at..end, param });
470                }
471                at = end;
472            }
473            _ if byte.is_ascii_alphabetic() || byte == b'_' => {
474                let end = ident_end(bytes, at);
475                if keyword.is_none() {
476                    keyword = Some(sql[at..end].to_ascii_uppercase());
477                }
478                at = end;
479            }
480            // Punctuation, operators, digits, and any non-ASCII byte: not
481            // something this scanner has a question about.  Advancing one
482            // byte through a multi-byte character is safe because a UTF-8
483            // continuation byte can never be one of the ASCII bytes matched
484            // above, and no slice is taken at this offset.
485            _ => at += 1,
486        }
487    }
488
489    let keyword = keyword.ok_or_else(|| {
490        KnlError::Validation("a query needs a statement; the SQL is empty".to_string())
491    })?;
492    Ok(Scanned { keyword, params })
493}
494
495/// The offset just past the quoted run starting at `start`.
496///
497/// Handles the four quotings SQLite accepts, and the doubled-quote escape
498/// (`'it''s'`, `"a""b"`) for the three that have one.  An unterminated run
499/// consumes the rest of the text: SQLite refuses it when it prepares, which
500/// is the right place for a syntax error to be reported from.
501fn skip_quoted(bytes: &[u8], start: usize) -> usize {
502    let open = bytes[start];
503    let close = if open == b'[' { b']' } else { open };
504    let doubles = open != b'[';
505    let mut at = start + 1;
506    while at < bytes.len() {
507        if bytes[at] == close {
508            if doubles && bytes.get(at + 1) == Some(&close) {
509                at += 2;
510                continue;
511            }
512            return at + 1;
513        }
514        at += 1;
515    }
516    at
517}
518
519/// The offset just past the identifier characters starting at `from`.
520fn ident_end(bytes: &[u8], from: usize) -> usize {
521    let mut at = from;
522    while at < bytes.len()
523        && (bytes[at].is_ascii_alphanumeric() || matches!(bytes[at], b'_' | b'$'))
524    {
525        at += 1;
526    }
527    at
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use serde_json::json;
534
535    /// A plan for `sql` over one stream, with everything else default.
536    fn plan_of(sql: &str) -> KnlResult<QueryPlan> {
537        plan(sql, QueryParams::None, &QueryOpts::default(), "s-1")
538    }
539
540    /// A plan for `sql` over the given set of streams.
541    fn plan_over(sql: &str, sessions: &[&str]) -> KnlResult<QueryPlan> {
542        let opts = QueryOpts {
543            sessions: Some(sessions.iter().map(|s| (*s).to_string()).collect()),
544            ..QueryOpts::default()
545        };
546        plan(sql, QueryParams::None, &opts, "s-1")
547    }
548
549    /// A read is a read: the two statements that only read are accepted, in
550    /// any casing and after any amount of leading noise.
551    #[test]
552    fn a_select_or_with_is_a_query_whatever_it_is_dressed_in() {
553        for sql in [
554            "SELECT 1",
555            "select 1",
556            "  \n\t select 1",
557            "-- a comment first\nSELECT 1",
558            "/* and a block one */ WITH x AS (SELECT 1) SELECT * FROM x",
559            "SELECT 1;",
560            "SELECT 1;  -- trailing comment\n",
561        ] {
562            plan_of(sql).unwrap_or_else(|e| panic!("{sql:?} must be a query: {e}"));
563        }
564    }
565
566    /// Everything else is refused before the connection is reached, and the
567    /// refusal is the caller's class: the arguments did not hold up.
568    #[test]
569    fn anything_that_is_not_a_read_is_refused_as_validation() {
570        for sql in [
571            "INSERT INTO events (stream) VALUES ('x')",
572            "UPDATE events SET kind = 'x'",
573            "DELETE FROM events",
574            "DROP TABLE events",
575            "PRAGMA table_info(events)",
576            "ATTACH DATABASE '/tmp/other.db' AS other",
577            "BEGIN",
578            "VACUUM",
579        ] {
580            let err = plan_of(sql).expect_err("a statement that is not a read must be refused");
581            assert_eq!(err.kind(), KnlError::VALIDATION, "{sql:?}: {err}");
582            assert!(
583                err.reason().contains("SELECT or WITH"),
584                "{sql:?}: {}",
585                err.reason()
586            );
587        }
588
589        let err = plan_of("   ").expect_err("empty SQL must be refused");
590        assert_eq!(err.kind(), KnlError::VALIDATION);
591    }
592
593    /// A second statement is refused whole, rather than the first being run
594    /// and the rest quietly dropped.
595    #[test]
596    fn a_second_statement_is_refused() {
597        for sql in [
598            "SELECT 1; DELETE FROM events",
599            "SELECT 1;SELECT 2",
600            "SELECT 1; -- a comment\n DROP TABLE events",
601        ] {
602            let err = plan_of(sql).expect_err("a second statement must be refused");
603            assert_eq!(err.kind(), KnlError::VALIDATION, "{sql:?}: {err}");
604            assert!(err.reason().contains("one statement"), "{}", err.reason());
605        }
606    }
607
608    /// A `;` inside a literal or a comment is text, not the end of the
609    /// statement — the scanner tells the three apart.
610    #[test]
611    fn a_semicolon_in_a_literal_or_a_comment_is_not_a_statement_boundary() {
612        for sql in [
613            r#"SELECT * FROM events WHERE kind = 'a;b'"#,
614            r#"SELECT * FROM events WHERE kind = 'it''s;fine'"#,
615            r#"SELECT "we;ird" FROM events"#,
616            "SELECT 1 -- ; not a boundary\n",
617            "SELECT /* ; */ 1",
618        ] {
619            plan_of(sql).unwrap_or_else(|e| panic!("{sql:?} must be one statement: {e}"));
620        }
621    }
622
623    /// `$sessions` becomes one placeholder per id, and the ids are bound
624    /// rather than written into the text.
625    #[test]
626    fn sessions_expands_to_one_placeholder_per_id() {
627        let plan = plan_over(
628            "SELECT * FROM events WHERE stream IN $sessions ORDER BY seq",
629            &["stream-one", "stream-two"],
630        )
631        .expect("plan");
632        assert_eq!(
633            plan.sql,
634            "SELECT * FROM events WHERE stream IN (?, ?) ORDER BY seq"
635        );
636        assert_eq!(plan.sessions, ["stream-one", "stream-two"]);
637        assert_eq!(plan.values, [json!("stream-one"), json!("stream-two")]);
638        for id in &plan.sessions {
639            assert!(
640                !plan.sql.contains(id.as_str()),
641                "an id is bound, never written into the SQL: {}",
642                plan.sql
643            );
644        }
645
646        // Omitted, the set is the session's own stream — so the same SQL
647        // reads one stream by default.
648        let plan = plan_of("SELECT * FROM events WHERE stream IN $sessions").expect("plan");
649        assert_eq!(plan.sql, "SELECT * FROM events WHERE stream IN (?)");
650        assert_eq!(plan.sessions, ["s-1"]);
651        assert_eq!(plan.stream, "s-1");
652        assert_eq!(plan.values, [json!("s-1")]);
653    }
654
655    /// The rewrite touches the tokens and nothing else: an occurrence inside
656    /// a literal, an identifier or a comment is left exactly as written.
657    #[test]
658    fn only_a_token_outside_quotes_and_comments_is_rewritten() {
659        for sql in [
660            r#"SELECT '$sessions' AS literal"#,
661            r#"SELECT "$sessions" FROM events"#,
662            "SELECT 1 -- $sessions in a comment\n",
663            "SELECT /* $sessions */ 1",
664            r#"SELECT 'a ? b' AS literal"#,
665            r#"SELECT ':kind' AS literal"#,
666            "SELECT 1 -- :kind ? @who\n",
667        ] {
668            let plan = plan_over(sql, &["a", "b"]).expect("plan");
669            assert_eq!(plan.sql, sql, "the text must be untouched: {sql:?}");
670            assert!(plan.values.is_empty(), "{sql:?}: {:?}", plan.values);
671        }
672
673        // Two occurrences are both expanded, and the rest of the statement is
674        // copied byte for byte — and each pushes its own values.
675        let plan = plan_over(
676            "SELECT * FROM events WHERE stream IN $sessions UNION \
677             SELECT * FROM events WHERE stream IN $sessions",
678            &["a"],
679        )
680        .expect("plan");
681        assert_eq!(
682            plan.sql,
683            "SELECT * FROM events WHERE stream IN (?) UNION \
684             SELECT * FROM events WHERE stream IN (?)"
685        );
686        assert_eq!(plan.values, [json!("a"), json!("a")]);
687    }
688
689    /// A longer name that merely starts with a reserved one is the caller's,
690    /// and is answered out of the caller's own table.
691    #[test]
692    fn a_name_that_starts_like_a_reserved_one_is_the_callers() {
693        let opts = QueryOpts::default();
694        let mut named = Map::new();
695        named.insert("sessions2".to_string(), json!("x"));
696        let plan =
697            plan("SELECT $sessions2", QueryParams::Named(named), &opts, "s-1").expect("plan");
698        assert_eq!(plan.sql, "SELECT ?");
699        assert_eq!(plan.values, [json!("x")]);
700    }
701
702    /// `$stream` is the session's own stream, resolved in the same pass.
703    #[test]
704    fn stream_resolves_to_the_sessions_own_stream() {
705        let plan =
706            plan_of("SELECT * FROM events WHERE stream = $stream ORDER BY seq").expect("plan");
707        assert_eq!(
708            plan.sql,
709            "SELECT * FROM events WHERE stream = ? ORDER BY seq"
710        );
711        assert_eq!(plan.values, [json!("s-1")]);
712    }
713
714    /// The caller's own parameters are rewritten in the order they appear,
715    /// and each pushes exactly the value that answers it — a named one by
716    /// its name without the prefix, or with it.
717    #[test]
718    fn the_callers_parameters_are_resolved_in_order() {
719        let opts = QueryOpts::default();
720
721        let positional = plan(
722            "SELECT * FROM events WHERE stream = $stream AND kind = ? AND seq > ?",
723            QueryParams::Positional(vec![json!("note"), json!(3)]),
724            &opts,
725            "s-1",
726        )
727        .expect("plan");
728        assert_eq!(
729            positional.sql,
730            "SELECT * FROM events WHERE stream = ? AND kind = ? AND seq > ?"
731        );
732        assert_eq!(positional.values, [json!("s-1"), json!("note"), json!(3)]);
733
734        let mut named = Map::new();
735        named.insert("kind".to_string(), json!("note"));
736        named.insert("@who".to_string(), json!("me"));
737        let by_name = plan(
738            "SELECT * FROM events WHERE kind = :kind AND stream = @who AND kind = $kind",
739            QueryParams::Named(named),
740            &opts,
741            "s-1",
742        )
743        .expect("plan");
744        assert_eq!(
745            by_name.sql,
746            "SELECT * FROM events WHERE kind = ? AND stream = ? AND kind = ?"
747        );
748        assert_eq!(by_name.values, [json!("note"), json!("me"), json!("note")]);
749    }
750
751    /// A parameter nobody answered, and a value nobody asked for, are both
752    /// errors: a silent NULL is how a query quietly stops meaning what it
753    /// says.
754    #[test]
755    fn every_parameter_is_answered_and_every_value_is_used() {
756        let opts = QueryOpts::default();
757
758        let err = plan_of("SELECT * FROM events WHERE kind = :kind")
759            .expect_err("an unanswered parameter must be refused");
760        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
761        assert!(err.reason().contains(":kind"), "{}", err.reason());
762
763        let err = plan(
764            "SELECT * FROM events WHERE kind = :kind",
765            QueryParams::Named(Map::new()),
766            &opts,
767            "s-1",
768        )
769        .expect_err("a name with no value must be refused");
770        assert!(err.reason().contains(":kind"), "{}", err.reason());
771
772        let err = plan(
773            "SELECT * FROM events WHERE kind = ?",
774            QueryParams::Positional(vec![json!("a"), json!("b")]),
775            &opts,
776            "s-1",
777        )
778        .expect_err("a value with no parameter must be refused");
779        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
780
781        let err = plan(
782            "SELECT * FROM events WHERE kind = ? AND seq = ?",
783            QueryParams::Positional(vec![json!("a")]),
784            &opts,
785            "s-1",
786        )
787        .expect_err("a parameter with no value must be refused");
788        assert!(err.reason().contains("more `?`"), "{}", err.reason());
789    }
790
791    /// A numbered parameter says where its value goes, and that is what the
792    /// walk decides — so it is refused rather than silently renumbered.
793    #[test]
794    fn a_numbered_parameter_is_refused() {
795        let opts = QueryOpts::default();
796        let err = plan(
797            "SELECT * FROM events WHERE kind = ?1",
798            QueryParams::Positional(vec![json!("a")]),
799            &opts,
800            "s-1",
801        )
802        .expect_err("a numbered parameter must be refused");
803        assert_eq!(err.kind(), KnlError::VALIDATION, "{err}");
804        assert!(err.reason().contains("?1"), "{}", err.reason());
805    }
806
807    /// A composite is not a SQLite value, and encoding one as its JSON text
808    /// would be the kernel guessing what the caller meant.
809    #[test]
810    fn a_list_or_a_table_is_not_a_value() {
811        let opts = QueryOpts::default();
812        for value in [json!([1, 2]), json!({ "a": 1 })] {
813            let err = plan(
814                "SELECT ?",
815                QueryParams::Positional(vec![value.clone()]),
816                &opts,
817                "s-1",
818            )
819            .expect_err("a composite must be refused");
820            assert_eq!(err.kind(), KnlError::VALIDATION, "{value}: {err}");
821            assert!(
822                err.reason().contains("not a SQLite value"),
823                "{}",
824                err.reason()
825            );
826        }
827
828        // The four scalars are values, and NULL is one of them.
829        let plan = plan(
830            "SELECT ?, ?, ?, ?",
831            QueryParams::Positional(vec![Value::Null, json!(true), json!(1.5), json!("text")]),
832            &opts,
833            "s-1",
834        )
835        .expect("plan");
836        assert_eq!(plan.values.len(), 4);
837    }
838
839    /// An empty set is refused: it is a mistake in the caller's own code, and
840    /// `IN ()` is not SQL anyway.
841    #[test]
842    fn an_empty_session_set_is_refused() {
843        let err = plan_over("SELECT 1", &[]).expect_err("an empty set must be refused");
844        assert_eq!(err.kind(), KnlError::VALIDATION);
845        assert!(err.reason().contains("sessions"), "{}", err.reason());
846    }
847
848    /// A zero timeout is refused rather than read as "no deadline".
849    #[test]
850    fn a_zero_timeout_is_refused() {
851        let opts = QueryOpts {
852            timeout_ms: 0,
853            ..QueryOpts::default()
854        };
855        let err = plan("SELECT 1", QueryParams::None, &opts, "s-1")
856            .expect_err("a zero timeout must be refused");
857        assert_eq!(err.kind(), KnlError::VALIDATION);
858        assert!(err.reason().contains("timeout_ms"), "{}", err.reason());
859    }
860
861    /// The caller's own values ride along untouched; the plan carries them
862    /// for the backend to bind.
863    #[test]
864    fn the_plan_carries_the_callers_values_for_binding() {
865        let opts = QueryOpts::default();
866        let plan = plan(
867            "SELECT * FROM events WHERE kind = ?",
868            QueryParams::Positional(vec![json!("note")]),
869            &opts,
870            "s-1",
871        )
872        .expect("plan");
873        assert_eq!(plan.params, QueryParams::Positional(vec![json!("note")]));
874        assert_eq!(plan.timeout, Duration::from_millis(DEFAULT_TIMEOUT_MS));
875        assert_eq!(plan.limit, DEFAULT_LIMIT);
876    }
877}