contextdb_engine/
cli_render.rs1use crate::Database;
4use contextdb_core::Value;
5use contextdb_core::table_meta::{ColumnType, TableMeta, VectorQuantization};
6use std::fmt::Write;
7
8pub fn render_column_type(col_type: &ColumnType) -> String {
10 match col_type {
11 ColumnType::Integer => "INTEGER".to_string(),
12 ColumnType::Real => "REAL".to_string(),
13 ColumnType::Text => "TEXT".to_string(),
14 ColumnType::Boolean => "BOOLEAN".to_string(),
15 ColumnType::Json => "JSON".to_string(),
16 ColumnType::Uuid => "UUID".to_string(),
17 ColumnType::Vector(dim) => format!("VECTOR({dim})"),
18 ColumnType::Timestamp => "TIMESTAMP".to_string(),
19 ColumnType::TxId => "TXID".to_string(),
20 }
21}
22
23pub fn render_table_meta(table: &str, meta: &TableMeta) -> String {
27 render_table_meta_inner(table, meta, false)
28}
29
30pub fn render_table_meta_verbose(table: &str, meta: &TableMeta) -> String {
34 render_table_meta_inner(table, meta, true)
35}
36
37fn render_table_meta_inner(table: &str, meta: &TableMeta, verbose: bool) -> String {
38 let mut buf = String::new();
39 writeln!(&mut buf, "CREATE TABLE {table} (").unwrap();
40 let mut first = true;
41 for col in &meta.columns {
42 if !first {
43 buf.push_str(",\n");
44 }
45 first = false;
46 let mut ty = render_column_type(&col.column_type);
47 if !matches!(col.quantization, VectorQuantization::F32) {
48 ty.push_str(&format!(
49 " WITH (quantization = '{}')",
50 col.quantization.as_str()
51 ));
52 }
53 if !col.nullable && !col.primary_key {
54 ty.push_str(" NOT NULL");
55 }
56 if col.primary_key {
57 ty.push_str(" PRIMARY KEY");
58 }
59 if col.immutable {
60 ty.push_str(" IMMUTABLE");
61 }
62 if let Some(policy) = &col.rank_policy {
63 ty.push_str(&format!(
64 " RANK_POLICY (JOIN {} ON {}, FORMULA '{}', SORT_KEY {})",
65 policy.joined_table,
66 policy.joined_column,
67 policy.formula.replace('\'', "''"),
68 policy.sort_key
69 ));
70 }
71 write!(&mut buf, " {} {}", col.name, ty).unwrap();
72 }
73 buf.push_str("\n)");
74 if meta.immutable {
75 buf.push_str(" IMMUTABLE");
76 }
77 if let Some(sm) = &meta.state_machine {
78 let mut entries: Vec<_> = sm.transitions.iter().collect();
79 entries.sort_by(|a, b| a.0.cmp(b.0));
80 let transitions: Vec<String> = entries
81 .into_iter()
82 .map(|(from, tos)| format!("{from} -> [{}]", tos.join(", ")))
83 .collect();
84 write!(
85 &mut buf,
86 " STATE MACHINE ({}: {})",
87 sm.column,
88 transitions.join(", ")
89 )
90 .unwrap();
91 }
92 if !meta.dag_edge_types.is_empty() {
93 let edge_types = meta
94 .dag_edge_types
95 .iter()
96 .map(|edge_type| format!("'{edge_type}'"))
97 .collect::<Vec<_>>()
98 .join(", ");
99 write!(&mut buf, " DAG({edge_types})").unwrap();
100 }
101 buf.push_str(";\n");
102 for decl in &meta.indexes {
103 if !verbose && decl.kind == contextdb_core::IndexKind::Auto {
104 continue;
105 }
106 let cols: Vec<String> = decl
107 .columns
108 .iter()
109 .map(|(c, dir)| {
110 let dir_str = match dir {
111 contextdb_core::SortDirection::Asc => "ASC",
112 contextdb_core::SortDirection::Desc => "DESC",
113 };
114 format!("{c} {dir_str}")
115 })
116 .collect();
117 writeln!(
118 &mut buf,
119 "CREATE INDEX {} ON {} ({});",
120 decl.name,
121 table,
122 cols.join(", ")
123 )
124 .unwrap();
125 }
126 buf
127}
128
129pub fn render_explain(
132 db: &Database,
133 sql: &str,
134 params: &std::collections::HashMap<String, Value>,
135) -> contextdb_core::Result<String> {
136 let result = db.execute(sql, params)?;
137 let mut out = String::new();
138 out.push_str(result.trace.physical_plan);
139 if let Some(idx) = &result.trace.index_used {
140 out.push_str(&format!(" {{ index: {idx} }}"));
141 }
142 out.push('\n');
143 if !result.trace.predicates_pushed.is_empty() {
144 out.push_str(" predicates_pushed: [");
145 for (i, p) in result.trace.predicates_pushed.iter().enumerate() {
146 if i > 0 {
147 out.push_str(", ");
148 }
149 out.push_str(p.as_ref());
150 }
151 out.push_str("]\n");
152 }
153 if !result.trace.indexes_considered.is_empty() {
154 out.push_str(" indexes_considered: [");
155 for (i, c) in result.trace.indexes_considered.iter().enumerate() {
156 if i > 0 {
157 out.push_str(", ");
158 }
159 out.push_str(&format!("{}: {}", c.name, c.rejected_reason));
160 }
161 out.push_str("]\n");
162 }
163 if result.trace.sort_elided {
164 out.push_str(" sort_elided: true\n");
165 }
166 Ok(out)
167}
168
169pub fn value_to_string(v: &Value) -> String {
171 match v {
172 Value::Null => "NULL".to_string(),
173 Value::Bool(b) => b.to_string(),
174 Value::Int64(n) => n.to_string(),
175 Value::Float64(f) => f.to_string(),
176 Value::Text(s) => s.clone(),
177 Value::Uuid(u) => u.to_string(),
178 Value::Timestamp(ts) => ts.to_string(),
179 Value::Json(j) => j.to_string(),
180 Value::Vector(vs) => format!("{vs:?}"),
181 Value::TxId(tx) => tx.0.to_string(),
182 }
183}
184
185pub fn render_sync_status(db: &Database) -> String {
187 format!("Committed TxId: {}\n", db.committed_watermark().0)
188}