Repository: TobikoData/sqlmesh
Pull request #4269 (2025-04-28 19:11:54)

Fix: Don't log SQL in the engine adapter if the statement contains values
Values may contain sensitive information and unnecessarily bloat the logs, making them incomprehensible..


Patch:
diff --git a/sqlmesh/core/engine_adapter/base.py b/sqlmesh/core/engine_adapter/base.py
index aa28066a..29e87001 100644
--- a/sqlmesh/core/engine_adapter/base.py
+++ b/sqlmesh/core/engine_adapter/base.py
@@ -2126,11 +2126,30 @@ class EngineAdapter:
                         else e
                     ),
                 )
-                self._log_sql(sql)
+                self._log_sql(
+                    sql,
+                    expression=e if isinstance(e, exp.Expression) else None,
+                    quote_identifiers=quote_identifiers,
+                )
                 self._execute(sql, **kwargs)
 
-    def _log_sql(self, sql: str) -> None:
-        logger.log(self._execute_log_level, "Executing SQL: %s", sql)
+    def _log_sql(
+        self,
+        sql: str,
+        expression: t.Optional[exp.Expression] = None,
+        quote_identifiers: bool = True,
+    ) -> None:
+        if not logger.isEnabledFor(self._execute_log_level):
+            return
+
+        sql_to_log = sql
+        if expression is not None and not isinstance(expression, exp.Query):
+            values = expression.find(exp.Values)
+            if values:
+                values.set("expressions", [exp.to_identifier("<REDACTED VALUES>")])
+                sql_to_log = self._to_sql(expression, quote=quote_identifiers)
+
+        logger.log(self._execute_log_level, "Executing SQL: %s", sql_to_log)
 
     def _execute(self, sql: str, **kwargs: t.Any) -> None:
         self.cursor.execute(sql, **kwargs)


Test patch:
diff --git a/tests/core/engine_adapter/test_base.py b/tests/core/engine_adapter/test_base.py
index fdbe6dac..edf22246 100644
--- a/tests/core/engine_adapter/test_base.py
+++ b/tests/core/engine_adapter/test_base.py
@@ -3071,3 +3071,33 @@ def test_insert_overwrite_by_partition_query_insert_overwrite_strategy(
     assert sql_calls == [
         'INSERT OVERWRITE TABLE "test_schema"."test_table" ("a", "ds", "b") SELECT "a", "ds", "b" FROM "tbl"'
     ]
+
+
+def test_log_sql(make_mocked_engine_adapter: t.Callable, mocker: MockerFixture):
+    adapter = make_mocked_engine_adapter(EngineAdapter)
+
+    mock_logger = mocker.patch("sqlmesh.core.engine_adapter.base.logger")
+
+    df = pd.DataFrame({"id": [1, 2, 3], "value": ["test1", "test2", "test3"]})
+
+    adapter.execute(parse_one("SELECT 1"))
+    adapter.execute(parse_one("INSERT INTO test SELECT * FROM source"))
+    adapter.execute(parse_one("INSERT INTO test (id, value) VALUES (1, 'test')"))
+    adapter.insert_append("test", df)
+    adapter.replace_query("test", df)
+
+    assert mock_logger.log.call_count == 5
+    assert mock_logger.log.call_args_list[0][0][2] == "SELECT 1"
+    assert mock_logger.log.call_args_list[1][0][2] == 'INSERT INTO "test" SELECT * FROM "source"'
+    assert (
+        mock_logger.log.call_args_list[2][0][2]
+        == 'INSERT INTO "test" ("id", "value") VALUES "<REDACTED VALUES>"'
+    )
+    assert (
+        mock_logger.log.call_args_list[3][0][2]
+        == 'INSERT INTO "test" ("id", "value") SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (VALUES "<REDACTED VALUES>") AS "t"("id", "value")'
+    )
+    assert (
+        mock_logger.log.call_args_list[4][0][2]
+        == 'CREATE OR REPLACE TABLE "test" AS SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (SELECT CAST("id" AS BIGINT) AS "id", CAST("value" AS TEXT) AS "value" FROM (VALUES "<REDACTED VALUES>") AS "t"("id", "value")) AS "_subquery"'
+    )