Parser Implementation
The PSL parser transforms Prisma schema text into a validated Abstract Syntax Tree (AST). This page covers the schema-ast crate and the parsing pipeline.
Schema AST Crate
The schema-ast crate provides the foundation for all PSL operations:
[ dependencies ]
diagnostics.workspace = true
pest.workspace = true
pest_derive.workspace = true
serde.workspace = true
Core Components
Parser
Built with the Pest parser generator:
#[derive(pest_derive :: Parser )]
#[grammar = "parser/datamodel.pest" ]
pub ( crate ) struct PrismaDatamodelParser ;
The parser is defined using PEG (Parsing Expression Grammar) in datamodel.pest.
Source Files
Wraps schema content with file information:
pub struct SourceFile {
content : String ,
// Internal tracking
}
impl From < String > for SourceFile {
fn from ( s : String ) -> Self {
SourceFile :: new ( s )
}
}
SourceFile can represent content from files, strings, or stdin. It’s designed to be lightweight and cloneable.
Main API
The primary parsing function:
use schema_ast :: parse_schema;
let ast = parse_schema (
schema_string ,
& mut diagnostics ,
);
Key characteristics:
Never fails - Always returns an AST, even with errors
Accumulates diagnostics - Errors added to the diagnostics collection
Source spans - Every AST node tracks its location
AST Structure
The AST faithfully represents Prisma Schema syntax with complete source span information.
Top-Level Items
pub struct SchemaAst {
/// All top-level items (models, enums, datasources, etc.)
pub tops : Vec < Top >,
}
pub enum Top {
Model ( Model ),
View ( View ),
CompositeType ( CompositeType ),
Enum ( Enum ),
Source ( SourceConfig ),
Generator ( GeneratorConfig ),
}
Models
pub struct Model {
pub name : Identifier ,
pub fields : Vec < Field >,
pub attributes : Vec < Attribute >,
pub span : Span ,
// ... other fields
}
Fields
pub struct Field {
pub name : Identifier ,
pub field_type : FieldType ,
pub arity : FieldArity ,
pub attributes : Vec < Attribute >,
pub span : Span ,
// ... other fields
}
pub enum FieldArity {
Required ,
Optional ,
List ,
}
Attributes
pub struct Attribute {
pub name : Identifier ,
pub arguments : ArgumentsList ,
pub span : Span ,
}
pub struct ArgumentsList {
pub arguments : Vec < Argument >,
// Can be empty, unnamed, or named
}
Expressions
Schema expressions represent values:
pub enum Expression {
StringValue ( String , Span ),
NumericValue ( String , Span ),
BooleanValue ( bool , Span ),
ConstantValue ( String , Span ),
Function ( String , Vec < Expression >, Span ),
Array ( Vec < Expression >, Span ),
}
Parsing Modules
The parser is organized into focused modules:
parse_schema - Top-level schema parsing
parse_model - Model blocks
parse_view - View blocks (read-only models)
parse_field - Field definitions
parse_enum - Enum types
parse_composite_type - Composite/embedded types
parse_attribute - Attributes (@ and @@)
parse_arguments - Attribute arguments
parse_expression - Value expressions
parse_types - Type references
parse_source_and_generator - Configuration blocks
parse_comments - Comment preservation
Each parsing module follows a consistent pattern: convert Pest pairs into AST nodes while tracking spans and reporting errors.
Error Recovery
The parser is designed for robust error recovery:
Continue on errors - Parse as much as possible
Collect diagnostics - Don’t stop at first error
Meaningful spans - Precise error locations
IDE-friendly - Supports incremental parsing
Example Error Handling
let schema = r#"
model User {
id Int @id
email String @unique
// Missing field type
name @default("")
}
"# ;
let mut diagnostics = Diagnostics :: new ();
let ast = parse_schema ( schema , & mut diagnostics );
// AST is returned with partial information
// diagnostics contains error about missing type
assert! ( diagnostics . has_errors ());
Multi-File Support
PSL supports schemas split across multiple files:
let files = vec! [
( "schema.prisma" . to_string (), SourceFile :: from ( content1 )),
( "models/user.prisma" . to_string (), SourceFile :: from ( content2 )),
];
let validated = parse_schema_multi_without_extensions ( & files ) ? ;
Each file is parsed independently, then merged during validation.
The reformat module provides code formatting:
use schema_ast :: reformat;
let formatted = reformat ( & schema_string , 2 ); // 2-space indent
Consistent indentation - Configurable tab size
Preserves comments - Maintains comment placement
Idempotent - Running twice produces same result
Block alignment - Aligns field types and attributes
Reformatting tests are declarative:
tests/reformatter/
model.prisma # Input
model.reformatted.prisma # Expected output
# Run reformat tests
cargo test -p psl --test reformat_tests
# Update expectations
UPDATE_EXPECT = 1 cargo test -p psl --test reformat_tests
String Literals
PSL string literals follow JSON string syntax:
use schema_ast :: string_literal;
let input = "hello \n world" ;
let literal = string_literal ( input ) . to_string ();
// Returns: "\"hello\\nworld\""
Escaping rules:
\t - Tab
\n - Newline
\r - Carriage return
\\ - Backslash
\" - Quote
\uXXXX - Unicode escape
Renderer
The renderer converts AST back to schema text:
use schema_ast :: renderer;
let schema_text = renderer :: render ( & ast );
Used by:
The formatter
Code generation
Schema migrations
The renderer preserves semantic meaning but may not preserve exact formatting. Use reformat() for user-facing formatting.
Span Tracking
Every AST node includes span information:
pub struct Span {
pub file : FileId ,
pub start : usize ,
pub end : usize ,
}
impl Span {
pub fn contains ( & self , offset : usize ) -> bool {
self . start <= offset && offset < self . end
}
}
Spans enable:
Precise error messages with line/column
IDE features like go-to-definition
Refactoring tools with exact locations
Syntax highlighting in editors
The parser is optimized for IDE usage:
Fast incremental parsing
Minimal allocations
Efficient string interning in later phases
Streaming diagnostics
For large schemas, parsing typically takes less than 10ms. Validation is the more expensive phase.
Integration with Parser Database
The AST feeds into the parser-database for semantic analysis:
use schema_ast :: parse_schema;
use parser_database :: ParserDatabase ;
let mut diagnostics = Diagnostics :: new ();
let ast = parse_schema ( schema , & mut diagnostics );
let db = ParserDatabase :: new (
ast ,
& mut diagnostics ,
extension_types ,
);
See Validation for details on the semantic analysis phase.
Testing
Parser tests use standard Rust tests:
#[test]
fn parse_model_with_fields () {
let schema = r#"
model User {
id Int @id
email String @unique
}
"# ;
let mut diag = Diagnostics :: new ();
let ast = parse_schema ( schema , & mut diag );
assert! ( ! diag . has_errors ());
assert_eq! ( ast . tops . len (), 1 );
}
Running Parser Tests
# All parser tests
cargo test -p schema-ast
# Specific test
cargo test -p schema-ast parse_model_with_fields
# With output
cargo test -p schema-ast -- --nocapture
Common Patterns
Walking the AST
for top in & ast . tops {
match top {
Top :: Model ( model ) => {
println! ( "Model: {}" , model . name . name);
for field in & model . fields {
println! ( " Field: {}" , field . name . name);
}
}
Top :: Enum ( enum_ ) => {
println! ( "Enum: {}" , enum_ . name . name);
}
_ => {}
}
}
Finding Attributes
fn find_attribute <' a >(
attrs : & ' a [ Attribute ],
name : & str ,
) -> Option < & ' a Attribute > {
attrs . iter () . find ( | a | a . name . name == name )
}
if let Some ( id_attr ) = find_attribute ( & field . attributes, "id" ) {
// Handle @id attribute
}
Next Steps
Validation Learn about semantic validation with parser-database
Connectors Explore database-specific validations