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, ¶ms, 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}