Skip to main content

syntaqlite_cli/
lib.rs

1// Copyright 2025 The syntaqlite Authors. All rights reserved.
2// Licensed under the Apache License, Version 2.0.
3
4#![expect(
5    missing_docs,
6    reason = "bin crate; internal lib shim exists only to support integration tests"
7)]
8
9use clap::{Parser, Subcommand, ValueEnum};
10
11#[cfg(feature = "builtin-sqlite")]
12mod config;
13
14#[cfg(feature = "builtin-sqlite")]
15mod runtime;
16
17#[cfg(feature = "builtin-sqlite")]
18mod codegen;
19
20#[cfg(feature = "mcp")]
21#[expect(
22    clippy::needless_pass_by_value,
23    reason = "rmcp #[tool(aggr)] requires by-value params"
24)]
25mod mcp;
26
27#[derive(Clone, Copy, ValueEnum)]
28pub(crate) enum ParseOutput {
29    /// Print statement/error counts (compact, for benchmarks) [maintainer]
30    Summary,
31    /// Print the AST as human-readable text
32    Text,
33    /// Print the AST as JSON
34    Json,
35}
36
37#[derive(Clone, Copy, Default, ValueEnum)]
38pub(crate) enum FmtOutput {
39    /// Formatted SQL (default)
40    #[default]
41    Formatted,
42    /// Dump raw interpreter bytecode for each statement [maintainer]
43    Bytecode,
44    /// Dump the Wadler-Lindig document tree after interpretation [maintainer]
45    DocTree,
46}
47
48#[derive(Parser)]
49#[command(
50    name = "syntaqlite",
51    about = "SQL formatting and analysis tools",
52    version
53)]
54pub(crate) struct Cli {
55    /// Path to `syntaqlite.toml` config file.
56    /// When omitted, discovered by walking up from the current directory.
57    #[cfg(feature = "builtin-sqlite")]
58    #[arg(short = 'c', long = "config", global = true)]
59    pub(crate) config: Option<String>,
60
61    /// Disable automatic config file discovery.
62    #[cfg(feature = "builtin-sqlite")]
63    #[arg(long = "no-config", global = true, conflicts_with = "config")]
64    pub(crate) no_config: bool,
65
66    /// Path to a shared library (.so/.dylib/.dll) providing a dialect.
67    #[cfg(feature = "builtin-sqlite")]
68    #[arg(long = "dialect")]
69    pub(crate) dialect_path: Option<String>,
70
71    /// Dialect name for symbol lookup.
72    /// When omitted, the loader resolves `syntaqlite_grammar`.
73    /// With a name, it resolves `syntaqlite_<name>_grammar`.
74    #[cfg(feature = "builtin-sqlite")]
75    #[arg(long, requires = "dialect_path")]
76    pub(crate) dialect_name: Option<String>,
77
78    /// `SQLite` version to emulate (e.g. "3.47.0", "latest").
79    #[cfg(feature = "builtin-sqlite")]
80    #[arg(long)]
81    pub(crate) sqlite_version: Option<String>,
82
83    /// Enable a `SQLite` compile-time flag (e.g. `SQLITE_ENABLE_ORDERED_SET_AGGREGATES`).
84    /// Can be specified multiple times.
85    #[cfg(feature = "builtin-sqlite")]
86    #[arg(long)]
87    pub(crate) sqlite_cflag: Vec<String>,
88
89    #[command(subcommand)]
90    pub(crate) command: Command,
91}
92
93#[derive(Subcommand)]
94pub(crate) enum Command {
95    /// Parse SQL and report results
96    #[cfg(feature = "builtin-sqlite")]
97    Parse {
98        /// SQL files or glob patterns (reads stdin if omitted)
99        files: Vec<String>,
100        /// SQL expression to process directly (instead of files or stdin)
101        #[arg(short = 'e', long = "expression", conflicts_with = "files")]
102        expression: Option<String>,
103        /// Output format
104        #[arg(short, long, value_enum, default_value_t = ParseOutput::Text)]
105        output: ParseOutput,
106    },
107    /// Format SQL
108    #[cfg(feature = "builtin-sqlite")]
109    Fmt {
110        /// SQL files or glob patterns (reads stdin if omitted)
111        files: Vec<String>,
112        /// SQL expression to format directly (instead of files or stdin)
113        #[arg(short = 'e', long = "expression", conflicts_with = "files")]
114        expression: Option<String>,
115        /// Maximum line width
116        #[arg(short = 'w', long)]
117        line_width: Option<usize>,
118        /// Spaces per indentation level
119        #[arg(short = 't', long)]
120        indent_width: Option<usize>,
121        /// Keyword casing
122        #[arg(short = 'k', long, value_enum)]
123        keyword_case: Option<runtime::KeywordCasing>,
124        /// Write formatted output back to file(s) in place
125        #[arg(short = 'i', long)]
126        in_place: bool,
127        /// Check if files are formatted (exit 1 if not)
128        #[arg(long, conflicts_with = "in_place")]
129        check: bool,
130        /// Append semicolons after each statement
131        #[arg(long)]
132        semicolons: Option<bool>,
133        /// Output mode (formatted, bytecode, doc-tree)
134        #[arg(short, long, value_enum, default_value_t = FmtOutput::Formatted)]
135        output: FmtOutput,
136    },
137    /// Validate SQL and report diagnostics
138    #[cfg(feature = "builtin-sqlite")]
139    Validate {
140        /// SQL files or glob patterns (reads stdin if omitted)
141        files: Vec<String>,
142        /// SQL expression to validate directly (instead of files or stdin)
143        #[arg(short = 'e', long = "expression", conflicts_with = "files")]
144        expression: Option<String>,
145        /// Schema DDL file(s) to load before validation (repeatable, supports globs)
146        #[arg(long)]
147        schema: Vec<String>,
148        /// Allow (suppress) a check category (repeatable; use "schema" or "all" for groups)
149        #[arg(short = 'A', long = "allow")]
150        allow: Vec<String>,
151        /// Warn on a check category (repeatable)
152        #[arg(short = 'W', long = "warn")]
153        warn: Vec<String>,
154        /// Deny (error) a check category (repeatable)
155        #[arg(short = 'D', long = "deny")]
156        deny: Vec<String>,
157        /// [experimental] Host language for embedded SQL extraction (python, typescript)
158        #[arg(long = "experimental-lang")]
159        lang: Option<runtime::HostLanguage>,
160    },
161    /// Start the language server (stdio)
162    #[cfg(feature = "builtin-sqlite")]
163    Lsp,
164    /// Start the MCP server (stdio)
165    #[cfg(feature = "mcp")]
166    Mcp,
167    /// Generate dialect C sources and Rust bindings for external dialects.
168    #[cfg(feature = "builtin-sqlite")]
169    Dialect(codegen::DialectArgs),
170    /// Print version information
171    Version,
172    #[cfg(feature = "builtin-sqlite")]
173    #[command(flatten)]
174    DialectTool(codegen::ToolCommand),
175}
176
177/// Run the CLI.
178#[cfg(feature = "builtin-sqlite")]
179pub fn run(name: &str, dialect: Option<syntaqlite::any::AnyDialect>) {
180    let cli =
181        Cli::try_parse_from(std::iter::once(name.to_string()).chain(std::env::args().skip(1)))
182            .unwrap_or_else(|e| e.exit());
183
184    let result = runtime::dispatch(cli, dialect);
185
186    if let Err(e) = result {
187        eprintln!("error: {e}");
188        std::process::exit(1);
189    }
190}