diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 3ed19fff7..34687fad6 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -23,7 +23,22 @@ jobs:
# compatible/incompatible licenses addressed here: https://www.apache.org/legal/resolved.html
# find SPDX identifiers here: https://spdx.org/licenses/
deny-licenses: MS-LPL, BUSL-1.1, QPL-1.0, Sleepycat, SSPL-1.0, CPOL-1.02, AGPL-3.0, GPL-1.0+, BSD-4-Clause-UC, NPL-1.0, NPL-1.1, JSON
- # adding an exception for an ambigious license on store2, which has been resolved in the latest version. It's MIT: https://github.com/nbubna/store/blob/master/LICENSE-MIT
- # adding exception for all applitools modules (eyes-cypress and its dependencies), which has an explicit OSS license approved by ASF
- # license: https://applitools.com/legal/open-source-terms-of-use/
- allow-dependencies-licenses: 'pkg:npm/store2@2.14.2, pkg:npm/applitools/core, pkg:npm/applitools/core-base, pkg:npm/applitools/css-tree, pkg:npm/applitools/ec-client, pkg:npm/applitools/eg-socks5-proxy-server, pkg:npm/applitools/eyes, pkg:npm/applitools/eyes-cypress, pkg:npm/applitools/nml-client, pkg:npm/applitools/tunnel-client, pkg:npm/applitools/utils'
+ allow-dependencies-licenses:
+ # adding an exception for an ambigious license on store2, which has been resolved in
+ # the latest version. It's MIT: https://github.com/nbubna/store/blob/master/LICENSE-MIT
+ - 'pkg:npm/store2@2.14.2'
+ # adding exception for all applitools modules (eyes-cypress and its dependencies),
+ # which has an explicit OSS license approved by ASF
+ # license: https://applitools.com/legal/open-source-terms-of-use/
+ - 'pkg:npm/applitools/core'
+ - 'pkg:npm/applitools/core-base'
+ - 'pkg:npm/applitools/css-tree'
+ - 'pkg:npm/applitools/ec-client'
+ - 'pkg:npm/applitools/eg-socks5-proxy-server'
+ - 'pkg:npm/applitools/eyes'
+ - 'pkg:npm/applitools/eyes-cypress'
+ - 'pkg:npm/applitools/nml-client'
+ - 'pkg:npm/applitools/tunnel-client'
+ - 'pkg:npm/applitools/utils'
+ # Selecting BSD-3-Clause licensing terms for node-forge to ensure compatibility with Apache
+ - 'pkg:npm/node-forge@1.3.1'
diff --git a/.github/workflows/superset-docs-deploy.yml b/.github/workflows/superset-docs-deploy.yml
index a6eefe364..5563ce60b 100644
--- a/.github/workflows/superset-docs-deploy.yml
+++ b/.github/workflows/superset-docs-deploy.yml
@@ -39,6 +39,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '18'
+ - name: Setup Python
+ uses: ./.github/actions/setup-backend/
+ - name: Compute Entity Relationship diagram (ERD)
+ run: |
+ python scripts/erd.py
+ curl -L http://sourceforge.net/projects/plantuml/files/1.2023.7/plantuml.1.2023.7.jar/download > ~/plantuml.jar
+ java -jar ~/plantuml.jar -v -tsvg -r -o "${{ github.workspace }}/docs/static/img/erd.svg" "${{ github.workspace }}/scripts/erd/erd.puml"
- name: yarn install
run: |
yarn install --check-cache
diff --git a/.rat-excludes b/.rat-excludes
index e7f7b47a5..d5d11d9bb 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -66,3 +66,7 @@ google-big-query.svg
google-sheets.svg
postgresql.svg
snowflake.svg
+
+# docs-related
+erd.puml
+erd.svg
diff --git a/docs/docs/contributing/erd.mdx b/docs/docs/contributing/erd.mdx
new file mode 100644
index 000000000..da42bd054
--- /dev/null
+++ b/docs/docs/contributing/erd.mdx
@@ -0,0 +1,11 @@
+import InteractiveSVG from '../../src/components/InteractiveERDSVG';
+
+# Entity-Relationship Diagram
+
+Here is our interactive ERD:
+
+
+
+
+
+[Download the .svg](https://github.com/apache/superset/tree/master/docs/static/img/erd.svg)
diff --git a/docs/package.json b/docs/package.json
index a6d136cbc..3bb14910e 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -39,6 +39,7 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-github-btn": "^1.4.0",
+ "react-svg-pan-zoom": "^3.12.1",
"stream": "^0.0.2",
"swagger-ui-react": "^4.1.3",
"url-loader": "^4.1.1"
diff --git a/docs/src/components/InteractiveERDSVG.jsx b/docs/src/components/InteractiveERDSVG.jsx
new file mode 100644
index 000000000..70c26005c
--- /dev/null
+++ b/docs/src/components/InteractiveERDSVG.jsx
@@ -0,0 +1,38 @@
+/**
+ * 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 React from 'react';
+import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
+import ErdSvg from '../../static/img/erd.svg';
+
+function InteractiveERDSVG() {
+ return (
+
+
+
+ );
+}
+
+export default InteractiveERDSVG;
diff --git a/docs/static/img/erd.svg b/docs/static/img/erd.svg
new file mode 100644
index 000000000..3e4703e72
--- /dev/null
+++ b/docs/static/img/erd.svg
@@ -0,0 +1 @@
+
diff --git a/docs/yarn.lock b/docs/yarn.lock
index acb78d2ae..0e40a408f 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -1994,7 +1994,7 @@
"@docusaurus/theme-search-algolia" "2.4.3"
"@docusaurus/types" "2.4.3"
-"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
+"@docusaurus/react-loadable@5.5.2":
version "5.5.2"
resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz"
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
@@ -8223,6 +8223,15 @@ prop-types@^15.0.0, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
+prop-types@^15.8.1:
+ version "15.8.1"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
property-information@^5.0.0, property-information@^5.3.0:
version "5.6.0"
resolved "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz"
@@ -8841,7 +8850,7 @@ react-inspector@^5.1.1:
is-dom "^1.0.0"
prop-types "^15.0.0"
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -8878,6 +8887,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
+"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
+ version "5.5.2"
+ resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz"
+ integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
+ dependencies:
+ "@types/react" "*"
+ prop-types "^15.6.2"
+
react-redux@^7.2.4:
version "7.2.6"
resolved "https://registry.npmjs.org/react-redux/-/react-redux-7.2.6.tgz"
@@ -8925,6 +8942,14 @@ react-router@5.3.4, react-router@^5.3.3:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
+react-svg-pan-zoom@^3.12.1:
+ version "3.12.1"
+ resolved "https://registry.yarnpkg.com/react-svg-pan-zoom/-/react-svg-pan-zoom-3.12.1.tgz#971de6163fbad0d2a98d3ad7eb09bd1941564376"
+ integrity sha512-ug1LHCN5qed56C64xFypr/ClajuMFkig1OKvwJrIgGeSyHOjWM7XGgSgeP3IfHAkNw8QEc6a31ggZRpTijWYRw==
+ dependencies:
+ prop-types "^15.8.1"
+ transformation-matrix "^2.14.0"
+
react-syntax-highlighter@^15.4.5:
version "15.4.5"
resolved "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.4.5.tgz"
@@ -10069,6 +10094,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+transformation-matrix@^2.14.0:
+ version "2.16.1"
+ resolved "https://registry.yarnpkg.com/transformation-matrix/-/transformation-matrix-2.16.1.tgz#4a2de06331b94ae953193d1b9a5ba002ec5f658a"
+ integrity sha512-tdtC3wxVEuzU7X/ydL131Q3JU5cPMEn37oqVLITjRDSDsnSHVFzW2JiCLfZLIQEgWzZHdSy3J6bZzvKEN24jGA==
+
traverse@~0.6.6:
version "0.6.6"
resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz"
diff --git a/scripts/erd/erd.puml b/scripts/erd/erd.puml
new file mode 100644
index 000000000..5451b155b
--- /dev/null
+++ b/scripts/erd/erd.puml
@@ -0,0 +1,676 @@
+
+@startuml erd
+
+title Apache Superset ERD
+
+!theme blueprint
+
+' avoid problems with angled crows feet
+
+skinparam linetype ortho
+skinparam classBorderColor #grey
+
+skinparam classBorderColor<> #white
+skinparam classBorderThickness<> 1
+skinparam classLineStyle<> Dashed
+skinparam ClassBackgroundColor<> #204143
+
+' Models
+rectangle "Data Assets" #black {
+ entity "SqlMetric (sql_metrics)" as sql_metrics {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ metric_name: VARCHAR(255)
+ verbose_name: VARCHAR(1024)
+ metric_type: VARCHAR(32)
+ description: TEXT
+ d3format: VARCHAR(128)
+ currency: VARCHAR(128)
+ warning_text: TEXT
+ table_id: INTEGER
+ expression: TEXT
+ extra: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "DatabaseUserOAuth2Tokens (database_user_oauth2_tokens)" as database_user_oauth2_tokens {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ user_id: INTEGER
+ database_id: INTEGER
+ access_token: BLOB
+ access_token_expiration: DATETIME
+ refresh_token: BLOB
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Table (sl_tables)" as sl_tables {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ database_id: INTEGER
+ catalog: TEXT
+ schema: TEXT
+ name: TEXT
+ is_managed_externally: BOOLEAN
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Database (dbs)" as dbs {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ verbose_name: VARCHAR(250)
+ database_name: VARCHAR(250)
+ sqlalchemy_uri: VARCHAR(1024)
+ password: BLOB
+ cache_timeout: INTEGER
+ select_as_create_table_as: BOOLEAN
+ expose_in_sqllab: BOOLEAN
+ configuration_method: VARCHAR(255)
+ allow_run_async: BOOLEAN
+ allow_file_upload: BOOLEAN
+ allow_ctas: BOOLEAN
+ allow_cvas: BOOLEAN
+ allow_dml: BOOLEAN
+ force_ctas_schema: VARCHAR(250)
+ extra: TEXT
+ encrypted_extra: BLOB
+ impersonate_user: BOOLEAN
+ server_cert: BLOB
+ is_managed_externally: BOOLEAN
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Dataset (sl_datasets)" as sl_datasets {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ database_id: INTEGER
+ is_physical: BOOLEAN
+ is_managed_externally: BOOLEAN
+ name: TEXT
+ expression: TEXT
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "SqlaTable (tables)" as tables {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ description: TEXT
+ default_endpoint: TEXT
+ is_featured: BOOLEAN
+ filter_select_enabled: BOOLEAN
+ offset: INTEGER
+ cache_timeout: INTEGER
+ params: VARCHAR(1000)
+ perm: VARCHAR(1000)
+ schema_perm: VARCHAR(1000)
+ is_managed_externally: BOOLEAN
+ external_url: TEXT
+ table_name: VARCHAR(250)
+ main_dttm_col: VARCHAR(250)
+ database_id: INTEGER
+ fetch_values_predicate: TEXT
+ schema: VARCHAR(255)
+ sql: TEXT
+ is_sqllab_view: BOOLEAN
+ template_params: TEXT
+ extra: TEXT
+ normalize_columns: BOOLEAN
+ always_filter_main_dttm: BOOLEAN
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "TableColumn (table_columns)" as table_columns {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ column_name: VARCHAR(255)
+ verbose_name: VARCHAR(1024)
+ is_active: BOOLEAN
+ type: TEXT
+ advanced_data_type: VARCHAR(255)
+ groupby: BOOLEAN
+ filterable: BOOLEAN
+ description: TEXT
+ table_id: INTEGER
+ is_dttm: BOOLEAN
+ expression: TEXT
+ python_date_format: VARCHAR(255)
+ extra: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Column (sl_columns)" as sl_columns {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ is_additive: BOOLEAN
+ is_aggregation: BOOLEAN
+ is_filterable: BOOLEAN
+ is_dimensional: BOOLEAN
+ is_increase_desired: BOOLEAN
+ is_managed_externally: BOOLEAN
+ is_partition: BOOLEAN
+ is_physical: BOOLEAN
+ is_spatial: BOOLEAN
+ is_temporal: BOOLEAN
+ name: TEXT
+ type: TEXT
+ advanced_data_type: TEXT
+ expression: TEXT
+ unit: TEXT
+ description: TEXT
+ warning_text: TEXT
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "RowLevelSecurityFilter (row_level_security_filters)" as row_level_security_filters {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ name: VARCHAR(255)
+ description: TEXT
+ filter_type: VARCHAR(7)
+ group_key: VARCHAR(255)
+ clause: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ }
+rectangle "System" #black {
+ entity "KeyValueEntry (key_value)" as key_value {
+ uuid: BINARY(16)
+ id: INTEGER
+ resource: VARCHAR(32)
+ value: BLOB
+ created_on: DATETIME
+ created_by_fk: INTEGER
+ changed_on: DATETIME
+ expires_on: DATETIME
+ changed_by_fk: INTEGER
+ }
+ entity "SSHTunnel (ssh_tunnels)" as ssh_tunnels {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ database_id: INTEGER
+ server_address: TEXT
+ server_port: INTEGER
+ username: BLOB
+ password: BLOB
+ private_key: BLOB
+ private_key_password: BLOB
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "KeyValue (keyvalue)" as keyvalue {
+ id: INTEGER
+ value: TEXT
+ }
+ entity "CacheKey (cache_keys)" as cache_keys {
+ id: INTEGER
+ cache_key: VARCHAR(256)
+ cache_timeout: INTEGER
+ datasource_uid: VARCHAR(64)
+ created_on: DATETIME
+ }
+ entity "Log (logs)" as logs {
+ id: INTEGER
+ action: VARCHAR(512)
+ user_id: INTEGER
+ dashboard_id: INTEGER
+ slice_id: INTEGER
+ json: TEXT
+ dttm: DATETIME
+ duration_ms: INTEGER
+ referrer: VARCHAR(1024)
+ }
+ }
+rectangle "SQL Lab" #black {
+ entity "SavedQuery (saved_query)" as saved_query {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ user_id: INTEGER
+ db_id: INTEGER
+ schema: VARCHAR(128)
+ label: VARCHAR(256)
+ description: TEXT
+ sql: TEXT
+ template_parameters: TEXT
+ rows: INTEGER
+ last_run: DATETIME
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "TableSchema (table_schema)" as table_schema {
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ tab_state_id: INTEGER
+ database_id: INTEGER
+ schema: VARCHAR(256)
+ table: VARCHAR(256)
+ description: TEXT
+ expanded: BOOLEAN
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Query (query)" as query {
+ tracking_url: TEXT
+ extra_json: TEXT
+ id: INTEGER
+ client_id: VARCHAR(11)
+ database_id: INTEGER
+ tmp_table_name: VARCHAR(256)
+ tmp_schema_name: VARCHAR(256)
+ user_id: INTEGER
+ status: VARCHAR(16)
+ tab_name: VARCHAR(256)
+ sql_editor_id: VARCHAR(256)
+ schema: VARCHAR(256)
+ sql: TEXT
+ select_sql: TEXT
+ executed_sql: TEXT
+ limit: INTEGER
+ limiting_factor: VARCHAR(18)
+ select_as_cta: BOOLEAN
+ select_as_cta_used: BOOLEAN
+ ctas_method: VARCHAR(16)
+ progress: INTEGER
+ rows: INTEGER
+ error_message: TEXT
+ results_key: VARCHAR(64)
+ start_time: NUMERIC(20, 6)
+ start_running_time: NUMERIC(20, 6)
+ end_time: NUMERIC(20, 6)
+ end_result_backend_time: NUMERIC(20, 6)
+ changed_on: DATETIME
+ }
+ entity "TabState (tab_state)" as tab_state {
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ user_id: INTEGER
+ label: VARCHAR(256)
+ active: BOOLEAN
+ database_id: INTEGER
+ schema: VARCHAR(256)
+ sql: TEXT
+ query_limit: INTEGER
+ latest_query_id: INTEGER
+ autorun: BOOLEAN
+ template_params: TEXT
+ hide_left_bar: BOOLEAN
+ saved_query_id: INTEGER
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ }
+rectangle "Core" #black {
+ entity "FavStar (favstar)" as favstar {
+ id: INTEGER
+ user_id: INTEGER
+ class_name: VARCHAR(50)
+ obj_id: INTEGER
+ dttm: DATETIME
+ }
+ entity "Dashboard (dashboards)" as dashboards {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ dashboard_title: VARCHAR(500)
+ position_json: TEXT
+ description: TEXT
+ css: TEXT
+ certified_by: TEXT
+ certification_details: TEXT
+ json_metadata: TEXT
+ slug: VARCHAR(255)
+ published: BOOLEAN
+ is_managed_externally: BOOLEAN
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Annotation (annotation)" as annotation {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ start_dttm: DATETIME
+ end_dttm: DATETIME
+ layer_id: INTEGER
+ short_descr: VARCHAR(500)
+ long_descr: TEXT
+ json_metadata: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "EmbeddedDashboard (embedded_dashboards)" as embedded_dashboards {
+ created_on: DATETIME
+ changed_on: DATETIME
+ uuid: BINARY(16)
+ allow_domain_list: TEXT
+ dashboard_id: INTEGER
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Slice (slices)" as slices {
+ uuid: BINARY(16)
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ slice_name: VARCHAR(250)
+ datasource_id: INTEGER
+ datasource_type: VARCHAR(200)
+ datasource_name: VARCHAR(2000)
+ viz_type: VARCHAR(250)
+ params: TEXT
+ query_context: TEXT
+ description: TEXT
+ cache_timeout: INTEGER
+ perm: VARCHAR(1000)
+ schema_perm: VARCHAR(1000)
+ last_saved_at: DATETIME
+ last_saved_by_fk: INTEGER
+ certified_by: TEXT
+ certification_details: TEXT
+ is_managed_externally: BOOLEAN
+ external_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "TaggedObject (tagged_object)" as tagged_object {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ tag_id: INTEGER
+ object_id: INTEGER
+ object_type: VARCHAR(9)
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "AnnotationLayer (annotation_layer)" as annotation_layer {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ name: VARCHAR(250)
+ descr: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "DynamicPlugin (dynamic_plugin)" as dynamic_plugin {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ name: TEXT
+ key: TEXT
+ bundle_url: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "Tag (tag)" as tag {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ name: VARCHAR(250)
+ type: VARCHAR(12)
+ description: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "CssTemplate (css_templates)" as css_templates {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ template_name: VARCHAR(250)
+ css: TEXT
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "UserAttribute (user_attribute)" as user_attribute {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ user_id: INTEGER
+ welcome_dashboard_id: INTEGER
+ avatar_url: VARCHAR(100)
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ }
+rectangle "Inherited from Flask App Builder (FAB)" #black {
+ entity "ViewMenu (ab_view_menu)" as ab_view_menu {
+ id: INTEGER
+ name: VARCHAR(250)
+ }
+ entity "Permission (ab_permission)" as ab_permission {
+ id: INTEGER
+ name: VARCHAR(100)
+ }
+ entity "User (ab_user)" as ab_user {
+ id: INTEGER
+ first_name: VARCHAR(64)
+ last_name: VARCHAR(64)
+ username: VARCHAR(64)
+ password: VARCHAR(256)
+ active: BOOLEAN
+ email: VARCHAR(320)
+ last_login: DATETIME
+ login_count: INTEGER
+ fail_login_count: INTEGER
+ created_on: DATETIME
+ changed_on: DATETIME
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "RegisterUser (ab_register_user)" as ab_register_user {
+ id: INTEGER
+ first_name: VARCHAR(64)
+ last_name: VARCHAR(64)
+ username: VARCHAR(64)
+ password: VARCHAR(256)
+ email: VARCHAR(64)
+ registration_date: DATETIME
+ registration_hash: VARCHAR(256)
+ }
+ entity "PermissionView (ab_permission_view)" as ab_permission_view {
+ id: INTEGER
+ permission_id: INTEGER
+ view_menu_id: INTEGER
+ }
+ entity "Role (ab_role)" as ab_role {
+ id: INTEGER
+ name: VARCHAR(64)
+ }
+ }
+rectangle "Alerts & Reports" #black {
+ entity "ReportRecipients (report_recipient)" as report_recipient {
+ created_on: DATETIME
+ changed_on: DATETIME
+ id: INTEGER
+ type: VARCHAR(50)
+ recipient_config_json: TEXT
+ report_schedule_id: INTEGER
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ entity "ReportExecutionLog (report_execution_log)" as report_execution_log {
+ id: INTEGER
+ uuid: BINARY(16)
+ scheduled_dttm: DATETIME
+ start_dttm: DATETIME
+ end_dttm: DATETIME
+ value: FLOAT
+ value_row_json: TEXT
+ state: VARCHAR(50)
+ error_message: TEXT
+ report_schedule_id: INTEGER
+ }
+ entity "ReportSchedule (report_schedule)" as report_schedule {
+ created_on: DATETIME
+ changed_on: DATETIME
+ extra_json: TEXT
+ id: INTEGER
+ type: VARCHAR(50)
+ name: VARCHAR(150)
+ description: TEXT
+ context_markdown: TEXT
+ active: BOOLEAN
+ crontab: VARCHAR(1000)
+ creation_method: VARCHAR(255)
+ timezone: VARCHAR(100)
+ report_format: VARCHAR(50)
+ sql: TEXT
+ chart_id: INTEGER
+ dashboard_id: INTEGER
+ database_id: INTEGER
+ last_eval_dttm: DATETIME
+ last_state: VARCHAR(50)
+ last_value: FLOAT
+ last_value_row_json: TEXT
+ validator_type: VARCHAR(100)
+ validator_config_json: TEXT
+ log_retention: INTEGER
+ grace_period: INTEGER
+ working_timeout: INTEGER
+ force_screenshot: BOOLEAN
+ custom_width: INTEGER
+ custom_height: INTEGER
+ created_by_fk: INTEGER
+ changed_by_fk: INTEGER
+ }
+ }
+' Relationships
+
+ sql_metrics }|--|| tables
+ sql_metrics }|--|| ab_user
+
+ database_user_oauth2_tokens }|--|| ab_user
+ database_user_oauth2_tokens }|--|| dbs
+
+ sl_tables }|--|| dbs
+ sl_tables }|--|{ sl_columns
+ sl_tables }|--|| ab_user
+ sl_tables }|--|{ sl_datasets
+
+ dbs }|--|| ab_user
+ dbs ||--|{ tables
+ dbs ||--|{ sl_datasets
+
+ sl_datasets }|--|{ sl_columns
+ sl_datasets }|--|{ ab_user
+
+ tables ||--|{ table_columns
+ tables }|--|{ row_level_security_filters
+
+ table_columns }|--|| ab_user
+
+ sl_columns }|--|| ab_user
+
+ row_level_security_filters }|--|| ab_user
+
+ key_value }|--|| ab_user
+
+ ssh_tunnels }|--|| dbs
+ ssh_tunnels }|--|| ab_user
+
+
+
+
+ saved_query }|--|| ab_user
+ saved_query }|--|| dbs
+ saved_query }|--|{ tag
+
+ table_schema }|--|| dbs
+ table_schema }|--|| ab_user
+ table_schema }|--|| tab_state
+
+ query }|--|| dbs
+ query }|--|| ab_user
+
+ tab_state }|--|| dbs
+ tab_state }|--|| query
+ tab_state }|--|| saved_query
+ tab_state }|--|| ab_user
+
+
+ dashboards }|--|{ slices
+ dashboards }|--|{ ab_user
+ dashboards }|--|{ tag
+ dashboards }|--|{ ab_role
+ dashboards ||--|{ embedded_dashboards
+ dashboards ||--|{ report_schedule
+
+ annotation }|--|| annotation_layer
+ annotation }|--|| ab_user
+
+ embedded_dashboards }|--|| ab_user
+
+ slices }|--|| ab_user
+ slices }|--|{ tag
+ slices }|--|| tables
+ slices ||--|{ report_schedule
+
+ tagged_object }|--|| tag
+ tagged_object }|--|| ab_user
+
+ annotation_layer }|--|| ab_user
+
+ dynamic_plugin }|--|| ab_user
+
+ tag }|--|{ ab_user
+
+ css_templates }|--|| ab_user
+
+ user_attribute }|--|| dashboards
+
+
+
+ ab_user }|--|{ ab_role
+ ab_user }|--|| ab_user
+ ab_user ||--|{ logs
+ ab_user ||--|{ user_attribute
+ ab_user }|--|{ tables
+
+
+ ab_permission_view }|--|| ab_permission
+ ab_permission_view }|--|| ab_view_menu
+ ab_permission_view }|--|{ ab_role
+
+ ab_role }|--|{ row_level_security_filters
+
+ report_recipient }|--|| report_schedule
+ report_recipient }|--|| ab_user
+
+ report_execution_log }|--|| report_schedule
+
+ report_schedule }|--|| dbs
+ report_schedule }|--|{ ab_user
+@enduml
diff --git a/scripts/erd/erd.py b/scripts/erd/erd.py
new file mode 100644
index 000000000..0622e0d85
--- /dev/null
+++ b/scripts/erd/erd.py
@@ -0,0 +1,211 @@
+# 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.
+"""
+This module contains utilities to auto-generate an
+Entity-Relationship Diagram (ERD) from SQLAlchemy
+and onto a plantuml file.
+"""
+import json
+import os
+from collections import defaultdict
+from collections.abc import Iterable
+from typing import Any, Optional
+
+import click
+import jinja2
+from flask.cli import FlaskGroup, with_appcontext
+
+from superset import app, db
+
+GROUPINGS: dict[str, Iterable[str]] = {
+ "Core": [
+ "css_templates",
+ "dynamic_plugin",
+ "favstar",
+ "dashboards",
+ "slices",
+ "user_attribute",
+ "embedded_dashboards",
+ "annotation",
+ "annotation_layer",
+ "tag",
+ "tagged_object",
+ ],
+ "System": ["ssh_tunnels", "keyvalue", "cache_keys", "key_value", "logs"],
+ "Alerts & Reports": ["report_recipient", "report_execution_log", "report_schedule"],
+ "Inherited from Flask App Builder (FAB)": [
+ "ab_user",
+ "ab_permission",
+ "ab_permission_view",
+ "ab_view_menu",
+ "ab_role",
+ "ab_register_user",
+ ],
+ "SQL Lab": ["query", "saved_query", "tab_state", "table_schema"],
+ "Data Assets": [
+ "dbs",
+ "table_columns",
+ "sql_metrics",
+ "tables",
+ "row_level_security_filters",
+ "sl_tables",
+ "sl_datasets",
+ "sl_columns",
+ "database_user_oauth2_tokens",
+ ],
+}
+# Table name to group name mapping (reversing the above one for easy lookup)
+TABLE_TO_GROUP_MAP: dict[str, str] = {}
+for group, tables in GROUPINGS.items():
+ for table in tables:
+ TABLE_TO_GROUP_MAP[table] = group
+
+
+def sort_data_structure(data): # type: ignore
+ sorted_json = json.dumps(data, sort_keys=True)
+ sorted_data = json.loads(sorted_json)
+ return sorted_data
+
+
+def introspect_sqla_model(mapper: Any, seen: set[str]) -> dict[str, Any]:
+ """
+ Introspects a SQLAlchemy model and returns a data structure that
+ can be pass to a jinja2 template for instance
+
+ Parameters:
+ -----------
+ mapper: SQLAlchemy model mapper
+ seen: set of model identifiers to avoid duplicates
+
+ Returns:
+ --------
+ Dict[str, Any]: data structure for jinja2 template
+ """
+ table_name = mapper.persist_selectable.name
+ model_info: dict[str, Any] = {
+ "class_name": mapper.class_.__name__,
+ "table_name": table_name,
+ "fields": [],
+ "relationships": [],
+ }
+ # Collect fields (columns) and their types
+ for column in mapper.columns:
+ field_info: dict[str, str] = {
+ "field_name": column.key,
+ "type": str(column.type),
+ }
+ model_info["fields"].append(field_info)
+
+ # Collect relationships and identify types
+ for attr, relationship in mapper.relationships.items():
+ related_table = relationship.mapper.persist_selectable.name
+ # Create a unique identifier for the relationship to avoid duplicates
+ relationship_id = "-".join(sorted([table_name, related_table]))
+
+ if relationship_id not in seen:
+ seen.add(relationship_id)
+ squiggle = "||--|{"
+ if relationship.direction.name == "MANYTOONE":
+ squiggle = "}|--||"
+
+ relationship_info: dict[str, str] = {
+ "relationship_name": attr,
+ "related_model": relationship.mapper.class_.__name__,
+ "type": relationship.direction.name,
+ "related_table": related_table,
+ }
+ # Identify many-to-many by checking for secondary table
+ if relationship.secondary is not None:
+ squiggle = "}|--|{"
+ relationship_info["type"] = "many-to-many"
+ relationship_info["secondary_table"] = relationship.secondary.name
+
+ relationship_info["squiggle"] = squiggle
+ model_info["relationships"].append(relationship_info)
+ return sort_data_structure(model_info) # type: ignore
+
+
+def introspect_models() -> dict[str, list[dict[str, Any]]]:
+ """
+ Introspects SQLAlchemy models and returns a data structure that
+ can be pass to a jinja2 template for rendering an ERD.
+
+ Returns:
+ --------
+ Dict[str, List[Dict[str, Any]]]: data structure for jinja2 template
+ """
+ data: dict[str, list[dict[str, Any]]] = defaultdict(list)
+ seen_models: set[str] = set()
+ for model in db.Model.registry.mappers:
+ group_name = (
+ TABLE_TO_GROUP_MAP.get(model.mapper.persist_selectable.name)
+ or "Uncategorized Models"
+ )
+ model_data = introspect_sqla_model(model, seen_models)
+ data[group_name].append(model_data)
+ return data
+
+
+def generate_erd(file_path: str) -> None:
+ """
+ Generates a PlantUML ERD of the models/database
+
+ Parameters:
+ -----------
+ file_path: str
+ File path to write the ERD to
+ """
+ data = introspect_models()
+ templates_path = os.path.dirname(__file__)
+ env = jinja2.Environment(loader=jinja2.FileSystemLoader(templates_path))
+
+ # Load the template
+ template = env.get_template("erd.template.puml")
+ rendered = template.render(data=data)
+ with open(file_path, "w") as f:
+ click.secho(f"Writing to {file_path}...", fg="green")
+ f.write(rendered)
+
+
+@click.command()
+@click.option(
+ "--output",
+ "-o",
+ type=click.Path(dir_okay=False, writable=True),
+ help="File to write the ERD to",
+)
+def erd(output: Optional[str] = None) -> None:
+ """
+ Generates a PlantUML ERD of the models/database
+
+ Parameters:
+ -----------
+ output: str, optional
+ File to write the ERD to, defaults to erd.plantuml if not provided
+ """
+ path = os.path.dirname(__file__)
+ output = output or os.path.join(path, "erd.puml")
+
+ from superset.app import create_app
+
+ app = create_app()
+ with app.app_context():
+ generate_erd(output)
+
+
+if __name__ == "__main__":
+ erd()
diff --git a/scripts/erd/erd.svg b/scripts/erd/erd.svg
new file mode 100644
index 000000000..e69de29bb
diff --git a/scripts/erd/erd.template.puml b/scripts/erd/erd.template.puml
new file mode 100644
index 000000000..2e342d0be
--- /dev/null
+++ b/scripts/erd/erd.template.puml
@@ -0,0 +1,57 @@
+{#
+ 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.
+#}
+@startuml erd
+
+title Apache Superset ERD
+
+!theme blueprint
+
+' avoid problems with angled crows feet
+
+skinparam linetype ortho
+skinparam classBorderColor #grey
+
+skinparam classBorderColor<> #white
+skinparam classBorderThickness<> 1
+skinparam classLineStyle<> Dashed
+skinparam ClassBackgroundColor<> #204143
+
+' Models
+{% for group_name, models in data.items() -%}
+ rectangle "{{ group_name }}" #black {
+ {% for model in models -%}
+ entity "{{ model.class_name }} ({{ model.table_name }})" as {{ model.table_name }} {
+ {%- for field in model.fields %}
+ {{ field.field_name }}: {{ field.type -}}
+ {%- endfor %}
+ }
+ {% endfor -%}
+ }
+{% endfor -%}
+
+' Relationships
+{% for models in data.values() -%}
+{% for model in models -%}
+ {%- for rel in model.relationships %}
+ {{ model.table_name }} {{ rel.squiggle }} {{ rel.related_table }}
+ {%- endfor %}
+{% endfor -%}
+{% endfor -%}
+
+@enduml
diff --git a/scripts/templates/erd.plantuml.template b/scripts/templates/erd.plantuml.template
new file mode 100644
index 000000000..1c164dd87
--- /dev/null
+++ b/scripts/templates/erd.plantuml.template
@@ -0,0 +1,57 @@
+{#
+ 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.
+#}
+@startuml entity-relationship-diagram
+
+title Apache Superset ERD
+
+!theme blueprint
+
+' avoid problems with angled crows feet
+
+skinparam linetype ortho
+skinparam classBorderColor #grey
+
+skinparam classBorderColor<> #white
+skinparam classBorderThickness<> 1
+skinparam classLineStyle<> Dashed
+skinparam ClassBackgroundColor<> #204143
+
+' Models
+{% for group_name, models in data.items() -%}
+ rectangle "{{ group_name }}" #black {
+ {% for model in models -%}
+ entity "{{ model.class_name }} ({{ model.table_name }})" as {{ model.table_name }} {
+ {%- for field in model.fields %}
+ {{ field.field_name }}: {{ field.type -}}
+ {%- endfor %}
+ }
+ {% endfor -%}
+ }
+{% endfor -%}
+
+' Relationships
+{% for models in data.values() -%}
+{% for model in models -%}
+ {%- for rel in model.relationships %}
+ {{ model.table_name }} {{ rel.squiggle }} {{ rel.related_table }}
+ {%- endfor %}
+{% endfor -%}
+{% endfor -%}
+
+@enduml