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 @@ +Apache Superset ERDSQL LabSystemCoreInherited from Flask App Builder (FAB)Alerts & ReportsData AssetsQuery (query)tracking_url: TEXTextra_json: TEXTid: INTEGERdatabase_id: INTEGERuser_id: INTEGERsql: TEXTselect_sql: TEXTexecuted_sql: TEXTlimit: INTEGERselect_as_cta: BOOLEANselect_as_cta_used: BOOLEANprogress: INTEGERrows: INTEGERerror_message: TEXTchanged_on: DATETIMEclient_id: VARCHAR(11)tmp_table_name: VARCHAR(256)tmp_schema_name: VARCHAR(256)status: VARCHAR(16)tab_name: VARCHAR(256)sql_editor_id: VARCHAR(256)schema: VARCHAR(256)limiting_factor: VARCHAR(18)ctas_method: VARCHAR(16)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)TableSchema (table_schema)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERtab_state_id: INTEGERdatabase_id: INTEGERdescription: TEXTexpanded: BOOLEANcreated_by_fk: INTEGERchanged_by_fk: INTEGERschema: VARCHAR(256)table: VARCHAR(256)TabState (tab_state)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERuser_id: INTEGERactive: BOOLEANdatabase_id: INTEGERsql: TEXTquery_limit: INTEGERlatest_query_id: INTEGERautorun: BOOLEANtemplate_params: TEXThide_left_bar: BOOLEANsaved_query_id: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERlabel: VARCHAR(256)schema: VARCHAR(256)SavedQuery (saved_query)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERuser_id: INTEGERdb_id: INTEGERdescription: TEXTsql: TEXTtemplate_parameters: TEXTrows: INTEGERlast_run: DATETIMEcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)schema: VARCHAR(128)label: VARCHAR(256)KeyValue (keyvalue)id: INTEGERvalue: TEXTCacheKey (cache_keys)id: INTEGERcache_timeout: INTEGERcreated_on: DATETIMEcache_key: VARCHAR(256)datasource_uid: VARCHAR(64)Log (logs)id: INTEGERuser_id: INTEGERdashboard_id: INTEGERslice_id: INTEGERjson: TEXTdttm: DATETIMEduration_ms: INTEGERaction: VARCHAR(512)referrer: VARCHAR(1024)KeyValueEntry (key_value)id: INTEGERvalue: BLOBcreated_on: DATETIMEcreated_by_fk: INTEGERchanged_on: DATETIMEexpires_on: DATETIMEchanged_by_fk: INTEGERuuid: BINARY(16)resource: VARCHAR(32)SSHTunnel (ssh_tunnels)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERdatabase_id: INTEGERserver_address: TEXTserver_port: INTEGERusername: BLOBpassword: BLOBprivate_key: BLOBprivate_key_password: BLOBcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)Slice (slices)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdatasource_id: INTEGERparams: TEXTquery_context: TEXTdescription: TEXTcache_timeout: INTEGERlast_saved_at: DATETIMElast_saved_by_fk: INTEGERcertified_by: TEXTcertification_details: TEXTis_managed_externally: BOOLEANexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)slice_name: VARCHAR(250)datasource_type: VARCHAR(200)datasource_name: VARCHAR(2000)viz_type: VARCHAR(250)perm: VARCHAR(1000)schema_perm: VARCHAR(1000)TaggedObject (tagged_object)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERtag_id: INTEGERobject_id: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERobject_type: VARCHAR(9)AnnotationLayer (annotation_layer)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdescr: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERname: VARCHAR(250)DynamicPlugin (dynamic_plugin)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERname: TEXTkey: TEXTbundle_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERTag (tag)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdescription: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERname: VARCHAR(250)type: VARCHAR(12)CssTemplate (css_templates)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERcss: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERtemplate_name: VARCHAR(250)UserAttribute (user_attribute)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERuser_id: INTEGERwelcome_dashboard_id: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERavatar_url: VARCHAR(100)FavStar (favstar)id: INTEGERuser_id: INTEGERobj_id: INTEGERdttm: DATETIMEclass_name: VARCHAR(50)Dashboard (dashboards)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERposition_json: TEXTdescription: TEXTcss: TEXTcertified_by: TEXTcertification_details: TEXTjson_metadata: TEXTpublished: BOOLEANis_managed_externally: BOOLEANexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)dashboard_title: VARCHAR(500)slug: VARCHAR(255)Annotation (annotation)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERstart_dttm: DATETIMEend_dttm: DATETIMElayer_id: INTEGERlong_descr: TEXTjson_metadata: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERshort_descr: VARCHAR(500)EmbeddedDashboard (embedded_dashboards)created_on: DATETIMEchanged_on: DATETIMEallow_domain_list: TEXTdashboard_id: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)RegisterUser (ab_register_user)id: INTEGERregistration_date: DATETIMEfirst_name: VARCHAR(64)last_name: VARCHAR(64)username: VARCHAR(64)password: VARCHAR(256)email: VARCHAR(64)registration_hash: VARCHAR(256)Role (ab_role)id: INTEGERname: VARCHAR(64)PermissionView (ab_permission_view)id: INTEGERpermission_id: INTEGERview_menu_id: INTEGERViewMenu (ab_view_menu)id: INTEGERname: VARCHAR(250)Permission (ab_permission)id: INTEGERname: VARCHAR(100)User (ab_user)id: INTEGERactive: BOOLEANlast_login: DATETIMElogin_count: INTEGERfail_login_count: INTEGERcreated_on: DATETIMEchanged_on: DATETIMEcreated_by_fk: INTEGERchanged_by_fk: INTEGERfirst_name: VARCHAR(64)last_name: VARCHAR(64)username: VARCHAR(64)password: VARCHAR(256)email: VARCHAR(320)ReportExecutionLog (report_execution_log)id: INTEGERscheduled_dttm: DATETIMEstart_dttm: DATETIMEend_dttm: DATETIMEvalue: FLOATvalue_row_json: TEXTerror_message: TEXTreport_schedule_id: INTEGERuuid: BINARY(16)state: VARCHAR(50)ReportSchedule (report_schedule)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERdescription: TEXTcontext_markdown: TEXTactive: BOOLEANsql: TEXTchart_id: INTEGERdashboard_id: INTEGERdatabase_id: INTEGERlast_eval_dttm: DATETIMElast_value: FLOATlast_value_row_json: TEXTvalidator_config_json: TEXTlog_retention: INTEGERgrace_period: INTEGERworking_timeout: INTEGERforce_screenshot: BOOLEANcustom_width: INTEGERcustom_height: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERtype: VARCHAR(50)name: VARCHAR(150)crontab: VARCHAR(1000)creation_method: VARCHAR(255)timezone: VARCHAR(100)report_format: VARCHAR(50)last_state: VARCHAR(50)validator_type: VARCHAR(100)ReportRecipients (report_recipient)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERrecipient_config_json: TEXTreport_schedule_id: INTEGERcreated_by_fk: INTEGERchanged_by_fk: INTEGERtype: VARCHAR(50)Table (sl_tables)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERdatabase_id: INTEGERcatalog: TEXTschema: TEXTname: TEXTis_managed_externally: BOOLEANexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)Database (dbs)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERpassword: BLOBcache_timeout: INTEGERselect_as_create_table_as: BOOLEANexpose_in_sqllab: BOOLEANallow_run_async: BOOLEANallow_file_upload: BOOLEANallow_ctas: BOOLEANallow_cvas: BOOLEANallow_dml: BOOLEANextra: TEXTencrypted_extra: BLOBimpersonate_user: BOOLEANserver_cert: BLOBis_managed_externally: BOOLEANexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)verbose_name: VARCHAR(250)database_name: VARCHAR(250)sqlalchemy_uri: VARCHAR(1024)configuration_method: VARCHAR(255)force_ctas_schema: VARCHAR(250)Dataset (sl_datasets)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERdatabase_id: INTEGERis_physical: BOOLEANis_managed_externally: BOOLEANname: TEXTexpression: TEXTexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)SqlaTable (tables)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdescription: TEXTdefault_endpoint: TEXTis_featured: BOOLEANfilter_select_enabled: BOOLEANoffset: INTEGERcache_timeout: INTEGERis_managed_externally: BOOLEANexternal_url: TEXTdatabase_id: INTEGERfetch_values_predicate: TEXTsql: TEXTis_sqllab_view: BOOLEANtemplate_params: TEXTextra: TEXTnormalize_columns: BOOLEANalways_filter_main_dttm: BOOLEANcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)params: VARCHAR(1000)perm: VARCHAR(1000)schema_perm: VARCHAR(1000)table_name: VARCHAR(250)main_dttm_col: VARCHAR(250)schema: VARCHAR(255)TableColumn (table_columns)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERis_active: BOOLEANtype: TEXTgroupby: BOOLEANfilterable: BOOLEANdescription: TEXTtable_id: INTEGERis_dttm: BOOLEANexpression: TEXTextra: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)column_name: VARCHAR(255)verbose_name: VARCHAR(1024)advanced_data_type: VARCHAR(255)python_date_format: VARCHAR(255)Column (sl_columns)created_on: DATETIMEchanged_on: DATETIMEextra_json: TEXTid: INTEGERis_additive: BOOLEANis_aggregation: BOOLEANis_filterable: BOOLEANis_dimensional: BOOLEANis_increase_desired: BOOLEANis_managed_externally: BOOLEANis_partition: BOOLEANis_physical: BOOLEANis_spatial: BOOLEANis_temporal: BOOLEANname: TEXTtype: TEXTadvanced_data_type: TEXTexpression: TEXTunit: TEXTdescription: TEXTwarning_text: TEXTexternal_url: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)RowLevelSecurityFilter (row_level_security_filters)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdescription: TEXTclause: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERname: VARCHAR(255)filter_type: VARCHAR(7)group_key: VARCHAR(255)SqlMetric (sql_metrics)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERdescription: TEXTwarning_text: TEXTtable_id: INTEGERexpression: TEXTextra: TEXTcreated_by_fk: INTEGERchanged_by_fk: INTEGERuuid: BINARY(16)metric_name: VARCHAR(255)verbose_name: VARCHAR(1024)metric_type: VARCHAR(32)d3format: VARCHAR(128)currency: VARCHAR(128)DatabaseUserOAuth2Tokens (database_user_oauth2_tokens)created_on: DATETIMEchanged_on: DATETIMEid: INTEGERuser_id: INTEGERdatabase_id: INTEGERaccess_token: BLOBaccess_token_expiration: DATETIMErefresh_token: BLOBcreated_by_fk: INTEGERchanged_by_fk: INTEGER 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