Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */
```

Expand Down
7 changes: 7 additions & 0 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions examples/ffi_example.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
14 changes: 12 additions & 2 deletions examples/ffi_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ std::optional<std::string> 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<std::string> 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() {
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 24 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<crate::ast::Statement>(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.
Expand Down
6 changes: 3 additions & 3 deletions src/generator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
17 changes: 15 additions & 2 deletions src/generator/sql_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
Expand Down
26 changes: 26 additions & 0 deletions tests/test_ffi.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
22 changes: 22 additions & 0 deletions tests/test_generate_pretty.rs
Original file line number Diff line number Diff line change
@@ -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]"));
}
Loading