hkc-8010 opened a new issue, #71921:
URL: https://github.com/apache/airflow/issues/71921

   ### Apache Airflow version
   
   3.2.2 (and current `main`)
   
   ### What happened
   
   Upgrading 2.11.2 -> 3.2.2 on PostgreSQL aborts in 
`0049_3_0_0_remove_pickled_data_from_xcom_table`:
   
   ```
   sqlalchemy.exc.DataError: (psycopg2.errors.InvalidTextRepresentation) 
invalid input syntax for type json
   DETAIL:  Token "NaN" is invalid.
   CONTEXT:  JSON data, line 1: ..._amount_ugx\": 604441.0, \"commission_ugx\": 
"NaN...
   
   [SQL:
               ALTER TABLE xcom
               ALTER COLUMN value TYPE JSONB
               USING CASE
                   WHEN value IS NOT NULL THEN CAST(CONVERT_FROM(value, 'UTF8') 
AS JSONB)
                   ELSE NULL
               END
               ]
   ```
   
   The migration job exits 1 and retries. On the deployment where we hit this 
it looped 144 times over 35 hours, each attempt rewriting the whole 39 GB xcom 
table before failing, with no scheduler or API server up in between.
   
   ### What you think went wrong
   
   The sanitize step wraps non-finite tokens in unescaped double quotes:
   
   ```sql
   regexp_replace(txt, '([:,\[]\s*|^)(NaN|-?Infinity)(?=\s*[,}\]]|$)', 
'\1"\2"', 'g')
   ```
   
   That is only correct when the token sits in a top-level JSON document. When 
a task pushes
   already-serialized JSON, the stored value is a JSON string wrapping a JSON 
document, so every
   interior quote is backslash-escaped:
   
   ```
   "[{\"recipient_country\": \"Uganda\", ..., \"commission_ugx\": NaN}, ...]"
   ```
   
   The sanitizer rewrites `\"commission_ugx\": NaN}` to `\"commission_ugx\": 
"NaN"}`. The injected
   raw quote closes the wrapping string, the following `NaN` becomes a bare 
token, and the JSONB cast
   rejects it.
   
   Worth being explicit about: the sanitizer creates the invalid JSON here. The 
stored bytea is
   exactly what Airflow 2's `json.dumps` writes for a NaN float, and it casts 
to JSONB fine before
   sanitization.
   
   ### How to reproduce
   
   On PostgreSQL 16:
   
   ```sql
   CREATE TABLE t(id int primary key, value bytea);
   
   -- json.dumps(json.dumps({"amount": 604441.0, "commission": float("nan")}))
   INSERT INTO t VALUES (1, convert_to('"{\"amount\": 604441.0, \"commission\": 
NaN}"', 'UTF8'));
   
   -- as stored, this casts fine
   SELECT CAST(CONVERT_FROM(value, 'UTF8') AS JSONB) FROM t;
   
   -- now apply the migration's sanitize step
   UPDATE t SET value = convert_to(
       regexp_replace(convert_from(value, 'UTF8'),
           '([:,\[]\s*|^)(NaN|-?Infinity)(?=\s*[,}\]]|$)', '\1"\2"', 'g'),
       'UTF8');
   
   SELECT CAST(CONVERT_FROM(value, 'UTF8') AS JSONB) FROM t;
   -- ERROR:  invalid input syntax for type json
   -- DETAIL:  Token "NaN" is invalid.
   -- CONTEXT:  JSON data, line 1: "{\"amount\": 604441.0, \"commission\": 
"NaN...
   ```
   
   Two shapes fail the same way: the whole XCom value being the serialized 
document (above), and the
   serialized document sitting under a key in a dict, `{"report": "{\"amount\": 
NaN}"}`. MySQL and
   SQLite have the same problem in their branches of the same migration.
   
   ### Affected versions
   
   Every release carrying the quoting sanitizer, and current `main`. #53812, 
#57614, #57866 and #62686
   each refined which tokens get matched, but all of them insert unescaped 
quotes, and #69064 added
   NUL and invalid-byte guards without touching the replacement text. So there 
is no version to
   upgrade to, and the only way out for an affected deployment is editing the 
offending rows in the
   metadata DB by hand.
   
   ### Suggested fix
   
   Substitute the bare literal `null` instead of a quoted string. The 
replacement has to be valid at
   whatever nesting depth the token happens to occupy, and a quote character is 
not. `null` needs no
   escaping, so it is correct at every depth, and it is what most JSON encoders 
emit for a non-finite
   float anyway. This does change the outcome for the already-working top-level 
case, from the string
   `"NaN"` to `null`.
   
   Migration 0055 (`dag_run.conf`) is not affected. It converts the 
deserialized Python object and
   lets `json.dumps` handle the quoting, which gets the escaping right at any 
depth.
   
   ### Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to