Skip to content

fix(ios): list PostgreSQL materialized views and read indexes with the plugin's catalog queries - #3074

Merged
datlechin merged 4 commits into
mainfrom
fix/ios-postgres-matviews-indexes
Sep 23, 2026
Merged

datlechin merged 4 commits into
mainfrom
fix/ios-postgres-matviews-indexes

Conversation

@datlechin

@datlechin datlechin commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

On iPhone and iPad, a PostgreSQL schema listed no materialized views, and Table Structure got indexes wrong: expression keys and expression-only indexes were dropped, INCLUDE columns were reported as key columns, key order was lost, partial index predicates were lost, and every index was typed BTREE. PostgreSQL foreign tables were listed as plain tables with Truncate and Drop Table offered, and the server refuses both ("ft" is not a table).

The iOS driver now runs the macOS plugin's own catalog reads for the table list, the index list and the materialized view columns, shared by file reference the way the foreign key read already is. Redshift on iOS runs the Mac Redshift driver's listing and its DISTKEY / SORTKEY read.

Root cause

The iOS PostgreSQLDriver hand-wrote its own catalog SQL instead of sharing the plugin's audited reads:

  • The listing read information_schema.tables, which has no row for relkind = 'm'.
  • The index read joined pg_attribute on attnum = ANY(indkey). An expression key has attribute number 0, so it vanished; indkey also holds the INCLUDE columns after the key; the rows were ordered by attnum, not key position; pg_am.amname and indpred were never read. IndexInfo had nowhere to put INCLUDE columns or a predicate.
  • information_schema.columns has no rows for a materialized view either, so listing one alone would have left its Columns tab empty.
  • The same driver served Redshift without knowing it was on Redshift.
  • TableProModels.TableInfo.TableKind had no foreign table kind, so TableInfo(from:) sent both FOREIGN TABLE (the shared listing) and FOREIGN (information_schema.tables) to its default: arm, .table, which allows Truncate and Drop.

The plugin's correct reads were tangled with plugin-only code (PostgreSQLSchemaQueries needs ColumnQueryShape, the sequence files and more), which is why iOS never shared them.

What changed

Plugin (behaviour unchanged on the Mac, SQL proven byte-identical, see Measured):

  • PostgreSQLTableListing (new, nonisolated): the table listing moved out of PostgreSQLSchemaQueries as-is, plus the row decoder that used to sit inline in PostgreSQLPluginDriver.fetchTables.
  • PostgreSQLMaterializedViewColumnSource (new, nonisolated): the matview column arm's type, nullability, ordinal and FROM / WHERE fragments. The ERROR: relation "pg_matviews"does not exist #1383 arm in columnsQuery now builds from it.
  • RedshiftTableCatalog (new, nonisolated): the Redshift listing and the pg_table_def DISTKEY / SORTKEY read with its decoder, moved out of RedshiftPluginDriver.
  • PostgreSQLIndexQueries, PostgreSQLCatalogIndexDDL, PostgreSQLIndexRow, PostgreSQLTextArray, PostgreSQLCatalogBoolean and PostgreSQLCatalogPresence are marked nonisolated and join the iOS target. scripts/ci/check-ios-shared-isolation.py passes on all 32 shared sources.

