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 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 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 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 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 let (stream_tx, stream_rx) =
483 tokio::sync::mpsc::channel::<Result<hyper::body::Frame<bytes::Bytes>, std::io::Error>>(512);
484
485 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 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 let mut schema_write = agent.schema().write();
571
572 let mut new_schema = {
574 let mut schema = schema_write.clone();
575 for (name, def) in partial_schema.tables.iter() {
576 schema.tables.insert(name.clone(), def.clone());
578 }
579 schema
580 };
581
582 new_schema.constrain()?;
583
584 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 agent.pool().drain_read();
602
603 Ok::<_, eyre::Report>(())
604 });
605
606 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
663pub 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 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 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 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 {
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 let create_stmt = "CREATE TABLE tests3 (id BIGINT NOT NULL PRIMARY KEY, foo TEXT, updated_at INTEGER NOT NULL DEFAULT 0);";
1062
1063 {
1064 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 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 assert_eq!(count, 4);
1153
1154 Ok(())
1155 }
1156}