risingwavelabs/risingwave · error · ConnectorError

failed to parse mysql timestamp value

Error message

failed to parse mysql timestamp value

What it means

timestamp_val_to_timestamptz parses a MySQL timestamp literal in the fixed format '%Y-%m-%d %H:%M:%S' and converts it to a UTC timestamptz string. If the text does not match this exact format, chrono's NaiveDateTime::parse_from_str fails and the error (with the chrono error as context) is returned.

Source

Thrown at src/connector/src/source/cdc/external/mysql.rs:264

            // interpret its value
            if data_type == &DataType::Timestamptz {
                val = timestamp_val_to_timestamptz(val.as_str())?;
            }
            Some(ScalarImpl::from_text(val.as_str(), data_type).map_err(|e| anyhow!(e)).context(
                "failed to parse mysql default value expression, only constant is supported",
            )?)
        }
        ColumnDefault::CurrentTimestamp | ColumnDefault::CustomExpr(_) => {
            bail!("MySQL CURRENT_TIMESTAMP and custom expression default value not supported")
        }
    };
    Ok(datum)
}

pub fn timestamp_val_to_timestamptz(value_text: &str) -> ConnectorResult<String> {
    let format = "%Y-%m-%d %H:%M:%S";
    let naive_datetime = NaiveDateTime::parse_from_str(value_text, format)
        .map_err(|err| anyhow!("failed to parse mysql timestamp value").context(err))?;
    let postgres_timestamptz: DateTime<chrono::Utc> =
        DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive_datetime, chrono::Utc);
    Ok(postgres_timestamptz
        .format("%Y-%m-%d %H:%M:%S%:z")
        .to_string())
}

pub fn type_name_to_mysql_type(ty_name: &str) -> Option<ColumnType> {
    // Debezium schema change message may include extra qualifiers, e.g. `BIGINT UNSIGNED`,
    // `BIGINT(20) UNSIGNED`, `INT UNSIGNED ZEROFILL`, etc.
    let ty = ty_name.trim().to_lowercase();
    let tokens = ty
        .split(|c: char| c.is_whitespace() || matches!(c, '(' | ')' | ','))
        .filter(|token| !token.is_empty())
        .collect_vec();
    let base = tokens.first().copied().unwrap_or_default();
    let second = tokens.get(1).copied();
    let is_unsigned = tokens.contains(&"unsigned");

View on GitHub (pinned to 6469eb736d)

Solutions

  1. Normalize the MySQL default to 'YYYY-MM-DD HH:MM:SS' format (no fractional seconds, no 'T').
  2. Replace the default with a constant literal in the accepted format.
  3. Patch timestamp_val_to_timestamptz to accept fractional seconds (e.g. try multiple chrono formats) if you control the connector code.

Example fix

// before (MySQL)
ALTER TABLE t MODIFY ts TIMESTAMP(3) DEFAULT '2024-01-01 00:00:00.000';
// after
ALTER TABLE t MODIFY ts TIMESTAMP DEFAULT '2024-01-01 00:00:00';
Defensive patterns

Strategy: validation

Validate before calling

function isMysqlTimestampLiteral(v) {
  return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(String(v));
}

Type guard

function canParseAsNaiveDateTime(v) {
  return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(v) && !isNaN(Date.parse(v.replace(" ", "T") + "Z"));
}

Try / catch

try { await createCdcTable('t'); } catch (e) { if (String(e).includes('failed to parse mysql timestamp value')) { /* reformat the default to 'YYYY-MM-DD HH:MM:SS' in MySQL and retry */ } else throw e; }

Prevention

When it happens

Trigger: A MySQL TIMESTAMP/DATETIME column default whose string value includes fractional seconds ('2024-01-01 00:00:00.123'), timezone offsets, or another formatting variant; also reached via parse_schema_change handling default values.

Common situations: MySQL defaults like CURRENT_TIMESTAMP(3) resolved to values with microseconds; locale or zero-date values ('0000-00-00 00:00:00'); defaults written with 'T' separators.

Understand the failure class

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/b3cb9f340cb19f34. Report an issue: GitHub.