iOS:

  • PostgreSQLDriver takes databaseType (as MySQLDriver does); IOSDriverFactory passes it.
  • At connect, PostgreSQL (not Redshift) runs PostgreSQLCatalogPresence.probeQuery. A failed probe means the optional catalogs are absent; there is no version-based fallback.
  • PostgreSQLDriver+Catalog.swift (a nonisolated extension, reading through execute(query:) so the actor stays private):
    • the listing: PostgreSQLTableListing with matviews and foreign tables gated by the probe, comments and partition awareness off; Redshift keeps RedshiftTableCatalog.listingQuery. Rows map through TableInfo(from:), so MATERIALIZED VIEW becomes .materializedView, which already withholds editing, Truncate and Drop.
    • the index list: PostgreSQLIndexQueries.indexList with .assumingModernWhenUnknown, decoded by PostgreSQLIndexRow and IndexInfo(from:). Redshift gets RedshiftTableCatalog and never receives pg_index SQL.
    • the columns: the existing information_schema arm plus a matview arm, both inside a derived table, ordinal_position kept out of the result and named only by ORDER BY cols.ordinal_position.
  • PostgreSQLColumnReadSupport replaces the reportsIdentityColumns catch-all. The attempts run identity + matview arm, then no identity, then no matview arm, then neither, and the first success decides what is remembered, so a failing matview arm never turns identity columns off, and a failing identity projection never drops the matview arm.
  • Access changes in PostgreSQLDriver.swift: effectiveSchema and logger are internal, columnReadSupport is an internal nonisolated(unsafe) var, catalogPresence is private(set).
  • IndexInfo (TableProModels) gains includedColumns and whereClause, both defaulted; IndexInfo(from:) maps them. Structure shows INCLUDE (...) and WHERE ... as verbatim SQL under the key parts, and the type badge shows the real access method.
  • TableKind gains .foreignTable. TableInfo(from:) maps both FOREIGN TABLE (the spelling the Mac's PluginTableKindDecoder reads) and FOREIGN. It lists under Tables, allows no Truncate and no Drop (the iOS list writes a literal DROP TABLE), and keeps row editing, as the Mac's TableType.foreignTable does.
  • TableKindPresentation: an exhaustive per-kind SF Symbol and VoiceOver kind, matching the Mac's TableRowLogic (materialized view square.stack.3d.up / "Materialized View", foreign table link / "Foreign Table"). Five new strings added to the iOS catalog.

Measured

  • SQL identity across the move. Before touching any builder I compiled origin/main's query sources in a swiftc probe and recorded 93 statements: fetchTables for two schemas (one holding ' and \) across all 16 flag combinations plus the defaulted call, columnsQuery at server versions 0, 9.6, 10, 11, 12 and 17.11 for all tables, orders and it's, with and without matviews, indexList for the same grid, the Redshift listing and key reads, and the presence probe. The same probe over the moved code produced a byte-identical file (SHA-256 8ac5b9952d47c220f7f39af6a7df38c319787e557c9085c111c3db6570b7f49d both sides, 5,554 lines). PostgreSQLCatalogSQLPinTests pins the shipped shapes as exact strings recorded from origin/main.
  • PostgreSQL 17.11, old iOS reads (from the design pass, schema with t, v, mv, partitioned p / p_2024, foreign ft): the listing returned ft, p, p_2024, t, v with mv missing; on t, t_expr_only was absent, t_mixed read [tenant_id], t_include read [a, b], t_order (created (b, a)) read [a, b], t_partial lost (a > 0), and GIN, HASH and BRIN all read BTREE; on mv, mv_expr was absent; information_schema.columns returned 0 rows for the matview.
  • PostgreSQL 17.11, new iOS reads, through the real driver in the simulator (PostgreSQLDriverCatalogTests): mv lists as .materializedView; t_pkey first; t_mixed [tenant_id, lower(email)]; t_include [a] including [b]; t_order [b, a]; t_partial WHERE (a > 0); t_hash HASH; the matview has columns mv_id integer, mv_expr text, label character varying with length 20, and indexes mv_id_key and mv_expr_idx on lower(mv_expr); the first column read keeps both identity columns and the matview arm.
  • Foreign table statements on PostgreSQL 17.11: information_schema.tables reports the table as FOREIGN; DROP TABLE fails with "ft" is not a table and the hint Use DROP FOREIGN TABLE to remove a foreign table.; DROP FOREIGN TABLE succeeds; TRUNCATE depends on the foreign data wrapper (one without a handler refuses it), and the Mac never offers it on a foreign table.
  • currentPluginKitVersion is untouched and nothing under Plugins/TableProPluginKit changed, so no ABI check applies.

Tests

Where Suites Cases Result
iOS, simulator (iPhone Duo, iOS 27.1, Xcode 27.0, ARCHS=arm64) 18 139 139 passed, 0 skipped, 0 failed; the live suite ran against PostgreSQL 17.11
macOS TableProTests via verify.sh test 21 116 116 passed
swift test --filter TableProModelsTests 6 55 55 passed

iOS suites run: PostgreSQLCatalogQueryTests (new, 16), PostgreSQLColumnReadSupportTests (new, 5), PostgreSQLDriverCatalogTests (new, live, 5), TableKindPresentationTests (new, 10), and the existing PostgreSQLForeignKeyQueryTests, PostgreSQLConnectionStringTests, PostgreSQLCopyStateTests, PostgreSQLTransactionStatementTests, IOSDriverFactoryLocalFileTests, MySQLTableListingTests, MySQLVariantSupportTests, SQLBuilderPaginationTests, SQLDDLFallbackPolicyIOSTests, SQLDialectParityTests, ColumnMetadataRulesTests, TableKindListBehaviourTests, RowDetailViewModelTests, DataBrowserViewModelTests. TableKindListBehaviourTests now expects .foreignTable under Tables and row-editable, like the Mac; its Truncate and Drop loops already require both to be off for every kind but a table (and a sequence for Drop). The live suite skips without POSTGRES_TEST_HOST (pass it as TEST_RUNNER_POSTGRES_TEST_HOST) or /tmp/postgres-test.json, like OracleDriverTests.

macOS suites run: PostgreSQLCatalogSQLPinTests, PostgreSQLTableListingTests, RedshiftTableCatalogTests (new), and PostgreSQLFetchTablesQueryTests, PostgreSQLFetchTablesCommentTests, PostgreSQLLegacyCatalogQueryTests, PostgreSQLLiteralQuotingTests, PostgreSQLLiteralQuotingSourceScanTests, PostgreSQLPartitionFilterTests, PostgreSQLTableListingLadderTests, PostgreSQLColumnsQueryTests, PostgreSQLMaterializedViewColumnsQueryTests, PostgreSQLIndexQueryTests, PostgreSQLIndexKeyPartTests, PostgreSQLIndexDDLQueryTests, TableStructureIndexReplayTests, PostgreSQLCatalogBooleanTests, PostgreSQLCatalogPresenceTests, RedshiftColumnsQueryTests, PluginPartitionRelationTypeTests, PostgreSQLSchemaEscapeTests. The five suites that called PostgreSQLSchemaQueries.fetchTables are repointed to PostgreSQLTableListing.query in this commit with their expectations unchanged.

What turns each new test red. A mutation run applied the first three edits at once and the listed cases failed:

  • tablesQuery passing includeMaterializedViews: false: optionalCatalogsFollowTheProbe and live testMaterializedViewIsListedAsOne (measured red).
  • columnsQuery never adding the matview arm: materializedViewArm, columnQueryQuotes and live testMaterializedViewHasColumnsAndIndexes (measured red).
  • IOSDriverFactory not passing databaseType: redshiftRoutesWithItsType (measured red).
  • .materializedView given the view's eye symbol: kindsAreDistinct (measured red) and materializedViewIsNotAView.
  • indexesQuery without its Redshift guard: redshiftKeys.
  • Presence falling back to the server version on a failed probe: failedProbeMeansAbsent.
  • The old attnum = ANY(indkey) read, or IndexInfo(from:) not mapping the new fields: indexRowsDecode, live testIndexesKeepKeyOrderExpressionsIncludeColumnsAndPredicates, mapPluginIndexInfoIncludeAndPredicate.
  • Attributing every column read failure to identity columns, as the old catch-all did: materializedViewArmFailureKeepsIdentity, identityFailureKeepsMaterializedViews.
  • Removing the FOREIGN TABLE / FOREIGN arm from TableInfo(from:): mapPluginForeignTable (measured red, 4 issues), foreignTableKind, live testForeignTableIsListedAsOne. Removing the .foreignTable case fails foreignTableIsNotATable and TableKindListBehaviourTests at compile time.
  • PostgreSQLCatalogSQLPinTests, PostgreSQLTableListingTests and RedshiftTableCatalogTests pin moved code: they pass on both sides by construction, and any change to the moved SQL or decoders turns them red.

Also: verify.sh build (TablePro) PASS, verify.sh build PostgreSQLDriver PASS, iOS build-for-testing PASS with no warnings in touched files, verify.sh docs PASS, check-ios-shared-isolation.py PASS. verify.sh lint over every touched file: 0 violations on changed lines; the 6 it reports are pre-existing (listed below).

Before / After

Screenshots to be added. States to capture on iPhone (light and dark), against a PostgreSQL schema holding a table with the indexes t_mixed (tenant_id, lower(email)), t_include (a) INCLUDE (b), t_order (b, a), t_partial (a) WHERE a > 0, a GIN, a HASH and a BRIN index, a view, and a materialized view with an expression index:

  1. Tables list. Before: no materialized view. After: the materialized view under Views with the stacked-squares symbol.
  2. Table Structure > Indexes for the table. Before: missing and wrong columns, every badge BTREE. After: key parts in order, the INCLUDE (b) line, the WHERE (a > 0) line, GIN / HASH / BRIN badges.
  3. Table Structure > Columns for the materialized view. After only (before, the view was unreachable).
  4. Table Structure > Indexes for the materialized view, showing lower(...).
  5. VoiceOver on the materialized view row reads "Materialized View, mv".
  6. A foreign table row under Tables with the link symbol, and its context menu. Before: Truncate Table and Drop Table. After: Copy Name only.

