Skip to content

feat(launch): identify database files by their contents, and open .parquet from Finder (#2476) - #2587

Merged
datlechin merged 1 commit into
mainfrom
feat/identify-database-files-by-contents
Aug 31, 2026
Merged

feat(launch): identify database files by their contents, and open .parquet from Finder (#2476)#2587
datlechin merged 1 commit into
mainfrom
feat/identify-database-files-by-contents

Conversation

@datlechin

Copy link
Copy Markdown
Member

What was wrong

URLClassifier.classifyFile decided a file's type from url.pathExtension and nothing else. It is the single funnel for a Finder double-click, open -a, Open With, and a drag onto a window or the Dock icon, so a database with no extension or a foreign one (store.bin, ledger) was unrecognised however unambiguous its bytes were, and a .db file written by DuckDB was handed to the SQLite driver, which reports "file is not a database".

Two smaller gaps came with it. Info.plist declared no Parquet document type, so LaunchServices never offered TablePro for one even though the DuckDB driver already reads Parquet. And File > Open File… was a window command restricted to sql, psql, pgsql, so no database file could be picked from inside the app at all.

Two of the four claims in #2476 were already shipped: the SQLite extension family and duckdb/ddb are declared (#1331), and DuckDBFileKinds.readOnlyData already opens Parquet read-only.

What the engines actually do

Every claim here was measured against the shipping tools, sqlite3 3.43 and DuckDB 1.5.4, not taken from documentation.

Recognising a format is worth nothing unless the driver can then open the file, and the three candidates differ on exactly that:

Format Marker Opens under a wrong name?
SQLite SQLite format 3\0 at 0 yes, store.bin, ledger and notes.csv all query fine
DuckDB storage DUCK at 8 yes, warehouse.db, plain and thing.bin all query fine
Parquet PAR1 at both ends no, IO Error: … not a valid DuckDB database file

duckdb_open picks a data format's reader from the extension. A Parquet file named export.bin is refused outright, and one named fake.csv gets the CSV sniffer and fails on the first query rather than at connect. So Parquet is deliberately not recognised by content: the classifier would route a file the driver cannot open, which is worse than not recognising it. It reaches TablePro by name, through the new Info.plist claim, which is what the issue asked for. Both classifier and routing tests pin that decision with the reason.

The DuckDB marker needed hardening for the opposite reason. Four bytes are not enough to name a format, and SELECT 'DUCK'; spells them at exactly offset 8, so an ordinary SQL file would have been routed to DuckDB ahead of its own extension. The real header is [8-byte checksum][DUCK][storage version as little-endian uint64], so the signature also requires the version's high six bytes to be zero. Text cannot satisfy that, and a storage version past 65535 would cost recognition by content rather than break it.

The fix

DatabaseFileSignature is a set of byte markers at fixed offsets, all of which must match. DatabaseFileClassifier reads only as far as the longest one reaches, refuses anything that is not a regular file so a FIFO cannot block the main thread, and answers nil rather than throwing so the caller falls back to the extension.

The signatures live on PluginMetadataSnapshot.SchemaInfo, beside the fileExtensions they belong with, curated per type in the app. They are curated rather than declared by the plugin because claiming a format from the system also needs a CFBundleDocumentTypes entry only the app bundle can make. That means buildMetadataSnapshot has to carry them over or registering the plugin resets them to [], which is #1970's failure mode, so PluginMetadataRegistryCuratedCapabilityTests now pins that too.

classifyFile consults the contents after TablePro's own .tableplugin and .tablepro, and before every other extension, so contents win a disagreement.

Recognising a .db file as DuckDB exposed a latent defect in the route it then takes. TabRouter.openDatabaseFile deduplicated live sessions on connection.database, but a driver that opens a local file keeps its path in whichever field it declares, and DuckDB and libSQL leave database empty. So the scan missed an already-open DuckDB connection and opened a second duckdb_open on the same file, which CLAUDE.md documents as two independent read-write instances whose writes are invisible to each other. Both the scan and the connection it builds now go through localFilePathField.

Other consequences handled in the same change:

  • FileDropDestination.isOpenable now reads from disk, and EditorWindow.draggingUpdated: calls it on every pointer move. A drag session's pasteboard cannot change while it is in flight, so the answer is settled once in draggingEntered: and released on draggingExited: / draggingEnded:.
  • File > Open File… moves from MainSplitViewController to AppDelegate, which is where the File menu's other window-independent commands already live. It was gated on context.isConnected and unreachable from the welcome window, which was defensible while it only opened SQL files into a session and wrong now that it opens databases, connection shares and plugins that need no connection at all.
  • The panel offers everything TablePro can open. allowedContentTypes can only match a name, so it disables the very files this fixes; NSOpenSavePanelDelegate.panel(_:shouldEnable:) asks the classifier instead. It asks the new name-only URLClassifier.classifyByName first, so browsing a folder of files the app already recognises never reads from a stalled mount on the main actor, and caches per panel session.
  • Installing a driver can take long enough for the file to move, and both engines create a database at a path that no longer exists. The path is rechecked after that await and reported gone rather than recreated empty.

Info.plist gains a Parquet document type as Viewer / Alternate with an imported org.apache.parquet. DuckDB opens Parquet read-only, so Viewer is the honest role, and no app owns the format, so Alternate leaves a dedicated tool its association. This follows what #1594 established rather than claiming ownership.

Finally, DuckDB ships from the registry, so opening a .parquet without it used to raise a window headlined "Could not connect" whose only action left for the connection list. MissingDriverPluginPrompt asks about the file instead, before anything opens, and installs on a yes. It awaits waitForInitialLoad() before deciding, because a plugin without TableProProvidesDatabaseTypeIds registers on the eager path and a Finder open beats it to the question, which would offer to install a plugin the user already has.

PluginMetadataRegistry+RegistryDefaults.swift sat at exactly the 1200-line limit, so DuckDB moved into PluginMetadataRegistry+DuckDBDefaults.swift, following Elasticsearch, SurrealDB, Kafka and Turso, and joining the +DuckDBConnectionFields.swift that was already beside it.

What this cannot do

A Finder double-click on an extensionless file still will not open in TablePro. LaunchServices has nothing to match on, and declaring public.data would put TablePro in Open With for every file on the disk. Fixed here: drag onto a window, drag onto the Dock icon, Open With > Other, open -a TablePro, File > Open File…, and any SQLite or DuckDB file whose extension contradicts its contents.

Testing

  • DatabaseFileClassifierTests: both markers against files written with the measured header bytes, SELECT 'DUCK'; and a longer SQL statement asserted not to match, a truncated header, an empty file, a directory, a missing path, a remote URL, and Parquet under both names asserting it is left alone.
  • URLClassifierTests: a SQLite database named ledger, store.bin, notes.csv and query.sql; SELECT 'DUCK'; still routed to the editor; Parquet refused as export.bin and routed as export.parquet; classifyByName answering without reading; a JPEG still refused; .tablepro still decided by name.
  • FileDropDestinationTests: an extensionless SQLite database is accepted by a drop.
  • PluginMetadataRegistryCuratedCapabilityTests: signatures survive plugin registration.
  • DocumentTypeDeclarationTests: the Parquet claim, and every claimed extension is one its driver actually declares.

Built the app and ran 205 cases across 29 suites, plus swiftlint --strict on both targets and the docs checks. Reviewed by Codex twice; the review pass caught the Parquet routing defect and the connection-state gate, and the adversarial pass caught the DUCK collision, the duplicate DuckDB writer and the cold-launch plugin race.

No TableProUITests coverage: the flows are a Finder drag, a system open panel hosted by another process, and a plugin download. None runs deterministically here.

Fixes #2476

@mintlify

mintlify Bot commented Aug 31, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Aug 31, 2026, 12:57 PM

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit 99bf8fb into main Aug 31, 2026
14 checks passed
@datlechin
datlechin deleted the feat/identify-database-files-by-contents branch August 31, 2026 15:39
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.

Identify database files by their contents, and open .sqlite, .sqlite3 and .parquet from Finder

1 participant