This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new e7c060466d fix(SqlLab): enhance SQL formatting with Jinja template 
support. (#36277)
e7c060466d is described below

commit e7c060466d36da98f76d7695aeb4d625ca6a22d0
Author: Luis Sánchez <[email protected]>
AuthorDate: Mon Dec 8 20:32:05 2025 -0300

    fix(SqlLab): enhance SQL formatting with Jinja template support. (#36277)
---
 superset-frontend/src/SqlLab/actions/sqlLab.js     |  22 ++-
 .../src/SqlLab/actions/sqlLab.test.js              | 195 ++++++++++++++++++++-
 superset/sqllab/api.py                             |  29 ++-
 superset/sqllab/schemas.py                         |   8 +
 tests/integration_tests/sql_lab/api_tests.py       |  19 ++
 5 files changed, 268 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.js
index 644d3e3346..fd23cb5753 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -914,11 +914,27 @@ export function 
queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
 
 export function formatQuery(queryEditor) {
   return function (dispatch, getState) {
-    const { sql } = getUpToDateQuery(getState(), queryEditor);
+    const { sql, dbId, templateParams } = getUpToDateQuery(
+      getState(),
+      queryEditor,
+    );
+    const body = { sql };
+
+    // Include database_id and template_params if available for Jinja 
processing
+    if (dbId) {
+      body.database_id = dbId;
+    }
+    if (templateParams) {
+      // Send templateParams as a JSON string to match the backend schema
+      body.template_params =
+        typeof templateParams === 'string'
+          ? templateParams
+          : JSON.stringify(templateParams);
+    }
+
     return SupersetClient.post({
       endpoint: `/api/v1/sqllab/format_sql/`,
-      // TODO (betodealmeida): pass engine as a parameter for better formatting
-      body: JSON.stringify({ sql }),
+      body: JSON.stringify(body),
       headers: { 'Content-Type': 'application/json' },
     }).then(({ json }) => {
       dispatch(queryEditorSetSql(queryEditor, json.result));
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 0e9644b69c..a90355b876 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -170,7 +170,16 @@ describe('async actions', () => {
   describe('formatQuery', () => {
     const formatQueryEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
     const expectedSql = 'SELECT 1';
-    fetchMock.post(formatQueryEndpoint, { result: expectedSql });
+
+    beforeEach(() => {
+      fetchMock.post(
+        formatQueryEndpoint,
+        { result: expectedSql },
+        {
+          overwriteRoutes: true,
+        },
+      );
+    });
 
     test('posts to the correct url', async () => {
       const store = mockStore(initialState);
@@ -181,6 +190,190 @@ describe('async actions', () => {
       expect(store.getActions()[0].type).toBe(actions.QUERY_EDITOR_SET_SQL);
       expect(store.getActions()[0].sql).toBe(expectedSql);
     });
+
+    test('sends only sql in request body when no dbId or templateParams', 
async () => {
+      const queryEditorWithoutExtras = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM table',
+        dbId: null,
+        templateParams: null,
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [queryEditorWithoutExtras],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      store.dispatch(actions.formatQuery(queryEditorWithoutExtras));
+
+      await waitFor(() =>
+        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+      );
+
+      const call = fetchMock.calls(formatQueryEndpoint)[0];
+      const body = JSON.parse(call[1].body);
+
+      expect(body).toEqual({ sql: 'SELECT * FROM table' });
+      expect(body.database_id).toBeUndefined();
+      expect(body.template_params).toBeUndefined();
+    });
+
+    test('includes database_id in request when dbId is provided', async () => {
+      const queryEditorWithDb = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM table',
+        dbId: 5,
+        templateParams: null,
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [queryEditorWithDb],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      store.dispatch(actions.formatQuery(queryEditorWithDb));
+
+      await waitFor(() =>
+        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+      );
+
+      const call = fetchMock.calls(formatQueryEndpoint)[0];
+      const body = JSON.parse(call[1].body);
+
+      expect(body).toEqual({
+        sql: 'SELECT * FROM table',
+        database_id: 5,
+      });
+    });
+
+    test('includes template_params as string when provided as string', async 
() => {
+      const queryEditorWithTemplateString = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
+        dbId: 5,
+        templateParams: '{"user_id": 123}',
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [queryEditorWithTemplateString],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      store.dispatch(actions.formatQuery(queryEditorWithTemplateString));
+
+      await waitFor(() =>
+        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+      );
+
+      const call = fetchMock.calls(formatQueryEndpoint)[0];
+      const body = JSON.parse(call[1].body);
+
+      expect(body).toEqual({
+        sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
+        database_id: 5,
+        template_params: '{"user_id": 123}',
+      });
+    });
+
+    test('stringifies template_params when provided as object', async () => {
+      const queryEditorWithTemplateObject = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
+        dbId: 5,
+        templateParams: { user_id: 123, status: 'active' },
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [queryEditorWithTemplateObject],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      store.dispatch(actions.formatQuery(queryEditorWithTemplateObject));
+
+      await waitFor(() =>
+        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+      );
+
+      const call = fetchMock.calls(formatQueryEndpoint)[0];
+      const body = JSON.parse(call[1].body);
+
+      expect(body).toEqual({
+        sql: 'SELECT * FROM table WHERE id = {{ user_id }}',
+        database_id: 5,
+        template_params: '{"user_id":123,"status":"active"}',
+      });
+    });
+
+    test('dispatches QUERY_EDITOR_SET_SQL with formatted result', async () => {
+      const formattedSql = 'SELECT\n  *\nFROM\n  table';
+      fetchMock.post(
+        formatQueryEndpoint,
+        { result: formattedSql },
+        {
+          overwriteRoutes: true,
+        },
+      );
+
+      const queryEditorToFormat = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM table',
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [queryEditorToFormat],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      await store.dispatch(actions.formatQuery(queryEditorToFormat));
+
+      const dispatchedActions = store.getActions();
+      expect(dispatchedActions).toHaveLength(1);
+      expect(dispatchedActions[0].type).toBe(actions.QUERY_EDITOR_SET_SQL);
+      expect(dispatchedActions[0].sql).toBe(formattedSql);
+    });
+
+    test('uses up-to-date query editor state from store', async () => {
+      const outdatedQueryEditor = {
+        ...defaultQueryEditor,
+        sql: 'OLD SQL',
+        dbId: 1,
+      };
+      const upToDateQueryEditor = {
+        ...defaultQueryEditor,
+        sql: 'SELECT * FROM updated_table',
+        dbId: 10,
+      };
+      const state = {
+        sqlLab: {
+          queryEditors: [upToDateQueryEditor],
+          unsavedQueryEditor: {},
+        },
+      };
+      const store = mockStore(state);
+
+      // Pass outdated query editor, but expect the function to use the 
up-to-date one from store
+      store.dispatch(actions.formatQuery(outdatedQueryEditor));
+
+      await waitFor(() =>
+        expect(fetchMock.calls(formatQueryEndpoint)).toHaveLength(1),
+      );
+
+      const call = fetchMock.calls(formatQueryEndpoint)[0];
+      const body = JSON.parse(call[1].body);
+
+      expect(body.sql).toBe('SELECT * FROM updated_table');
+      expect(body.database_id).toBe(10);
+    });
   });
 
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py
index d3cd123346..f81031a601 100644
--- a/superset/sqllab/api.py
+++ b/superset/sqllab/api.py
@@ -234,7 +234,34 @@ class SqlLabRestApi(BaseSupersetApi):
         """
         try:
             model = self.format_model_schema.load(request.json)
-            result = SQLScript(model["sql"], model.get("engine")).format()
+            sql = model["sql"]
+            template_params = model.get("template_params")
+            database_id = model.get("database_id")
+
+            # Process Jinja templates if template_params and database_id are 
provided
+            if template_params and database_id is not None:
+                database = DatabaseDAO.find_by_id(database_id)
+                if database:
+                    try:
+                        template_params = (
+                            json.loads(template_params)
+                            if isinstance(template_params, str)
+                            else template_params
+                        )
+                        if template_params:
+                            template_processor = get_template_processor(
+                                database=database
+                            )
+                            sql = template_processor.process_template(
+                                sql, **template_params
+                            )
+                    except json.JSONDecodeError:
+                        logger.warning(
+                            "Invalid template parameter %s. Skipping 
processing",
+                            str(template_params),
+                        )
+
+            result = SQLScript(sql, model.get("engine")).format()
             return self.response(200, result=result)
         except ValidationError as error:
             return self.response_400(message=error.messages)
diff --git a/superset/sqllab/schemas.py b/superset/sqllab/schemas.py
index ce86a990c5..3c2413f8d3 100644
--- a/superset/sqllab/schemas.py
+++ b/superset/sqllab/schemas.py
@@ -48,6 +48,14 @@ class EstimateQueryCostSchema(Schema):
 class FormatQueryPayloadSchema(Schema):
     sql = fields.String(required=True)
     engine = fields.String(required=False, allow_none=True)
+    database_id = fields.Integer(
+        required=False, allow_none=True, metadata={"description": "The 
database id"}
+    )
+    template_params = fields.String(
+        required=False,
+        allow_none=True,
+        metadata={"description": "The SQL query template params as JSON 
string"},
+    )
 
 
 class ExecutePayloadSchema(Schema):
diff --git a/tests/integration_tests/sql_lab/api_tests.py 
b/tests/integration_tests/sql_lab/api_tests.py
index 97a7eab9e8..9454e7c9b1 100644
--- a/tests/integration_tests/sql_lab/api_tests.py
+++ b/tests/integration_tests/sql_lab/api_tests.py
@@ -288,6 +288,25 @@ class TestSqlLabApi(SupersetTestCase):
         self.assertDictEqual(resp_data, success_resp)  # noqa: PT009
         assert rv.status_code == 200
 
+    def test_format_sql_request_with_jinja(self):
+        self.login(ADMIN_USERNAME)
+        example_db = get_example_database()
+
+        data = {
+            "sql": "select * from {{tbl}}",
+            "database_id": example_db.id,
+            "template_params": json.dumps({"tbl": '"Vehicle Sales"'}),
+        }
+        rv = self.client.post(
+            "/api/v1/sqllab/format_sql/",
+            json=data,
+        )
+        resp_data = json.loads(rv.data.decode("utf-8"))
+        # Verify that Jinja template was processed before formatting
+        assert "{{tbl}}" not in resp_data["result"]
+        assert '"Vehicle Sales"' in resp_data["result"]
+        assert rv.status_code == 200
+
     
@mock.patch("superset.commands.sql_lab.results.results_backend_use_msgpack", 
False)
     def test_execute_required_params(self):
         self.login(ADMIN_USERNAME)

Reply via email to