Critique points not taken

None. Every correction in the critique is applied:

  • SQL recorded before the move and asserted byte-for-byte after it (probe diff over 93 statements, plus exact-string pins in tests).
  • A failed catalog probe means absent on iOS, with a test; the version fallback was not copied.
  • The iOS columns UNION sits in a derived table, ordinal_position stays out of the result, and identity columns are dropped only when they are what failed.
  • PostgreSQLDriver+Catalog.swift is a nonisolated extension reading through execute(query:); the access changes are listed above. Every top-level declaration in the shared files is nonisolated.
  • Redshift shares the Mac's listing (RedshiftTableCatalog.listingQuery) rather than moving to an unmeasured query.
  • The iOS MSSQL index defects are listed below.
  • Review follow-up: foreign tables, first listed below as a follow-up, are fixed in this PR (second commit).

Deviations from the design, both in naming only: PostgreSQLTableListingQueries shipped as PostgreSQLTableListing (query and table(fromRow:)), and RedshiftTableKeys shipped as RedshiftTableCatalog, which holds the listing too, as the critique asked.

Deliberately not fixed here

  • iOS SQL Server indexes. TableProMobile/TableProMobile/Drivers/MSSQLDriver.swift:268 hard-codes CLUSTERED. MSSQLSchemaQueries.indexes (Packages/TableProCore/Sources/TableProMSSQLCore/MSSQLSchemaQueries.swift:194) reads neither is_included_column nor filter_definition and orders by key_ordinal, which is 0 for an INCLUDE column, so those come first and read as key columns. IndexInfo now carries includedColumns and whereClause for it.
  • iOS MySQL indexes. TableProMobile/TableProMobile/Drivers/MySQLDriver.swift:231 hard-codes BTREE over SHOW INDEX's Index_type (FULLTEXT, SPATIAL, HASH), and the guard ... let colName = row[4] at line 212 drops a functional key part, whose Column_name is NULL.
  • iOS Oracle indexes. TableProMobile/TableProMobile/Drivers/OracleDriver.swift:262 hard-codes BTREE for bitmap and function-based indexes.
  • Drop Foreign Table on iOS. The Mac offers Drop Foreign Table; the iOS list writes one literal DROP TABLE for every kind it offers, so a foreign table gets no Drop here until that list builds its statement per kind (TableProMobile/TableProMobile/Views/TableListView.swift:193).
  • Partitions on iOS stay listed flat: partition awareness is off because the iOS list has no parent row to expand.
  • A transient column read failure followed by a success still teaches the iOS driver to drop that projection for the session, as the old identity catch-all did.
  • Pre-existing lint errors (public_error_text_in_log) on lines this PR does not touch: Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift:815, :840, and Plugins/PostgreSQLDriverPlugin/RedshiftPluginDriver.swift:61, :108, :138, :241.
  • Risk to note: a PostgreSQL-compatible engine saved as PostgreSQL on iOS now gets the plugin's index SQL (generate_series, pg_am, pg_get_indexdef(oid, k, true)), which the Mac already sends it; if one of those is missing, that Structure load fails.

@mintlify

mintlify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

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

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 23, 2026, 7:17 PM

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

…iews-indexes

# Conflicts:
#	CHANGELOG.md
#	Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift
#	Plugins/PostgreSQLDriverPlugin/PostgreSQLSchemaQueries.swift
…iews-indexes

# Conflicts:
#	CHANGELOG.md
#	TableProMobile/TableProMobile/Localizable.xcstrings
#	TableProMobile/project.yml
@datlechin
datlechin merged commit 561cecf into main Sep 23, 2026
7 of 8 checks passed
@datlechin
datlechin deleted the fix/ios-postgres-matviews-indexes branch September 23, 2026 19:19

This branch was successfully deployed

1 active deployment
staging - docs — be091765 Deployed Sep 23, 2026 by mintlify[bot]
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.

1 participant