Skip to main content

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;