Skip to main content

klukai_agent/api/public/
mod.rs

1use std::{
2    collections::BTreeSet,
3    net::SocketAddr,
4    ops::Deref,
5    time::{Duration, Instant},
6};
7
8use antithesis_sdk::assert_sometimes;
9use axum::{Extension, extract::ConnectInfo};
10use bytes::{BufMut, BytesMut};
11use compact_str::ToCompactString;
12use http_body_util::StreamBody;
13use hyper::StatusCode;
14use klukai_types::{
15    agent::{Agent, ChangeError},
16    api::{
17        ColumnName, ExecResponse, ExecResult, QueryEvent, Statement, TableStatRequest,
18        TableStatResponse,
19    },
20    base::CrsqlDbVersion,
21    broadcast::Timestamp,
22    change::{InsertChangesInfo, SqliteValue, insert_local_changes},
23    schema::{apply_schema, parse_sql},
24    sqlite::SqlitePoolError,
25};
26use klukai_types::{
27    spawn::spawn_counted,
28    sqlite_pool::{Committable, InterruptibleTransaction},
29};
30use metrics::{counter, histogram};
31use rusqlite::{ToSql, Transaction, params_from_iter};
32use serde::Deserialize;
33use tokio_stream::wrappers::ReceiverStream;
34
35use tokio::{
36    sync::{
37        mpsc::{self, channel},
38        oneshot,
39    },
40    task::block_in_place,
41};
42use tokio_util::sync::CancellationToken;
43use tracing::{debug, error, info, trace, warn};
44
45use klukai_types::broadcast::broadcast_changes;
46
47pub mod pubsub;
48
49pub mod update;
50
51#[derive(Clone, Copy, Debug, Default, Deserialize)]
52pub struct TimeoutParams {
53    #[serde(default)]
54    pub timeout: Option<u64>,
55}
56
57pub async fn make_broadcastable_changes<F, T>(
58    agent: &Agent,
59    timeout: Option<u64>,
60    f: F,
61) -> Result<(T, Option<CrsqlDbVersion>, Duration), ChangeError>
62where
63    F: Fn(&InterruptibleTransaction<Transaction>) -> Result<T, ChangeError>,
64{
65    trace!("getting conn...");
66    let mut conn = agent.pool().write_priority().await?;
67    trace!("got conn");
68
69    let actor_id = agent.actor_id();
70    // maybe we should do this earlier, but there can only ever be 1 write conn at a time,
71    // so it probably doesn't matter too much, except for reads of internal state
72    let mut book_writer = agent
73        .booked()
74        .write::<&str, _>("make_broadcastable_changes(booked writer)", None)
75        .await;
76
77    let start = Instant::now();
78    let ts = Timestamp::from(agent.clock().new_timestamp());
79
80    block_in_place(move || {
81        let tx = conn
82            .immediate_transaction()
83            .map_err(|source| ChangeError::Rusqlite {
84                source,
85                actor_id: Some(actor_id),
86                version: None,
87            })?;
88
89        let timeout = timeout.map(Duration::from_secs);
90        let tx = InterruptibleTransaction::new(tx, timeout, "local_changes");
91
92        let _ = tx
93            .prepare_cached("SELECT crsql_set_ts(?)")
94            .map_err(|source| ChangeError::Rusqlite {
95                source,
96                actor_id: Some(actor_id),
97                version: None,
98            })?
99            .query_row([&ts], |row| row.get::<_, String>(0))
100            .map_err(|source| ChangeError::Rusqlite {
101                source,
102                actor_id: Some(actor_id),
103                version: None,
104            })?;
105
106        // Execute whatever might mutate state data
107        let ret = f(&tx)?;
108
109        let insert_info = insert_local_changes(agent, &tx, &mut book_writer)?;
110        tx.commit().map_err(|source| ChangeError::Rusqlite {
111            source,
112            actor_id: Some(actor_id),
113            version: insert_info.as_ref().map(|info| info.db_version),
114        })?;
115
116        let elapsed = start.elapsed();
117        histogram!("corro.agent.changes.processing.time.seconds", "source" => "local")
118            .record(start.elapsed());
119
120        match insert_info {
121            None => Ok((ret, None, elapsed)),
122            Some(InsertChangesInfo {
123                db_version,
124                last_seq,
125                ts,
126                snap,
127            }) => {
128                trace!("committed tx, db_version: {db_version}, last_seq: {last_seq:?}");
129
130                book_writer.commit_snapshot(snap);
131
132                let agent = agent.clone();
133
134                spawn_counted(
135                    async move { broadcast_changes(agent, db_version, last_seq, ts).await },
136                );
137
138                Ok::<_, ChangeError>((ret, Some(db_version), elapsed))
139            }
140        }
141    })
142}
143
144#[tracing::instrument(skip_all, err)]
145fn execute_statement<T>(
146    tx: &InterruptibleTransaction<T>,
147    stmt: &Statement,
148) -> rusqlite::Result<usize>
149where
150    T: Deref<Target = rusqlite::Connection> + Committable,
151{
152    let mut prepped = tx.prepare(stmt.query())?;
153
154    match stmt {
155        Statement::Simple(_)
156        | Statement::Verbose {
157            params: None,
158            named_params: None,
159            ..
160        } => prepped.execute([]),
161        Statement::WithParams(_, params)
162        | Statement::Verbose {
163            params: Some(params),
164            ..
165        } => prepped.execute(params_from_iter(params)),
166        Statement::WithNamedParams(_, params)
167        | Statement::Verbose {
168            named_params: Some(params),
169            ..
170        } => prepped.execute(
171            params
172                .iter()
173                .map(|(k, v)| (k.as_str(), v as &dyn ToSql))
174                .collect::<Vec<(&str, &dyn ToSql)>>()
175                .as_slice(),
176        ),
177    }
178}
179
180#[tracing::instrument(skip_all)]
181pub async fn api_v1_transactions(
182    // axum::extract::RawQuery(raw_query): axum::extract::RawQuery,
183    Extension(agent): Extension<Agent>,
184    axum::extract::Query(params): axum::extract::Query<TimeoutParams>,
185    axum::extract::Json(statements): axum::extract::Json<Vec<Statement>>,
186) -> (StatusCode, axum::Json<ExecResponse>) {
187    let actor_id = agent.actor_id().to_string();
188    if statements.is_empty() {
189        return (
190            StatusCode::BAD_REQUEST,
191            axum::Json(ExecResponse {
192                results: vec![ExecResult::Error {
193                    error: "at least 1 statement is required".into(),
194                }],
195                time: 0.0,
196                version: None,
197                actor_id: Some(actor_id),
198            }),
199        );
200    }
201
202    assert_sometimes!(true, "Corrosion receives transactions through HTTP API");
203    let res = make_broadcastable_changes(&agent, params.timeout, move |tx| {
204        let mut total_rows_affected = 0;
205
206        statements
207            .iter()
208            .map(|stmt| {
209                let start = Instant::now();
210                let res = execute_statement(tx, stmt).map_err(|e| ChangeError::Rusqlite {
211                    source: e,
212                    actor_id: None,
213                    version: None,
214                });
215
216                match res {
217                    Ok(rows_affected) => {
218                        total_rows_affected += rows_affected;
219                        Ok(ExecResult::Execute {
220                            rows_affected,
221                            time: start.elapsed().as_secs_f64(),
222                        })
223                    }
224                    Err(e) => Err(e),
225                }
226            })
227            .collect::<Result<Vec<ExecResult>, ChangeError>>()
228    })
229    .await;
230
231    let (results, version, elapsed) = match res {
232        Ok(res) => res,
233        Err(e) => {
234            error!("could not execute statement(s): {e}");
235            return (
236                StatusCode::INTERNAL_SERVER_ERROR,
237                axum::Json(ExecResponse {
238                    results: vec![ExecResult::Error {
239                        error: e.to_string(),
240                    }],
241                    time: 0.0,
242                    version: None,
243                    actor_id: Some(actor_id),
244                }),
245            );
246        }
247    };
248
249    (
250        StatusCode::OK,
251        axum::Json(ExecResponse {
252            results,
253            time: elapsed.as_secs_f64(),
254            version: version.map(Into::into),
255            actor_id: Some(actor_id),
256        }),
257    )
258}
259
260#[derive(Debug, thiserror::Error)]
261pub enum QueryError {
262    #[error("pool connection acquisition error")]
263    Pool(#[from] SqlitePoolError),
264    #[error("sqlite error: {0}")]
265    Rusqlite(#[from] rusqlite::Error),
266}
267
268async fn build_query_rows_response(
269    agent: &Agent,
270    client_addr: SocketAddr,
271    data_tx: mpsc::Sender<QueryEvent>,
272    stmt: Statement,
273    timeout: Option<u64>,
274) -> Result<(), (StatusCode, ExecResult)> {
275    let (res_tx, res_rx) = oneshot::channel();
276
277    let pool = agent.pool().clone();
278
279    tokio::spawn(async move {
280        let conn = match pool.read().await {
281            Ok(conn) => conn,
282            Err(e) => {
283                _ = res_tx.send(Err((
284                    StatusCode::INTERNAL_SERVER_ERROR,
285                    ExecResult::Error {
286                        error: e.to_string(),
287                    },
288                )));
289                return;
290            }
291        };
292
293        trace!(%client_addr, "Preparing statement {}", stmt.query());
294
295        let prepped_res = block_in_place(|| conn.prepare(stmt.query()));
296
297        let mut prepped = match prepped_res {
298            Ok(prepped) => prepped,
299            Err(e) => {
300                _ = res_tx.send(Err((
301                    StatusCode::BAD_REQUEST,
302                    ExecResult::Error {
303                        error: e.to_string(),
304                    },
305                )));
306                return;
307            }
308        };
309
310        if !prepped.readonly() {
311            _ = res_tx.send(Err((
312                StatusCode::BAD_REQUEST,
313                ExecResult::Error {
314                    error: "statement is not readonly".into(),
315                },
316            )));
317            return;
318        }
319
320        let timeout = timeout.unwrap_or(4);
321        let timeout: Option<Duration> = if timeout > 0 {
322            Some(Duration::from_secs(timeout * 60))
323        } else {
324            None
325        };
326
327        let int_handle = conn.get_interrupt_handle();
328        let token = CancellationToken::new();
329        if let Some(timeout) = timeout {
330            let cloned_token = token.clone();
331            let stmt_query = stmt.query().to_string();
332            tokio::spawn(async move {
333                tokio::select! {
334                    _ = cloned_token.cancelled() => {}
335                    _ = tokio::time::sleep(timeout) => {
336                        warn!("sql call took more than {timeout:?}, interrupting stmt- {:?}", stmt_query);
337                        int_handle.interrupt();
338                        counter!("corro.sqlite.interrupt", "source" => "timeout").increment(1);
339                    }
340                };
341            });
342        }
343
344        let _dropguard = token.drop_guard();
345        block_in_place(|| {
346            let col_count = prepped.column_count();
347            trace!("inside block in place, col count: {col_count}");
348
349            if let Err(e) = data_tx.blocking_send(QueryEvent::Columns(
350                prepped
351                    .columns()
352                    .into_iter()
353                    .map(|col| ColumnName(col.name().to_compact_string()))
354                    .collect(),
355            )) {
356                error!("could not send back columns: {e}");
357                return;
358            }
359
360            let start = Instant::now();
361
362            trace!(%client_addr, "Executing statement {}", stmt.query());
363            let elapsed = start.elapsed();
364
365            let query = match &stmt {
366                Statement::Simple(_)
367                | Statement::Verbose {
368                    params: None,
369                    named_params: None,
370                    ..
371                } => prepped.query(()),
372                Statement::WithParams(_, params)
373                | Statement::Verbose {
374                    params: Some(params),
375                    ..
376                } => prepped.query(params_from_iter(params)),
377                Statement::WithNamedParams(_, params)
378                | Statement::Verbose {
379                    named_params: Some(params),
380                    ..
381                } => prepped.query(
382                    params
383                        .iter()
384                        .map(|(k, v)| (k.as_str(), v as &dyn ToSql))
385                        .collect::<Vec<(&str, &dyn ToSql)>>()
386                        .as_slice(),
387                ),
388            };
389
390            let mut rows = match query {
391                Ok(rows) => rows,
392                Err(e) => {
393                    _ = res_tx.send(Err((
394                        StatusCode::INTERNAL_SERVER_ERROR,
395                        ExecResult::Error {
396                            error: e.to_string(),
397                        },
398                    )));
399                    return;
400                }
401            };
402
403            trace!(%client_addr, elapsed = %elapsed.as_secs(), "Statement finished executing {}", stmt.query());
404
405            if elapsed > Duration::from_secs(10) {
406                warn!(%client_addr, elapsed = %elapsed.as_secs(), "Slow read statement {}!", stmt.query());
407            }
408
409            if let Err(_e) = res_tx.send(Ok(())) {
410                error!("could not send back response through oneshot channel, aborting");
411                return;
412            }
413
414            let mut rowid = 1;
415
416            trace!("about to loop through rows!");
417
418            loop {
419                match rows.next() {
420                    Ok(Some(row)) => {
421                        trace!("got a row: {row:?}");
422                        match (0..col_count)
423                            .map(|i| row.get::<_, SqliteValue>(i))
424                            .collect::<rusqlite::Result<Vec<_>>>()
425                        {
426                            Ok(cells) => {
427                                if let Err(e) =
428                                    data_tx.blocking_send(QueryEvent::Row(rowid.into(), cells))
429                                {
430                                    error!("could not send back row: {e}");
431                                    return;
432                                }
433                                rowid += 1;
434                            }
435                            Err(e) => {
436                                _ = data_tx.blocking_send(QueryEvent::Error(e.to_compact_string()));
437                                return;
438                            }
439                        }
440                    }
441                    Ok(None) => {
442                        // done!
443                        break;
444                    }
445                    Err(e) => {
446                        _ = data_tx.blocking_send(QueryEvent::Error(e.to_compact_string()));
447                        return;
448                    }
449                }
450            }
451
452            _ = data_tx.blocking_send(QueryEvent::EndOfQuery {
453                time: elapsed.as_secs_f64(),
454                change_id: None,
455            });
456        });
457    });
458
459    match res_rx.await {
460        Ok(res) => res,
461        Err(e) => Err((
462            StatusCode::INTERNAL_SERVER_ERROR,
463            ExecResult::Error {
464                error: e.to_string(),
465            },
466        )),
467    }
468}
469
470pub async fn api_v1_queries(
471    Extension(agent): Extension<Agent>,
472    ConnectInfo(client_addr): ConnectInfo<SocketAddr>,
473    axum::extract::Query(params): axum::extract::Query<TimeoutParams>,
474    axum::extract::Json(stmt): axum::extract::Json<Statement>,
475) -> impl axum::response::IntoResponse {
476    counter!("corro.api.queries.count").increment(1);
477    let (data_tx, mut data_rx) = channel(512);
478
479    let start = Instant::now();
480
481    // Create a channel for streaming
482    let (stream_tx, stream_rx) =
483        tokio::sync::mpsc::channel::<Result<hyper::body::Frame<bytes::Bytes>, std::io::Error>>(512);
484
485    // Create a streaming body from the receiver
486    let stream = ReceiverStream::new(stream_rx);
487    let body = StreamBody::new(stream);
488
489    let stream_tx_clone = stream_tx.clone();
490    tokio::spawn(async move {
491        let mut buf = BytesMut::new();
492
493        while let Some(row_res) = data_rx.recv().await {
494            {
495                let mut writer = (&mut buf).writer();
496                if let Err(e) = serde_json::to_writer(&mut writer, &row_res) {
497                    let error_json = serde_json::to_vec(&serde_json::json!(QueryEvent::Error(
498                        e.to_compact_string()
499                    )))
500                    .expect("could not serialize error json");
501                    let _ = stream_tx_clone
502                        .send(Ok(hyper::body::Frame::data(bytes::Bytes::from(error_json))))
503                        .await;
504                    return;
505                }
506            }
507
508            buf.extend_from_slice(b"\n");
509
510            if stream_tx_clone
511                .send(Ok(hyper::body::Frame::data(buf.split().freeze())))
512                .await
513                .is_err()
514            {
515                error!("could not send data through stream channel");
516                return;
517            }
518        }
519        debug!("query body channel done");
520    });
521
522    trace!("building query rows response...");
523    assert_sometimes!(true, "Corrosion accepts queries");
524
525    match build_query_rows_response(&agent, client_addr, data_tx, stmt, params.timeout).await {
526        Ok(_) => {
527            histogram!("corro.api.queries.processing.time.seconds", "result" => "success")
528                .record(start.elapsed());
529            hyper::Response::builder()
530                .status(StatusCode::OK)
531                .body(body)
532                .expect("could not build query response body")
533        }
534        Err((status, res)) => {
535            histogram!("corro.api.queries.processing.time.seconds", "result" => "error")
536                .record(start.elapsed());
537
538            // For error responses, we need to create a new stream with the error data
539            let error_bytes =
540                serde_json::to_vec(&res).expect("could not serialize query error response");
541            let (error_tx, error_rx) = tokio::sync::mpsc::channel::<
542                Result<hyper::body::Frame<bytes::Bytes>, std::io::Error>,
543            >(1);
544            let _ = error_tx
545                .send(Ok(hyper::body::Frame::data(bytes::Bytes::from(
546                    error_bytes,
547                ))))
548                .await;
549            let error_stream = ReceiverStream::new(error_rx);
550            let error_body = StreamBody::new(error_stream);
551
552            hyper::Response::builder()
553                .status(status)
554                .body(error_body)
555                .expect("could not build query response body")
556        }
557    }
558}
559
560pub(crate) async fn execute_schema(agent: &Agent, statements: Vec<String>) -> eyre::Result<()> {
561    let new_sql: String = statements.join(";");
562
563    let partial_schema = parse_sql(&new_sql)?;
564
565    info!("getting write connection to update schema");
566    let mut conn = agent.pool().write_priority().await?;
567    info!("got write connection to update schema");
568
569    // hold onto this lock so nothing else makes changes
570    let mut schema_write = agent.schema().write();
571
572    // clone the previous schema and apply
573    let mut new_schema = {
574        let mut schema = schema_write.clone();
575        for (name, def) in partial_schema.tables.iter() {
576            // overwrite table because users are expected to return a full table def
577            schema.tables.insert(name.clone(), def.clone());
578        }
579        schema
580    };
581
582    new_schema.constrain()?;
583
584    // conn.trace(Some(|sql| debug!(sql)));
585
586    let apply_res = block_in_place(|| {
587        let tx = conn.immediate_transaction()?;
588
589        apply_schema(&tx, &schema_write, &mut new_schema)?;
590
591        for tbl_name in partial_schema.tables.keys() {
592            tx.execute("DELETE FROM __corro_schema WHERE tbl_name = ?", [tbl_name])?;
593
594            let n = tx.execute("INSERT INTO __corro_schema SELECT tbl_name, type, name, sql, 'api' AS source FROM sqlite_schema WHERE tbl_name = ? AND type IN ('table', 'index') AND name IS NOT NULL AND sql IS NOT NULL", [tbl_name])?;
595            info!("Updated {n} rows in __corro_schema for table {tbl_name}");
596        }
597
598        tx.commit()?;
599
600        // drain the pool of RO connections because they might not get the new tables in cr-sqlite!
601        agent.pool().drain_read();
602
603        Ok::<_, eyre::Report>(())
604    });
605
606    // conn.trace(None);
607
608    apply_res?;
609
610    *schema_write = new_schema;
611
612    Ok(())
613}
614
615pub async fn api_v1_db_schema(
616    Extension(agent): Extension<Agent>,
617    axum::extract::Json(statements): axum::extract::Json<Vec<String>>,
618) -> (StatusCode, axum::Json<ExecResponse>) {
619    let actor_id = agent.actor_id().to_string();
620    if statements.is_empty() {
621        return (
622            StatusCode::BAD_REQUEST,
623            axum::Json(ExecResponse {
624                results: vec![ExecResult::Error {
625                    error: "at least 1 statement is required".into(),
626                }],
627                time: 0.0,
628                version: None,
629                actor_id: Some(actor_id),
630            }),
631        );
632    }
633
634    let start = Instant::now();
635
636    assert_sometimes!(true, "Corrosion applies schema");
637    if let Err(e) = execute_schema(&agent, statements).await {
638        error!("could not merge schemas: {e}");
639        return (
640            StatusCode::INTERNAL_SERVER_ERROR,
641            axum::Json(ExecResponse {
642                results: vec![ExecResult::Error {
643                    error: e.to_string(),
644                }],
645                time: 0.0,
646                version: None,
647                actor_id: Some(actor_id),
648            }),
649        );
650    }
651
652    (
653        StatusCode::OK,
654        axum::Json(ExecResponse {
655            results: vec![],
656            time: start.elapsed().as_secs_f64(),
657            version: None,
658            actor_id: Some(actor_id),
659        }),
660    )
661}
662
663/// Query the table status of the current node
664///
665/// Currently this endpoint only supports querying the row count for a
666/// selection of provided tables.  Table names are checked for
667/// existence before querying
668pub async fn api_v1_table_stats(
669    Extension(agent): Extension<Agent>,
670    axum::extract::Json(ts_req): axum::extract::Json<TableStatRequest>,
671) -> (StatusCode, axum::Json<TableStatResponse>) {
672    async fn count_table_lengths(
673        agent: &Agent,
674        ts_req: TableStatRequest,
675    ) -> eyre::Result<(i64, Vec<String>)> {
676        debug!("Querying row count for {} tables", ts_req.tables.len());
677        let conn = agent.pool().read().await?;
678
679        block_in_place(move || -> eyre::Result<(i64, Vec<String>)> {
680            let valid_tables: BTreeSet<String> = conn
681                .prepare_cached("select name from sqlite_schema where type = 'table'")?
682                .query_map([], |row| row.get(0))?
683                .filter_map(|name| name.ok())
684                .collect();
685
686            let mut invalid_tables = vec![];
687            let mut total_count = 0;
688            for table in ts_req.tables.into_iter() {
689                if !valid_tables.contains(&table) {
690                    error!("Table name {} doesn't exist!", &table);
691                    invalid_tables.push(table);
692                    continue;
693                }
694
695                let count: i64 = conn
696                    .prepare_cached(&format!("SELECT COUNT(*) FROM {}", &table))?
697                    .query_row((), |row| row.get(0))?;
698
699                total_count += count;
700            }
701            Ok((total_count, invalid_tables))
702        })
703    }
704
705    match count_table_lengths(&agent, ts_req).await {
706        Ok((count, invalid_tables)) => (
707            StatusCode::OK,
708            axum::Json(TableStatResponse {
709                total_row_count: count,
710                invalid_tables,
711            }),
712        ),
713        Err(_) => (
714            StatusCode::INTERNAL_SERVER_ERROR,
715            axum::Json(TableStatResponse {
716                total_row_count: 0,
717                // Since we don't know what error occured or if any
718                // tables were valid, we just return an empty list
719                invalid_tables: vec![],
720            }),
721        ),
722    }
723}
724
725#[cfg(test)]
726mod tests {
727    use axum::response::IntoResponse;
728    use http_body_util::BodyExt;
729    use klukai_types::{
730        api::RowId,
731        base::CrsqlDbVersion,
732        broadcast::{BroadcastInput, BroadcastV1, ChangeV1, Changeset},
733        config::Config,
734        schema::SqliteType,
735        tripwire::Tripwire,
736    };
737    use tokio::sync::mpsc::error::TryRecvError;
738    use tokio_util::codec::{Decoder, LinesCodec};
739
740    use super::*;
741
742    use crate::agent::setup;
743
744    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
745    async fn test_api_db_execute() -> eyre::Result<()> {
746        _ = tracing_subscriber::fmt::try_init();
747
748        let (tripwire, _tripwire_worker, _tripwire_tx) = Tripwire::new_simple();
749
750        let dir = tempfile::tempdir()?;
751
752        let (agent, mut agent_options) = setup(
753            Config::builder()
754                .db_path(dir.path().join("corrosion.db").display().to_string())
755                .gossip_addr("127.0.0.1:0".parse()?)
756                .api_addr("127.0.0.1:0".parse()?)
757                .build()?,
758            tripwire,
759        )
760        .await?;
761
762        let rx_bcast = &mut agent_options.rx_bcast;
763
764        let (status_code, _body) = api_v1_db_schema(
765            Extension(agent.clone()),
766            axum::Json(vec![klukai_tests::TEST_SCHEMA.into()]),
767        )
768        .await;
769
770        assert_eq!(status_code, StatusCode::OK);
771
772        let (status_code, body) = api_v1_transactions(
773            Extension(agent.clone()),
774            axum::extract::Query(TimeoutParams { timeout: None }),
775            axum::Json(vec![Statement::WithParams(
776                "insert into tests (id, text) values (?,?)".into(),
777                vec![1i64.into(), "service-name".into()],
778            )]),
779        )
780        .await;
781
782        println!("{body:?}");
783
784        assert_eq!(status_code, StatusCode::OK);
785
786        assert!(body.0.results.len() == 1);
787
788        let msg = rx_bcast
789            .recv()
790            .await
791            .expect("not msg received on bcast channel");
792
793        assert!(matches!(
794            msg,
795            BroadcastInput::AddBroadcast(BroadcastV1::Change(ChangeV1 {
796                changeset: Changeset::FullV2 {
797                    version: CrsqlDbVersion(1),
798                    ..
799                },
800                ..
801            }))
802        ));
803
804        assert_eq!(
805            agent.booked().read::<&str, _>("test", None).await.last(),
806            Some(CrsqlDbVersion(1))
807        );
808
809        println!("second req...");
810
811        let (status_code, body) = api_v1_transactions(
812            Extension(agent.clone()),
813            axum::extract::Query(TimeoutParams { timeout: None }),
814            axum::Json(vec![Statement::WithParams(
815                "update tests SET text = ? where id = ?".into(),
816                vec!["service-name".into(), 1i64.into()],
817            )]),
818        )
819        .await;
820
821        println!("{body:?}");
822
823        assert_eq!(status_code, StatusCode::OK);
824
825        assert!(body.0.results.len() == 1);
826
827        // no actual changes!
828        assert!(matches!(rx_bcast.try_recv(), Err(TryRecvError::Empty)));
829
830        Ok(())
831    }
832
833    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
834    async fn test_api_db_query() -> eyre::Result<()> {
835        _ = tracing_subscriber::fmt::try_init();
836
837        let (tripwire, _tripwire_worker, _tripwire_tx) = Tripwire::new_simple();
838
839        let dir = tempfile::tempdir()?;
840
841        let (agent, _agent_options) = setup(
842            Config::builder()
843                .db_path(dir.path().join("corrosion.db").display().to_string())
844                .gossip_addr("127.0.0.1:0".parse()?)
845                .api_addr("127.0.0.1:0".parse()?)
846                .build()?,
847            tripwire,
848        )
849        .await?;
850
851        let (status_code, _body) = api_v1_db_schema(
852            Extension(agent.clone()),
853            axum::Json(vec![klukai_tests::TEST_SCHEMA.into()]),
854        )
855        .await;
856
857        assert_eq!(status_code, StatusCode::OK);
858
859        let (status_code, body) = api_v1_transactions(
860            Extension(agent.clone()),
861            axum::extract::Query(TimeoutParams { timeout: None }),
862            axum::Json(vec![
863                Statement::WithParams(
864                    "insert into tests (id, text) values (?,?)".into(),
865                    vec![1i64.into(), "service-name".into()],
866                ),
867                Statement::WithParams(
868                    "insert into tests (id, text) values (?,?)".into(),
869                    vec![2i64.into(), "service-name-2".into()],
870                ),
871            ]),
872        )
873        .await;
874
875        // println!("{body:?}");
876
877        assert_eq!(status_code, StatusCode::OK);
878
879        assert!(body.0.results.len() == 2);
880
881        println!("transaction body: {body:?}");
882
883        let res = api_v1_queries(
884            Extension(agent.clone()),
885            ConnectInfo("127.0.0.1:1234".parse().unwrap()),
886            axum::extract::Query(TimeoutParams { timeout: None }),
887            axum::Json(Statement::Simple("select * from tests".into())),
888        )
889        .await
890        .into_response();
891
892        assert_eq!(res.status(), StatusCode::OK);
893
894        let mut body = res.into_body();
895
896        let mut lines = LinesCodec::new();
897
898        let mut buf = BytesMut::new();
899
900        if let Some(frame) = body.frame().await
901            && let Ok(frame) = frame
902            && let Some(data) = frame.data_ref()
903        {
904            buf.extend_from_slice(data);
905        }
906
907        let s = lines.decode(&mut buf).unwrap().unwrap();
908
909        let cols: QueryEvent = serde_json::from_str(&s).unwrap();
910
911        assert_eq!(cols, QueryEvent::Columns(vec!["id".into(), "text".into()]));
912
913        if let Some(frame) = body.frame().await
914            && let Ok(frame) = frame
915            && let Some(data) = frame.data_ref()
916        {
917            buf.extend_from_slice(data);
918        }
919
920        let s = lines.decode(&mut buf).unwrap().unwrap();
921
922        let row: QueryEvent = serde_json::from_str(&s).unwrap();
923
924        assert_eq!(
925            row,
926            QueryEvent::Row(RowId(1), vec![1i64.into(), "service-name".into()])
927        );
928
929        if let Some(frame) = body.frame().await
930            && let Ok(frame) = frame
931            && let Some(data) = frame.data_ref()
932        {
933            buf.extend_from_slice(data);
934        }
935
936        let s = lines.decode(&mut buf).unwrap().unwrap();
937
938        let row: QueryEvent = serde_json::from_str(&s).unwrap();
939
940        assert_eq!(
941            row,
942            QueryEvent::Row(RowId(2), vec![2i64.into(), "service-name-2".into()])
943        );
944
945        if let Some(frame) = body.frame().await
946            && let Ok(frame) = frame
947            && let Some(data) = frame.data_ref()
948        {
949            buf.extend_from_slice(data);
950        }
951
952        let s = lines.decode(&mut buf).unwrap().unwrap();
953
954        let query_evt: QueryEvent = serde_json::from_str(&s).unwrap();
955
956        assert!(matches!(query_evt, QueryEvent::EndOfQuery { .. }));
957
958        assert!(body.frame().await.is_none());
959
960        Ok(())
961    }
962
963    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
964    async fn test_api_db_schema() -> eyre::Result<()> {
965        _ = tracing_subscriber::fmt::try_init();
966        let (tripwire, _tripwire_worker, _tripwire_tx) = Tripwire::new_simple();
967
968        let dir = tempfile::tempdir()?;
969
970        let (agent, _agent_options) = setup(
971            Config::builder()
972                .db_path(dir.path().join("corrosion.db").display().to_string())
973                .gossip_addr("127.0.0.1:0".parse()?)
974                .api_addr("127.0.0.1:0".parse()?)
975                .build()?,
976            tripwire,
977        )
978        .await?;
979
980        let (status_code, _body) = api_v1_db_schema(
981            Extension(agent.clone()),
982            axum::Json(vec![
983                "CREATE TABLE tests (id BIGINT NOT NULL PRIMARY KEY, foo TEXT);".into(),
984            ]),
985        )
986        .await;
987
988        assert_eq!(status_code, StatusCode::OK);
989
990        // scope the schema reader in here
991        {
992            let schema = agent.schema().read();
993            let tests = schema
994                .tables
995                .get("tests")
996                .expect("no tests table in schema");
997
998            let id_col = tests.columns.get("id").unwrap();
999            assert_eq!(id_col.name, "id");
1000            assert_eq!(id_col.sql_type(), (SqliteType::Integer, Some("BIGINT")));
1001            assert!(!id_col.nullable);
1002            assert!(id_col.primary_key);
1003
1004            let foo_col = tests.columns.get("foo").unwrap();
1005            assert_eq!(foo_col.name, "foo");
1006            assert_eq!(foo_col.sql_type(), (SqliteType::Text, Some("TEXT")));
1007            assert!(foo_col.nullable);
1008            assert!(!foo_col.primary_key);
1009        }
1010
1011        let (status_code, _body) = api_v1_db_schema(
1012            Extension(agent.clone()),
1013            axum::Json(vec![
1014                "CREATE TABLE tests2 (id BIGINT NOT NULL PRIMARY KEY, foo TEXT);".into(),
1015                "CREATE TABLE tests (id BIGINT NOT NULL PRIMARY KEY, foo TEXT);".into(),
1016            ]),
1017        )
1018        .await;
1019
1020        assert_eq!(status_code, StatusCode::OK);
1021
1022        {
1023            let schema = agent.schema().read();
1024            let tests = schema
1025                .tables
1026                .get("tests")
1027                .expect("no tests table in schema");
1028
1029            let id_col = tests.columns.get("id").unwrap();
1030            assert_eq!(id_col.name, "id");
1031            assert_eq!(id_col.sql_type(), (SqliteType::Integer, Some("BIGINT")));
1032            assert!(!id_col.nullable);
1033            assert!(id_col.primary_key);
1034
1035            let foo_col = tests.columns.get("foo").unwrap();
1036            assert_eq!(foo_col.name, "foo");
1037            assert_eq!(foo_col.sql_type(), (SqliteType::Text, Some("TEXT")));
1038            assert!(foo_col.nullable);
1039            assert!(!foo_col.primary_key);
1040
1041            let tests = schema
1042                .tables
1043                .get("tests2")
1044                .expect("no tests2 table in schema");
1045
1046            let id_col = tests.columns.get("id").unwrap();
1047            assert_eq!(id_col.name, "id");
1048            assert_eq!(id_col.sql_type(), (SqliteType::Integer, Some("BIGINT")));
1049            assert!(!id_col.nullable);
1050            assert!(id_col.primary_key);
1051
1052            let foo_col = tests.columns.get("foo").unwrap();
1053            assert_eq!(foo_col.name, "foo");
1054            assert_eq!(foo_col.sql_type(), (SqliteType::Text, Some("TEXT")));
1055            assert!(foo_col.nullable);
1056            assert!(!foo_col.primary_key);
1057        }
1058
1059        // w/ existing table!
1060
1061        let create_stmt = "CREATE TABLE tests3 (id BIGINT NOT NULL PRIMARY KEY, foo TEXT, updated_at INTEGER NOT NULL DEFAULT 0);";
1062
1063        {
1064            // adding the table and an index
1065            let conn = agent.pool().write_priority().await?;
1066            conn.execute_batch(create_stmt)?;
1067            conn.execute_batch("CREATE INDEX tests3_updated_at ON tests3 (updated_at);")?;
1068            assert_eq!(
1069                conn.execute(
1070                    "INSERT INTO tests3 VALUES (123, 'some foo text', 123456789);",
1071                    ()
1072                )?,
1073                1
1074            );
1075            assert_eq!(
1076                conn.execute(
1077                    "INSERT INTO tests3 VALUES (1234, 'some foo text 2', 1234567890);",
1078                    ()
1079                )?,
1080                1
1081            );
1082        }
1083
1084        let (status_code, _body) = api_v1_db_schema(
1085            Extension(agent.clone()),
1086            axum::Json(vec![create_stmt.into()]),
1087        )
1088        .await;
1089
1090        assert_eq!(status_code, StatusCode::OK);
1091
1092        {
1093            let schema = agent.schema().read();
1094
1095            // check that the tests table is still there!
1096            let tests = schema
1097                .tables
1098                .get("tests")
1099                .expect("no tests table in schema");
1100
1101            let id_col = tests.columns.get("id").unwrap();
1102            assert_eq!(id_col.name, "id");
1103            assert_eq!(id_col.sql_type(), (SqliteType::Integer, Some("BIGINT")));
1104            assert!(!id_col.nullable);
1105            assert!(id_col.primary_key);
1106
1107            let foo_col = tests.columns.get("foo").unwrap();
1108            assert_eq!(foo_col.name, "foo");
1109            assert_eq!(foo_col.sql_type(), (SqliteType::Text, Some("TEXT")));
1110            assert!(foo_col.nullable);
1111            assert!(!foo_col.primary_key);
1112
1113            let tests = schema
1114                .tables
1115                .get("tests3")
1116                .expect("no tests3 table in schema");
1117
1118            let id_col = tests.columns.get("id").unwrap();
1119            assert_eq!(id_col.name, "id");
1120            assert_eq!(id_col.sql_type(), (SqliteType::Integer, Some("BIGINT")));
1121            assert!(!id_col.nullable);
1122            assert!(id_col.primary_key);
1123
1124            let foo_col = tests.columns.get("foo").unwrap();
1125            assert_eq!(foo_col.name, "foo");
1126            assert_eq!(foo_col.sql_type(), (SqliteType::Text, Some("TEXT")));
1127            assert!(foo_col.nullable);
1128            assert!(!foo_col.primary_key);
1129
1130            let updated_at_col = tests.columns.get("updated_at").unwrap();
1131            assert_eq!(updated_at_col.name, "updated_at");
1132            assert_eq!(
1133                updated_at_col.sql_type(),
1134                (SqliteType::Integer, Some("INTEGER"))
1135            );
1136            assert!(!updated_at_col.nullable);
1137            assert!(!updated_at_col.primary_key);
1138
1139            let updated_at_idx = tests.indexes.get("tests3_updated_at").unwrap();
1140            assert_eq!(updated_at_idx.name, "tests3_updated_at");
1141            assert_eq!(updated_at_idx.tbl_name, "tests3");
1142            assert_eq!(updated_at_idx.columns.len(), 1);
1143            assert!(updated_at_idx.where_clause.is_none());
1144        }
1145
1146        let conn = agent.pool().read().await?;
1147        let count: usize =
1148            conn.query_row("SELECT COUNT(*) FROM tests3__crsql_clock;", (), |row| {
1149                row.get(0)
1150            })?;
1151        // should've created a specific qty of clock table rows, just a sanity check!
1152        assert_eq!(count, 4);
1153
1154        Ok(())
1155    }
1156}