Skip to content

PartiQL (#411): the parser's nesting caps refuse statements that DynamoDB runs #885

Open

Description

@LeeroyHannigan

Describe the bug

Follow-up to #411. The PartiQL parser at 0e4b4a8 caps the nesting of a statement in crates/core/src/partiql/parser/limits.rs: 1000 parenthesized groups (MAX_GROUP_DEPTH), 1000 literal containers (MAX_LITERAL_DEPTH) and 100 nested operands (MAX_OPERAND_DEPTH). Past a cap, ExtendDB refuses the statement with Statement wasn't well formed, can't be processed: expression nesting depth exceeded. The service runs, or refuses for another reason, several forms that these caps refuse:

  • Parentheses round an operand (a = (((b))), a = ((('x'))), SET x = (((a)))) from 100 levels. Each level counts as a group and as an operand (operand_in_parentheses, crates/core/src/partiql/parser/operands.rs), so the statement meets the operand cap first. The service runs 100, 101, 999, 1000 and 1001 levels.
  • Parentheses round a path on the left of a comparison ((((a))) = 1) and round an INSERT value ('x': ((('v')))) from 1001 levels, and round an IN member (a IN (((1)))) from 1002 levels. The service runs them.
  • if_not_exists and list_append calls nested in a SET value from 100 levels. The service runs 100 and 101.
  • Forms the service refuses for another reason at depths past the caps. The service reads size nested 101 to 1,355 times, NOT 100 to 2,031 times before a SET value, and CASE 100 to 1,626 times. It also reads 1,160 nested maps in an INSERT value, 2,033 nested bags, and 1,354 levels of f(j[ inside the bracket of a path.

Measured against Amazon DynamoDB in us-east-1 on 2026-11-06. Table: pk S hash key, sk S range key, with an item pk = 'h', sk = 's' that holds a = 1 and a map m. Operation: ExecuteStatement. A condition form in the table is the tail of SELECT * FROM "<t>" WHERE pk = 'h' AND . In a statement, x N after a code span means N copies of that span. ConditionalCheckFailedException on an UPDATE means the statement ran and found no item with sk = 'w'. DuplicateItemException on an INSERT means the statement ran and found an item with that key. The ExtendDB answers are from 0e4b4a8 with the SQLite backend, debug build.

Statement DynamoDB ExtendDB at 0e4b4a8
a = ( x 99 b ) x 99 runs, 0 items runs, 0 items
a = ( x 100 b ) x 100 runs, 0 items expression nesting depth exceeded
a = ( x 1001 b ) x 1001 runs, 0 items the depth refusal
a = ( x 100 'x' ) x 100 runs, 0 items the depth refusal
UPDATE "<t>" SET x = ( x 100 a ) x 100 WHERE pk = 'h' AND sk = 'w' ConditionalCheckFailedException the depth refusal
( x 1000 a ) x 1000 = 1 runs, 1 item runs, 1 item
( x 1001 a ) x 1001 = 1 runs, 1 item the depth refusal
INSERT INTO "<t>" VALUE {'pk': 'h', 'sk': 'i', 'x': ( x 1001 'v' ) x 1001 } DuplicateItemException the depth refusal
a IN ( x 1001 1 ) x 1001 runs, 1 item runs, 1 item
a IN ( x 1002 1 ) x 1002 runs, 1 item the depth refusal
UPDATE "<t>" SET x = if_not_exists(x, x 99 1 ) x 99 WHERE pk = 'h' AND sk = 'w' ConditionalCheckFailedException ConditionalCheckFailedException
the same with 100 and with 101 nested if_not_exists calls ConditionalCheckFailedException the depth refusal
UPDATE "<t>" SET l = list_append( x 100 l , [1]) x 100 WHERE pk = 'h' AND sk = 'w', and the same with 101 ConditionalCheckFailedException the depth refusal
(size( x 101 a )) x 101 = 1, and the same with 1,355 Incorrect operand type for operator or function; operator or functi (the measurement cut the text there) the depth refusal
UPDATE "<t>" SET x = NOT x 100 a WHERE pk = 'h' AND sk = 'w', and the same with 2,031 Unsupported operator in Update Expression. Operator: not the depth refusal
a = CASE x 100 b, and the same with 1,626 Statement wasn't well formed, can't be processed: Expected a WHEN clause in CASE the depth refusal
INSERT INTO "<t>" VALUE {'pk': 'h', 'sk': 'i', 'x': {'a': x 1160 1 } x 1160 } Nesting Levels have exceeded supported limits under root.a with .a repeated to 32 levels the depth refusal
a = << x 2033 1 >> x 2033 Unsupported data type in Bag. DynamoDB only supports either numbers or strings in bags the depth refusal
1,354 levels of f(j[ inside the bracket of a path Unexpected path component at the first k the depth refusal

The operand type and the operator rows show that the service read the whole statement at those depths.

To Reproduce

  1. Start ExtendDB at 0e4b4a8 with the SQLite backend.
  2. Create the table above and put the item.
  3. Send the statements in the table with ExecuteStatement.
  4. Compare the answers with the DynamoDB column.

Expected behavior

The DynamoDB column. The service runs 1,001 levels of parentheses round a path or a value, and 101 nested if_not_exists or list_append calls. It reads 1,355 nested size calls, 2,031 NOT operators, 1,626 CASE words, 1,160 nested maps and 2,033 nested bags to their end.

Actual behavior

The ExtendDB column. ExtendDB refuses an operand in parentheses from 100 levels, the other parenthesized forms from 1001 levels, and nested calls from 100 levels. It refuses every form in the last six rows for its depth.

Environment

Proposed change

Raise the operand cap (MAX_OPERAND_DEPTH, 100) and the group cap (MAX_GROUP_DEPTH, 1000). Stop counting an operand in parentheses against the operand cap. Size LARGE_STACK_BYTES (64 MiB) for the higher caps. Parser::nested (limits.rs) applies the caps, and parse_on_a_fitting_stack parses a deep statement again on a thread with the large stack.

The service's limit for nested operands is not measured (the comment on MAX_OPERAND_DEPTH). The deepest forms the service read in this pass are the ones in the table: 1,001 parentheses and 101 nested calls. It also read 1,355 nested size calls and refused them for the operand type. The comments in limits.rs record that the service fails 2000 parenthesized groups and 4000 nested lists with an internal error.

The module comment of limits.rs gives the stack use per level on a debug build. A parenthesized group uses 8.6 KB, a nested operand 6.8 KB, and a literal container 2.5 to 2.8 KB. The worst parse the present caps allow used 12.2 MB on its own thread. Three tests in crates/core/src/partiql/parser/limits/tests.rs check the bounds on a thread with a 1 MiB stack and need the new caps: every_nesting_form_at_the_length_limit_parses_on_a_small_stack, a_deep_statement_takes_little_of_the_callers_stack and the_deepest_statements_that_parse_are_walked_on_a_small_stack. The last one runs the checks the server makes on a parsed statement (check_condition_values, check_ordering_operands and check_written_values) and the plan walk over the deepest statements that parse. Its comment records 870 KB for the literal checks of a condition with a value 1,000 levels deep.

Additional context

At the statement length limit of 8,192 characters the service answers InternalFailure for most of these forms. ExtendDB refuses them with the depth text or runs them. limits.rs documents that refusal as the design for a statement the service fails with an internal error. This issue proposes no change for it.

Two groups of rows are parser forms, which #887 covers. In the size rows at 10 to 100 levels, ExtendDB runs the statement and the service refuses the operand type. In the CASE and NOT rows at 10 and 99 levels, the two syntax texts differ.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions