mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-15 01:55:13 +02:00
Allow mutation queries to contain multiple sequential statements that
execute atomically within a single transactional run. This enables
patterns like inserting a node and its edges in one query:
query add_and_link($name: String, $age: I32, $friend: String) {
insert Person { name: $name, age: $age }
insert Knows { from: $name, to: $friend }
}
Changes span the full compiler-to-execution pipeline:
- Grammar: mutation_body = { mutation_stmt+ }
- AST: QueryDecl.mutations: Vec<Mutation>
- IR: MutationIR.ops: Vec<MutationOpIR>
- Execution: loop over ops, accumulate affected counts
Cross-statement visibility works because each statement's commit_updates
advances the manifest state, so subsequent statements see prior writes.
Atomicity comes from the existing run mechanism (begin_run/publish_run).
https://claude.ai/code/session_01E4VG2WXrZW8aeXFiqr8NwF
115 lines
4.4 KiB
Text
115 lines
4.4 KiB
Text
// NanoGraph Query Grammar (.gq files)
|
|
|
|
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
|
|
COMMENT = _{ LINE_COMMENT | BLOCK_COMMENT }
|
|
LINE_COMMENT = _{ "//" ~ (!"\n" ~ ANY)* }
|
|
BLOCK_COMMENT = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" }
|
|
|
|
query_file = { SOI ~ query_decl* ~ EOI }
|
|
|
|
query_decl = {
|
|
"query" ~ ident ~ "(" ~ param_list? ~ ")" ~ query_annotation* ~ "{"
|
|
~ query_body
|
|
~ "}"
|
|
}
|
|
query_annotation = { description_annotation | instruction_annotation }
|
|
description_annotation = { "@description" ~ "(" ~ string_lit ~ ")" }
|
|
instruction_annotation = { "@instruction" ~ "(" ~ string_lit ~ ")" }
|
|
|
|
query_body = { read_query_body | mutation_body }
|
|
mutation_body = { mutation_stmt+ }
|
|
read_query_body = {
|
|
match_clause
|
|
~ return_clause
|
|
~ order_clause?
|
|
~ limit_clause?
|
|
}
|
|
|
|
mutation_stmt = { insert_stmt | update_stmt | delete_stmt }
|
|
insert_stmt = { "insert" ~ type_name ~ "{" ~ mutation_assignment+ ~ "}" }
|
|
update_stmt = { "update" ~ type_name ~ "set" ~ "{" ~ mutation_assignment+ ~ "}" ~ "where" ~ mutation_predicate }
|
|
delete_stmt = { "delete" ~ type_name ~ "where" ~ mutation_predicate }
|
|
mutation_assignment = { ident ~ ":" ~ match_value ~ ","? }
|
|
mutation_predicate = { ident ~ comp_op ~ match_value }
|
|
|
|
param_list = { param ~ ("," ~ param)* }
|
|
param = { variable ~ ":" ~ type_ref }
|
|
|
|
type_ref = { (list_type | base_type | vector_type) ~ "?"? }
|
|
list_type = { "[" ~ base_type ~ "]" }
|
|
vector_type = { "Vector" ~ "(" ~ integer ~ ")" }
|
|
base_type = { "String" | "Blob" | "Bool" | "I32" | "I64" | "U32" | "U64" | "F32" | "F64" | "DateTime" | "Date" }
|
|
|
|
match_clause = { "match" ~ "{" ~ clause+ ~ "}" }
|
|
|
|
clause = { negation | binding | traversal | filter | text_search_clause }
|
|
text_search_clause = { search_call | fuzzy_call | match_text_call }
|
|
|
|
// Binding: $p: Person { name: "Alice" }
|
|
binding = { variable ~ ":" ~ type_name ~ ("{" ~ prop_match_list ~ "}")? }
|
|
|
|
prop_match_list = { prop_match ~ ("," ~ prop_match)* ~ ","? }
|
|
prop_match = { ident ~ ":" ~ match_value }
|
|
match_value = { literal | variable | now_call }
|
|
|
|
// Traversal: $p knows $f
|
|
traversal = { variable ~ edge_ident ~ traversal_bounds? ~ variable }
|
|
traversal_bounds = { "{" ~ integer ~ "," ~ integer? ~ "}" }
|
|
|
|
// Filter: $f.age > 25
|
|
filter = { expr ~ filter_op ~ expr }
|
|
|
|
// Negation: not { ... }
|
|
negation = { "not" ~ "{" ~ clause+ ~ "}" }
|
|
|
|
// Return clause — projections separated by commas or newlines
|
|
return_clause = { "return" ~ "{" ~ projection+ ~ "}" }
|
|
projection = { expr ~ ("as" ~ ident)? ~ ","? }
|
|
|
|
// Order clause
|
|
order_clause = { "order" ~ "{" ~ ordering ~ ("," ~ ordering)* ~ "}" }
|
|
ordering = { nearest_ordering | (expr ~ order_dir?) }
|
|
nearest_ordering = { "nearest" ~ "(" ~ prop_access ~ "," ~ expr ~ ")" }
|
|
order_dir = { "asc" | "desc" }
|
|
|
|
// Limit clause
|
|
limit_clause = { "limit" ~ integer }
|
|
|
|
// Expressions
|
|
expr = { now_call | nearest_ordering | search_call | fuzzy_call | match_text_call | bm25_call | rrf_call | agg_call | prop_access | variable | literal | ident }
|
|
now_call = { "now" ~ "(" ~ ")" }
|
|
search_call = { "search" ~ "(" ~ expr ~ "," ~ expr ~ ")" }
|
|
fuzzy_call = { "fuzzy" ~ "(" ~ expr ~ "," ~ expr ~ ("," ~ expr)? ~ ")" }
|
|
match_text_call = { "match_text" ~ "(" ~ expr ~ "," ~ expr ~ ")" }
|
|
bm25_call = { "bm25" ~ "(" ~ expr ~ "," ~ expr ~ ")" }
|
|
rank_expr = { nearest_ordering | bm25_call }
|
|
rrf_call = { "rrf" ~ "(" ~ rank_expr ~ "," ~ rank_expr ~ ("," ~ expr)? ~ ")" }
|
|
|
|
prop_access = { variable ~ "." ~ ident }
|
|
|
|
agg_call = { agg_func ~ "(" ~ expr ~ ")" }
|
|
agg_func = { "count" | "sum" | "avg" | "min" | "max" }
|
|
|
|
comp_op = { ">=" | "<=" | "!=" | ">" | "<" | "=" }
|
|
filter_op = { "contains" | comp_op }
|
|
|
|
// Terminals
|
|
variable = @{ "$" ~ (ident_chars | "_") }
|
|
ident_chars = @{ (ASCII_ALPHA_LOWER | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
|
|
|
// Edge identifier — lowercase start, same as ident but used in traversal context
|
|
// Must not match keywords
|
|
edge_ident = @{ !("not" ~ !ASCII_ALPHANUMERIC) ~ (ASCII_ALPHA_LOWER | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
|
|
|
type_name = @{ ASCII_ALPHA_UPPER ~ (ASCII_ALPHANUMERIC | "_")* }
|
|
ident = @{ (ASCII_ALPHA_LOWER | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
|
|
|
|
literal = { list_lit | datetime_lit | date_lit | string_lit | float_lit | integer | bool_lit }
|
|
date_lit = { "date" ~ "(" ~ string_lit ~ ")" }
|
|
datetime_lit = { "datetime" ~ "(" ~ string_lit ~ ")" }
|
|
list_lit = { "[" ~ (literal ~ ("," ~ literal)*)? ~ "]" }
|
|
string_lit = @{ "\"" ~ string_char* ~ "\"" }
|
|
string_char = @{ !("\"" | "\\") ~ ANY | "\\" ~ ANY }
|
|
float_lit = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
|
|
integer = @{ ASCII_DIGIT+ }
|
|
bool_lit = { "true" | "false" }
|