Skip to content

FIX: query and remove seeds by literal metadata keys - #3125

Open
Chen Yufeiyang (feiiiiii5) wants to merge 3 commits into
microsoft:mainfrom
feiiiiii5:fix/prompt-metadata-non-identifier-keys
Open

Chen Yufeiyang (feiiiiii5) wants to merge 3 commits into
microsoft:mainfrom
feiiiiii5:fix/prompt-metadata-non-identifier-keys

Conversation

@feiiiiii5

@feiiiiii5 Chen Yufeiyang (feiiiiii5) commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Description

Prompt and seed metadata accept free-form keys, but keys with dots, spaces or punctuation were used as bind-parameter names and JSON paths. Quoting paths is insufficient: quotes or backslashes inside a key can select a different record, including when deleting seeds.

Changes

SQLite compares literal keys through json_each(...).key; Azure SQL compares OPENJSON keys. Azure also checks Unicode byte lengths, since SQL Server equality pads trailing spaces. Keys and values remain bound parameters; value comparison semantics are unchanged. Prompt retrieval, seed retrieval and seed/group removal share these filters. Related: #3063, which addressed label filters.

Tests

Eight quote/backslash retrieval and removal regressions fail on fab86809 with unrelated records selected or removed. Three generated-MSSQL predicate tests fail on 2efea640 because exact-key length guards are absent. These inspect generated SQL, rather than executing SQL Server.

SQLite database tests distinguish plain from plain , empty from space-only keys, and Unicode keys with trailing spaces, including removal. Those cases already pass on 2efea640 and pin the behavior Azure should preserve.

Tests and Documentation

Memory-interface, SQLite and Azure unit suites: 683 passed, 1 skipped. Ruff check and format check passed on the three newly changed files. No docs changed; JupyText was not run.

Not addressed

Live Azure SQL integration was not run. Azure checks cover generated MSSQL predicates and literal bound keys; SQLite checks execute against a local database.

`prompt_metadata` is documented as a free-form dictionary, but the SQLite
and Azure SQL backends spelled each key into the JSON path and reused it
as the bind-parameter name:

    JSON_EXTRACT(prompt_metadata, '$.{key}') = :{key}

Only `[A-Za-z0-9_]` survives that. Anything else raises before the query
runs, so the filter cannot be expressed at all:

    ArgumentError: This text() construct doesn't define a bound
    parameter named 'attack.id'

`get_message_pieces_async(prompt_metadata={"attack.id": "v"})` against
SQLiteMemory raises that today. A dot was also read as a path step, so a
key that happened to survive would have looked up a nested object.

Both backends now bind the path with the key quoted into it (`'$."{key}"'`)
and give each condition positional parameter names, matching how microsoft#3063
fixed the same interpolation in the label conditions:

    JSON_EXTRACT(prompt_metadata, :pm_path_0) = :pm_value_0

Binding the path also keeps the value out of the statement text entirely.

Seed metadata filtering goes through the same two code paths
(`_get_seed_metadata_conditions`), so it is fixed as well.

Regression tests query four keys holding a dot, hyphen, space and colon,
plus one query naming three keys at once. Against the parent commit they
fail with the ArgumentError above; the plain-key cases the suite already
covered still pass.

Not addressed: a key containing a double quote cannot be expressed in a
SQLite or T-SQL JSON path, so it matches nothing rather than raising.
That is the same residual microsoft#3063 left in the label path.

Validation at this head: tests/unit/memory/memory_interface/,
tests/unit/memory/test_sqlite_memory.py, tests/unit/memory/test_azure_sql_memory.py
-- 648 passed, 1 skipped; ruff check and format clean.
Comment thread pyrit/memory/sqlite_memory.py Outdated
@feiiiiii5 Chen Yufeiyang (feiiiiii5) changed the title fix(memory): query prompt and seed metadata whose keys are not SQL identifiers FIX: query and remove seeds by literal metadata keys Oct 12, 2026
@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Thanks for reproducing the deletion case. Fixed in 2efea640: prompt and seed filters now compare literal object keys with json_each in SQLite and OPENJSON in Azure SQL, rather than interpreting the keys as paths.

Added quote and backslash cases for prompt/seed retrieval, seed removal and group removal, each asserting that unrelated records remain. All eight fail on fab86809 with the wrong records selected or removed; the revised memory suites pass (656 passed, 1 skipped). Ruff check/format also pass. Live Azure SQL was not run.

The PR description now reflects the literal-key implementation and removes the incorrect claim that quoted keys would simply match nothing.

Comment thread pyrit/memory/azure_sql_memory.py Outdated
alias = f"metadata_{index}"
json_conditions.append(
f"EXISTS (SELECT 1 FROM OPENJSON(prompt_metadata) AS [{alias}] "
f"WHERE [{alias}].[key] = :{key_param} AND [{alias}].[value] = :{value_param})"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Must Fix: SQL Server still treats different metadata keys as equal.

OPENJSON gives [key] a BIN2 collation, but SQL Server's = comparison still pads strings with spaces. That means "plain" and "plain " compare equal here. Two seeds with these different dictionaries would both match metadata={"plain": "v"}:

{"plain": "v"}
{"plain ": "v"}

The same predicate backs prompt retrieval, seed retrieval, and seed/group removal, so this can remove the seed or group with the wrong key. Previously, JSON_VALUE(..., '$.plain') only selected the actual plain property. SQLite's new literal-key comparison does distinguish these keys.

Please keep the equality check and also compare the key lengths, using the same Unicode type on both sides. For example:

AND DATALENGTH([metadata_0].[key])
    = DATALENGTH(CAST(:md_key_0 AS nvarchar(max)))

Add coverage that distinguishes plain, plain , and an empty key from a space-only key, including removal. The current main branch's _get_scenario_role_match_condition uses the same equality-plus-length approach for exact strings.

I checked the actual generated MSSQL predicate and SQL Server's documented padding behavior, but did not run a live SQL Server. Am I missing an exact-length check elsewhere?

@feiiiiii5

Copy link
Copy Markdown
Contributor Author

Thanks Roman Lutz (@romanlutz) — the shared Azure predicate was missing that length guard. Fixed in b9b669f: key equality now also requires equal DATALENGTH, with the bound key cast to NVARCHAR(MAX). Metadata value comparisons are unchanged.

Added generated-MSSQL predicate checks for trailing spaces, empty versus space-only keys, Unicode and multiple filters; all three fail on 2efea640 because the guard is absent. The SQLite database regressions now cover the same key pairs for prompt/seed retrieval and seed/group removal, asserting unrelated records remain.

The memory suites pass: 683 passed, 1 skipped. Ruff check/format passed. Live SQL Server was not run; the Azure regression checks inspect generated SQL, and the PR description now makes that limit explicit.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants