Compare commits

..

2 Commits

Author SHA1 Message Date
Evan Rusackas
108cf2530e Merge branch 'master' into fix-year-pdf-time-grain 2026-07-27 08:58:24 -07:00
Claude Code
73ca8fb727 fix(db-engine-specs): handle bare-year python_date_format columns in time-grain expressions
get_timestamp_expr() only knew how to convert epoch_s/epoch_ms raw values
into a proper timestamp before applying a time-grain truncation function;
any other python_date_format (e.g. "%Y") fell through and passed the raw
value straight into the grain function. On SQLite that means
DATETIME(year, 'start of year') interprets a bare integer year as a Julian
day number rather than a calendar year, silently returning NULL for every
row. This exact pattern ships in Superset's own video_game_sales example
dataset (year: BIGINT, is_dttm=true, python_date_format: '%Y'), so any
time-grain-truncated chart against it (Nightingale Rose included) returns
no data.

Add a year_to_dttm() hook (mirroring epoch_to_dttm/epoch_ms_to_dttm) that
converts a bare year into a proper date before truncation, implemented for
SQLite via printf()-based date construction with an explicit NULL guard
(printf() otherwise treats a NULL argument as 0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 23:30:57 -07:00
20 changed files with 122 additions and 371 deletions

View File

@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-supersetbot/
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"

View File

@@ -18,6 +18,16 @@ on:
workflow_dispatch: {}
# Serialize deploys: the action pushes to apache/superset-site without
# rebasing, so concurrent runs race on the final push and the loser fails
# with `! [rejected] asf-site -> asf-site (fetch first)`. Cancel any
# in-progress run as soon as a newer one starts — the destination repo
# isn't touched until the final push step, so canceling mid-build is safe,
# and the freshest content always wins.
concurrency:
group: docs-deploy-asf-site
cancel-in-progress: true
permissions:
contents: read
@@ -37,59 +47,17 @@ jobs:
env:
SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }}
# Master gets frequent, sometimes bursty pushes, and each one can trigger a
# deploy attempt. Rather than let every superseded attempt get force-killed
# by the build-deploy concurrency group below (which shows up as a
# `cancelled` — i.e. red/failing-looking — check on that commit), have each
# run check up front whether it's still building master's current tip and,
# if not, skip cleanly. Deliberately outside the docs-deploy-asf-site
# concurrency group so it runs immediately for every trigger without
# blocking or being blocked by anything.
check-freshness:
runs-on: ubuntu-26.04
outputs:
is-current: ${{ steps.check.outputs.is-current }}
steps:
- name: "Check whether this is still master's current commit"
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
run: |
latest_sha="$(gh api "repos/${{ github.repository }}/commits/master" --jq .sha)"
if [ "${latest_sha}" = "${BUILD_SHA}" ]; then
echo "is-current=true" >> "$GITHUB_OUTPUT"
else
echo "is-current=false" >> "$GITHUB_OUTPUT"
echo "::notice::master has moved on to ${latest_sha} since ${BUILD_SHA} was triggered — a newer run will deploy it, so skipping this one."
fi
build-deploy:
needs: [config, check-freshness]
# Only the run for master's current tip proceeds; anything superseded
# already skipped at check-freshness above instead of landing here.
needs: config
# For workflow_run triggers, only deploy when the triggering run originated
# from this repository (not a fork), ensuring the checked-out code and any
# local actions executed with deploy credentials are trusted.
if: >-
needs.config.outputs.has-secrets &&
needs.check-freshness.outputs.is-current == 'true' &&
(github.event_name != 'workflow_run' ||
github.event.workflow_run.head_repository.full_name == github.repository)
name: Build & Deploy
runs-on: ubuntu-26.04
# Serialize deploys: the action pushes to apache/superset-site without
# rebasing, so concurrent runs race on the final push and the loser fails
# with `! [rejected] asf-site -> asf-site (fetch first)`. Cancel any
# in-progress run as soon as a newer one starts — the destination repo
# isn't touched until the final push step, so canceling mid-build is safe,
# and the freshest content always wins. The check-freshness gate above
# means it should be rare for more than one run to reach this point, but
# this stays as a backstop against the actual push race.
concurrency:
group: docs-deploy-asf-site
cancel-in-progress: true
steps:
- name: "Checkout ${{ github.event.workflow_run.head_sha || github.sha }}"
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

View File

@@ -122,13 +122,25 @@ jobs:
pattern: coverage-artifacts-*
path: coverage/
- name: Reorganize test result reports
run: |
find coverage/
for i in {1..8}; do
mv coverage/coverage-artifacts-${i}/coverage-final.json coverage/coverage-shard-${i}.json
done
shell: bash
- name: Merge Code Coverage
run: npx nyc merge coverage/ merged-output/coverage-summary.json
- name: Upload Code Coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: javascript
use_oidc: true
verbose: true
directory: coverage
disable_search: true
files: merged-output/coverage-summary.json
slug: apache/superset
lint-frontend:

View File

@@ -54,7 +54,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.3.0",
"@mdx-js/react": "^3.1.1",
"@saucelabs/theme-github-codeblock": "^0.3.0",

View File

@@ -2534,10 +2534,10 @@
resolved "https://registry.yarnpkg.com/@fontsource/fira-code/-/fira-code-5.3.0.tgz#487475cedfa1f7ba7650810ddce00b6c7b1b7a25"
integrity sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==
"@fontsource/ibm-plex-mono@^5.3.0":
version "5.3.0"
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz#1879699d104602d5331e28e103f33c3b1b766b17"
integrity sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==
"@fontsource/ibm-plex-mono@^5.2.7":
version "5.2.7"
resolved "https://registry.yarnpkg.com/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz#ef5b6f052115fdf6666208a5f8a0f13fcd7ba1fd"
integrity sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==
"@fontsource/inter@^5.3.0":
version "5.3.0"

View File

@@ -29,7 +29,7 @@ maintainers:
- name: craig-rueda
email: craig@craigrueda.com
url: https://github.com/craig-rueda
version: 0.22.4 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
version: 0.22.3 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
dependencies:
- name: postgresql
version: 16.7.27

View File

@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
# superset
![Version: 0.22.4](https://img.shields.io/badge/Version-0.22.4-informational?style=flat-square)
![Version: 0.22.3](https://img.shields.io/badge/Version-0.22.3-informational?style=flat-square)
Apache Superset is a modern, enterprise-ready business intelligence web application
@@ -159,10 +159,6 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
| init.extraInitContainers | list | `[]` | Extra init containers appended after init job initContainers |
| init.initContainers | list | a container waiting for postgres | List of initContainers |
| init.initscript | string | unused; kept for backwards-compatibility only | DEPRECATED: this field is no longer used by the chart. The init script is rendered entirely from the internal `superset.initScript` template (which runs `superset db upgrade`, `superset init`, admin creation, and examples). Any customization placed here is silently ignored. See UPGRADING.md. |
| init.istio | object | `{"disableSidecarInjection":false,"quitEndpoint":"http://localhost:15020/quitquitquit","terminateSidecarOnExit":false}` | Configuration for compatibility with the Istio service mesh. Without these mitigations, when the init-db Job runs in a namespace with automatic Istio sidecar injection enabled, the injected envoy-proxy container keeps running after the init container has exited, preventing the Job from ever reaching the Completed state. See https://github.com/apache/superset/issues/25798 |
| init.istio.disableSidecarInjection | bool | `false` | When true, adds the `sidecar.istio.io/inject: "false"` label to the init job pod template, opting it out of Istio automatic sidecar injection. This is the recommended fix when the cluster's mesh policy allows opting out per pod. |
| init.istio.quitEndpoint | string | `"http://localhost:15020/quitquitquit"` | Endpoint that the trap POSTs to in order to terminate the sidecar. The default matches the standard Istio pilot-agent admin port. |
| init.istio.terminateSidecarOnExit | bool | `false` | When true, the rendered `superset_init.sh` script (from the internal `superset.initScript` template) registers an `EXIT` trap that POSTs to the Istio pilot-agent's `/quitquitquit` endpoint after the init logic finishes, gracefully terminating an injected envoy-proxy sidecar so the Job can complete. Enable this when sidecar injection cannot be disabled per pod (e.g. when enforced by a cluster-wide Istio policy). Note: requires `curl` to be available in the init container image (it is included in the default `apache/superset` image) and only takes effect when `init.command` sources the rendered `superset_init.sh`. |
| init.jobAnnotations."helm.sh/hook" | string | `"post-install,post-upgrade"` | |
| init.jobAnnotations."helm.sh/hook-delete-policy" | string | `"before-hook-creation"` | |
| init.loadExamples | bool | `false` | |

View File

@@ -685,14 +685,6 @@ TALISMAN_CONFIG = {
{{- define "superset.initScript" -}}
#!/bin/sh
set -eu
{{- if dig "istio" "terminateSidecarOnExit" false .Values.init }}
# Notify the Istio pilot-agent sidecar to exit when this script completes
# (whether successfully or via `set -e`), so that the Job can reach the
# Completed state instead of hanging on a still-running envoy-proxy.
# See https://github.com/apache/superset/issues/25798
ISTIO_QUIT_ENDPOINT={{ dig "istio" "quitEndpoint" "http://localhost:15020/quitquitquit" .Values.init | replace "'" "'\\''" | squote }}
trap 'rc=$?; curl -fsS -m 5 -X POST "$ISTIO_QUIT_ENDPOINT" >/dev/null 2>&1 || echo "WARNING: failed to notify Istio sidecar at $ISTIO_QUIT_ENDPOINT to quit; the Job may hang if sidecar injection is active" >&2; exit $rc' EXIT
{{- end }}
echo "Upgrading DB schema..."
superset db upgrade
echo "Initializing roles and permissions..."

View File

@@ -35,7 +35,6 @@ spec:
{{- if .Values.init.podAnnotations }}
annotations: {{- toYaml .Values.init.podAnnotations | nindent 8 }}
{{- end }}
{{- $istioDisableInject := dig "istio" "disableSidecarInjection" false .Values.init }}
labels:
{{- include "superset.componentSelectorLabels" (dict "component" "init" "root" .) | nindent 8 }}
job: {{ template "superset.fullname" . }}-init-db
@@ -45,9 +44,6 @@ spec:
{{- if .Values.init.podLabels }}
{{- toYaml .Values.init.podLabels | nindent 8 }}
{{- end }}
{{- if $istioDisableInject }}
sidecar.istio.io/inject: "false"
{{- end }}
spec:
{{- if .Values.init.additionalPodSpec }}
{{- tpl (toYaml .Values.init.additionalPodSpec) . | nindent 6 }}

View File

@@ -1,183 +0,0 @@
#!/usr/bin/env bash
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Renders the chart with `helm template` for several `init.istio.*` value
# combinations and asserts that the relevant manifests contain (or omit)
# the expected fields. Intended to be run from the chart directory or via
# `bash helm/superset/tests/test-istio.sh` from the repo root.
#
# Covers the fix for:
# https://github.com/apache/superset/issues/25798
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CHART_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
pass=0
fail=0
assert_contains() {
local label="$1"
local needle="$2"
local haystack="$3"
if grep -qF -- "${needle}" <<<"${haystack}"; then
echo " PASS: ${label}"
pass=$((pass + 1))
else
echo " FAIL: ${label}"
echo " expected to contain: ${needle}"
fail=$((fail + 1))
fi
}
assert_not_contains() {
local label="$1"
local needle="$2"
local haystack="$3"
if ! grep -qF -- "${needle}" <<<"${haystack}"; then
echo " PASS: ${label}"
pass=$((pass + 1))
else
echo " FAIL: ${label}"
echo " expected NOT to contain: ${needle}"
fail=$((fail + 1))
fi
}
render() {
helm template release "${CHART_DIR}" "$@"
}
extract_init_job() {
awk '
/^# Source: superset\/templates\/init-job\.yaml/ { capture = 1 }
capture && /^---$/ { capture = 0 }
capture { print }
' <<<"$1"
}
extract_config_secret() {
awk '
/^# Source: superset\/templates\/secret-superset-config\.yaml/ { capture = 1 }
capture && /^---$/ { capture = 0 }
capture { print }
' <<<"$1"
}
echo "==> defaults: no istio mitigations applied"
out_default="$(render)"
init_job_default="$(extract_init_job "${out_default}")"
config_default="$(extract_config_secret "${out_default}")"
assert_not_contains "default does not set sidecar.istio.io/inject label" \
'sidecar.istio.io/inject' "${init_job_default}"
assert_not_contains "default initscript does not register quitquitquit trap" \
'quitquitquit' "${config_default}"
echo "==> init.istio.disableSidecarInjection=true"
out_disable="$(render --set init.istio.disableSidecarInjection=true)"
init_job_disable="$(extract_init_job "${out_disable}")"
config_disable="$(extract_config_secret "${out_disable}")"
assert_contains "init job sets sidecar.istio.io/inject: \"false\"" \
'sidecar.istio.io/inject: "false"' "${init_job_disable}"
assert_not_contains "disableSidecarInjection alone does not add quitquitquit trap" \
'quitquitquit' "${config_disable}"
echo "==> init.istio.terminateSidecarOnExit=true"
out_terminate="$(render --set init.istio.terminateSidecarOnExit=true)"
init_job_terminate="$(extract_init_job "${out_terminate}")"
config_terminate="$(extract_config_secret "${out_terminate}")"
assert_not_contains "terminateSidecarOnExit alone does not add inject label" \
'sidecar.istio.io/inject' "${init_job_terminate}"
assert_contains "initscript registers EXIT trap that calls quitquitquit" \
"trap 'rc=\$?; curl -fsS -m 5 -X POST" "${config_terminate}"
assert_contains "initscript trap targets the configured quit endpoint" \
'http://localhost:15020/quitquitquit' "${config_terminate}"
assert_contains "initscript trap binds to the EXIT signal" \
"' EXIT" "${config_terminate}"
echo "==> init.istio.quitEndpoint override"
out_endpoint="$(render --set init.istio.terminateSidecarOnExit=true \
--set init.istio.quitEndpoint=http://127.0.0.1:15020/quitquitquit)"
config_endpoint="$(extract_config_secret "${out_endpoint}")"
assert_contains "trap honours custom quitEndpoint" \
'http://127.0.0.1:15020/quitquitquit' "${config_endpoint}"
echo "==> both options combined"
out_both="$(render --set init.istio.disableSidecarInjection=true \
--set init.istio.terminateSidecarOnExit=true)"
init_job_both="$(extract_init_job "${out_both}")"
config_both="$(extract_config_secret "${out_both}")"
assert_contains "combined: inject label present" \
'sidecar.istio.io/inject: "false"' "${init_job_both}"
assert_contains "combined: trap present" \
'quitquitquit' "${config_both}"
echo "==> existing init.podLabels are preserved alongside istio label"
out_labels="$(render --set init.istio.disableSidecarInjection=true \
--set init.podLabels.team=data-platform)"
init_job_labels="$(extract_init_job "${out_labels}")"
assert_contains "user-supplied podLabel still rendered" \
'team: data-platform' "${init_job_labels}"
assert_contains "istio inject label rendered alongside" \
'sidecar.istio.io/inject: "false"' "${init_job_labels}"
echo "==> init.istio explicitly overridden to null"
out_null_istio="$(render --set init.istio=null)"
init_job_null_istio="$(extract_init_job "${out_null_istio}")"
config_null_istio="$(extract_config_secret "${out_null_istio}")"
assert_not_contains "null init.istio does not set inject label" \
'sidecar.istio.io/inject' "${init_job_null_istio}"
assert_not_contains "null init.istio does not register quitquitquit trap" \
'quitquitquit' "${config_null_istio}"
echo "==> EXIT trap propagates the script's exit code, not the notification's"
# The trap's own curl call is best-effort (failures are logged with
# `|| echo ... >&2`, not swallowed with `|| true`) and must not mask a
# failed migration. Extract the two rendered lines and actually run them,
# with curl pointed at a closed local port so the notification itself fails,
# to make sure the wrapped script's real exit code still comes through.
quit_endpoint_line="$(grep -F 'ISTIO_QUIT_ENDPOINT=' <<<"${config_terminate}" || true)"
trap_line="$(grep -F "trap 'rc=\$?; curl" <<<"${config_terminate}" || true)"
if [[ -z "${quit_endpoint_line}" || -z "${trap_line}" ]]; then
echo " FAIL: script exit code (42) survives a failing quitquitquit notification"
echo " could not locate the rendered ISTIO_QUIT_ENDPOINT/trap lines to exercise"
fail=$((fail + 1))
else
set +e
(
eval "${quit_endpoint_line}"
ISTIO_QUIT_ENDPOINT="http://127.0.0.1:1/quitquitquit"
eval "${trap_line}"
exit 42
)
trap_test_rc=$?
set -e
if [[ "${trap_test_rc}" -eq 42 ]]; then
echo " PASS: script exit code (42) survives a failing quitquitquit notification"
pass=$((pass + 1))
else
echo " FAIL: script exit code (42) survives a failing quitquitquit notification"
echo " got exit code: ${trap_test_rc}"
fail=$((fail + 1))
fi
fi
echo
echo "passed: ${pass}, failed: ${fail}"
if [[ ${fail} -gt 0 ]]; then
exit 1
fi

View File

@@ -1241,31 +1241,6 @@ init:
topologySpreadConstraints: []
# -- Set priorityClassName for init job pods
priorityClassName: ~
# -- Configuration for compatibility with the Istio service mesh.
# Without these mitigations, when the init-db Job runs in a namespace with
# automatic Istio sidecar injection enabled, the injected envoy-proxy
# container keeps running after the init container has exited, preventing
# the Job from ever reaching the Completed state.
# See https://github.com/apache/superset/issues/25798
istio:
# -- When true, adds the `sidecar.istio.io/inject: "false"` label to the
# init job pod template, opting it out of Istio automatic sidecar
# injection. This is the recommended fix when the cluster's mesh policy
# allows opting out per pod.
disableSidecarInjection: false
# -- When true, the rendered `superset_init.sh` script (from the internal
# `superset.initScript` template) registers an `EXIT` trap that POSTs to
# the Istio pilot-agent's `/quitquitquit` endpoint after the init logic
# finishes, gracefully terminating an injected envoy-proxy sidecar so
# the Job can complete. Enable this when sidecar injection cannot be
# disabled per pod (e.g. when enforced by a cluster-wide Istio policy).
# Note: requires `curl` to be available in the init container image
# (it is included in the default `apache/superset` image) and only
# takes effect when `init.command` sources the rendered `superset_init.sh`.
terminateSidecarOnExit: false
# -- Endpoint that the trap POSTs to in order to terminate the sidecar.
# The default matches the standard Istio pilot-agent admin port.
quitEndpoint: "http://localhost:15020/quitquitquit"
# -- Configuration values for the postgresql dependency.
# ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql

View File

@@ -31,7 +31,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.3.0",
"@googleapis/sheets": "^13.0.2",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
@@ -45,9 +45,9 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^6.7.0",
"@rjsf/core": "^6.6.2",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.7.0",
"@rjsf/validator-ajv8": "^6.6.2",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",
@@ -4073,9 +4073,9 @@
}
},
"node_modules/@fontsource/ibm-plex-mono": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
"integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.2.7.tgz",
"integrity": "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -9963,33 +9963,33 @@
"license": "MIT"
},
"node_modules/@rjsf/core": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.7.0.tgz",
"integrity": "sha512-TdiiRf9H6R7mYXyRq1TQHhTSdX9lpvYHsfn1obWksICbZEg4jBFBh7BuyrcHhzGos6976GsLO4TTYw11fQUbpQ==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.6.2.tgz",
"integrity": "sha512-rmckOHIc0N3Vu0egcjrm4aRnw4ETW1i4fGdm0m63qd/298zHfF7x2md2rDh8F5HLgoxW3X/LdvrCHcsD1lxbGw==",
"license": "Apache-2.0",
"dependencies": {
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
"markdown-to-jsx": "^9.8.2",
"markdown-to-jsx": "^9.8.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@rjsf/utils": "^6.7.0",
"@rjsf/utils": "^6.6.x",
"react": ">=18"
}
},
"node_modules/@rjsf/utils": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.7.1.tgz",
"integrity": "sha512-6goBapMwyHcXvjLkCnFs4S3P1oKUi1H083BdPk4pDZALFWn5ZdG50ECNfHSddBmL3O0pyx1/WFq1C/MuR7Y54A==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.6.2.tgz",
"integrity": "sha512-npqdWuuFmCkiXETrDLRx8a4EwDJoUdTTnGhJA/774bwDgT99u4nAEyXbJd75VuobunewcLm2FOS2cErD3e28cg==",
"license": "Apache-2.0",
"dependencies": {
"@x0k/json-schema-merge": "^1.0.3",
"fast-equals": "^6.0.0",
"fast-uri": "^4.1.1",
"fast-uri": "^3.1.2",
"jsonpointer": "^5.0.1",
"lodash": "^4.18.1",
"lodash-es": "^4.18.1",
@@ -10003,9 +10003,9 @@
}
},
"node_modules/@rjsf/validator-ajv8": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.7.0.tgz",
"integrity": "sha512-GAo1BknPXVncMwCsnAg/UpLPvdzVuyB73FbdPe5p3VjefrdVFjbbtaYMsFUN5iGMKe5fIQOZD9ke5ajvTZjJPA==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.6.2.tgz",
"integrity": "sha512-pi+CBfkXxyR1JhEveuwi7qA9NcqYvFSvadY8LkQDC6wP89bZS8TYXzolirvXC7okDdI3WCtPjx/FYnxYpXbj7Q==",
"license": "Apache-2.0",
"dependencies": {
"ajv": "^8.20.0",
@@ -10017,7 +10017,7 @@
"node": ">=20"
},
"peerDependencies": {
"@rjsf/utils": "^6.7.0"
"@rjsf/utils": "^6.6.x"
}
},
"node_modules/@rtsao/scc": {

View File

@@ -116,7 +116,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@fontsource/fira-code": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/inter": "^5.3.0",
"@googleapis/sheets": "^13.0.2",
"@great-expectations/jsonforms-antd-renderers": "^2.2.10",
@@ -130,9 +130,9 @@
"@luma.gl/shadertools": "~9.2.5",
"@luma.gl/webgl": "~9.2.5",
"@reduxjs/toolkit": "^1.9.3",
"@rjsf/core": "^6.7.0",
"@rjsf/core": "^6.6.2",
"@rjsf/utils": "^6.6.2",
"@rjsf/validator-ajv8": "^6.7.0",
"@rjsf/validator-ajv8": "^6.6.2",
"@scarf/scarf": "^1.4.0",
"@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls",
"@superset-ui/core": "file:./packages/superset-ui-core",

View File

@@ -1,67 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { executeQuery } from './actions';
import fetchMock from 'fetch-mock';
fetchMock.post('glob:*/sqllab/execute', { result: [] });
afterAll(() => {
fetchMock.clearHistory().removeRoutes();
});
test('executeQuery', async () => {
const mockDispatch = jest.fn();
const mockedQueryExecutePayload = {
client_id: 'client_id_1',
database_id: 1,
runAsync: false,
catalog: null,
schema: 'schema_1',
sql: '1',
tmp_table_name: 'tmp_table_1',
select_as_cta: false,
ctas_method: 'SELECT',
queryLimit: 10,
expand_data: false,
};
const returnedDispatchFunc = executeQuery(mockedQueryExecutePayload);
await returnedDispatchFunc(mockDispatch);
const [
[setQueryIsLoadingActionObject],
[setQueryResultActionObject],
[setQueryIsNotLoadingActionObject],
] = mockDispatch.mock.calls;
expect(setQueryIsLoadingActionObject).toStrictEqual({
type: 'SET_QUERY_IS_LOADING',
payload: true,
});
expect(setQueryResultActionObject).toStrictEqual({
type: 'SET_QUERY_RESULT',
payload: {
result: [],
},
});
expect(setQueryIsNotLoadingActionObject).toStrictEqual({
type: 'SET_QUERY_IS_LOADING',
payload: false,
});
});

View File

@@ -67,7 +67,7 @@ export function executeQuery(payload: QueryExecutePayload) {
const result = await executeQueryApi(payload);
dispatch(setQueryResult(result as QueryExecuteResponse));
} catch (error) {
dispatch(setQueryError((error as Error).message));
dispatch(setQueryError(error.message));
} finally {
dispatch(setQueryIsLoading(false));
}

View File

@@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"cookie": "^2.0.1",
"hot-shots": "^17.0.1",
"hot-shots": "^17.0.0",
"ioredis": "^5.11.1",
"jsonwebtoken": "^9.0.3",
"lodash-es": "^4.18.1",
@@ -1737,9 +1737,9 @@
}
},
"node_modules/hot-shots": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-17.0.1.tgz",
"integrity": "sha512-szraf3bR/vhzRajfq0SnUf07hpraR5JMuwJ2zOoFdqfYrW8V3qdLTf8qJEgfPpjs19AVIqxJrZXaLgeL3IYlBA==",
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-17.0.0.tgz",
"integrity": "sha512-d3URpxEO5b0HQfZsrU2qRJ+cUPr9WBrsgHCVnD1bRdFI7KIZokNVI8n1uthsl4WB2l50WWS9vTlwJSOF/6I12w==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"

