Skip to content

Add DDL workflow support and restore datamingle-app#19

Merged
jruszo merged 4 commits intomasterfrom
feature/ddl-workflow-spa
Apr 2, 2026
Merged

Add DDL workflow support and restore datamingle-app#19
jruszo merged 4 commits intomasterfrom
feature/ddl-workflow-spa

Conversation

@jruszo
Copy link
Copy Markdown
Owner

@jruszo jruszo commented Apr 1, 2026

What changed

This PR adds end-to-end DDL workflow support alongside the existing SQL workflow flow.

  • adds backend workflow metadata, detail, scheduling, execution-window, and approval support needed by the SPA
  • adds direct MySQL DDL execution for workflow tickets without pt-osc or gh-ost
  • adds a real workflow management SPA with list, detail, review, execute, and create flows
  • restores the local ARM compose app container name to datamingle-app
  • updates the local ARM Docker image so the full Django test suite can run in the container
  • makes Oracle engine imports/tests tolerant of missing cx_Oracle in the local ARM environment

Why

The repo previously supported SQL workflow behavior mainly through backend endpoints and an incomplete SPA placeholder. DDL tickets needed the same submit, approve, and execute lifecycle as DML requests, plus a usable frontend and a direct execution path for MySQL-family DDL.

The local container rename and test-environment fixes were also needed so validation could run cleanly in the expected datamingle-app container.

Impact

  • users can submit DDL workflow tickets from the SPA
  • reviewers and executors can work DDL tickets through the same approval flow as DML
  • local development instructions and compose naming are aligned again on datamingle-app
  • the full backend suite runs in the local ARM container image

Validation

  • docker exec datamingle-app python manage.py test
  • npm run build
  • targeted workflow and engine tests during implementation

Summary by CodeRabbit

Release Notes

  • New Features

    • Added new workflow creation interface with SQL validation and submission.
    • Enhanced workflow list filtering by search, status, syntax type, resource group, instance, engineer, and date range.
    • Added direct DDL execution support for workflows.
    • Introduced workflow rejection capability for reviewers.
  • Improvements

    • Redesigned workflow detail view with consolidated review and execution results.
    • Updated execution window management endpoint.
    • Expanded audit user permissions for workflow access.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

This PR introduces a refactored workflow submission and management system across the frontend and backend. It adds a new workflow creation UI component, restructures workflow API types and endpoints, enhances the MySQL execution engine for direct DDL execution, and updates access control and permission handling for workflow operations.

Changes

Cohort / File(s) Summary
Frontend API Types & Functions
frontend/src/lib/api.ts
Replaced workflow type definitions (e.g., WorkflowSyntaxType now `0
Frontend Routing & UI
frontend/src/router/index.ts, frontend/src/views/WorkflowsCreateView.vue, frontend/src/views/WorkflowsView.vue
Added new route for workflow creation (/workflows/new). Introduced WorkflowsCreateView component with SQL validation, resource/instance/database selection, and workflow submission logic. Refactored WorkflowsView to use reactive filter object, simplified pagination/detail loading, updated review/execution action handlers, and rewrote list/detail rendering with new badge styling and result-row tables.
Backend API Endpoints & Serializers
sql_api/api_workflow.py, sql_api/serializers.py, sql_api/urls.py
Migrated WorkflowList to ListAPIView with query-parameter filtering; added new WorkflowMetadata endpoint and supporting serializers (WorkflowSummarySerializer, WorkflowMetadataSerializer, WorkflowExecutionWindowSerializer); refactored detail/review/execution/schedule flows with updated permission checks and datetime normalization; added support for "reject" audit type; updated URLs to include new /window/ and /metadata/ endpoints while removing /content/ and /rollback/.
Database Execution Layer
sql/engines/mysql.py, sql/engines/oracle.py
Added direct DDL execution path for MySQL (execute_direct_workflow method) that executes statements via single connection with per-statement timing and result recording. Enhanced Oracle engine with resilient cx_Oracle import handling and conditional connection closure.
Access Control & Permissions
sql/utils/sql_review.py, sql_api/api_user.py
Expanded can_view permission to include audit\_user role. Updated user permission serialization to include "sql.menu_sqlworkflow" when user has query\_dml or query\_dml\_ddl instance access.
Test Coverage
sql/engines/test_mysql.py, sql/engines/tests.py, sql/utils/tests.py, sql_api/tests.py
Added direct DDL execution unit test; updated Oracle exception handling test; extended SQL review access tests for audit\_user; significantly expanded workflow API tests to cover new metadata, submission, detail, audit rejection, and execution-window endpoints.
Docker Build
src/docker/Dockerfile.local-arm
Added installation of dev-requirements.txt dependencies during ARM image build.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Browser)
    participant Frontend as WorkflowsCreateView
    participant API as Backend API
    participant DB as Database

    User->>Frontend: Load workflow creation page
    Frontend->>API: fetchWorkflowMetadata()
    API->>DB: Query resource groups & instances
    DB-->>API: Metadata records
    API-->>Frontend: Metadata payload
    Frontend-->>User: Render form with options

    User->>Frontend: Select resource group, instance, database
    Frontend->>Frontend: Filter instance options by group
    Frontend->>API: Fetch databases for instance
    API->>DB: Query instance databases
    DB-->>API: Database list
    API-->>Frontend: Database options
    Frontend-->>User: Update form fields

    User->>Frontend: Enter SQL & click "Check SQL"
    Frontend->>API: checkWorkflowSql(payload)
    API->>API: Parse & validate SQL
    API-->>Frontend: Syntax/warnings/errors & column list
    Frontend-->>User: Display check results

    User->>Frontend: Fill remaining fields & submit
    Frontend->>API: createWorkflow(payload)
    API->>DB: Insert workflow record
    DB-->>API: Workflow created with ID
    API-->>Frontend: Success response with workflow ID
    Frontend->>User: Navigate to workflow list & show new workflow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Add DML workflow SPA and local demo bootstrap #18 — Both PRs modify the same workflow API and SPA code paths, refactoring frontend types/functions, backend endpoints/serializers, and URL routing to implement metadata, submission, detail, review, execute, and schedule workflows.

Poem

🐰 A workflow garden now takes shape,
With seeds of SQL and forms to drape,
Where rabbits check syntax, approve with care,
And execute queries through data's air.
New metadata blooms where submissions begin! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add DDL workflow support and restore datamingle-app' clearly and accurately summarizes the main changes: introducing DDL workflow functionality and fixing container naming.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ddl-workflow-spa

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread sql_api/api_workflow.py Dismissed
Comment thread sql_api/api_workflow.py Dismissed
@jruszo jruszo changed the title [codex] Add DDL workflow support and restore datamingle-app Add DDL workflow support and restore datamingle-app Apr 2, 2026
@jruszo jruszo marked this pull request as ready for review April 2, 2026 07:26
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
sql_api/api_workflow.py (3)

762-769: Consider using not instead of is False for boolean check.

While is False is explicit, it's unconventional for boolean returns. If can_review returns a truthy/falsy value (not strictly boolean), is False would only match explicit False. Using not is more idiomatic.

♻️ Proposed simplification
-        if (
-            Audit.can_review(request.user, workflow_id, WorkflowType.SQL_REVIEW)
-            is False
-        ):
+        if not Audit.can_review(request.user, workflow_id, WorkflowType.SQL_REVIEW):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql_api/api_workflow.py` around lines 762 - 769, Replace the strict identity
check against False with an idiomatic boolean negation: instead of comparing the
result of Audit.can_review(request.user, workflow_id, WorkflowType.SQL_REVIEW)
using "is False", use "not" to test falsiness; keep the same behavior that when
the user cannot review the fetched SqlWorkflow (obtained via get_object_or_404
with workflow_id) you raise PermissionDenied with the same message.

963-966: Preserve exception chain when re-raising AuditException.

Similar to the earlier exception handling, use raise ... from e to maintain the traceback chain for debugging.

♻️ Proposed fix
         try:
             workflow_audit_detail = auditor.operate(action, user, data["audit_remark"])
         except AuditException as e:
-            raise serializers.ValidationError({"errors": f"Operation failed, {str(e)}"})
+            raise serializers.ValidationError({"errors": f"Operation failed, {e}"}) from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql_api/api_workflow.py` around lines 963 - 966, The except block that
catches AuditException should preserve the original exception chain when
re-raising as a serializers.ValidationError: change the raise to include "from
e" so the traceback is maintained (i.e., when handling AuditException from
auditor.operate(action, user, data["audit_remark"]) replace the current raise
serializers.ValidationError(...) with a re-raise that uses "from e" to keep the
original AuditException in the chain).

463-466: Broad exception catch may expose internal details and loses traceback context.

Catching bare Exception and re-raising loses the exception chain. Consider:

  1. Catching more specific exceptions when possible
  2. Using raise ... from e to preserve context for debugging
♻️ Proposed fix
         check_result = check_engine.execute_check(db_name=db_name, sql=full_sql)
-        except Exception as e:
-            raise serializers.ValidationError({"errors": f"{e}"})
+        except Exception as e:
+            raise serializers.ValidationError({"errors": f"{e}"}) from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql_api/api_workflow.py` around lines 463 - 466, The current broad except
Exception block around check_engine.escape_string and check_engine.execute_check
swallows the original traceback and may expose internals; update the handler to
catch the most specific errors possible (e.g., the DB/client error thrown by
check_engine and input errors like ValueError) instead of Exception, and when
re-raising use "raise serializers.ValidationError({'errors': str(e)}) from e" to
preserve the exception chain; reference the check_engine.escape_string,
check_engine.execute_check call sites and the serializers.ValidationError raise
so you update those exact places and add specific exception types with "raise
... from e" fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/views/WorkflowsCreateView.vue`:
- Around line 25-28: Concurrent fetchInstanceResources() calls can complete out
of order and overwrite state such as databaseOptions and the loading flags
(metadataLoading, databasesLoading, checkLoading, submitting). Fix it by adding
a per-call sentinel (e.g., incrementing requestId or using an AbortController
stored on the component) before each fetchInstanceResources() invocation, pass
that token into the async flow, and when a response/error arrives only update
databaseOptions and loading refs if the token matches the latest stored token
(or the request wasn't aborted); ensure any previous pending request is aborted
or ignored. Apply the same pattern for all places that call
fetchInstanceResources() (including the other block covering lines 155-180) so
only the most recent response mutates state.
- Around line 102-108: buildCheckSignature currently omits groupId and is
recomputed after the async sql-check, allowing stale in-flight results to be
treated as current; compute the signature once per request (capture it before
any await) and include form.groupId in buildCheckSignature (in addition to
form.instanceId, form.dbName, form.sqlContent), then pass that request-bound
signature/token through the async sql-check call and only accept/store the
response if the returned signature matches the captured one; also ensure the
invalidation logic that watches group/target/sql changes uses the same
signature/token so submission gating (the check-before-submit code paths)
rejects results that don’t match the current signature.

In `@sql/engines/mysql.py`:
- Around line 819-852: The helper _direct_workflow_statements currently falls
back to raw sql when review_content is malformed or when review rows are
filtered out, and it also ignores review verdicts (errlevel) — change it to
“fail closed”: if json.loads(review_content) raises (TypeError, JSONDecodeError)
return [] immediately; if any review row is unreadable (not list/dict) or any
parsed ReviewResult has errlevel == 2 (rejected/high-risk) return []
immediately; otherwise build and return the reviewed statements as before;
ensure execute_check and execute_direct_workflow behavior is preserved by
returning an empty list when reviews are unusable or rejected so execution is
blocked.

---

Nitpick comments:
In `@sql_api/api_workflow.py`:
- Around line 762-769: Replace the strict identity check against False with an
idiomatic boolean negation: instead of comparing the result of
Audit.can_review(request.user, workflow_id, WorkflowType.SQL_REVIEW) using "is
False", use "not" to test falsiness; keep the same behavior that when the user
cannot review the fetched SqlWorkflow (obtained via get_object_or_404 with
workflow_id) you raise PermissionDenied with the same message.
- Around line 963-966: The except block that catches AuditException should
preserve the original exception chain when re-raising as a
serializers.ValidationError: change the raise to include "from e" so the
traceback is maintained (i.e., when handling AuditException from
auditor.operate(action, user, data["audit_remark"]) replace the current raise
serializers.ValidationError(...) with a re-raise that uses "from e" to keep the
original AuditException in the chain).
- Around line 463-466: The current broad except Exception block around
check_engine.escape_string and check_engine.execute_check swallows the original
traceback and may expose internals; update the handler to catch the most
specific errors possible (e.g., the DB/client error thrown by check_engine and
input errors like ValueError) instead of Exception, and when re-raising use
"raise serializers.ValidationError({'errors': str(e)}) from e" to preserve the
exception chain; reference the check_engine.escape_string,
check_engine.execute_check call sites and the serializers.ValidationError raise
so you update those exact places and add specific exception types with "raise
... from e" fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22c27261-1374-4d03-8da2-3f3d51e8943f

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9159a and 63e29ea.

📒 Files selected for processing (16)
  • frontend/src/lib/api.ts
  • frontend/src/router/index.ts
  • frontend/src/views/WorkflowsCreateView.vue
  • frontend/src/views/WorkflowsView.vue
  • sql/engines/mysql.py
  • sql/engines/oracle.py
  • sql/engines/test_mysql.py
  • sql/engines/tests.py
  • sql/utils/sql_review.py
  • sql/utils/tests.py
  • sql_api/api_user.py
  • sql_api/api_workflow.py
  • sql_api/serializers.py
  • sql_api/tests.py
  • sql_api/urls.py
  • src/docker/Dockerfile.local-arm

Comment on lines +25 to +28
const metadataLoading = ref(false)
const databasesLoading = ref(false)
const checkLoading = ref(false)
const submitting = ref(false)
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.

⚠️ Potential issue | 🟠 Major

Ignore out-of-order database lookups.

Switching instances quickly can leave multiple fetchInstanceResources() calls in flight. An older response can then overwrite databaseOptions, clear the spinner, or surface an error for the wrong instance.

🧭 Proposed fix
 const metadataLoading = ref(false)
 const databasesLoading = ref(false)
 const checkLoading = ref(false)
 const submitting = ref(false)
+const databaseRequestId = ref(0)
@@
 watch(
   () => form.instanceId,
   async (instanceId) => {
+    const requestId = ++databaseRequestId.value
     form.dbName = ''
     databaseOptions.value = []
 
     if (!instanceId) {
+      databasesLoading.value = false
       return
     }
@@
     try {
       const payload = await fetchInstanceResources(
         Number(instanceId),
         'database',
         requireToken(),
       )
+      if (requestId !== databaseRequestId.value) {
+        return
+      }
       databaseOptions.value = payload.result
     } catch (errorValue) {
+      if (requestId !== databaseRequestId.value) {
+        return
+      }
       formError.value = toUserFacingMessage(errorValue, 'Failed to load databases for the selected instance.')
     } finally {
-      databasesLoading.value = false
+      if (requestId === databaseRequestId.value) {
+        databasesLoading.value = false
+      }
     }
   },
 )

Also applies to: 155-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/WorkflowsCreateView.vue` around lines 25 - 28, Concurrent
fetchInstanceResources() calls can complete out of order and overwrite state
such as databaseOptions and the loading flags (metadataLoading,
databasesLoading, checkLoading, submitting). Fix it by adding a per-call
sentinel (e.g., incrementing requestId or using an AbortController stored on the
component) before each fetchInstanceResources() invocation, pass that token into
the async flow, and when a response/error arrives only update databaseOptions
and loading refs if the token matches the latest stored token (or the request
wasn't aborted); ensure any previous pending request is aborted or ignored.
Apply the same pattern for all places that call fetchInstanceResources()
(including the other block covering lines 155-180) so only the most recent
response mutates state.

Comment on lines +102 to +108
function buildCheckSignature() {
return JSON.stringify([
form.instanceId,
form.dbName,
form.sqlContent,
])
}
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.

⚠️ Potential issue | 🔴 Critical

Make the SQL-check signature request-bound.

Line 230 recomputes the signature after the await, and buildCheckSignature() omits groupId even though Lines 132-137 invalidate the check on group changes. If the user changes the group/target/SQL while the request is in flight, a stale result can be stamped as current and Lines 261-263 will allow submission without a fresh review. The API still accepts workflow creation without a prior sqlcheck, so this client-side gate needs to fail closed.

🛡️ Proposed fix
 function buildCheckSignature() {
   return JSON.stringify([
+    form.groupId,
     form.instanceId,
     form.dbName,
     form.sqlContent,
   ])
 }
@@
 async function runSqlCheck() {
   formError.value = ''
@@
+  const requestSignature = buildCheckSignature()
   checkLoading.value = true
 
   try {
-    checkResult.value = await checkWorkflowSql(
+    const result = await checkWorkflowSql(
       {
         instance_id: Number(form.instanceId),
         db_name: form.dbName.trim(),
         full_sql: form.sqlContent,
       },
       requireToken(),
     )
-    lastCheckedSignature.value = buildCheckSignature()
+    if (requestSignature !== buildCheckSignature()) {
+      return
+    }
+    checkResult.value = result
+    lastCheckedSignature.value = requestSignature
   } catch (errorValue) {
+    if (requestSignature !== buildCheckSignature()) {
+      return
+    }
     formError.value = toUserFacingMessage(errorValue, 'Failed to run SQL check.')
   } finally {
     checkLoading.value = false
   }
 }

Also applies to: 132-137, 221-230, 261-263

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/views/WorkflowsCreateView.vue` around lines 102 - 108,
buildCheckSignature currently omits groupId and is recomputed after the async
sql-check, allowing stale in-flight results to be treated as current; compute
the signature once per request (capture it before any await) and include
form.groupId in buildCheckSignature (in addition to form.instanceId,
form.dbName, form.sqlContent), then pass that request-bound signature/token
through the async sql-check call and only accept/store the response if the
returned signature matches the captured one; also ensure the invalidation logic
that watches group/target/sql changes uses the same signature/token so
submission gating (the check-before-submit code paths) rejects results that
don’t match the current signature.

Comment thread sql/engines/mysql.py
Comment on lines +819 to +852
def _direct_workflow_statements(self, workflow):
"""Prefer reviewed statements so direct execution matches the checked SQL."""
review_content = workflow.sqlworkflowcontent.review_content or "[]"
statements = []

try:
review_rows = json.loads(review_content)
except (TypeError, json.JSONDecodeError):
review_rows = []

for row in review_rows:
if isinstance(row, list):
review_row = ReviewResult(inception_result=row)
elif isinstance(row, dict):
review_row = ReviewResult(**row)
else:
continue

statement = remove_comments(review_row.sql, db_type="mysql").strip()
if statement:
statements.append(statement.rstrip(";"))

if statements:
return statements

normalized_sql = sqlparse.format(
workflow.sqlworkflowcontent.sql_content,
strip_comments=True,
)
return [
statement.strip().rstrip(";")
for statement in sqlparse.split(normalized_sql)
if statement.strip()
]
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.

⚠️ Potential issue | 🔴 Critical

Fail closed when reviewed statements are blocked or unreadable.

execute_check() marks rejected/high-risk rows with errlevel = 2, but this helper currently executes every review_row.sql regardless. It also falls back to raw sql_content whenever review_content is malformed or filters down to nothing, which bypasses the reviewed statement set. Returning [] here is safer because execute_direct_workflow() already converts that into a failed execution.

🔒 Proposed fix
     def _direct_workflow_statements(self, workflow):
         """Prefer reviewed statements so direct execution matches the checked SQL."""
-        review_content = workflow.sqlworkflowcontent.review_content or "[]"
+        review_content = workflow.sqlworkflowcontent.review_content
         statements = []
+        has_review_content = bool(review_content)
 
-        try:
-            review_rows = json.loads(review_content)
-        except (TypeError, json.JSONDecodeError):
-            review_rows = []
+        if has_review_content:
+            try:
+                review_rows = json.loads(review_content)
+            except (TypeError, json.JSONDecodeError):
+                return []
+        else:
+            review_rows = []
 
         for row in review_rows:
             if isinstance(row, list):
                 review_row = ReviewResult(inception_result=row)
             elif isinstance(row, dict):
                 review_row = ReviewResult(**row)
             else:
                 continue
+            if int(getattr(review_row, "errlevel", 0) or 0) >= 2:
+                return []
 
             statement = remove_comments(review_row.sql, db_type="mysql").strip()
             if statement:
                 statements.append(statement.rstrip(";"))
 
-        if statements:
+        if has_review_content:
             return statements
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sql/engines/mysql.py` around lines 819 - 852, The helper
_direct_workflow_statements currently falls back to raw sql when review_content
is malformed or when review rows are filtered out, and it also ignores review
verdicts (errlevel) — change it to “fail closed”: if json.loads(review_content)
raises (TypeError, JSONDecodeError) return [] immediately; if any review row is
unreadable (not list/dict) or any parsed ReviewResult has errlevel == 2
(rejected/high-risk) return [] immediately; otherwise build and return the
reviewed statements as before; ensure execute_check and execute_direct_workflow
behavior is preserved by returning an empty list when reviews are unusable or
rejected so execution is blocked.

@jruszo jruszo merged commit 5bb072a into master Apr 2, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants