metricflow-sql 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metricflow_sql-0.1.1/.gitignore +218 -0
- metricflow_sql-0.1.1/LICENSE +29 -0
- metricflow_sql-0.1.1/PKG-INFO +30 -0
- metricflow_sql-0.1.1/PYPI.md +5 -0
- metricflow_sql-0.1.1/README.md +3 -0
- metricflow_sql-0.1.1/pyproject.toml +47 -0
- metricflow_sql-0.1.1/src/metricflow_sql/__init__.py +8 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/cluster.py +152 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/information_schema.py +47 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/metadata.py +65 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/passthrough.py +184 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/protocols.py +67 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/catalog/semantic.py +59 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/compiler.py +297 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/expand_constellations.py +790 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/expand_datasets.py +1208 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/expand_metadata.py +88 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/isolate_fragments.py +244 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/rules/passthrough_tables.py +36 -0
- metricflow_sql-0.1.1/src/metricflow_sql/compiler/table_arguments.py +35 -0
- metricflow_sql-0.1.1/src/metricflow_sql/converters/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/converters/lookml.py +1041 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/base.py +308 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/constellation.py +210 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/dataset.py +275 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/entity.py +131 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/reference.py +35 -0
- metricflow_sql-0.1.1/src/metricflow_sql/objects/wiki.py +101 -0
- metricflow_sql-0.1.1/src/metricflow_sql/py.typed +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/memory.py +409 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/operation.py +25 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/reference_resolution.py +68 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/registry.py +225 -0
- metricflow_sql-0.1.1/src/metricflow_sql/registry/type_resolver.py +460 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/bundle.py +327 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/fragment.schema.json +1165 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/generate.py +70 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/load.py +333 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/v1/__init__.py +51 -0
- metricflow_sql-0.1.1/src/metricflow_sql/schemas/v1/fragment.py +544 -0
- metricflow_sql-0.1.1/src/metricflow_sql/server/__init__.py +4 -0
- metricflow_sql-0.1.1/src/metricflow_sql/server/app.py +33 -0
- metricflow_sql-0.1.1/src/metricflow_sql/server/compile.py +69 -0
- metricflow_sql-0.1.1/src/metricflow_sql/server/errors.py +10 -0
- metricflow_sql-0.1.1/src/metricflow_sql/server/serve.py +161 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/__init__.py +0 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/composed_set.py +32 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/normalize.py +74 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/ordered_set.py +24 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/sql.py +226 -0
- metricflow_sql-0.1.1/src/metricflow_sql/utils/template.py +57 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
.idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Copyright (c) 2026 Fivetran, Inc.
|
|
2
|
+
|
|
3
|
+
By accessing or using this software, you agree to the following terms:
|
|
4
|
+
|
|
5
|
+
PERMITTED USE
|
|
6
|
+
This software is made available for internal use and evaluation purposes
|
|
7
|
+
only. You may install and use the software solely to access and interact
|
|
8
|
+
with Fivetran's services.
|
|
9
|
+
|
|
10
|
+
RESTRICTIONS
|
|
11
|
+
You may not use this software to:
|
|
12
|
+
- Build competing products or services
|
|
13
|
+
- Redistribute, sublicense, or transfer the software
|
|
14
|
+
|
|
15
|
+
INTELLECTUAL PROPERTY
|
|
16
|
+
All rights, title, and interest in the software remain exclusively with
|
|
17
|
+
Fivetran, Inc. No license to any intellectual property is granted
|
|
18
|
+
except as expressly stated herein.
|
|
19
|
+
|
|
20
|
+
NO WARRANTIES
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS". FIVETRAN, INC. MAKES NO WARRANTIES,
|
|
22
|
+
EXPRESS OR IMPLIED, AND HEREBY DISCLAIMS ALL IMPLIED WARRANTIES,
|
|
23
|
+
INCLUDING ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR
|
|
24
|
+
PURPOSE.
|
|
25
|
+
|
|
26
|
+
LIMITATION OF LIABILITY
|
|
27
|
+
FIVETRAN, INC. SHALL NOT BE LIABLE FOR ANY DAMAGES OF ANY KIND ARISING
|
|
28
|
+
FROM USE OF THIS SOFTWARE, INCLUDING DIRECT, INDIRECT, INCIDENTAL,
|
|
29
|
+
PUNITIVE, AND CONSEQUENTIAL DAMAGES.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: metricflow-sql
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MetricFlow's core semantic layer engine
|
|
5
|
+
Project-URL: Documentation, https://congenial-bassoon-7zkjv8w.pages.github.io/
|
|
6
|
+
License-Expression: LicenseRef-Fivetran
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: jinja2>=3.1
|
|
14
|
+
Requires-Dist: msgspec>=0.19
|
|
15
|
+
Requires-Dist: sqlglot[c]>=30.27.0
|
|
16
|
+
Requires-Dist: wenmode>=0.15.2
|
|
17
|
+
Provides-Extra: lookml
|
|
18
|
+
Requires-Dist: lkml>=1.3.7; extra == 'lookml'
|
|
19
|
+
Provides-Extra: server
|
|
20
|
+
Requires-Dist: starlette>=0.47.0; extra == 'server'
|
|
21
|
+
Requires-Dist: uvicorn>=0.34.0; extra == 'server'
|
|
22
|
+
Provides-Extra: yaml
|
|
23
|
+
Requires-Dist: ruamel-yaml>=0.18; extra == 'yaml'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# metricflow-sql
|
|
27
|
+
|
|
28
|
+
Something is cooking...
|
|
29
|
+
|
|
30
|
+
[Documentation](https://congenial-bassoon-7zkjv8w.pages.github.io/), for design partners with access.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "metricflow-sql"
|
|
3
|
+
# A placeholder: a release sets its tag's version, with scripts/set_version.py.
|
|
4
|
+
version = "0.1.1"
|
|
5
|
+
description = "MetricFlow's core semantic layer engine"
|
|
6
|
+
# Its PyPI page: README.md is for this repository.
|
|
7
|
+
readme = "PYPI.md"
|
|
8
|
+
# Fivetran's own, in LICENSE, which no SPDX identifier names.
|
|
9
|
+
license = "LicenseRef-Fivetran"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"jinja2>=3.1",
|
|
19
|
+
"msgspec>=0.19",
|
|
20
|
+
"sqlglot[c]>=30.27.0",
|
|
21
|
+
"wenmode>=0.15.2",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
metricflow-serve = "metricflow_sql.server.serve:main"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
lookml = [
|
|
29
|
+
"lkml>=1.3.7",
|
|
30
|
+
]
|
|
31
|
+
yaml = [
|
|
32
|
+
"ruamel-yaml>=0.18",
|
|
33
|
+
]
|
|
34
|
+
server = [
|
|
35
|
+
"starlette>=0.47.0",
|
|
36
|
+
"uvicorn>=0.34.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Documentation = "https://congenial-bassoon-7zkjv8w.pages.github.io/"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["hatchling"]
|
|
44
|
+
build-backend = "hatchling.build"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["src/metricflow_sql"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Sequence
|
|
4
|
+
from typing import TYPE_CHECKING, override
|
|
5
|
+
|
|
6
|
+
from sqlglot import Dialect, Schema
|
|
7
|
+
from sqlglot import expressions as exp
|
|
8
|
+
from sqlglot.dialects.dialect import DialectType, NormalizationStrategy
|
|
9
|
+
from sqlglot.errors import OptimizeError, SchemaError
|
|
10
|
+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
11
|
+
|
|
12
|
+
from metricflow_sql.compiler.catalog.protocols import CatalogLike, ColumnLike, TableLike
|
|
13
|
+
from metricflow_sql.registry.registry import ConstellationProxy, DatasetProxy, Registry
|
|
14
|
+
from metricflow_sql.utils.normalize import exact_identifier, normalizing_dialect
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sqlglot.schema import ColumnMapping
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Cluster(Schema):
|
|
21
|
+
"""
|
|
22
|
+
Collection of catalogs.
|
|
23
|
+
|
|
24
|
+
This serves as the database schema for the compiler and sqlglot optimizer.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
catalogs: dict[str, CatalogLike] | None = None,
|
|
30
|
+
dialect: DialectType = None,
|
|
31
|
+
fallback: Callable[[str], CatalogLike | None] | None = None,
|
|
32
|
+
normalization_strategy: NormalizationStrategy | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
`catalogs` maps names to catalogs. A name matches a query's once both are normalized with
|
|
36
|
+
`normalization_strategy`, e.g. `env_1` matches `ENV_1` if names are uppercase. Give it
|
|
37
|
+
the registry's strategy, as the compiler's.
|
|
38
|
+
|
|
39
|
+
`fallback` builds a catalog for any other name, e.g. a warehouse catalog.
|
|
40
|
+
"""
|
|
41
|
+
self.catalogs = catalogs or {}
|
|
42
|
+
self._dialect = dialect
|
|
43
|
+
self._normalizing = (
|
|
44
|
+
normalizing_dialect(None, normalization_strategy) if normalization_strategy else None
|
|
45
|
+
)
|
|
46
|
+
self._fallback = fallback
|
|
47
|
+
self._fallen_back: dict[str, CatalogLike | None] = {}
|
|
48
|
+
self._normalized_names: dict[str, str] = {}
|
|
49
|
+
|
|
50
|
+
def get_catalog(self, name: str) -> CatalogLike | None:
|
|
51
|
+
"""The catalog named `name`, which is normalized: one of `catalogs`, else the fallback's."""
|
|
52
|
+
catalog = next(
|
|
53
|
+
(own for key, own in self.catalogs.items() if self._normalized(key) == name), None
|
|
54
|
+
)
|
|
55
|
+
if catalog is not None or self._fallback is None or not name:
|
|
56
|
+
return catalog
|
|
57
|
+
# Built once, so it looks up each table once.
|
|
58
|
+
if name not in self._fallen_back:
|
|
59
|
+
self._fallen_back[name] = self._fallback(name)
|
|
60
|
+
return self._fallen_back[name]
|
|
61
|
+
|
|
62
|
+
def _normalized(self, name: str) -> str:
|
|
63
|
+
if name not in self._normalized_names:
|
|
64
|
+
identifier = exp.to_identifier(name)
|
|
65
|
+
self._normalized_names[name] = normalize_identifiers(
|
|
66
|
+
identifier, dialect=self._normalizing
|
|
67
|
+
).name
|
|
68
|
+
return self._normalized_names[name]
|
|
69
|
+
|
|
70
|
+
def find_table(self, table: exp.Table | str) -> TableLike[ColumnLike] | None:
|
|
71
|
+
catalog, name = self._resolve(table)
|
|
72
|
+
return catalog.get_table(name)
|
|
73
|
+
|
|
74
|
+
def get_table(self, table: exp.Table | str) -> TableLike[ColumnLike]:
|
|
75
|
+
catalog, name = self._resolve(table)
|
|
76
|
+
table_obj = catalog.get_table(name)
|
|
77
|
+
if not table_obj:
|
|
78
|
+
catalog_name = exp.to_table(table, dialect=self._dialect, copy=False).args["catalog"]
|
|
79
|
+
name.set("catalog", catalog_name.copy())
|
|
80
|
+
raise SchemaError(f"Unknown table: {name.sql(self._dialect)}")
|
|
81
|
+
return table_obj
|
|
82
|
+
|
|
83
|
+
def get_dataset(self, table: exp.Table | str) -> tuple[Registry, DatasetProxy] | None:
|
|
84
|
+
catalog, name = self._resolve(table)
|
|
85
|
+
return catalog.get_dataset(name)
|
|
86
|
+
|
|
87
|
+
def get_constellation(
|
|
88
|
+
self, table: exp.Table | str
|
|
89
|
+
) -> tuple[Registry, ConstellationProxy] | None:
|
|
90
|
+
catalog, name = self._resolve(table)
|
|
91
|
+
return catalog.get_constellation(name)
|
|
92
|
+
|
|
93
|
+
def _resolve(self, table: exp.Table | str) -> tuple[CatalogLike, exp.Table]:
|
|
94
|
+
"""The table's catalog, and its name in it."""
|
|
95
|
+
table = exp.to_table(table, dialect=self._dialect, copy=False)
|
|
96
|
+
if not table.catalog:
|
|
97
|
+
raise OptimizeError(f"Expected table to have been qualified with catalog: {table}")
|
|
98
|
+
if not table.db:
|
|
99
|
+
raise OptimizeError(f"Expected table to have been qualified with db: {table}")
|
|
100
|
+
catalog = self.get_catalog(table.catalog)
|
|
101
|
+
if not catalog:
|
|
102
|
+
raise SchemaError(f"Unknown catalog: {table.sql(self._dialect)}")
|
|
103
|
+
# Not `exp.table_`, which a table function's name, e.g. `f('x')`, can't be passed to.
|
|
104
|
+
return catalog, exp.Table(this=table.this.copy(), db=table.args["db"].copy())
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
@override
|
|
108
|
+
def empty(self) -> bool:
|
|
109
|
+
# Tables load lazily, so there's no mapping to show; to sqlglot, an empty schema knows no
|
|
110
|
+
# columns, and an alias shadows one, e.g. `b` in `SELECT CONCAT(a, b) AS b ... WHERE b = 1`.
|
|
111
|
+
return not self.catalogs
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
@override
|
|
115
|
+
def dialect(self) -> Dialect | None:
|
|
116
|
+
return Dialect.get_or_raise(self._dialect)
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
def add_table(
|
|
120
|
+
self,
|
|
121
|
+
table: exp.Table | str,
|
|
122
|
+
column_mapping: ColumnMapping | None = None,
|
|
123
|
+
dialect: DialectType = None,
|
|
124
|
+
normalize: bool | None = None,
|
|
125
|
+
match_depth: bool = True,
|
|
126
|
+
) -> None:
|
|
127
|
+
raise NotImplementedError
|
|
128
|
+
|
|
129
|
+
@override
|
|
130
|
+
def column_names(
|
|
131
|
+
self,
|
|
132
|
+
table: exp.Table | str,
|
|
133
|
+
only_visible: bool = False,
|
|
134
|
+
dialect: DialectType = None,
|
|
135
|
+
normalize: bool | None = None,
|
|
136
|
+
) -> Sequence[str]:
|
|
137
|
+
return [name.name for name in self.get_table(table).column_names()]
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
def get_column_type(
|
|
141
|
+
self,
|
|
142
|
+
table: exp.Table | str,
|
|
143
|
+
column: exp.Column | str,
|
|
144
|
+
dialect: DialectType = None,
|
|
145
|
+
normalize: bool | None = None,
|
|
146
|
+
) -> exp.DataType:
|
|
147
|
+
return self.get_table(table).get_column_type(exact_identifier(column))
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
@override
|
|
151
|
+
def supported_table_args(self) -> tuple[str, ...]:
|
|
152
|
+
return "this", "db", "catalog"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from sqlglot import exp
|
|
6
|
+
|
|
7
|
+
from metricflow_sql.compiler.catalog.metadata import MetadataColumn, MetadataTable
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Trino's public information_schema.tables columns, all with SQL type VARCHAR.
|
|
12
|
+
TABLE_COLUMNS = (
|
|
13
|
+
"table_catalog",
|
|
14
|
+
"table_schema",
|
|
15
|
+
"table_name",
|
|
16
|
+
"table_type",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# The information_schema.columns columns clients read, e.g. Looker, all VARCHAR.
|
|
20
|
+
COLUMN_COLUMNS = (
|
|
21
|
+
"table_catalog",
|
|
22
|
+
"table_schema",
|
|
23
|
+
"table_name",
|
|
24
|
+
"column_name",
|
|
25
|
+
"data_type",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def table_metadata(source: exp.Query, *, engine: str) -> MetadataTable:
|
|
30
|
+
"""information_schema.tables, whose rows `source` produces on `engine`."""
|
|
31
|
+
return _varchar_table("tables", TABLE_COLUMNS, source, engine)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def column_metadata(source: exp.Query, *, engine: str) -> MetadataTable:
|
|
35
|
+
"""information_schema.columns, whose rows `source` produces on `engine`."""
|
|
36
|
+
return _varchar_table("columns", COLUMN_COLUMNS, source, engine)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _varchar_table(
|
|
40
|
+
name: str, columns: tuple[str, ...], source: exp.Query, engine: str
|
|
41
|
+
) -> MetadataTable:
|
|
42
|
+
return MetadataTable(
|
|
43
|
+
name=name,
|
|
44
|
+
columns={column: MetadataColumn(exp.DType.VARCHAR.into_expr()) for column in columns},
|
|
45
|
+
source=source,
|
|
46
|
+
engine=engine,
|
|
47
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Callable, Collection, Mapping, Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from sqlglot import exp
|
|
8
|
+
from sqlglot.errors import SchemaError
|
|
9
|
+
|
|
10
|
+
from metricflow_sql.compiler.catalog.protocols import CatalogLike, ColumnLike, TableLike
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# The db a semantic catalog serves its metadata tables under.
|
|
15
|
+
INFORMATION_SCHEMA = "information_schema"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class MetadataColumn:
|
|
20
|
+
datatype: exp.DataType
|
|
21
|
+
description: str | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class MetadataTable:
|
|
26
|
+
name: str
|
|
27
|
+
columns: dict[str, MetadataColumn]
|
|
28
|
+
|
|
29
|
+
# The query that replaces the table, or a function that builds it when it's read
|
|
30
|
+
source: exp.Query | Callable[[], exp.Query]
|
|
31
|
+
|
|
32
|
+
# The engine that runs queries over the table, e.g. "metadata"
|
|
33
|
+
engine: str
|
|
34
|
+
|
|
35
|
+
def query(self) -> exp.Query:
|
|
36
|
+
"""A copy of the query that replaces the table."""
|
|
37
|
+
return self.source() if callable(self.source) else self.source.copy()
|
|
38
|
+
|
|
39
|
+
def column_names(self) -> Collection[exp.Identifier]:
|
|
40
|
+
return [exp.to_identifier(name) for name in self.columns]
|
|
41
|
+
|
|
42
|
+
# information_schema's names are case-insensitive.
|
|
43
|
+
def get_column(self, name: exp.Identifier) -> MetadataColumn | None:
|
|
44
|
+
return self.columns.get(name.name.lower())
|
|
45
|
+
|
|
46
|
+
def get_column_type(self, name: exp.Identifier) -> exp.DataType:
|
|
47
|
+
column = self.get_column(name)
|
|
48
|
+
if column is None:
|
|
49
|
+
raise SchemaError(f"Unknown column: {self.name}.{name.name}")
|
|
50
|
+
return column.datatype
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MetadataCatalog(CatalogLike):
|
|
54
|
+
"""A catalog of only metadata tables, e.g. a Trino client's `system.runtime.nodes`."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, tables: Mapping[str, Sequence[MetadataTable]]) -> None:
|
|
57
|
+
"""`tables` are the catalog's metadata tables, by db."""
|
|
58
|
+
self._tables = {
|
|
59
|
+
(db.lower(), table.name.lower()): table
|
|
60
|
+
for db, db_tables in tables.items()
|
|
61
|
+
for table in db_tables
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def get_table(self, table: exp.Table) -> TableLike[ColumnLike] | None:
|
|
65
|
+
return self._tables.get((table.db.lower(), table.name.lower()))
|