1pub(crate) mod ddl;
14pub(crate) mod expr;
15pub(crate) mod filter;
16pub(crate) mod formula;
17pub(crate) mod index_quota;
18pub(crate) mod mutation;
19pub(crate) mod query;
20pub(crate) mod query_expr;
21
22#[cfg(test)]
23mod tests;
24
25pub use filter::render_filter;
26pub(crate) use mutation::render_point_selector;
27pub(crate) use query::render_search_params;
28
29use crate::ast::Stmt;
30use crate::error::QqlError;
31use crate::parser::Parser;
32use alloc::string::String;
33use alloc::vec::Vec;
34
35pub const DEFAULT_PREVIEW_MAX_DIMS: usize = 2;
37
38pub fn format(source: &str) -> Result<String, QqlError> {
40 let statements_with_spans = Parser::parse_all_with_spans(source)?;
41 if statements_with_spans.is_empty() {
42 let (comments, _) = parse_trivia_lines_with_trailing_blank(source);
43 if comments.is_empty() {
44 return Ok(String::new());
45 }
46 let mut out = String::new();
47 for line in comments {
48 out.push_str(&line);
49 out.push('\n');
50 }
51 return Ok(out);
52 }
53
54 let mut out = String::new();
55 let n = statements_with_spans.len();
56
57 let first_stmt_start = statements_with_spans[0].1.start;
59 let header_slice = &source[..first_stmt_start];
60 let (header_comments, had_blank_after_header) =
61 parse_trivia_lines_with_trailing_blank(header_slice);
62 if !header_comments.is_empty() {
63 for line in &header_comments {
64 out.push_str(line);
65 out.push('\n');
66 }
67 if had_blank_after_header {
68 out.push('\n');
69 }
70 }
71
72 for i in 0..n {
74 let (stmt, stmt_span) = &statements_with_spans[i];
75
76 let inner_slice = &source[stmt_span.start..stmt_span.end];
78 let inline_comments = find_comments_in_slice(inner_slice);
79 for comment in inline_comments {
80 out.push_str(comment);
81 out.push('\n');
82 }
83
84 out.push_str(&format_stmt(stmt));
86 out.push(';');
87
88 let gap_start = stmt_span.end;
90 let gap_end = if i + 1 < n {
91 statements_with_spans[i + 1].1.start
92 } else {
93 source.len()
94 };
95 let gap_slice = &source[gap_start..gap_end];
96
97 let (same_line, rest) = match gap_slice.find('\n') {
98 Some(idx) => (&gap_slice[..idx], &gap_slice[idx + 1..]),
99 None => (gap_slice, ""),
100 };
101
102 if let Some(pos) = same_line.find("--") {
104 out.push(' ');
105 out.push_str(same_line[pos..].trim_end());
106 }
107 out.push('\n');
108
109 if i + 1 < n {
110 let (gap_comments, had_blank_after_gap) = parse_trivia_lines_with_trailing_blank(rest);
112 if gap_comments.is_empty() {
113 if gap_slice.matches('\n').count() >= 2 {
115 out.push('\n');
116 }
117 } else {
118 out.push('\n');
120 for line in &gap_comments {
121 out.push_str(line);
122 out.push('\n');
123 }
124 if had_blank_after_gap {
125 out.push('\n');
126 }
127 }
128 } else {
129 let (trailer_comments, _) = parse_trivia_lines_with_trailing_blank(rest);
131 if !trailer_comments.is_empty() {
132 out.push('\n');
133 for line in &trailer_comments {
134 out.push_str(line);
135 out.push('\n');
136 }
137 }
138 }
139 }
140
141 Ok(out)
142}
143
144fn parse_trivia_lines_with_trailing_blank(slice: &str) -> (Vec<String>, bool) {
145 let mut comments = Vec::new();
146 let mut pending_blank = false;
147
148 let had_blank_after = if let Some(last_comment_idx) = slice.rfind("--") {
149 let after_comment = &slice[last_comment_idx..];
150 if let Some(nl_idx) = after_comment.find('\n') {
151 after_comment[nl_idx + 1..].contains('\n')
152 } else {
153 false
154 }
155 } else {
156 false
157 };
158
159 for line in slice.lines() {
160 let trimmed = line.trim();
161 if trimmed.is_empty() {
162 if !comments.is_empty() {
163 pending_blank = true;
164 }
165 } else if trimmed.starts_with("--") {
166 if pending_blank {
167 comments.push(String::new());
168 pending_blank = false;
169 }
170 comments.push(trimmed.into());
171 }
172 }
173
174 (comments, had_blank_after)
175}
176
177fn find_comments_in_slice(slice: &str) -> Vec<&str> {
178 let mut comments = Vec::new();
179 let bytes = slice.as_bytes();
180 let mut pos = 0;
181 while pos < bytes.len() {
182 match bytes[pos] {
183 b'\'' | b'"' | b'`' => {
184 let quote = bytes[pos];
185 pos += 1;
186 let is_triple =
187 pos + 1 < bytes.len() && bytes[pos] == quote && bytes[pos + 1] == quote;
188 if is_triple {
189 pos += 2;
190 let delim = if quote == b'\'' { "'''" } else { "\"\"\"" };
191 if let Some(idx) = slice[pos..].find(delim) {
192 pos += idx + 3;
193 } else {
194 break;
195 }
196 } else {
197 while pos < bytes.len() {
198 if bytes[pos] == b'\\' {
199 pos += 2;
200 continue;
201 }
202 if bytes[pos] == quote {
203 if quote == b'\'' && pos + 1 < bytes.len() && bytes[pos + 1] == b'\'' {
204 pos += 2;
205 continue;
206 }
207 pos += 1;
208 break;
209 }
210 pos += 1;
211 }
212 }
213 }
214 b'-' if pos + 1 < bytes.len() && bytes[pos + 1] == b'-' => {
215 let start = pos;
216 pos += 2;
217 while pos < bytes.len() && bytes[pos] != b'\n' {
218 pos += 1;
219 }
220 comments.push(slice[start..pos].trim_end());
221 }
222 _ => {
223 pos += 1;
224 }
225 }
226 }
227 comments
228}
229
230pub fn format_script(statements: &[Stmt]) -> String {
233 let mut out = String::new();
234 for (i, statement) in statements.iter().enumerate() {
235 if i > 0 {
236 out.push('\n');
237 }
238 out.push_str(&format_stmt(statement));
239 out.push(';');
240 }
241 out
242}
243
244pub fn format_stmt(statement: &Stmt) -> String {
246 match statement {
247 Stmt::Query(query) => query::render_query_body(query),
248 Stmt::Scroll(statement) => mutation::render_scroll(statement),
249 Stmt::Upsert(statement) => mutation::render_upsert(statement),
250 Stmt::CreateCollection(statement) => ddl::render_create_collection(statement),
251 Stmt::CreateIndex(statement) => ddl::render_create_index(statement),
252 Stmt::DropIndex(statement) => ddl::render_drop_index(statement),
253 Stmt::CreateShardKey(statement) => ddl::render_create_shard_key(statement),
254 Stmt::DropShardKey(statement) => ddl::render_drop_shard_key(statement),
255 Stmt::AlterCollection(statement) => ddl::render_alter_collection(statement),
256 Stmt::DropCollection(statement) => ddl::render_drop_collection(statement),
257 Stmt::ShowCollections => ddl::render_show_collections(),
258 Stmt::ShowCollection(collection) => ddl::render_show_collection(collection),
259 Stmt::ShowShardKeys(collection) => ddl::render_show_shard_keys(collection),
260 Stmt::ShowQuotas => ddl::render_show_quotas(),
261 Stmt::SetQuota(stmt) => ddl::render_set_quota(stmt),
262 Stmt::Delete(statement) => mutation::render_delete(statement),
263 Stmt::ClearPayload(statement) => mutation::render_clear_payload(statement),
264 Stmt::DeletePayload(statement) => mutation::render_delete_payload(statement),
265 Stmt::DeleteVector(statement) => mutation::render_delete_vector(statement),
266 Stmt::UpdateVector(statement) => mutation::render_update_vector(statement),
267 Stmt::UpdatePayload(statement) => mutation::render_update_payload(statement),
268 Stmt::Count(statement) => mutation::render_count(statement),
269 Stmt::Facet(statement) => mutation::render_facet(statement),
270 Stmt::Batch(statement) => render_batch(statement),
271 }
272}
273
274fn render_batch(statement: &crate::ast::BatchStmt) -> String {
276 let mut out = String::from("BATCH {");
277 for member in &statement.statements {
278 out.push_str("\n ");
279 let rendered = format_stmt(member).replace('\n', "\n ");
282 out.push_str(&rendered);
283 out.push(';');
284 }
285 out.push_str("\n}");
286 if let Some(wait) = statement.wait {
287 out.push_str(&alloc::format!(" WAIT {wait}"));
288 }
289 if let Some(params) = &statement.params {
290 out.push_str(&alloc::format!(
291 " PARAMS ({})",
292 render_search_params(params)
293 ));
294 }
295 out
296}
297
298pub fn format_stmt_readable(statement: &Stmt) -> String {
300 crate::params::truncate_vector_literals(&format_stmt(statement), DEFAULT_PREVIEW_MAX_DIMS)
301}