Using syntaqlite from C

syntaqlite provides a C API for embedding in non-Rust projects. There are two distribution options with different scope:

Source amalgamationPrebuilt shared library
Packagesyntaqlite-syntax-amalgamationsyntaqlite-clib
What you getTwo C files to compile yourselfShared library + header per platform
Parser & tokenizerYesYes
FormatterNoYes
ValidatorNoYes
DependenciesNone (just a C compiler)Link against the shared library

Option 1: Source amalgamation (parser and tokenizer only)

The source amalgamation contains the parser and tokenizer as two compilable C files. It does not include the formatter or validator; those require the Rust runtime and are only available via the prebuilt shared library (option 2).

Download from the latest release:

curl -LO https://github.com/LalitMaganti/syntaqlite/releases/latest/download/syntaqlite-syntax-amalgamation.tar.gz
tar xf syntaqlite-syntax-amalgamation.tar.gz

You get syntaqlite_syntax.h and syntaqlite_syntax.c. Add them to your project and compile:

cc -c -O2 syntaqlite_syntax.c -o syntaqlite_syntax.o
cc -o my_program my_program.c syntaqlite_syntax.o

See the C parser tutorial for a complete walkthrough.

Option 2: Prebuilt shared library (full API)

The prebuilt shared library includes the full API: parser, tokenizer, formatter, and validator. Download syntaqlite-clib from the latest release. The archive contains a single syntaqlite.h header and shared libraries for each platform:

curl -LO https://github.com/LalitMaganti/syntaqlite/releases/latest/download/syntaqlite-clib.tar.gz
tar xf syntaqlite-clib.tar.gz
syntaqlite.h
macos-arm64/libsyntaqlite.dylib
macos-x64/libsyntaqlite.dylib
linux-x64/libsyntaqlite.so
linux-arm64/libsyntaqlite.so
windows-x64/syntaqlite.dll
windows-arm64/syntaqlite.dll

Compile and link against the library for your platform:

# Linux x64
cc -o my_program my_program.c -I. -Llinux-x64 -lsyntaqlite -Wl,-rpath,'$ORIGIN/linux-x64'

# macOS arm64
cc -o my_program my_program.c -I. -Lmacos-arm64 -lsyntaqlite -Wl,-rpath,@executable_path/macos-arm64

Format a query

#include "syntaqlite.h"
#include <string.h>
#include <stdio.h>

int main(void) {
    SyntaqliteFormatter* f = syntaqlite_formatter_create_sqlite();

    const char* sql = "select a,b from t where x=1";
    if (syntaqlite_formatter_format(f, sql, strlen(sql)) == 0) {
        printf("%.*s\n",
            syntaqlite_formatter_output_len(f),
            syntaqlite_formatter_output(f));
    }

    syntaqlite_formatter_destroy(f);
    return 0;
}

The formatter handle is reusable: create once, call format() repeatedly. Output is borrowed and valid until the next call to format or destroy.

Format with custom config

SyntaqliteFormatConfig config = {
    .line_width   = 120,
    .indent_width = 4,
    .keyword_case = SYNTAQLITE_KEYWORD_LOWER,
    .semicolons   = 1,
};
SyntaqliteFormatter* f =
    syntaqlite_formatter_create_sqlite_with_config(&config);

const char* sql = "select a,b from t where x=1";
if (syntaqlite_formatter_format(f, sql, strlen(sql)) == 0) {
    printf("%.*s\n",
        syntaqlite_formatter_output_len(f),
        syntaqlite_formatter_output(f));
}

syntaqlite_formatter_destroy(f);

Validate a query

#include "syntaqlite.h"
#include <string.h>
#include <stdio.h>

int main(void) {
    SyntaqliteValidator* v = syntaqlite_validator_create_sqlite();

    // Register your schema
    SyntaqliteRelationDef tables[] = {
        {"users",  (const char*[]){"id", "name", "email"}, 3},
        {"posts",  (const char*[]){"id", "user_id", "body"}, 3},
    };
    syntaqlite_validator_add_tables(v, tables, 2);

    // Analyze
    const char* sql = "SELECT nme FROM users";
    syntaqlite_validator_analyze(v, sql, strlen(sql));

    // Print human-readable diagnostics
    const char* rendered =
        syntaqlite_validator_render_diagnostics(v, "query.sql");
    if (rendered[0] != '\0') {
        printf("%s\n", rendered);
    }

    syntaqlite_validator_destroy(v);
    return 0;
}

Column lineage

After analyze(), you can inspect which tables and columns each result column traces back to:

#include "syntaqlite.h"
#include <string.h>
#include <stdio.h>

int main(void) {
    SyntaqliteValidator* v = syntaqlite_validator_create_sqlite();

    // Register schema
    SyntaqliteRelationDef tables[] = {
        {"users", (const char*[]){"id", "name", "email"}, 3},
        {"posts", (const char*[]){"id", "user_id", "body"},  3},
    };
    syntaqlite_validator_add_tables(v, tables, 2);

    // Analyze a query
    const char* sql =
        "SELECT u.name, p.body "
        "FROM users u JOIN posts p ON u.id = p.user_id";
    syntaqlite_validator_analyze(v, sql, strlen(sql));

    // Column lineage
    printf("Lineage complete: %s\n",
        syntaqlite_validator_lineage_complete(v) ? "yes" : "no");

    uint32_t col_count = syntaqlite_validator_column_lineage_count(v);
    const SyntaqliteColumnLineage* cols =
        syntaqlite_validator_column_lineage(v);
    for (uint32_t i = 0; i < col_count; i++) {
        printf("  column %u: %s", cols[i].index, cols[i].name);
        if (cols[i].origin.table) {
            printf(" <- %s.%s",
                cols[i].origin.table, cols[i].origin.column);
        }
        printf("\n");
    }

    // Physical tables accessed
    uint32_t tbl_count = syntaqlite_validator_table_count(v);
    const SyntaqliteTableAccess* tbls = syntaqlite_validator_tables(v);
    for (uint32_t i = 0; i < tbl_count; i++) {
        printf("  table: %s\n", tbls[i].name);
    }

    syntaqlite_validator_destroy(v);
    return 0;
}

Output:

Lineage complete: yes
  column 0: name <- users.name
  column 1: body <- posts.body
  table: posts
  table: users

Lineage is "complete" when all sources resolve to physical tables. It becomes "partial" when a view is referenced but its body is unavailable. For non-SELECT statements, all count functions return 0 and pointer functions return NULL.

Next steps