Skip to main content

sql_dialect_fmt_wasm/
lib.rs

1//! Raw WebAssembly bindings for browser extension use.
2//!
3//! This crate deliberately avoids `wasm-bindgen` so the Chrome extension can load a single local
4//! `.wasm` file without generated JavaScript glue. JavaScript owns input buffers allocated through
5//! `sql_dialect_fmt_alloc`; this module owns the last formatted result until the next call or
6//! `sql_dialect_fmt_clear_result`.
7
8use 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/// Allocate a writable byte buffer in Wasm memory.
17///
18/// JavaScript should copy UTF-8 bytes into the returned pointer, call `sql_dialect_fmt_format`, then free
19/// this input buffer with `sql_dialect_fmt_dealloc(ptr, len)`.
20#[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/// Free a buffer previously allocated by `sql_dialect_fmt_alloc`.
29///
30/// # Safety
31///
32/// `ptr` must be a pointer returned by `sql_dialect_fmt_alloc` with the same `capacity`, and it must not
33/// have already been freed. Passing any other pointer or capacity is undefined behavior.
34#[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/// Format the UTF-8 SQL source stored at `ptr..ptr + len`.
43///
44/// Returns:
45/// - `0`: success; read `sql_dialect_fmt_result_ptr()` and `sql_dialect_fmt_result_len()`.
46/// - `1`: invalid UTF-8 input; no result is stored.
47///
48/// # Safety
49///
50/// `ptr` must point to `len` initialized bytes in Wasm memory for the duration of the call.
51#[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/// Format the UTF-8 SQL source stored at `ptr..ptr + len` using an explicit dialect.
63///
64/// `dialect` values:
65/// - `0`: Snowflake
66/// - `1`: Databricks
67///
68/// Unknown values fall back to Snowflake for forwards-compatible callers.
69///
70/// # Safety
71///
72/// `ptr` must point to `len` initialized bytes in Wasm memory for the duration of the call.
73#[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    clear_last_result();
83
84    let bytes = slice::from_raw_parts(ptr as *const u8, len as usize);
85    let Ok(source) = str::from_utf8(bytes) else {
86        return 1;
87    };
88
89    let options = FormatOptions::default()
90        .with_line_width(line_width.max(1) as usize)
91        .with_indent_width(indent_width.clamp(1, 16) as usize)
92        .with_uppercase_keywords(uppercase_keywords != 0)
93        .with_dialect(dialect_from_u32(dialect));
94
95    store_last_result(format(source, &options).into_bytes().into_boxed_slice());
96    0
97}
98
99fn dialect_from_u32(dialect: u32) -> Dialect {
100    match dialect {
101        1 => Dialect::Databricks,
102        _ => Dialect::Snowflake,
103    }
104}
105
106/// Pointer to the most recent formatted result.
107///
108/// # Safety
109///
110/// The returned pointer is valid only until the next `sql_dialect_fmt_format` or `sql_dialect_fmt_clear_result`
111/// call. Callers must pair it with `sql_dialect_fmt_result_len` and copy the bytes before releasing it.
112#[no_mangle]
113pub unsafe extern "C" fn sql_dialect_fmt_result_ptr() -> u32 {
114    last_result_ptr() as u32
115}
116
117/// Byte length of the most recent formatted result.
118///
119/// # Safety
120///
121/// The value describes the buffer returned by `sql_dialect_fmt_result_ptr` and is valid only until the next
122/// `sql_dialect_fmt_format` or `sql_dialect_fmt_clear_result` call.
123#[no_mangle]
124pub unsafe extern "C" fn sql_dialect_fmt_result_len() -> u32 {
125    last_result_len() as u32
126}
127
128/// Release the most recent formatted result, if any.
129///
130/// # Safety
131///
132/// Callers must not read from a previously returned result pointer after this function runs.
133#[no_mangle]
134pub unsafe extern "C" fn sql_dialect_fmt_clear_result() {
135    clear_last_result();
136}
137
138fn clear_last_result() {
139    LAST_RESULT.with(|last_result| {
140        last_result.borrow_mut().take();
141    });
142}
143
144fn store_last_result(result: Box<[u8]>) {
145    LAST_RESULT.with(|last_result| {
146        *last_result.borrow_mut() = Some(result);
147    });
148}
149
150fn last_result_ptr() -> *const u8 {
151    LAST_RESULT.with(|last_result| {
152        last_result
153            .borrow()
154            .as_deref()
155            .map_or(ptr::null(), |result| result.as_ptr())
156    })
157}
158
159fn last_result_len() -> usize {
160    LAST_RESULT.with(|last_result| last_result.borrow().as_deref().map_or(0, <[u8]>::len))
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn last_result_bytes() -> Vec<u8> {
168        let ptr = last_result_ptr();
169        let len = last_result_len();
170
171        if len == 0 {
172            return Vec::new();
173        }
174
175        assert!(!ptr.is_null());
176        unsafe { slice::from_raw_parts(ptr, len).to_vec() }
177    }
178
179    #[test]
180    fn stores_result_bytes() {
181        clear_last_result();
182
183        store_last_result(b"select 1".to_vec().into_boxed_slice());
184
185        assert_eq!(last_result_len(), 8);
186        assert_eq!(last_result_bytes(), b"select 1");
187
188        clear_last_result();
189    }
190
191    #[test]
192    fn replacing_result_exposes_only_new_bytes() {
193        clear_last_result();
194
195        store_last_result(b"old result".to_vec().into_boxed_slice());
196        store_last_result(b"new".to_vec().into_boxed_slice());
197
198        assert_eq!(last_result_len(), 3);
199        assert_eq!(last_result_bytes(), b"new");
200
201        clear_last_result();
202    }
203
204    #[test]
205    fn clear_result_removes_state_and_is_idempotent() {
206        clear_last_result();
207        store_last_result(b"temporary".to_vec().into_boxed_slice());
208
209        clear_last_result();
210        assert!(last_result_ptr().is_null());
211        assert_eq!(last_result_len(), 0);
212        assert_eq!(last_result_bytes(), b"");
213
214        clear_last_result();
215        assert!(last_result_ptr().is_null());
216        assert_eq!(last_result_len(), 0);
217    }
218}