Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coalescing assignment operators #7101

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion edb/edgeql-parser/edgeql-parser-python/src/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ pub fn normalize(text: &str) -> Result<Entry, Error> {
fn is_operator(token: &Token) -> bool {
use edgeql_parser::tokenizer::Kind::*;
match token.kind {
Assign | SubAssign | AddAssign | Arrow | Coalesce | Namespace | DoubleSplat
Assign | SubAssign | AddAssign | AssignCoalesce | Arrow | Coalesce | Namespace | DoubleSplat
| BackwardLink | FloorDiv | Concat | GreaterEq | LessEq | NotEq | NotDistinctFrom
| DistinctFrom | Comma | OpenParen | CloseParen | OpenBracket | CloseBracket
| OpenBrace | CloseBrace | Dot | Semicolon | Colon | Add | Sub | Mul | Div | Modulo
Expand Down
1 change: 1 addition & 0 deletions edb/edgeql-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1810,6 +1810,7 @@ pub enum ShapeOp {
APPEND,
SUBTRACT,
ASSIGN,
ASSIGN_COALESCE,
MATERIALIZE,
}

Expand Down
1 change: 1 addition & 0 deletions edb/edgeql-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,7 @@ fn get_token_kind(token_name: &str) -> Kind {
"->" => Arrow,
":=" => Assign,
"-=" => SubAssign,
":=?" => AssignCoalesce,

"PARAMETER" => Parameter,
"PARAMETERANDTYPE" => ParameterAndType,
Expand Down
7 changes: 6 additions & 1 deletion edb/edgeql-parser/src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub enum Kind {
Assign, // :=
SubAssign, // -=
AddAssign, // +=
AssignCoalesce, // :=?
Arrow, // ->
Coalesce, // ??
Namespace, // ::
Expand Down Expand Up @@ -291,7 +292,10 @@ impl<'a> Tokenizer<'a> {

match cur_char {
':' => match iter.next() {
Some((_, '=')) => Ok((Assign, 2)),
Some((_, '=')) => match iter.next() {
Some((_, '?')) => Ok((AssignCoalesce, 3)),
_ => Ok((Assign, 2)),
},
Some((_, ':')) => Ok((Namespace, 2)),
_ => Ok((Colon, 1)),
},
Expand Down Expand Up @@ -1051,6 +1055,7 @@ impl Kind {
Arrow => "->",
Assign => ":=",
SubAssign => "-=",
AssignCoalesce => ":=?",

Keyword(keywords::Keyword(kw)) => kw,

Expand Down
3 changes: 2 additions & 1 deletion edb/edgeql-parser/tests/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ fn colon_tokens() {
assert_eq!(tok_typ("a : = b"), [Ident, Colon, Eq, Ident]);
assert_eq!(tok_str("a ::= b"), ["a", "::", "=", "b"]);
assert_eq!(tok_typ("a ::= b"), [Ident, Namespace, Eq, Ident]);
assert_eq!(tok_str("a :=?b"), ["a", ":=?", "b"]);
assert_eq!(tok_typ("a :=?b"), [Ident, AssignCoalesce, Ident]);
}

#[test]
Expand Down Expand Up @@ -146,7 +148,6 @@ fn question_tokens() {
assert_eq!(tok_err("a ? b"),
"Bare `?` is not an operator, \
did you mean `?=` or `??` ?");

assert_eq!(tok_err("something ?!"),
"`?!` is not an operator, \
did you mean `?!=` ?");
Expand Down
1 change: 1 addition & 0 deletions edb/edgeql/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ class ShapeOp(s_enum.StrEnum):
APPEND = 'APPEND'
SUBTRACT = 'SUBTRACT'
ASSIGN = 'ASSIGN'
ASSIGN_COALESCE = 'ASSIGN_COALESCE'
MATERIALIZE = 'MATERIALIZE' # This is an internal implementation artifact


Expand Down
2 changes: 2 additions & 0 deletions edb/edgeql/codegen.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,8 @@ def visit_ShapeElement(self, node: qlast.ShapeElement) -> None:
self.write(' += ')
elif node.operation.op is qlast.ShapeOp.SUBTRACT:
self.write(' -= ')
elif node.operation.op is qlast.ShapeOp.ASSIGN_COALESCE:
self.write(' :=? ')
else:
raise NotImplementedError(
f'unexpected shape operation: {node.operation.op!r}'
Expand Down
40 changes: 40 additions & 0 deletions edb/edgeql/compiler/viewgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1559,6 +1559,46 @@ def _normalize_view_ptr_expr(
if is_linkprop_mutation:
raise

if shape_el.operation.op == qlast.ShapeOp.ASSIGN_COALESCE:
assert shape_el.compexpr is not None

existing_expr: Optional[qlast.Expr] = None
if s_ctx.exprtype.is_insert():
default_expr = None
if ptrcls is not None:
default_expr = ptrcls.get_default(ctx.env.schema)
if default_expr is not None:
existing_expr = qlast.DetachedExpr(
expr=default_expr.qlast,
preserve_path_prefix=True,
)
elif s_ctx.exprtype.is_update():
existing_expr = qlast.SelectQuery(
result=qlast.Path(
steps=shape_el.expr.steps.copy(),
partial=True,
),
implicit=True,
)
else:
raise errors.QueryError(
f'coalescing assignments are prohibited outside of insert '
f'and update',
span=shape_el.operation.span
)

if existing_expr is not None:
left: qlast.Expr = shape_el.compexpr
right: qlast.Expr = existing_expr

compexpr = shape_el.compexpr = qlast.BinOp(
left=left,
op='??',
right=right
)

shape_el.operation = qlast.ShapeOperation(op=qlast.ShapeOp.ASSIGN)

qlexpr = astutils.ensure_ql_query(compexpr)
# HACK: For scope tree related reasons, DML inside of free objects
# needs to be wrapped in a SELECT. This is probably fixable.
Expand Down
8 changes: 8 additions & 0 deletions edb/edgeql/parser/grammar/expressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,14 @@ def reduce_SimpleShapePointer_REMASSIGN_Expr(self, *kids):
span=kids[1].span,
)

def reduce_SimpleShapePointer_ASSIGNCOALESCE_Expr(self, *kids):
self.val = kids[0].val
self.val.compexpr = kids[2].val
self.val.operation = qlast.ShapeOperation(
op=qlast.ShapeOp.ASSIGN_COALESCE,
span=kids[1].span,
)


# This is the same as the above ComputableShapePointer, except using
# FreeSimpleShapePointer and not allowing +=/-=.
Expand Down
4 changes: 4 additions & 0 deletions edb/edgeql/parser/grammar/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ class T_REMASSIGN(Token, lextoken='-='):
pass


class T_ASSIGNCOALESCE(Token, lextoken=':=?'):
pass


class T_ARROW(Token, lextoken='->'):
pass

Expand Down
10 changes: 10 additions & 0 deletions tests/schemas/insert.esdl
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,13 @@ type ExceptTest {
constraint exclusive on (.name) except (.deleted);
};
type ExceptTestSub extending ExceptTest;

type CoalesceTest {
required property tag1 -> str {
default := 'foo';
}
property tag2 -> str {
default := <std::str>{};
}
property tag3 -> str;
}
90 changes: 90 additions & 0 deletions tests/test_edgeql_insert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6723,3 +6723,93 @@ async def test_edgeql_insert_coalesce_nulls_08(self):
'select Note { note }',
[{'note': "note"}],
)

async def test_edgeql_insert_assign_coalesce_01(self):
# coalesce to property with default
await self.con.execute('''
INSERT CoalesceTest { tag1 :=? 'bar' };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag1, }; ",
[
{
'tag1': 'bar',
},
]
)

async def test_edgeql_insert_assign_coalesce_02(self):
# coalesce to property with default
await self.con.execute('''
INSERT CoalesceTest { tag1 :=? <std::str>{} };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag1, }; ",
[
{
'tag1': 'foo',
},
]
)

async def test_edgeql_insert_assign_coalesce_03(self):
# coalesce to property without default
await self.con.execute('''
INSERT CoalesceTest { tag2 :=? 'bar' };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag2, }; ",
[
{
'tag2': 'bar',
},
]
)

async def test_edgeql_insert_assign_coalesce_04(self):
# coalesce to property without default
await self.con.execute('''
INSERT CoalesceTest { tag2 :=? <std::str>{} };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag2, }; ",
[
{
'tag2': None,
},
]
)

async def test_edgeql_insert_assign_coalesce_05(self):
# coalesce to property without default
await self.con.execute('''
INSERT CoalesceTest { tag3 :=? 'bar' };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag3, }; ",
[
{
'tag3': 'bar',
},
]
)

async def test_edgeql_insert_assign_coalesce_06(self):
# coalesce to property without default
await self.con.execute('''
INSERT CoalesceTest { tag3 :=? {} };
''')

await self.assert_query_result(
r"SELECT CoalesceTest { tag3, }; ",
[
{
'tag3': None,
},
]
)
9 changes: 9 additions & 0 deletions tests/test_edgeql_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -8255,3 +8255,12 @@ async def test_edgeql_type_pointer_backlink_01(self):
''',
__typenames__=True
)

async def test_edgeql_select_assign_coalesce_01(self):
async with self.assertRaisesRegexTx(
edgedb.QueryError,
"coalescing assignments are prohibited outside of insert and "
"update"):
await self.con.execute('''
select File { foo :=? .name };
''')