1use std::{cell::RefCell, mem, ptr, slice, str};
9
10use sql_dialect_fmt_formatter::{format, Dialect, FormatOptions};
11
12thread_local! {
13 static LAST_RESULT: RefCell<Option<Box<[u8]>>> = const { RefCell::new(None) };
14}
15
16#[no_mangle]
21pub extern "C" fn sql_dialect_fmt_alloc(len: u32) -> u32 {
22 let mut buffer = Vec::<u8>::with_capacity(len as usize);
23 let ptr = buffer.as_mut_ptr();
24 mem::forget(buffer);
25 ptr as u32
26}
27
28#[no_mangle]
35pub unsafe extern "C" fn sql_dialect_fmt_dealloc(ptr: u32, capacity: u32) {
36 if ptr == 0 || capacity == 0 {
37 return;
38 }
39 drop(Vec::from_raw_parts(ptr as *mut u8, 0, capacity as usize));
40}
41
42#[no_mangle]
52pub unsafe extern "C" fn sql_dialect_fmt_format(
53 ptr: u32,
54 len: u32,
55 line_width: u32,
56 indent_width: u32,
57 uppercase_keywords: u32,
58) -> u32 {
59 sql_dialect_fmt_format_with_dialect(ptr, len, line_width, indent_width, uppercase_keywords, 0)
60}
61
62#[no_mangle]
74pub unsafe extern "C" fn sql_dialect_fmt_format_with_dialect(
75 ptr: u32,
76 len: u32,
77 line_width: u32,
78 indent_width: u32,
79 uppercase_keywords: u32,
80 dialect: u32,
81) -> u32 {
82 let bytes = slice::from_raw_parts(ptr as *const u8, len as usize);
83 format_bytes(bytes, line_width, indent_width, uppercase_keywords, dialect)
84}
85
86fn format_bytes(
90 bytes: &[u8],
91 line_width: u32,
92 indent_width: u32,
93 uppercase_keywords: u32,
94 dialect: u32,
95) -> u32 {
96 clear_last_result();
97
98 let Ok(source) = str::from_utf8(bytes) else {
99 return 1;
100 };
101
102 let options = FormatOptions::default()
103 .with_line_width(line_width.max(1) as usize)
104 .with_indent_width(indent_width.clamp(1, 16) as usize)
105 .with_uppercase_keywords(uppercase_keywords != 0)
106 .with_dialect(dialect_from_u32(dialect));
107
108 store_last_result(format(source, &options).into_bytes().into_boxed_slice());
109 0
110}
111
112fn dialect_from_u32(dialect: u32) -> Dialect {
113 match dialect {
114 1 => Dialect::Databricks,
115 _ => Dialect::Snowflake,
116 }
117}
118
119#[no_mangle]
126pub unsafe extern "C" fn sql_dialect_fmt_result_ptr() -> u32 {
127 last_result_ptr() as u32
128}
129
130#[no_mangle]
137pub unsafe extern "C" fn sql_dialect_fmt_result_len() -> u32 {
138 last_result_len() as u32
139}
140
141#[no_mangle]
147pub unsafe extern "C" fn sql_dialect_fmt_clear_result() {
148 clear_last_result();
149}
150
151fn clear_last_result() {
152 LAST_RESULT.with(|last_result| {
153 last_result.borrow_mut().take();
154 });
155}
156
157fn store_last_result(result: Box<[u8]>) {
158 LAST_RESULT.with(|last_result| {
159 *last_result.borrow_mut() = Some(result);
160 });
161}
162
163fn last_result_ptr() -> *const u8 {
164 LAST_RESULT.with(|last_result| {
165 last_result
166 .borrow()
167 .as_deref()
168 .map_or(ptr::null(), |result| result.as_ptr())
169 })
170}
171
172fn last_result_len() -> usize {
173 LAST_RESULT.with(|last_result| last_result.borrow().as_deref().map_or(0, <[u8]>::len))
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use sql_dialect_fmt_test_fixtures::{
180 javascript_routine_trailing_whitespace_input,
181 JAVASCRIPT_ROUTINE_TRAILING_WHITESPACE_EXPECTED,
182 };
183
184 fn last_result_bytes() -> Vec<u8> {
185 let ptr = last_result_ptr();
186 let len = last_result_len();
187
188 if len == 0 {
189 return Vec::new();
190 }
191
192 assert!(!ptr.is_null());
193 unsafe { slice::from_raw_parts(ptr, len).to_vec() }
194 }
195
196 #[test]
197 fn stores_result_bytes() {
198 clear_last_result();
199
200 store_last_result(b"select 1".to_vec().into_boxed_slice());
201
202 assert_eq!(last_result_len(), 8);
203 assert_eq!(last_result_bytes(), b"select 1");
204
205 clear_last_result();
206 }
207
208 #[test]
209 fn replacing_result_exposes_only_new_bytes() {
210 clear_last_result();
211
212 store_last_result(b"old result".to_vec().into_boxed_slice());
213 store_last_result(b"new".to_vec().into_boxed_slice());
214
215 assert_eq!(last_result_len(), 3);
216 assert_eq!(last_result_bytes(), b"new");
217
218 clear_last_result();
219 }
220
221 #[test]
222 fn clear_result_removes_state_and_is_idempotent() {
223 clear_last_result();
224 store_last_result(b"temporary".to_vec().into_boxed_slice());
225
226 clear_last_result();
227 assert!(last_result_ptr().is_null());
228 assert_eq!(last_result_len(), 0);
229 assert_eq!(last_result_bytes(), b"");
230
231 clear_last_result();
232 assert!(last_result_ptr().is_null());
233 assert_eq!(last_result_len(), 0);
234 }
235
236 fn format_to_string(
239 source: &str,
240 line_width: u32,
241 indent_width: u32,
242 uppercase_keywords: u32,
243 dialect: u32,
244 ) -> String {
245 let status = format_bytes(
246 source.as_bytes(),
247 line_width,
248 indent_width,
249 uppercase_keywords,
250 dialect,
251 );
252 assert_eq!(status, 0, "format_bytes({source:?}) failed");
253 let result = String::from_utf8(last_result_bytes()).expect("UTF-8 result");
254 clear_last_result();
255 result
256 }
257
258 #[test]
259 fn format_defaults_produce_uppercase_snowflake_output() {
260 assert_eq!(
261 format_to_string("select a,b from t", 80, 4, 1, 0),
262 "SELECT a, b\nFROM t;\n"
263 );
264 }
265
266 #[test]
267 fn uppercase_flag_zero_preserves_source_keyword_case() {
268 assert_eq!(
270 format_to_string("select a from t", 80, 4, 0, 0),
271 "select a\nfrom t;\n"
272 );
273 assert_eq!(
274 format_to_string("SELECT a from t", 80, 4, 0, 0),
275 "SELECT a\nfrom t;\n"
276 );
277 }
278
279 #[test]
280 fn line_width_controls_select_list_wrapping() {
281 let source = "select aaaa, bbbb, cccc from t";
282 assert_eq!(
283 format_to_string(source, 100, 4, 1, 0),
284 "SELECT aaaa, bbbb, cccc\nFROM t;\n"
285 );
286 assert_eq!(
287 format_to_string(source, 10, 4, 1, 0),
288 "SELECT\n aaaa,\n bbbb,\n cccc\nFROM t;\n"
289 );
290 }
291
292 #[test]
293 fn indent_width_is_applied_and_clamped() {
294 let source = "select aaaa, bbbb from t";
295 assert_eq!(
296 format_to_string(source, 10, 8, 1, 0),
297 "SELECT\n aaaa,\n bbbb\nFROM t;\n"
298 );
299 assert_eq!(
301 format_to_string(source, 10, 0, 1, 0),
302 "SELECT\n aaaa,\n bbbb\nFROM t;\n"
303 );
304 assert_eq!(
305 format_to_string(source, 10, 999, 1, 0),
306 format_to_string(source, 10, 16, 1, 0)
307 );
308 }
309
310 #[test]
311 fn zero_line_width_is_clamped_instead_of_panicking() {
312 assert_eq!(
313 format_to_string("select a from t", 0, 4, 1, 0),
314 format_to_string("select a from t", 1, 4, 1, 0)
315 );
316 }
317
318 #[test]
319 fn dialect_one_selects_databricks_and_unknown_values_fall_back_to_snowflake() {
320 let source = "select a <=> b from t";
322 assert_eq!(
323 format_to_string(source, 80, 4, 1, 1),
324 "SELECT a <=> b\nFROM t;\n"
325 );
326 let snowflake = format_to_string(source, 80, 4, 1, 0);
327 assert_eq!(format_to_string(source, 80, 4, 1, 999), snowflake);
328 assert_eq!(dialect_from_u32(0), Dialect::Snowflake);
329 assert_eq!(dialect_from_u32(1), Dialect::Databricks);
330 assert_eq!(dialect_from_u32(u32::MAX), Dialect::Snowflake);
331 }
332
333 #[test]
334 fn formatting_is_idempotent() {
335 let first = format_to_string("select a,b from t where x=1", 80, 4, 1, 0);
336 assert_eq!(format_to_string(&first, 80, 4, 1, 0), first);
337 }
338
339 #[test]
340 fn javascript_routine_regression_formats_through_the_wasm_abi() {
341 let input = javascript_routine_trailing_whitespace_input();
342 let output = format_to_string(&input, 100, 4, 1, 0);
343
344 assert_eq!(output, JAVASCRIPT_ROUTINE_TRAILING_WHITESPACE_EXPECTED);
345 assert_eq!(format_to_string(&output, 100, 4, 1, 0), output);
346 }
347
348 #[test]
349 fn unparseable_statements_pass_through_verbatim() {
350 let source = "select from where";
351 let result = format_to_string(source, 80, 4, 1, 0);
352 assert!(
353 result.contains(source),
354 "broken input should survive verbatim, got {result:?}"
355 );
356 }
357
358 #[test]
359 fn invalid_utf8_input_reports_failure_and_stores_no_result() {
360 store_last_result(b"stale".to_vec().into_boxed_slice());
362
363 let status = format_bytes(&[0x66, 0xFF, 0xFE], 80, 4, 1, 0);
364
365 assert_eq!(status, 1);
366 assert!(last_result_ptr().is_null());
367 assert_eq!(last_result_len(), 0);
368 }
369
370 #[test]
371 fn empty_input_formats_to_empty_output() {
372 assert_eq!(format_bytes(b"", 80, 4, 1, 0), 0);
373 assert_eq!(last_result_len(), 0);
374 clear_last_result();
375 }
376
377 #[test]
378 fn sequential_calls_replace_the_stored_result() {
379 assert_eq!(format_bytes(b"select 1", 80, 4, 1, 0), 0);
380 assert_eq!(last_result_bytes(), b"SELECT 1;\n");
381
382 assert_eq!(format_bytes(b"select 2", 80, 4, 1, 0), 0);
383 assert_eq!(last_result_bytes(), b"SELECT 2;\n");
384 clear_last_result();
385 }
386
387 #[test]
388 fn dealloc_ignores_null_and_zero_capacity_buffers() {
389 unsafe {
391 sql_dialect_fmt_dealloc(0, 0);
392 sql_dialect_fmt_dealloc(0, 16);
393 sql_dialect_fmt_dealloc(4, 0);
394 }
395 }
396}