From 252b0e43537108d47e7d21ba0002f4b1092f2160 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 13 Jul 2026 11:42:41 -0700 Subject: [PATCH] feat(ffi): expose sqlglot_generate_pretty with dialect-aware formatting - fix generate_pretty to honor the target dialect (previously ignored) - add Generator::generate_pretty method; deprecate Generator::pretty - add sqlglot_generate_pretty FFI export; update C/C++ examples and docs - bump 0.10.17 --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 3 ++- docs/developer-guide.md | 7 +++++++ docs/installation.md | 2 +- docs/reference.md | 3 ++- examples/ffi_example.c | 7 +++++++ examples/ffi_example.cpp | 14 ++++++++++++-- src/ffi.rs | 24 ++++++++++++++++++++++++ src/generator/mod.rs | 6 +++--- src/generator/sql_generator.rs | 17 +++++++++++++++-- tests/test_ffi.rs | 26 ++++++++++++++++++++++++++ tests/test_generate_pretty.rs | 22 ++++++++++++++++++++++ 13 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 tests/test_ffi.rs create mode 100644 tests/test_generate_pretty.rs diff --git a/Cargo.lock b/Cargo.lock index fd169ea..06ae962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -713,7 +713,7 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sqlglot-rust" -version = "0.10.16" +version = "0.10.17" dependencies = [ "assert_cmd", "clap", diff --git a/Cargo.toml b/Cargo.toml index 18f89ba..cc909d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlglot-rust" -version = "0.10.16" +version = "0.10.17" edition = "2024" description = "A SQL parser, optimizer, and transpiler library inspired by Python's sqlglot" license = "MIT" diff --git a/README.md b/README.md index 42b083d..b3b1096 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Add to your `Cargo.toml`: ```toml [dependencies] -sqlglot-rust = "0.10.14" +sqlglot-rust = "0.10.17" ``` ### Parse and generate SQL @@ -358,6 +358,7 @@ const char *sqlglot_version(void); char *sqlglot_parse(const char *sql, const char *dialect); char *sqlglot_transpile(const char *sql, const char *from_dialect, const char *to_dialect); char *sqlglot_generate(const char *ast_json, const char *dialect); +char *sqlglot_generate_pretty(const char *ast_json, const char *dialect); void sqlglot_free(char *ptr); /* must be called on every non-NULL return */ ``` diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 5b58234..dded8d7 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -1830,6 +1830,13 @@ int main(void) { char *json = sqlglot_parse("SELECT a FROM t", "ansi"); if (json) { printf("AST: %s\n", json); + + char *pretty = sqlglot_generate_pretty(json, "ansi"); + if (pretty) { + printf("formatted: %s\n", pretty); + sqlglot_free(pretty); + } + sqlglot_free(json); } diff --git a/docs/installation.md b/docs/installation.md index ff936bd..748c232 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -25,7 +25,7 @@ Add `sqlglot-rust` to your project's `Cargo.toml`: ```toml [dependencies] -sqlglot-rust = "0.10.14" +sqlglot-rust = "0.10.17" ``` Then run: diff --git a/docs/reference.md b/docs/reference.md index 908b32a..aac077d 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -2216,12 +2216,13 @@ The C header is generated by `cbindgen` and placed at `target/ffi/include/sqlglo | `sqlglot_parse` | `char *sqlglot_parse(const char *sql, const char *dialect)` | Parse SQL into a JSON-serialised AST. Returns `NULL` on error. | | `sqlglot_transpile` | `char *sqlglot_transpile(const char *sql, const char *from_dialect, const char *to_dialect)` | Transpile a single SQL statement between dialects. Returns `NULL` on error. | | `sqlglot_generate` | `char *sqlglot_generate(const char *ast_json, const char *dialect)` | Generate SQL from a JSON-serialised `Statement`. Returns `NULL` on error. | +| `sqlglot_generate_pretty` | `char *sqlglot_generate_pretty(const char *ast_json, const char *dialect)` | Generate formatted SQL with indentation and newlines from a JSON-serialised `Statement`. Returns `NULL` on error. | | `sqlglot_version` | `const char *sqlglot_version(void)` | Return the library version as a static string. **Do not free.** | | `sqlglot_free` | `void sqlglot_free(char *ptr)` | Free a string returned by any `sqlglot_*` function. `NULL`-safe. | ### Memory Management -- Every non-`NULL` `char *` returned by `sqlglot_parse`, `sqlglot_transpile`, or `sqlglot_generate` **must** be freed by calling `sqlglot_free`. +- Every non-`NULL` `char *` returned by `sqlglot_parse`, `sqlglot_transpile`, `sqlglot_generate`, or `sqlglot_generate_pretty` **must** be freed by calling `sqlglot_free`. - `sqlglot_version` returns a pointer to static memory — **do not** free it. - Passing `NULL` to `sqlglot_free` is a safe no-op. diff --git a/examples/ffi_example.c b/examples/ffi_example.c index 09d08b2..3ceac02 100644 --- a/examples/ffi_example.c +++ b/examples/ffi_example.c @@ -52,6 +52,13 @@ int main(void) { printf("Regenerated (Postgres): %s\n", regenerated); sqlglot_free(regenerated); } + + char *pretty = sqlglot_generate_pretty(json, "postgres"); + if (pretty) { + printf("Pretty (Postgres):\n%s\n", pretty); + sqlglot_free(pretty); + } + sqlglot_free(json); } diff --git a/examples/ffi_example.cpp b/examples/ffi_example.cpp index 8c79e96..e172c17 100644 --- a/examples/ffi_example.cpp +++ b/examples/ffi_example.cpp @@ -59,6 +59,13 @@ std::optional generate(const char *ast_json, const char *dialect = return std::string(result.get()); } +/// Generate formatted SQL from a JSON AST, or std::nullopt on failure. +std::optional generate_pretty(const char *ast_json, const char *dialect = nullptr) { + SqlglotString result(sqlglot_generate_pretty(ast_json, dialect)); + if (!result) return std::nullopt; + return std::string(result.get()); +} + // ── Main ──────────────────────────────────────────────────────────────── int main() { @@ -90,10 +97,13 @@ int main() { auto json = parse(input, "ansi"); if (json) { auto sql = generate(json->c_str(), "snowflake"); + auto pretty = generate_pretty(json->c_str(), "snowflake"); std::printf("Round-trip through JSON AST:\n" " Original: %s\n" - " Generated: %s\n", - input, sql ? sql->c_str() : "(error)"); + " Generated: %s\n" + " Pretty:\n%s\n", + input, sql ? sql->c_str() : "(error)", + pretty ? pretty->c_str() : "(error)"); } return 0; diff --git a/src/ffi.rs b/src/ffi.rs index 933cbbe..c87c294 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -115,6 +115,30 @@ pub unsafe extern "C" fn sqlglot_generate( } } +/// Generate pretty-printed SQL from a JSON-serialised AST for the given dialect. +/// +/// * `ast_json` – null-terminated JSON string of a serialised `Statement`. +/// * `dialect` – target dialect name, or `NULL` for ANSI. +/// +/// Returns a heap-allocated SQL string on success, or `NULL` on failure. +/// The caller **must** free a non-null return value with [`sqlglot_free`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn sqlglot_generate_pretty( + ast_json: *const c_char, + dialect: *const c_char, +) -> *mut c_char { + let json_str = match unsafe { cstr_to_option(ast_json) } { + Some(s) => s, + None => return ptr::null_mut(), + }; + let dialect_enum = resolve_dialect(unsafe { cstr_to_option(dialect) }); + + match serde_json::from_str::(json_str) { + Ok(ast) => to_c_string(crate::generate_pretty(&ast, dialect_enum)), + Err(_) => ptr::null_mut(), + } +} + /// Return the library version as a static null-terminated string. /// /// The returned pointer **must not** be freed — it points to static memory. diff --git a/src/generator/mod.rs b/src/generator/mod.rs index fbaaf7e..b0cd433 100644 --- a/src/generator/mod.rs +++ b/src/generator/mod.rs @@ -16,7 +16,7 @@ pub fn generate(statement: &Statement, dialect: Dialect) -> String { /// /// Produces formatted SQL with newlines and indentation for readability. #[must_use] -pub fn generate_pretty(statement: &Statement, _dialect: Dialect) -> String { - let mut generator = Generator::pretty(); - generator.generate(statement) +pub fn generate_pretty(statement: &Statement, dialect: Dialect) -> String { + let mut generator = Generator::with_dialect(dialect); + generator.generate_pretty(statement) } diff --git a/src/generator/sql_generator.rs b/src/generator/sql_generator.rs index caf4221..d855eb0 100644 --- a/src/generator/sql_generator.rs +++ b/src/generator/sql_generator.rs @@ -27,6 +27,10 @@ impl Generator { } /// Create a generator that produces formatted SQL. + #[deprecated( + since = "0.10.17", + note = "use `with_dialect(dialect).generate_pretty(stmt)` to format for a target dialect" + )] #[must_use] pub fn pretty() -> Self { Self { @@ -56,6 +60,15 @@ impl Generator { self.output.clone() } + /// Generate formatted SQL (newlines and indentation) from a statement. + #[must_use] + pub fn generate_pretty(&mut self, statement: &Statement) -> String { + self.pretty = true; + self.output.clear(); + self.gen_statement(statement); + self.output.clone() + } + /// Generate SQL for an expression (static helper for `Expr::sql()`). #[must_use] pub fn expr_to_sql(expr: &Expr) -> String { @@ -3790,8 +3803,8 @@ mod tests { fn pretty_print(sql: &str) -> String { let stmt = Parser::new(sql).unwrap().parse_statement().unwrap(); - let mut g = Generator::pretty(); - g.generate(&stmt) + let mut g = Generator::with_dialect(Dialect::Ansi); + g.generate_pretty(&stmt) } #[test] diff --git a/tests/test_ffi.rs b/tests/test_ffi.rs new file mode 100644 index 0000000..48e62a1 --- /dev/null +++ b/tests/test_ffi.rs @@ -0,0 +1,26 @@ +use std::ffi::{CStr, CString}; + +use sqlglot_rust::ffi::{sqlglot_free, sqlglot_generate_pretty, sqlglot_parse}; + +#[test] +fn pretty_generation_is_available_through_the_ffi() { + let sql = CString::new("SELECT `select`, id FROM `events` WHERE active = true").unwrap(); + let mysql = CString::new("mysql").unwrap(); + let tsql = CString::new("tsql").unwrap(); + + let ast_json = unsafe { sqlglot_parse(sql.as_ptr(), mysql.as_ptr()) }; + assert!(!ast_json.is_null()); + + let generated = unsafe { sqlglot_generate_pretty(ast_json, tsql.as_ptr()) }; + assert!(!generated.is_null()); + + let output = unsafe { CStr::from_ptr(generated) }.to_str().unwrap(); + assert!(output.contains("SELECT\n")); + assert!(output.contains("[select]")); + assert!(output.contains("[events]")); + + unsafe { + sqlglot_free(generated); + sqlglot_free(ast_json); + } +} diff --git a/tests/test_generate_pretty.rs b/tests/test_generate_pretty.rs new file mode 100644 index 0000000..19487f0 --- /dev/null +++ b/tests/test_generate_pretty.rs @@ -0,0 +1,22 @@ +use sqlglot_rust::{Dialect, generate_pretty, parse}; + +#[test] +fn pretty_generation_formats_sql() { + let ast = parse("SELECT a, b FROM events WHERE active = true", Dialect::Ansi).unwrap(); + + let generated = generate_pretty(&ast, Dialect::Ansi); + + assert!(generated.contains("SELECT\n")); + assert!(generated.contains("\nFROM\n")); + assert!(generated.contains("\nWHERE\n")); +} + +#[test] +fn pretty_generation_preserves_the_target_dialect() { + let ast = parse("SELECT `select` FROM `events`", Dialect::Mysql).unwrap(); + + let generated = generate_pretty(&ast, Dialect::Tsql); + + assert!(generated.contains("[select]")); + assert!(generated.contains("[events]")); +}