mirror of
https://github.com/apache/superset.git
synced 2026-07-19 21:25:38 +00:00
Compare commits
8 Commits
superset-h
...
refactor/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
432fcd1545 | ||
|
|
1d2aa9439b | ||
|
|
6625a7c547 | ||
|
|
42a2aede78 | ||
|
|
c4d50472a9 | ||
|
|
cd045886d0 | ||
|
|
751f5eb663 | ||
|
|
4e098b6f38 |
@@ -125,6 +125,13 @@ Alternatively, perform a fresh install. This is a one-time migration; subsequent
|
||||
| fullnameOverride | string | `nil` | Provide a name to override the full names of resources |
|
||||
| globalPodAnnotations | object | `{}` | Global pod annotations to be added to all pods Use this to set annotations that apply to all Superset components Component-specific podAnnotations will be merged with these global annotations |
|
||||
| hostAliases | list | `[]` | Custom hostAliases for all superset pods # https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ |
|
||||
| httproute | object | see `values.yaml` | Gateway API HTTPRoute for exposing Superset via a Gateway. Requires the Gateway API CRDs (gateway.networking.k8s.io/v1) installed in the cluster. |
|
||||
| httproute.annotations | object | `{}` | Annotations to add to the HTTPRoute |
|
||||
| httproute.apiVersion | string | `"gateway.networking.k8s.io/v1"` | HTTPRoute apiVersion. Override to gateway.networking.k8s.io/v1beta1 for older Gateway API installations that have not promoted HTTPRoute to v1. |
|
||||
| httproute.hostnames | list | `[]` | Hostnames that match against the HTTP Host header (templated) |
|
||||
| httproute.labels | object | `{}` | Additional labels to add to the HTTPRoute |
|
||||
| httproute.parentRefs | list | `[]` | Gateways this HTTPRoute attaches to |
|
||||
| httproute.rules | list | `[{"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | Routing rules. Each rule is backed by the Superset service. Set `weight` per rule to leave room for traffic splitting (defaults to 1). When `supersetWebsockets.enabled` is true, an extra rule routing `supersetWebsockets.ingress.path` to the `-ws` service is appended automatically, mirroring the ingress behavior. |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"apachesuperset.docker.scarf.sh/apache/superset"` | |
|
||||
| image.tag | string | `nil` | |
|
||||
|
||||
83
helm/superset/templates/httproute.yaml
Normal file
83
helm/superset/templates/httproute.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
{{/*
|
||||
|
||||
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.
|
||||
|
||||
*/}}
|
||||
|
||||
{{- if .Values.httproute.enabled -}}
|
||||
{{- $fullName := include "superset.fullname" . -}}
|
||||
apiVersion: {{ .Values.httproute.apiVersion | default "gateway.networking.k8s.io/v1" }}
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app: {{ template "superset.name" . }}
|
||||
chart: {{ template "superset.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- if .Values.extraLabels }}
|
||||
{{- toYaml .Values.extraLabels | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.httproute.labels }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.httproute.annotations }}
|
||||
annotations: {{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- with .Values.httproute.parentRefs }}
|
||||
parentRefs:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.httproute.hostnames }}
|
||||
hostnames:
|
||||
{{- tpl (toYaml .) $ | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.httproute.rules }}
|
||||
- backendRefs:
|
||||
- group: ''
|
||||
kind: Service
|
||||
name: {{ $fullName }}
|
||||
port: {{ $.Values.service.port }}
|
||||
weight: {{ .weight | default 1 }}
|
||||
{{- with .filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .matches }}
|
||||
matches:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .timeouts }}
|
||||
timeouts:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.supersetWebsockets.enabled }}
|
||||
- backendRefs:
|
||||
- group: ''
|
||||
kind: Service
|
||||
name: {{ $fullName }}-ws
|
||||
port: {{ .Values.supersetWebsockets.service.port }}
|
||||
weight: 1
|
||||
matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: {{ .Values.supersetWebsockets.ingress.path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -246,6 +246,40 @@ ingress:
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
# -- Gateway API HTTPRoute for exposing Superset via a Gateway.
|
||||
# Requires the Gateway API CRDs (gateway.networking.k8s.io/v1) installed in the cluster.
|
||||
# @default -- see `values.yaml`
|
||||
httproute:
|
||||
enabled: false
|
||||
# -- HTTPRoute apiVersion. Override to gateway.networking.k8s.io/v1beta1 for
|
||||
# older Gateway API installations that have not promoted HTTPRoute to v1.
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
# -- Additional labels to add to the HTTPRoute
|
||||
labels: {}
|
||||
# -- Annotations to add to the HTTPRoute
|
||||
annotations: {}
|
||||
# -- Gateways this HTTPRoute attaches to
|
||||
parentRefs: []
|
||||
# - name: my-gateway
|
||||
# namespace: gateway-system
|
||||
# sectionName: https
|
||||
# -- Hostnames that match against the HTTP Host header (templated)
|
||||
hostnames: []
|
||||
# - chart-example.local
|
||||
# -- Routing rules. Each rule is backed by the Superset service. Set `weight`
|
||||
# per rule to leave room for traffic splitting (defaults to 1). When
|
||||
# `supersetWebsockets.enabled` is true, an extra rule routing
|
||||
# `supersetWebsockets.ingress.path` to the `-ws` service is appended
|
||||
# automatically, mirroring the ingress behavior.
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
# weight: 1
|
||||
# filters: []
|
||||
# timeouts: {}
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
|
||||
@@ -84,7 +84,7 @@ dependencies = [
|
||||
"packaging",
|
||||
# --------------------------
|
||||
# pandas and related (wanting pandas[performance] without numba as it's 100+MB and not needed)
|
||||
"pandas[excel]>=2.1.4, <2.4",
|
||||
"pandas[excel]>=2.3.3, <2.4",
|
||||
"bottleneck", # recommended performance dependency for pandas, see https://pandas.pydata.org/docs/getting_started/install.html#performance-dependencies-recommended
|
||||
# --------------------------
|
||||
"parsedatetime",
|
||||
|
||||
@@ -31,7 +31,7 @@ filterwarnings =
|
||||
error:Passing a string to Connection.execute\(\) is deprecated:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"Query" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"SavedQuery" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
error:"SqlaTable" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"SqlMetric" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"TableColumn" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
# error:"TaggedObject" object is being merged into a Session:sqlalchemy.exc.RemovedIn20Warning
|
||||
|
||||
@@ -275,7 +275,7 @@ packaging==25.0
|
||||
# kombu
|
||||
# limits
|
||||
# shillelagh
|
||||
pandas==2.1.4
|
||||
pandas==2.3.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
paramiko==3.5.1
|
||||
# via
|
||||
@@ -331,6 +331,8 @@ pyparsing==3.2.3
|
||||
# via apache-superset (pyproject.toml)
|
||||
pysocks==1.7.1
|
||||
# via urllib3
|
||||
python-calamine==0.8.2
|
||||
# via pandas
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# apache-superset (pyproject.toml)
|
||||
|
||||
@@ -638,7 +638,7 @@ packaging==25.0
|
||||
# pytest
|
||||
# shillelagh
|
||||
# sqlalchemy-bigquery
|
||||
pandas==2.1.4
|
||||
pandas==2.3.3
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# apache-superset
|
||||
@@ -819,6 +819,10 @@ pytest-mock==3.10.0
|
||||
# via
|
||||
# apache-superset
|
||||
# apache-superset-extensions-cli
|
||||
python-calamine==0.8.2
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
# pandas
|
||||
python-dateutil==2.9.0.post0
|
||||
# via
|
||||
# -c requirements/base-constraint.txt
|
||||
|
||||
@@ -100,20 +100,21 @@ const processComparisonTotals = (
|
||||
return totals;
|
||||
}
|
||||
const transformedTotals: DataRecord = {};
|
||||
const mainLabel = t('Main');
|
||||
totals.map((totalRecord: DataRecord) =>
|
||||
Object.keys(totalRecord).forEach(key => {
|
||||
if (totalRecord[key] !== undefined && !key.includes(comparisonSuffix)) {
|
||||
transformedTotals[`Main ${key}`] =
|
||||
parseInt(transformedTotals[`Main ${key}`]?.toString() || '0', 10) +
|
||||
parseInt(totalRecord[key]?.toString() || '0', 10);
|
||||
transformedTotals[`${mainLabel} ${key}`] =
|
||||
parseFloat(
|
||||
transformedTotals[`${mainLabel} ${key}`]?.toString() || '0',
|
||||
) + parseFloat(totalRecord[key]?.toString() || '0');
|
||||
transformedTotals[`# ${key}`] =
|
||||
parseInt(transformedTotals[`# ${key}`]?.toString() || '0', 10) +
|
||||
parseInt(
|
||||
parseFloat(transformedTotals[`# ${key}`]?.toString() || '0') +
|
||||
parseFloat(
|
||||
totalRecord[`${key}__${comparisonSuffix}`]?.toString() || '0',
|
||||
10,
|
||||
);
|
||||
const { valueDifference, percentDifferenceNum } = calculateDifferences(
|
||||
transformedTotals[`Main ${key}`] as number,
|
||||
transformedTotals[`${mainLabel} ${key}`] as number,
|
||||
transformedTotals[`# ${key}`] as number,
|
||||
);
|
||||
transformedTotals[`△ ${key}`] = valueDifference;
|
||||
@@ -172,6 +173,7 @@ const processComparisonDataRecords = memoizeOne(
|
||||
originalData: DataRecord[] | undefined,
|
||||
originalColumns: DataColumnMeta[],
|
||||
comparisonSuffix: string,
|
||||
mainLabel: string,
|
||||
) {
|
||||
// Transform data
|
||||
return originalData?.map(originalItem => {
|
||||
@@ -193,7 +195,7 @@ const processComparisonDataRecords = memoizeOne(
|
||||
comparisonValue as number,
|
||||
);
|
||||
|
||||
transformedItem[`Main ${origCol.key}`] = originalValue;
|
||||
transformedItem[`${mainLabel} ${origCol.key}`] = originalValue;
|
||||
transformedItem[`# ${origCol.key}`] = comparisonValue;
|
||||
transformedItem[`△ ${origCol.key}`] = valueDifference;
|
||||
transformedItem[`% ${origCol.key}`] = percentDifferenceNum;
|
||||
@@ -715,6 +717,7 @@ const transformProps = (
|
||||
baseQuery?.data,
|
||||
columns,
|
||||
comparisonSuffix,
|
||||
t('Main'),
|
||||
);
|
||||
|
||||
const passedData = isUsingTimeComparison ? comparisonData || [] : data;
|
||||
|
||||
@@ -522,9 +522,21 @@ test('should disable both buttons when no actions available', () => {
|
||||
expect(onRedo).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should render the "Discard changes" button', () => {
|
||||
test('should render the "Discard" button as disabled', () => {
|
||||
setup(editableState);
|
||||
expect(screen.getByText('Discard')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should enable the "Discard" button when there are unsaved changes', () => {
|
||||
const unsavedState = {
|
||||
...editableState,
|
||||
dashboardState: {
|
||||
...editableState.dashboardState,
|
||||
hasUnsavedChanges: true,
|
||||
},
|
||||
};
|
||||
setup(unsavedState);
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should render the "Save" button as disabled', () => {
|
||||
|
||||
@@ -722,6 +722,7 @@ const Header = (): JSX.Element => {
|
||||
<Button
|
||||
css={discardBtnStyle}
|
||||
buttonSize="small"
|
||||
disabled={!hasUnsavedChanges}
|
||||
onClick={discardChanges}
|
||||
buttonStyle="secondary"
|
||||
data-test="discard-changes-button"
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function Login() {
|
||||
sessionStorage.setItem('login_attempted', 'true');
|
||||
|
||||
// Use standard form submission for Flask-AppBuilder compatibility
|
||||
SupersetClient.postForm(loginEndpoint, values, '');
|
||||
SupersetClient.postForm(ensureAppRoot(loginEndpoint), values, '');
|
||||
};
|
||||
|
||||
const getAuthIconElement = (
|
||||
|
||||
@@ -28,6 +28,16 @@ from werkzeug.local import LocalProxy
|
||||
# form.
|
||||
flask_appbuilder.Model.__allow_unmapped__ = True
|
||||
|
||||
# pandas >= 2.2 advertises a minimum SQLAlchemy of 2.0 and silently ignores
|
||||
# older installations, breaking DataFrame.to_sql / read_sql with SQLAlchemy
|
||||
# 1.4 engines. Its SQL layer still works with 1.4, so restore support until
|
||||
# Superset itself requires SQLAlchemy >= 2. Must run before any pandas SQL IO.
|
||||
from superset.utils.pandas_sqlalchemy_compat import ( # noqa: E402
|
||||
restore_pandas_sqlalchemy_support,
|
||||
)
|
||||
|
||||
restore_pandas_sqlalchemy_support()
|
||||
|
||||
from superset.app import create_app # noqa: E402, F401
|
||||
from superset.extensions import ( # noqa: E402
|
||||
appbuilder, # noqa: F401
|
||||
|
||||
@@ -1364,7 +1364,14 @@ class SqlaTable(
|
||||
|
||||
database: Database = relationship(
|
||||
"Database",
|
||||
backref=backref("tables", cascade="all, delete-orphan"),
|
||||
backref=backref(
|
||||
"tables",
|
||||
cascade="all, delete-orphan",
|
||||
# SQLAlchemy 2.0 behavior: assigning `table.database` no longer
|
||||
# cascades the SqlaTable into the Database's session; callers must
|
||||
# add objects to a session explicitly.
|
||||
cascade_backrefs=False,
|
||||
),
|
||||
foreign_keys=[database_id],
|
||||
)
|
||||
schema = Column(String(255))
|
||||
|
||||
@@ -173,6 +173,13 @@ def load_parquet_table( # noqa: C901
|
||||
|
||||
if not tbl:
|
||||
tbl = SqlaTable(table_name=table_name, database_id=database.id)
|
||||
# Explicitly add the new table to the session. Assigning `tbl.database`
|
||||
# below no longer implicitly adds `tbl` to the session (SQLAlchemy 2.0
|
||||
# behavior, cascade_backrefs=False), so without this, the two
|
||||
# `db.session.merge()` calls below (one inside `fetch_metadata()`, one
|
||||
# at the end of this function) would each create a separate transient
|
||||
# copy of `tbl`, resulting in two pending inserts for the same uuid.
|
||||
db.session.add(tbl)
|
||||
# Set the database reference
|
||||
tbl.database = database
|
||||
|
||||
@@ -187,7 +194,7 @@ def load_parquet_table( # noqa: C901
|
||||
tbl.fetch_metadata()
|
||||
|
||||
db.session.merge(tbl)
|
||||
db.session.commit()
|
||||
db.session.commit() # pylint: disable=consider-using-transaction
|
||||
|
||||
return tbl
|
||||
|
||||
@@ -242,7 +249,7 @@ def create_generic_loader(
|
||||
if description and tbl:
|
||||
tbl.description = description
|
||||
db.session.merge(tbl)
|
||||
db.session.commit()
|
||||
db.session.commit() # pylint: disable=consider-using-transaction
|
||||
|
||||
# Set function name and docstring
|
||||
loader.__name__ = f"load_{parquet_file}"
|
||||
|
||||
80
superset/utils/pandas_sqlalchemy_compat.py
Normal file
80
superset/utils/pandas_sqlalchemy_compat.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# 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.
|
||||
"""Compatibility shim letting pandas >= 2.2 use SQLAlchemy 1.4 engines.
|
||||
|
||||
pandas 2.2 raised its advertised minimum SQLAlchemy version to 2.0 as a
|
||||
support-policy change. When an older SQLAlchemy is installed, pandas does not
|
||||
fail loudly: ``pandas.io.sql`` silently pretends SQLAlchemy is absent, treats
|
||||
Engine/Connection arguments as raw DBAPI connections, and falls back to its
|
||||
sqlite-only code path, breaking every ``DataFrame.to_sql`` / ``read_sql``
|
||||
call site (dataset uploads, example data loading, annotation queries, filter
|
||||
values).
|
||||
|
||||
The pandas SQL layer itself still works with SQLAlchemy 1.4 because it only
|
||||
uses the API subset common to SQLAlchemy 1.4 and 2.x. Lowering the advertised
|
||||
minimum back to the pandas 2.1 value restores the working behavior.
|
||||
|
||||
This module is obsolete once Superset requires SQLAlchemy >= 2; at that point
|
||||
the patch becomes a no-op and the module (and its call site in
|
||||
``superset/__init__.py``) can be deleted.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import sqlalchemy
|
||||
from packaging.version import Version
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The last pandas release line to support SQLAlchemy 1.4 (pandas 2.1)
|
||||
# required at least this version.
|
||||
_SQLALCHEMY_MINIMUM = "1.4.16"
|
||||
|
||||
|
||||
def restore_pandas_sqlalchemy_support() -> None:
|
||||
"""Lower pandas' advertised SQLAlchemy minimum so 1.4 engines work.
|
||||
|
||||
Only applies when the installed SQLAlchemy predates 2.0 and pandas
|
||||
advertises a 2.x minimum; in every other combination this is a no-op.
|
||||
Safe to call multiple times.
|
||||
"""
|
||||
if Version(sqlalchemy.__version__) >= Version("2.0.0"):
|
||||
# pandas supports SQLAlchemy 2.x natively; nothing to patch.
|
||||
return
|
||||
|
||||
try:
|
||||
from pandas.compat import _optional
|
||||
except ImportError:
|
||||
# The private module moved in a newer pandas; SQL IO with a pre-2.0
|
||||
# SQLAlchemy will misbehave, so make the situation diagnosable.
|
||||
logger.warning(
|
||||
"Could not adjust pandas' minimum SQLAlchemy version; "
|
||||
"DataFrame.to_sql/read_sql may not accept SQLAlchemy %s engines",
|
||||
sqlalchemy.__version__,
|
||||
)
|
||||
return
|
||||
|
||||
advertised = _optional.VERSIONS.get("sqlalchemy")
|
||||
if advertised and Version(advertised) > Version(_SQLALCHEMY_MINIMUM):
|
||||
_optional.VERSIONS["sqlalchemy"] = _SQLALCHEMY_MINIMUM
|
||||
logger.debug(
|
||||
"Lowered pandas' minimum SQLAlchemy version from %s to %s so "
|
||||
"pandas SQL IO keeps working with the installed SQLAlchemy %s",
|
||||
advertised,
|
||||
_SQLALCHEMY_MINIMUM,
|
||||
sqlalchemy.__version__,
|
||||
)
|
||||
@@ -173,6 +173,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"'{{ 'xyz_' + time_grain }}' as time_grain",
|
||||
database=get_example_database(),
|
||||
)
|
||||
db.session.add(table)
|
||||
TableColumn(
|
||||
column_name="expr",
|
||||
expression="case when '{{ current_username() }}' = 'abc' "
|
||||
@@ -275,6 +276,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
table = SqlaTable(
|
||||
table_name="test_validate_adhoc_sql", database=get_example_database()
|
||||
)
|
||||
db.session.add(table)
|
||||
db.session.commit()
|
||||
|
||||
with pytest.raises(QueryObjectValidationError):
|
||||
|
||||
67
tests/unit_tests/utils/pandas_sqlalchemy_compat_test.py
Normal file
67
tests/unit_tests/utils/pandas_sqlalchemy_compat_test.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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 pandas as pd
|
||||
from sqlalchemy import create_engine, types
|
||||
|
||||
from superset.utils.pandas_sqlalchemy_compat import (
|
||||
restore_pandas_sqlalchemy_support,
|
||||
)
|
||||
|
||||
|
||||
def test_to_sql_accepts_sqlalchemy_engine_and_dtypes() -> None:
|
||||
"""
|
||||
``DataFrame.to_sql`` must accept a SQLAlchemy engine plus SQLAlchemy
|
||||
``dtype`` objects regardless of the installed pandas/SQLAlchemy combo.
|
||||
|
||||
This is the exact call shape used by dataset uploads
|
||||
(``BaseEngineSpec.df_to_sql``), example data loading, and the test data
|
||||
loaders; it breaks when pandas silently rejects the installed SQLAlchemy
|
||||
as too old (pandas >= 2.2 with SQLAlchemy 1.x) and no compat shim is
|
||||
applied.
|
||||
"""
|
||||
restore_pandas_sqlalchemy_support()
|
||||
|
||||
engine = create_engine("sqlite://")
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"name": ["a", "b"],
|
||||
"num": [1, 2],
|
||||
"ds": pd.to_datetime(["2021-01-01", "2021-01-02"]),
|
||||
}
|
||||
)
|
||||
df.to_sql(
|
||||
"birth_names",
|
||||
engine,
|
||||
index=False,
|
||||
dtype={"ds": types.DateTime(), "name": types.String(255)},
|
||||
method="multi",
|
||||
chunksize=100,
|
||||
)
|
||||
df.to_sql("birth_names", engine, index=False, if_exists="replace")
|
||||
|
||||
result = pd.read_sql_query("SELECT name, num FROM birth_names", engine)
|
||||
assert result["name"].tolist() == ["a", "b"]
|
||||
assert result["num"].tolist() == [1, 2]
|
||||
|
||||
|
||||
def test_restore_pandas_sqlalchemy_support_is_idempotent() -> None:
|
||||
from pandas.compat import _optional
|
||||
|
||||
restore_pandas_sqlalchemy_support()
|
||||
first = _optional.VERSIONS.get("sqlalchemy")
|
||||
restore_pandas_sqlalchemy_support()
|
||||
assert _optional.VERSIONS.get("sqlalchemy") == first
|
||||
Reference in New Issue
Block a user