Skip to content

fix(plugins): stop generating SQL DDL for engines that have none - #2896

Merged
datlechin merged 1 commit into
mainfrom
fix/non-sql-drop-object-2884
Sep 15, 2026
Merged

datlechin merged 1 commit into
mainfrom
fix/non-sql-drop-object-2884

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2884.

The bug

Deleting an Elasticsearch index from the sidebar generated DROP TABLE "test_index" and failed with Invalid response: Enter a request like: GET /my-index/_search, which is the plugin's own console parser rejecting the text before anything reached the cluster.

Root cause

PluginDatabaseDriver gives dropObjectStatement and truncateTableStatements a default of nil, documented "return nil to use app-level fallback". nil says two different things. On MySQL it means the generic DDL is right for me, which is why eleven SQL plugins never implement either hook. On Elasticsearch it means I have no such statement at all.

PluginDriverAdapter could not tell those apart and resolved the ambiguity by inventing SQL (PluginDriverAdapter.swift:806 and :797), so a REST engine was handed SQL DDL. The Redis plugin already returned "" from dropObjectStatement with the comment "Return empty string to prevent adapter from synthesizing SQL DROP": a plugin author had hit this and worked around it with a sentinel.

This was never one engine. Truncate is the same fallback on the neighbouring menu item, and TableOperationEligibility.canTruncate only inspected the object's kind, never the engine, while the sidebar appended Delete unconditionally.

The fix

The adapter stops guessing, and the menu stops offering what the driver would refuse. That second half matters: returning nil without gating the menu would turn a visible error into a silent no-op.

App

  • SQLDDLFallbackPolicy states per engine whether the app may write DDL at all. Neither of the engine's other language facts can answer it: DynamoDB writes PartiQL and so declares an editor language of .sql while returning nil from both hooks on purpose, and the SQL dialect descriptor defaults to nil on several engines that do have DROP TABLE.
  • PluginDriverAdapter.dropObjectStatement / truncateTableStatements return optionals and fabricate only where the policy allows. tableOperationEligibility(for:isReadOnly:) resolves the answer per object name, so a plugin that refuses one object and not another is respected.
  • TableOperationSQLBuilder.generate throws objectOperationUnsupported for a staged ref the engine has no statement for, instead of coercing it to "". The menus keep one off the queue, but a driver answers from live session state, so a queued Redis truncate can stop being expressible between staging and Save. Dropping it silently would run the rest of the plan while the coordinator recorded every staged object as done.
  • TableOperationEligibility gained the engine dimension, all-or-nothing over a selection for the reason Truncate already gave.
  • Gated the sidebar menu, SidebarViewModel.batchToggleDelete, and the Database-menu Truncate and Edit-menu Delete validators, which were reaching the same staging calls ungated.

Hiding rather than disabling follows what DatabaseTreeMenuSpec already does for Rename, Truncate and container Drop.

Plugins

Plugin Change
Elasticsearch dropObjectStatement returns DELETE /<index>; truncate stays nil
etcd Both hooks had wrong signatures, so they witnessed no requirement and never ran
Redis "" sentinel removed; FLUSHDB refused unless the row is the session's own database
MongoDB truncateTableStatements returns deleteMany({})
Weaviate dropObjectStatement returns DELETE /v1/schema/<Class>
Typesense Emits console text instead of a base64 blob

Elasticsearch returns plain console text rather than the tagged base64 form used for row writes, because all three channels read that one string: the confirmation dialog shows it verbatim, QueryClassifier reads the leading verb to tier it destructive, and executeConsole sends it. A tagged blob would have shown the user base64 to approve and classified a drop as an ordinary write. That last point is why Typesense changed too: its tagged drop was classifying as .write, not .destructive, so it skipped the destructive-consent gate.

Elasticsearch has no truncate. _delete_by_query is asynchronous, reports conflicts per document and keeps the mapping, so it is a bulk delete rather than a truncate, and Truncate is now hidden there instead of offering something that means something else.

