Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"duckdb",
"dateparser",
"hyperscript",
"jinja2",
"pandas",
"pydantic",
"requests",
Expand Down
11 changes: 9 additions & 2 deletions sqlmesh/core/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from sqlmesh.core.console import Console, get_console
from sqlmesh.core.context_diff import ContextDiff
from sqlmesh.core.dag import DAG
from sqlmesh.core.dialect import extend_sqlglot, format_model_expressions
from sqlmesh.core.dialect import JinjaModel, extend_sqlglot, format_model_expressions
from sqlmesh.core.engine_adapter import EngineAdapter
from sqlmesh.core.environment import Environment
from sqlmesh.core.macros import macro
Expand All @@ -65,6 +65,7 @@
from sqlmesh.utils.date import TimeLike, yesterday_ds
from sqlmesh.utils.errors import ConfigError, MissingDependencyError, PlanError
from sqlmesh.utils.file_cache import FileCache
from sqlmesh.utils.jinja import JINJA_RE

if t.TYPE_CHECKING:
import graphviz
Expand Down Expand Up @@ -657,7 +658,13 @@ def _load_models(self):
for path in self._glob_path(self.models_directory_path, ".sql"):
self._path_mtimes[path] = path.stat().st_mtime
with open(path, "r", encoding="utf-8") as file:
expressions = parse(file.read(), read=self.dialect)
file_contents = file.read()

if JINJA_RE.search(file_contents):
expressions = [JinjaModel(this=file_contents)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a jinja model needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not needed per se. The idea for using it was that in this way we can still represent the query as a SQLGlot expression. An alternative would be to store a raw string for jinja queries, but then we'd have to take that new representation into account in places where query is expected to be an expression.

This is still in an early phase, though. As we're moving on, it might make sense to actually change this, so I'm not arguing that this is the way to go.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe render will return a sqlglot expression (jinja or no jinja), no?

Copy link
Copy Markdown
Collaborator Author

@georgesittas georgesittas Dec 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's correct -- maybe JinjaModel is not necessary after all. I'll think about it and continue working on this PR soon.

else:
expressions = parse(file_contents, read=self.dialect)

model = Model.load(
expressions,
module=module,
Expand Down
4 changes: 4 additions & 0 deletions sqlmesh/core/dialect.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class Model(exp.Expression):
arg_types = {"expressions": True}


class JinjaModel(exp.Expression):
"""Stores a model file that contains Jinja code as a raw string."""


class Audit(exp.Expression):
arg_types = {"expressions": True}

Expand Down
16 changes: 15 additions & 1 deletion sqlmesh/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ class Model(ModelMeta, frozen=True):
python_env: Dictionary containing all global variables needed to render the model's macros.
"""

query: t.Union[exp.Subqueryable, d.MacroVar]
query: t.Union[exp.Subqueryable, d.MacroVar, d.JinjaModel]
expressions_: t.Optional[t.List[exp.Expression]] = Field(
default=None, alias="expressions"
)
Expand Down Expand Up @@ -664,6 +664,15 @@ def load(
time_column_format: The default time column format to use if no model time column is configured.
"""
if len(expressions) < 2:
if expressions and isinstance(expressions[0], d.JinjaModel):
model = cls(
query=expressions[0], name="test"
) # We need the model's name for this instantiation to be valid
model._path = path

model.validate_definition()
return model

_raise_config_error(
"Incomplete model definition, missing MODEL and QUERY", path
)
Expand Down Expand Up @@ -953,6 +962,11 @@ def render_query(
Returns:
The rendered expression.
"""

# If the query is an instance of d.JinjaModel, render it and parse the produced string
# to create and validate the resulting model. Then we can extract the query and pass it
# as the query_ argument below. Do we want to do any validation earlier (e.g. inside load())?

return self._render_query(
start=start,
end=end,
Expand Down
5 changes: 5 additions & 0 deletions sqlmesh/utils/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import re

# Captures one of the following patterns: "{{", "{#", "{%" and "{%-".
# Q: this will also flag text that contains "{{" inside a string as Jinja. Is this a problem?
JINJA_RE = re.compile("{({|#|(%(-)?))")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not be necessary, maybe just render jinja and see if the outputs are different

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical as to whether this is going to add more overhead than a regex search, but we can try.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, then only run that after the regex search