surql_parser/lib.rs
1//! surql-parser — Standalone SurrealQL parser extracted from SurrealDB.
2//!
3//! Provides a complete SurrealQL parser without depending on the SurrealDB engine.
4//! Useful for building tools, linters, formatters, IDE extensions, and migration systems.
5//!
6//! # Quick Start
7//!
8//! ```
9//! let ast = surql_parser::parse("SELECT name, age FROM user WHERE age > 18").unwrap();
10//! assert!(!ast.expressions.is_empty());
11//! ```
12//!
13//! # Sync with SurrealDB
14//!
15//! Parser source is auto-extracted from SurrealDB via `tools/transform/`.
16//! See UPSTREAM_SYNC.md for details.
17
18#[macro_use]
19extern crate tracing;
20
21pub mod compat;
22pub mod config;
23
24#[allow(
25 clippy::useless_conversion,
26 clippy::large_enum_variant,
27 clippy::match_single_binding,
28 clippy::needless_borrow
29)]
30pub mod upstream;
31
32// ─── Public API ───
33
34/// Parse a SurrealQL query string into an AST.
35///
36/// Returns a list of top-level expressions (statements).
37///
38/// # Example
39///
40/// ```
41/// let ast = surql_parser::parse("CREATE user SET name = 'Alice'").unwrap();
42/// assert_eq!(ast.expressions.len(), 1);
43/// ```
44pub fn parse(input: &str) -> anyhow::Result<Ast> {
45 upstream::syn::parse(input)
46}
47
48/// Parse a SurrealQL query with custom parser settings.
49pub fn parse_with_settings(input: &str, settings: ParserSettings) -> anyhow::Result<Ast> {
50 upstream::syn::parse_with_settings(input.as_bytes(), settings, async |parser, stk| {
51 parser.parse_query(stk).await
52 })
53}
54
55/// Parse a SurrealQL type annotation (e.g., `record<user>`, `option<string>`).
56pub fn parse_kind(input: &str) -> anyhow::Result<Kind> {
57 upstream::syn::kind(input)
58}
59
60/// Check if a string could be a reserved keyword in certain contexts.
61pub fn is_reserved_keyword(s: &str) -> bool {
62 upstream::syn::could_be_reserved_keyword(s)
63}
64
65// ─── Schema Extraction ───
66
67/// All definitions found in a SurrealQL file.
68///
69/// Use `extract_definitions()` to get this from a .surql file.
70/// This is the primary tool for migration systems and schema analyzers.
71use upstream::sql::statements::define;
72
73#[derive(Debug, Default)]
74pub struct SchemaDefinitions {
75 pub namespaces: Vec<statements::DefineNamespaceStatement>,
76 pub databases: Vec<define::DefineDatabaseStatement>,
77 pub tables: Vec<statements::DefineTableStatement>,
78 pub fields: Vec<statements::DefineFieldStatement>,
79 pub indexes: Vec<statements::DefineIndexStatement>,
80 pub functions: Vec<statements::DefineFunctionStatement>,
81 pub analyzers: Vec<define::DefineAnalyzerStatement>,
82 pub events: Vec<statements::DefineEventStatement>,
83 pub params: Vec<define::DefineParamStatement>,
84 pub users: Vec<define::DefineUserStatement>,
85 pub accesses: Vec<define::DefineAccessStatement>,
86}
87
88/// Extract all DEFINE statements from a SurrealQL string.
89///
90/// Useful for schema analysis, migration tools, and validation.
91///
92/// # Example
93///
94/// ```
95/// let defs = surql_parser::extract_definitions("
96/// DEFINE TABLE user SCHEMAFULL;
97/// DEFINE FIELD name ON user TYPE string;
98/// DEFINE FIELD age ON user TYPE int DEFAULT 0;
99/// DEFINE INDEX email_idx ON user FIELDS email UNIQUE;
100/// DEFINE FUNCTION fn::greet($name: string) { RETURN 'Hello, ' + $name; };
101/// ").unwrap();
102///
103/// assert_eq!(defs.tables.len(), 1);
104/// assert_eq!(defs.fields.len(), 2);
105/// assert_eq!(defs.indexes.len(), 1);
106/// assert_eq!(defs.functions.len(), 1);
107/// ```
108pub fn extract_definitions(input: &str) -> anyhow::Result<SchemaDefinitions> {
109 let ast = parse(input)?;
110 let mut defs = SchemaDefinitions::default();
111
112 for top in &ast.expressions {
113 if let upstream::sql::ast::TopLevelExpr::Expr(Expr::Define(stmt)) = top {
114 use define::DefineStatement as DS;
115 match stmt.as_ref() {
116 DS::Namespace(s) => defs.namespaces.push(s.clone()),
117 DS::Database(s) => defs.databases.push(s.clone()),
118 DS::Table(s) => defs.tables.push(s.clone()),
119 DS::Field(s) => defs.fields.push(s.clone()),
120 DS::Index(s) => defs.indexes.push(s.clone()),
121 DS::Function(s) => defs.functions.push(s.clone()),
122 DS::Analyzer(s) => defs.analyzers.push(s.clone()),
123 DS::Event(s) => defs.events.push(s.clone()),
124 DS::Param(s) => defs.params.push(s.clone()),
125 DS::User(s) => defs.users.push(s.clone()),
126 DS::Access(s) => defs.accesses.push(s.clone()),
127 _ => {} // Config, Api, Bucket, Sequence, Module, Model — less common
128 }
129 }
130 }
131
132 Ok(defs)
133}
134
135/// List all function names defined in a SurrealQL string.
136///
137/// # Example
138///
139/// ```
140/// let fns = surql_parser::list_functions("
141/// DEFINE FUNCTION fn::greet($name: string) { RETURN 'Hello, ' + $name; };
142/// DEFINE FUNCTION fn::add($a: int, $b: int) { RETURN $a + $b; };
143/// ").unwrap();
144///
145/// assert_eq!(fns, vec!["greet", "add"]);
146/// ```
147pub fn list_functions(input: &str) -> anyhow::Result<Vec<String>> {
148 let defs = extract_definitions(input)?;
149 Ok(defs
150 .functions
151 .iter()
152 .map(|f| {
153 use surrealdb_types::{SqlFormat, ToSql};
154 let mut name = String::new();
155 f.name.fmt_sql(&mut name, SqlFormat::SingleLine);
156 name
157 })
158 .collect())
159}
160
161/// List all table names defined in a SurrealQL string.
162///
163/// # Example
164///
165/// ```
166/// let tables = surql_parser::list_tables("
167/// DEFINE TABLE user SCHEMAFULL;
168/// DEFINE TABLE post SCHEMALESS;
169/// SELECT * FROM user;
170/// ").unwrap();
171///
172/// assert_eq!(tables, vec!["user", "post"]);
173/// ```
174pub fn list_tables(input: &str) -> anyhow::Result<Vec<String>> {
175 let defs = extract_definitions(input)?;
176 Ok(defs
177 .tables
178 .iter()
179 .map(|t| {
180 use surrealdb_types::{SqlFormat, ToSql};
181 let mut name = String::new();
182 t.name.fmt_sql(&mut name, SqlFormat::SingleLine);
183 name
184 })
185 .collect())
186}
187
188/// Format an AST back to SurrealQL string.
189///
190/// # Example
191///
192/// ```
193/// let ast = surql_parser::parse("SELECT * FROM user").unwrap();
194/// let sql = surql_parser::format(&ast);
195/// assert!(sql.contains("SELECT"));
196/// ```
197pub fn format(ast: &Ast) -> String {
198 use surrealdb_types::{SqlFormat, ToSql};
199 let mut buf = String::new();
200 ast.fmt_sql(&mut buf, SqlFormat::SingleLine);
201 buf
202}
203
204// ─── Re-exports ───
205
206/// The parsed AST (list of top-level statements).
207pub use upstream::sql::Ast;
208
209/// A single expression in the AST.
210pub use upstream::sql::expression::Expr;
211
212/// Parser configuration settings.
213pub use upstream::syn::ParserSettings;
214
215/// SurrealQL type annotation (e.g., `string`, `record<user>`, `array<int>`).
216pub use upstream::sql::Kind;
217
218/// An identifier path (e.g., `user.name`, `->knows->person`).
219pub use upstream::sql::Idiom;
220
221/// A SurrealQL statement (SELECT, CREATE, DEFINE, etc.).
222pub use upstream::sql::statements;
223
224/// Syntax error type.
225pub use upstream::syn::error;