No PluginKit change and no ABI bump: neither method is on the DatabaseDriver protocol, so making them optional is app-internal.

Tests

  • SQLDDLFallbackPolicyTests scans DatabaseType.swift for declared constants so a new engine cannot inherit an answer by accident, and scans the plugin sources so an engine declaring a non-SQL editor language can never be left fabricating. The app holds the policy and the plugin holds the statement, so nothing else forces those to agree.
  • ElasticsearchOperationsTests round-trips the generated statement back through ElasticsearchConsoleParser, so the dialog can never again show text the driver rejects, and covers the names refused as multi-target.
  • TypesenseOperationsConsoleTextTests and WeaviateOperationsTests round-trip those statements too, and assert a Typesense drop and truncate now classify as destructive.
  • TableOperationSQLBuilderTests covers the rejection: one inexpressible ref rejects the whole batch rather than quietly running the rest.
  • TableOperationEligibilityEngineTests and new cases in PluginDriverAdapterTableOpsTests, DatabaseTreeMenuSpecTests and SidebarContextMenuLogicTests.

The existing adapter tests all build on .postgresql and pass unchanged, which is the check that no SQL engine regressed.

No UI automation: the flow needs a live Elasticsearch cluster and a registry plugin that never loads under XCTest, so it does not run deterministically.

Verified

Build PASS. 213 cases across 14 suites PASS. The Weaviate package suite passes. Each of the six changed plugin schemes builds. swiftlint --strict is clean over all 33 changed files, passed by explicit path because .swiftlint.yml scopes to TablePro and Packages. Both docs scripts pass.

AllPlugins reports one error, in the vendored oracle-nio SPM checkout (@TaskLocal macro expansion). It is pre-existing, no Oracle file is in this change, and there are zero errors under Plugins/.

Delivery

The app change ships with the next release. Elasticsearch, etcd, Redis, MongoDB, Weaviate and Typesense are registry plugins and need re-release; Elasticsearch also through scripts/release-plugin-for-shipped-app.sh so 0.74.0 users get the fix without waiting for an app release.

Not in this change

  • Elasticsearch export fabricates SELECT * FROM "<index>" the same way. I wrote the fix, found that the browse query with size: 0 would export an empty file, and reverted it: it needs real search_after streaming and a live cluster to verify.
  • TableProMobile/Views/TableListView.swift:149,175 hardcodes DROP TABLE / TRUNCATE TABLE in the view for every engine, with no driver hook at all. On iOS a Redis key long-press still offers Drop Table and sends DROP TABLE "session:42". The fix there is a different shape and would need the policy moved to a shared package.
  • Native drop for Kafka (its KafkaQL has no DDL verb) and DynamoDB (DeleteTable is an API call with no statement form). Both now refuse honestly instead of emitting SQL.

Review

Reviewed by Codex (review --scope working-tree), which raised seven findings. Six were real and are fixed here: the silent omission at save time, the Redis flush race that the rejection above closes, a SidebarViewModelTests regression from resolving capability through a hard-wired DatabaseManager lookup, a queued operation that could no longer be unstaged once it stopped being expressible, an Edit > Delete item left enabled as a no-op, and a Swift 6 actor-isolation error on the PluginManager assertion. The seventh, that the coverage test read the mutable plugin registry, was fixed differently: it now scans DatabaseType.swift for declared constants, which is deterministic and is the real list of engines.

@mintlify

mintlify Bot commented Sep 15, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 15, 2026, 10:39 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@datlechin
datlechin merged commit c602a9a into main Sep 15, 2026
8 of 9 checks passed
@datlechin
datlechin deleted the fix/non-sql-drop-object-2884 branch September 15, 2026 11:20
@JX-WANG117

Copy link
Copy Markdown

好耶QWQ

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.

Deleting an Elasticsearch index from the sidebar generates invalid SQL

2 participants