Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Code
339d6cac70 fix(core): don't blank a datetime column when its format coerces every value to NaT
When a temporal column's declared python_date_format doesn't match its
actual data, pd.to_datetime(..., errors='coerce') returns all-NaT and the
result was assigned unconditionally, silently nulling the whole column.
This surfaces in the Samples/drill-detail pane, where a chart's
granularity column (e.g. an epoch-millis 'year' that inherited a '%Y'
string format) renders as N/A for every row. Keep the original values
when a format coerces every non-null entry to NaT.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:46:11 -07:00
13 changed files with 60 additions and 25 deletions

View File

@@ -1,5 +1,3 @@
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one # Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file # or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information # distributed with this work for additional information
@@ -17,6 +15,8 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
#!/bin/bash
# Function to determine Python command # Function to determine Python command
get_python_command() { get_python_command() {
if command -v python3 &>/dev/null; then if command -v python3 &>/dev/null; then

View File

@@ -38,7 +38,7 @@ RESET='\033[0m'
echo -e "${GREEN}Updating package lists...${RESET}" echo -e "${GREEN}Updating package lists...${RESET}"
apt-get update -qq apt-get update -qq
echo -e "${GREEN}Installing packages: $*${RESET}" echo -e "${GREEN}Installing packages: $@${RESET}"
apt-get install -yqq --no-install-recommends "$@" apt-get install -yqq --no-install-recommends "$@"
echo -e "${GREEN}Autoremoving unnecessary packages...${RESET}" echo -e "${GREEN}Autoremoving unnecessary packages...${RESET}"

View File

@@ -163,10 +163,10 @@ do
# Iterate through the components of the version strings # Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}" echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater" compare_result="greater"
break break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser" compare_result="lesser"
break break
fi fi

View File

@@ -54,7 +54,7 @@ dependencies = [
"deprecation>=2.1.0, <2.2.0", "deprecation>=2.1.0, <2.2.0",
"flask>=2.2.5, <4.0.0", "flask>=2.2.5, <4.0.0",
"flask-appbuilder>=5.2.2, <6.0.0", "flask-appbuilder>=5.2.2, <6.0.0",
"flask-caching>=2.4.1, <3", "flask-caching>=2.1.0, <3",
"flask-compress>=1.13, <2.0", "flask-compress>=1.13, <2.0",
"flask-talisman>=1.0.0, <2.0", "flask-talisman>=1.0.0, <2.0",
"flask-login>=0.6.0, < 1.0", "flask-login>=0.6.0, < 1.0",
@@ -92,7 +92,7 @@ dependencies = [
"Pillow>=11.0.0, <13", "Pillow>=11.0.0, <13",
"polyline>=2.0.0, <3.0", "polyline>=2.0.0, <3.0",
"pydantic>=2.8.0", "pydantic>=2.8.0",
"pyparsing>=3.3.2, <4", "pyparsing>=3.0.6, <4",
"python-dateutil", "python-dateutil",
"python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies "python-dotenv", # optional dependencies for Flask but required for Superset, see https://flask.palletsprojects.com/en/stable/installation/#optional-dependencies
"pygeohash", "pygeohash",
@@ -127,7 +127,7 @@ aurora-data-api = ["preset-sqlalchemy-aurora-data-api>=0.2.8,<0.3"]
bigquery = [ bigquery = [
"pandas-gbq>=0.35.0", "pandas-gbq>=0.35.0",
"sqlalchemy-bigquery>=1.17.0", "sqlalchemy-bigquery>=1.17.0",
"google-cloud-bigquery>=3.42.2", "google-cloud-bigquery>=3.42.1",
] ]
clickhouse = ["clickhouse-connect>=1.4.2, <2.0"] clickhouse = ["clickhouse-connect>=1.4.2, <2.0"]
cockroachdb = ["cockroachdb>=0.3.5, <0.4"] cockroachdb = ["cockroachdb>=0.3.5, <0.4"]
@@ -142,8 +142,8 @@ databricks = [
"databricks-sql-connector>=4.2.6, <4.4.0", "databricks-sql-connector>=4.2.6, <4.4.0",
"databricks-sqlalchemy==1.0.5", "databricks-sqlalchemy==1.0.5",
] ]
datafusion = ["flightsql-dbapi>=0.2.2, <0.3"] datafusion = ["flightsql-dbapi>=0.2.0, <0.3"]
db2 = ["ibm-db-sa<=0.4.4, >=0.4.4"] db2 = ["ibm-db-sa>0.3.8, <=0.4.4"]
denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"] denodo = ["denodo-sqlalchemy>=2.0.5,<2.1.0"]
dremio = ["sqlalchemy-dremio>=1.2.1, <4"] dremio = ["sqlalchemy-dremio>=1.2.1, <4"]
drill = ["sqlalchemy-drill>=1.1.10, <2"] drill = ["sqlalchemy-drill>=1.1.10, <2"]
@@ -189,7 +189,7 @@ ocient = [
"shapely", "shapely",
"geojson", "geojson",
] ]
oracle = ["oracledb>=4.0.2, <5"] oracle = ["oracledb>=2.0.0, <5"]
parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"] parseable = ["sqlalchemy-parseable>=0.1.6,<0.2.0"]
pinot = ["pinotdb>=5.0.0, <10.0.0"] pinot = ["pinotdb>=5.0.0, <10.0.0"]
playwright = ["playwright>=1.61.0, <2"] playwright = ["playwright>=1.61.0, <2"]
@@ -201,7 +201,7 @@ redshift = ["sqlalchemy-redshift>=0.8.1, <0.9"]
risingwave = ["sqlalchemy-risingwave"] risingwave = ["sqlalchemy-risingwave"]
shillelagh = ["shillelagh[all]>=1.4.4, <2"] shillelagh = ["shillelagh[all]>=1.4.4, <2"]
singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"] singlestore = ["sqlalchemy-singlestoredb>=1.2.1, <2"]
snowflake = ["snowflake-sqlalchemy>=1.11.0, <2"] snowflake = ["snowflake-sqlalchemy>=1.10.2, <2"]
sqlite = ["syntaqlite>=0.7.0,<0.8.0"] sqlite = ["syntaqlite>=0.7.0,<0.8.0"]
spark = [ spark = [
"pyhive[hive_pure_sasl]>=0.7", "pyhive[hive_pure_sasl]>=0.7",
@@ -227,7 +227,7 @@ development = [
"docker", "docker",
"flask-testing", "flask-testing",
"freezegun", "freezegun",
"grpcio>=1.82.1", "grpcio>=1.81.1",
"openapi-spec-validator", "openapi-spec-validator",
"parameterized", "parameterized",
"pip", "pip",

View File

@@ -130,7 +130,7 @@ flask-appbuilder==5.2.2
# apache-superset-core # apache-superset-core
flask-babel==3.1.0 flask-babel==3.1.0
# via flask-appbuilder # via flask-appbuilder
flask-caching==2.4.1 flask-caching==2.3.1
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
flask-compress==1.24 flask-compress==1.24
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
@@ -329,7 +329,7 @@ pyopenssl==26.3.0
# via # via
# -r requirements/base.in # -r requirements/base.in
# shillelagh # shillelagh
pyparsing==3.3.2 pyparsing==3.2.3
# via apache-superset (pyproject.toml) # via apache-superset (pyproject.toml)
pysocks==1.7.1 pysocks==1.7.1
# via urllib3 # via urllib3

View File

@@ -276,7 +276,7 @@ flask-babel==3.1.0
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# flask-appbuilder # flask-appbuilder
flask-caching==2.4.1 flask-caching==2.3.1
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset
@@ -341,7 +341,7 @@ geopy==2.4.1
# apache-superset # apache-superset
gevent==26.4.0 gevent==26.4.0
# via apache-superset # via apache-superset
google-api-core==2.33.0 google-api-core==2.23.0
# via # via
# google-cloud-bigquery # google-cloud-bigquery
# google-cloud-core # google-cloud-core
@@ -362,7 +362,7 @@ google-auth-oauthlib==1.2.1
# via # via
# pandas-gbq # pandas-gbq
# pydata-google-auth # pydata-google-auth
google-cloud-bigquery==3.42.2 google-cloud-bigquery==3.42.1
# via # via
# apache-superset # apache-superset
# pandas-gbq # pandas-gbq
@@ -386,7 +386,7 @@ greenlet==3.5.3
# sqlalchemy # sqlalchemy
griffelib==2.0.2 griffelib==2.0.2
# via fastmcp-slim # via fastmcp-slim
grpcio==1.83.0 grpcio==1.81.1
# via # via
# apache-superset # apache-superset
# google-api-core # google-api-core
@@ -795,7 +795,7 @@ pyopenssl==26.3.0
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# google-auth # google-auth
# shillelagh # shillelagh
pyparsing==3.3.2 pyparsing==3.2.3
# via # via
# -c requirements/base-constraint.txt # -c requirements/base-constraint.txt
# apache-superset # apache-superset

View File

@@ -35,7 +35,7 @@ acquire_rat_jar () {
wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR" wget --quiet ${URL} -O "$JAR_DL" && mv "$JAR_DL" "$JAR"
else else
printf "You do not have curl or wget installed, please install rat manually.\n" printf "You do not have curl or wget installed, please install rat manually.\n"
exit 255 exit -1
fi fi
fi fi
@@ -44,7 +44,7 @@ acquire_rat_jar () {
# We failed to download # We failed to download
rm "$JAR" rm "$JAR"
printf "Our attempt to download rat locally to ${JAR} failed. Please install rat manually.\n" printf "Our attempt to download rat locally to ${JAR} failed. Please install rat manually.\n"
exit 255 exit -1
fi fi
printf "Done downloading.\n" printf "Done downloading.\n"
} }

View File

@@ -163,10 +163,10 @@ do
# Iterate through the components of the version strings # Iterate through the components of the version strings
for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do for (( j=0; j<${#THIS_TAG_NAME_ARRAY[@]}; j++ )); do
echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}" echo "Comparing ${THIS_TAG_NAME_ARRAY[$j]} to ${LATEST_RELEASE_TAG_ARRAY[$j]}"
if [[ $((THIS_TAG_NAME_ARRAY[$j])) -gt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then if [[ $((THIS_TAG_NAME_ARRAY[$j])) > $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="greater" compare_result="greater"
break break
elif [[ $((THIS_TAG_NAME_ARRAY[$j])) -lt $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then elif [[ $((THIS_TAG_NAME_ARRAY[$j])) < $((LATEST_RELEASE_TAG_ARRAY[$j])) ]]; then
compare_result="lesser" compare_result="lesser"
break break
fi fi

View File

@@ -66,6 +66,12 @@ const StyledTable = styled(Table)<{
showRowCount?: boolean; showRowCount?: boolean;
}>` }>`
${({ theme, isPaginationSticky, showRowCount }) => ` ${({ theme, isPaginationSticky, showRowCount }) => `
th.ant-column-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions { .actions {
opacity: 0; opacity: 0;
font-size: ${theme.fontSizeXL}px; font-size: ${theme.fontSizeXL}px;

View File

@@ -2017,13 +2017,26 @@ def _process_datetime_column(
# Parse with or without format (suppress warning if no format) # Parse with or without format (suppress warning if no format)
if format_to_use: if format_to_use:
df[col.col_label] = pd.to_datetime( converted = pd.to_datetime(
df[col.col_label], df[col.col_label],
utc=False, utc=False,
format=format_to_use, format=format_to_use,
errors="coerce", errors="coerce",
exact=False, exact=False,
) )
# A format that coerces every non-null value to NaT is a mismatch
# (e.g. an epoch-millis column that inherited a '%Y' string format
# when used as a chart's granularity). Assigning it would silently
# blank the whole column, so keep the original values instead.
if df[col.col_label].notna().any() and not converted.notna().any():
logger.warning(
"Datetime format %s coerced every value of column %s to NaT; "
"keeping the original values",
format_to_use,
col.col_label,
)
else:
df[col.col_label] = converted
else: else:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*Could not infer format.*") warnings.filterwarnings("ignore", message=".*Could not infer format.*")

View File

@@ -273,6 +273,22 @@ def test_normalize_dttm_col() -> None:
assert df["__time"].astype(str).tolist() == ["2017-07-01"] assert df["__time"].astype(str).tolist() == ["2017-07-01"]
def test_normalize_dttm_col_mismatched_format_keeps_values() -> None:
"""A datetime format that coerces every value to NaT is a mismatch (e.g. an
epoch-millis column that inherited a ``%Y`` string format when used as a
chart's granularity); applying it would silently blank the whole column, so
the original values are kept instead of being nulled. Regression for the
Samples pane showing N/A for such columns."""
df = pd.DataFrame({"year": [1136073600000, 473385600000]}) # epoch ms
before = df["year"].tolist()
normalize_dttm_col(df, (DateColumn(col_label="year", timestamp_format="%Y"),))
# not blanked to NaT/None
assert df["year"].notna().all()
assert df["year"].tolist() == before
def test_normalize_dttm_col_epoch_seconds() -> None: def test_normalize_dttm_col_epoch_seconds() -> None:
"""Test conversion of epoch seconds.""" """Test conversion of epoch seconds."""
df = pd.DataFrame( df = pd.DataFrame(