Skip to content

feat(search): find a value in every table of a database - #3737

Merged
datlechin merged 10 commits into
mainfrom
feat/3601-search-database
Oct 21, 2026
Merged

datlechin merged 10 commits into
mainfrom
feat/3601-search-database

Conversation

@datlechin

@datlechin datlechin commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Database > Search Database…, plus Search Database… / Search Schema… on a sidebar database or schema row (hierarchical schema rows too), open a sheet that finds which tables hold a value.
  • Sheet: value, match mode (Contains, Matches Exactly, Starts With), Match Case (dimmed where the collation decides, as in the filter bar), Include binary columns, and a checklist of tables grouped by schema.
  • Text columns always join. Number, date, timestamp and UUID columns join when the value parses as that type, as =. Binary only when ticked. A column joins only where the engine can compare it, since one bad predicate fails the table.
  • One SELECT per table on a pooled, cancellable read with the connection's query timeout: a CASE WHEN flag per searched column, conditions from FilterSQLGenerator, at most 1,001 rows through SQLRowLimitClause.
  • Progress counts tables. Stop aborts the statement in flight and keeps the results. A table that errors is listed with the error and the search goes on.
  • Results show each table's matching columns and row count (1,000+ past the cap). Open puts the table in a new tab filtered with Match any over the matched columns, or every searched column when capped.
  • The session stays on the window, so the sheet reopens on its results.
  • Not offered on MongoDB, Redis, Cassandra, ScyllaDB, DynamoDB or other engines without SQL, a row limit and Match any.
  • The value is not bound as a parameter: the search uses the filter bar's escaped conditions, so the tab runs the same predicate.
  • FilterSQLGenerator, also used by the filter bar and row counts:
    • Trino: case-insensitive regex as REGEXP_LIKE(col, '(?i)…'); its REGEXP_LIKE takes two arguments. The five pinned Trino cases in FilterSQLGeneratorCaseSensitivityTests are updated.
    • SQL Server: [ in a LIKE value is escaped.
    • Oracle family: date and timestamp values compare as ANSI DATE / TIMESTAMP literals instead of strings read through NLS_DATE_FORMAT.
  • ColumnTypeSQLQuoting.hasEqualityOperator takes the engine family: SQL Server TEXT has no =. The cell Filter menu passes it, so it no longer offers = on such a column.
  • Docs: docs(features): document Search Database docs#92.

Form, light
Results, light
Results, dark

Test plan

  • DatabaseSearchColumnPlannerTests: which columns join per type, mode and engine family.
  • DatabaseSearchQueryTests: flags, OR, cap + 1 on LIMIT and OFFSET/FETCH engines, escaping, cap reporting, the tab's filter state.
  • DatabaseSearchSessionTests: preselection, order, a failing table, Stop at the next table, Edit Search fencing a late read.
  • DatabaseSearchResultOpeningTests: the payload a result opens, and the session kept per scope.
  • FilterSQLGeneratorColumnTypeTests, FilterSQLGeneratorCaseSensitivityTests, CellFilterMenuBuilderTests: the generator and equality fixes.
  • DatabaseTreeMenuSpecTests, MainMenuBuilderTests: the commands and their gates.
  • SearchDatabaseUITests: on the fixture, search "brook", open the result, the artist tab shows only Brook.

Fixes #3601

…base

# Conflicts:
#	apple/TablePro/Core/Utilities/SQL/ColumnTypeSQLQuoting.swift
#	apple/TablePro/Resources/Localizable.xcstrings
…base

# Conflicts:
#	apple/TablePro/Views/Main/MainContentCoordinator.swift

@J2TeamNNL J2TeamNNL left a comment

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.

Code review: 7 findings, inline.

guard characters.count == 10 else { return false }
return isCalendarDate(characters)
}

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.

isDateTime accepts 1 to 9 fractional digits and a space separator for every engine. SQL Server datetime rejects more than 3 fractional digits and reads the space form by session language. Searching 2024-01-05 10:00:00.1234567 would fail the whole table query, so no column of that table gets searched.

}

// MARK: - Catalog

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.

loadTablesIfNeeded returns once the catalog is loaded, and the coordinator caches the session. After a table is dropped or created, reopening the sheet shows the old list: new tables are never searched and dropped ones come back as failed.

let namespaces = try await ObjectCopyCatalog(manager: manager).namespaces(of: endpoint, connection: connection)
let systemSchemas = Set(PluginManager.shared.systemSchemaNames(for: connection.type))
var found: [DatabaseSearchTarget] = []
for namespace in namespaces {

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.

tables() throws on the first schema whose fetchTables fails (no USAGE privilege, broken synonym). The catalog becomes failed and no table in any other schema can be searched.

case .null:
return false
case .bytes(let data):
return data.contains { $0 != 0 }

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.

isSet treats any non-zero byte in .bytes as set. A driver that returns the flag 0 as ASCII 0 (0x30) makes every column look matched, so the tab filters on columns that do not match.

)
databaseSearchSession = session
return session
}

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.

openDatabaseSearchResult always calls session.stop(). Double-clicking a result while the search runs cancels the remaining tables, though the footer Open button is hidden during a run.

? String(format: String(localized: "%@ row"), count)
: String(format: String(localized: "%@ rows"), count)
}

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.

The '%1$@ of %2$@ tables' formatting is duplicated in DatabaseSearchSummary.progress and DatabaseSearchSession.selectionSummary. A change to one leaves the other behind.

guard characters.count >= 19, characters[10] == " " || characters[10] == "T",
[11, 12, 14, 15, 17, 18].allSatisfy(isDigit), characters[13] == ":", characters[16] == ":"
else { return nil }
let fraction = characters.dropFirst(19)

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.

oracleTemporalLiteral turns every ANSI-shaped value on an Oracle date column into DATE '...', so = 2024-01-05 matches only midnight rows. This also changes the existing filter bar, not only search.

@J2TeamNNL

Copy link
Copy Markdown
Contributor

Follow-up on the review above.

Fixed in J2TeamNNL@f0f5b53:

  • Reader: one unreadable schema no longer fails the catalog. It errors only when no table could be read.
  • isSet: ASCII flag bytes such as 0 are read as text, with a test.

Skipped:

  • SQL Server datetime fraction: needs a per-engine shape rule I have not verified.
  • Stale cached catalog: needs a refresh path and a decision on what happens to results and selection.
  • Open stops a running search: looks intended, the maintainer's call.
  • Duplicated tables format: cosmetic.
  • Oracle date literal: touches the existing filter bar, out of scope here.

Checks: none run. SwiftLint fails here (sourcekitd missing) and there is no local Xcode build.

Cherry-pick: git fetch https://github.com/J2TeamNNL/TablePro review-fix/3737 && git cherry-pick f0f5b53b94ff410f2d7a54b72b2d380ae651d233

@datlechin
datlechin merged commit babb15c into main Oct 21, 2026
8 checks passed
@datlechin
datlechin deleted the feat/3601-search-database branch October 21, 2026 11:14
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.

Search for a value across every table in a database

2 participants