View File

@@ -19,7 +19,7 @@
"license": "Apache-2.0",
"dependencies": {
"cookie": "^2.0.1",
"hot-shots": "^17.0.1",
"hot-shots": "^17.0.0",
"ioredis": "^5.11.1",
"jsonwebtoken": "^9.0.3",
"lodash-es": "^4.18.1",

View File

@@ -1110,6 +1110,14 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
time_expr = time_expr.replace("{col}", cls.epoch_to_dttm())
elif pdf == "epoch_ms":
time_expr = time_expr.replace("{col}", cls.epoch_ms_to_dttm())
elif pdf == "%Y":
# a bare four-digit year (e.g. the `year` column on the `video_game_sales`
# example dataset) has no native date type to lean on; without this the
# column value is passed straight into the grain function below, which
# every engine interprets as something other than a calendar year (SQLite
# reads a bare integer as a Julian day number, for instance), silently
# producing NULL for every row.
time_expr = time_expr.replace("{col}", cls.year_to_dttm())
return TimestampExpression(time_expr, col, type_=col.type)
@@ -1326,6 +1334,17 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
"""
return cls.epoch_to_dttm().replace("{col}", "({col}/1000)")
@classmethod
def year_to_dttm(cls) -> str:
"""
SQL expression that converts a bare four-digit year value to the January 1st
datetime of that year, for use in a query. The reference column should be
denoted as `{col}` in the return expression, e.g. "MAKE_DATE({col}, 1, 1)"
:return: SQL Expression
"""
raise NotImplementedError()
@classmethod
def get_datatype(cls, type_code: Any) -> str | None:
"""

View File

@@ -126,6 +126,18 @@ class SqliteEngineSpec(BaseEngineSpec):
def epoch_to_dttm(cls) -> str:
return "datetime({col}, 'unixepoch')"
@classmethod
def year_to_dttm(cls) -> str:
# SQLite's date functions parse a 'YYYY-01-01' string just fine, but won't
# accept a bare integer/real year (it's read as a Julian day number instead).
# The CASE guard is needed because printf() treats a NULL argument as 0,
# which would otherwise turn a missing year into '0000-01-01' rather than
# propagating the NULL.
return (
"CASE WHEN {col} IS NULL THEN NULL "
"ELSE printf('%04d-01-01', CAST({col} AS INTEGER)) END"
)
@classmethod
def convert_dttm(
cls, target_type: str, dttm: datetime, db_extra: dict[str, Any] | None = None

View File

@@ -131,3 +131,34 @@ def test_time_grain_expressions(dttm: str, grain: str, expected: str) -> None:
with engine.connect() as connection:
result = connection.execute(text(sql)).scalar()
assert result == expected
@pytest.mark.parametrize(
"year,expected",
[
(2013, "2013-01-01 00:00:00"),
(2013.0, "2013-01-01 00:00:00"),
(None, None),
],
)
def test_year_pdf_time_grain(year: Optional[float], expected: Optional[str]) -> None:
"""A bare four-digit year (e.g. the `year` column on the `video_game_sales`
example dataset) has no native date type; without `year_to_dttm` the raw
value is passed straight into the grain function, which SQLite reads as a
Julian day number rather than a calendar year, silently producing NULL."""
from sqlalchemy import column
from superset.db_engine_specs.sqlite import SqliteEngineSpec
engine = create_engine("sqlite://", future=True)
with engine.begin() as connection:
connection.execute(text("CREATE TABLE t (year REAL)"))
connection.execute(text("INSERT INTO t VALUES (:year)"), {"year": year})
expression = SqliteEngineSpec.get_timestamp_expr(
col=column("year"), pdf="%Y", time_grain=TimeGrain.YEAR
)
sql = f"SELECT {expression} FROM t" # noqa: S608
with engine.connect() as connection:
result = connection.execute(text(sql)).scalar()
assert result == expected