MySQL/MariaDB database management for macOS
# AGENTS.md — guidance for AI coding agents
Sequel Ace is a native macOS GUI client for MySQL and MariaDB (a maintained fork
of Sequel Pro). It is an AppKit application with a large Objective-C legacy
codebase undergoing a gradual, deliberate modernization to Swift and SwiftUI.
Deployment target is macOS 12+.
## Language policy — the most important rule
- **All new code is Swift. No new Objective-C.** No new `.h`/`.m` files, and no
significant new ObjC code added to existing files. When a change touches
legacy ObjC, prefer extracting the logic into a new Swift type and leaving a
thin trampoline/bridge behind in the `.m` file.
- **New Swift types use the `SA` prefix** (`SAPrintUtility`, `SAArchiving`,
`SAConnectionService`). The `SP` prefix marks legacy Sequel Pro-era code.
- **New UI is SwiftUI** where feasible. Established hosting pattern:
`@objc final` `NSWindowController` subclass + `NSHostingView` + SwiftUI root
view — see `SAAboutWindowController`, `SABundleHTMLOutputWindowController`.
- **ObjC interop:** mark classes `@objc final class X: NSObject`. When a Swift
method's internal argument label would be dropped in the generated selector,
spell the selector explicitly — e.g. `func font(from data: Data?)` bridges to
`fontFrom:` and silently breaks ObjC call sites; declare it
`@objc(fontFromData:)`.
- **Refactors are behavior-preserving** and land as small, focused PRs. Pin
existing behavior with unit tests before/while extracting (several suites
assert byte-exact output of the code they replaced).
## Modernization conventions (do / don't)
- **Archiving:** never call `NSArchiver`/`NSUnarchiver` or the deprecated
`NSKeyedUnarchiver.unarchiveObject(with:)`. Use `SAArchiving`
(`Source/Other/Extensions/SAArchiving.swift`) — it writes keyed+secure and
reads keyed-first with a legacy non-keyed fallback. That fallback is the one
intentional deprecated call in the codebase; don't "fix" it — legacy user
data (fonts, colours in NSUserDefaults) is non-keyed and unreadable by
`NSKeyedUnarchiver`, so removing the fallback silently wipes user settings.
- **Web content:** never use legacy WebKit (`WebView`). Use `WKWebView` — via
`SAWebView` (SwiftUI `NSViewRepresentable` wrapper) for embedded views, and
`SAPrintUtility` / `SAHTMLPrintRenderer` for print flows. No legacy `WebView`
remains: the last island, the MySQL help viewer, is now `SAHelpViewer*`.
- **Persisted-format compatibility:** favorites plist, `.spf` documents, and
NSUserDefaults blobs written by old versions must stay readable. When
touching serialization, prove old data still decodes with a fixture test —
see `UnitTests/SAKeyedArchiveCompatTests.swift` for the pattern (embedded
legacy blob + wire-format assertion protecting old readers).
- **Keyed archiving:** use `archivedDataWithRootObject:requiringSecureCoding:`
/ `unarchivedObjectOfClass(es):fromData:` for session-local data (drag
pasteboards etc.); the `.spf` session paths in SPDatabaseDocument
deliberately use non-secure keyed archiving under the `"data"` key — that is
the cross-version wire format, don't "upgrade" it without a migration plan.
- **User notifications:** post via `SANotificationCenter`
(`Source/Other/Utility/SANotificationCenter.swift`), never the deprecated
`NSUserNotification` API. The wider warning burn-down (remaining: AppKit
deprecation batch, Swift 6 readiness, old drag-API delegate methods, and
the deferred SecKeychain/NSConnection projects) is tracked in
`docs/development/warnings-elimination-plan.md`.
## Repo layout (abridged)
- `Source/Controllers/` — window/view controllers (the bulk of the app)
- `Source/Model/`, `Source/Other/` — models, extensions, utilities
- `Source/Views/` — custom views incl. `SAWebView.swift`
- `Source/Interfaces/` — XIBs (legacy UI)
- `UnitTests/` — the "Unit Tests" target's sources
- `Frameworks/SPMySQLFramework/` — the MySQL wire-protocol framework (separate
Xcode project, its own tests)
- `docs/development/modernization-followup-plan.md` — the detailed modernization
roadmap: what's done (with rationale), what's next, and known sharp edges.
Read it before starting refactoring work.
Biggest legacy files (approx.): `SPDatabaseDocument.m` (~6.3k lines, god
object being decomposed), `SPTableContent.m` (~5k), `SPExportController.m`
(~4k), `SPCustomQuery.m` (~3.9k), `SPTextView.m` (~3.9k).
## Building and testing
- Open `sequel-ace.xcodeproj`; build scheme **"Sequel Ace Debug"**. "Sequel Ace
Beta" is a build configuration of the same target, not a separate target.
- Dependencies come via SPM (Firebase, Alamofire, SnapKit, OCMock, FMDB, …);
first resolve needs network access.
- Run the "Unit Tests" target's tests for any change. The full suite is ~700+
tests and should be fully green.
### Unit Tests target — sharp edges
- The target has **no TEST_HOST**: it compiles app sources directly into the
test bundle. There is no `@testable import` — tests reference app types
directly.
- A Swift file exercised by tests must be a member of **both** the "Sequel Ace"
and "Unit Tests" targets.
- Keep test-eligible Swift files **free of project ObjC types** (no bridging
header in the test target — adding one breaks shared `.m` files via the
generated `sequel-ace-Swift.h`; see the plan doc for the full analysis).
Established workaround: inline needed string constants as private literals
with a "keep in sync with SPConstants.m" comment, and put ObjC-touching
bridge code in a separate app-target-only file (e.g. `SAFavoriteItem.swift`
vs `SAFavoriteItem+Tree.swift`).
## Xcode project file (pbxproj) rules
- The project uses **classic groups**, not filesystem-synchronized folders.
Files must be registered in `project.pbxproj` with target membership.
- **Never hand-edit `project.pbxproj` while Xcode has the project open** —
Xcode clobbers on-disk edits with its in-memory model, and conversely a
behind-Xcode's-back change (branch switch, merge) leaves Xcode's in-memory
model stale: builds then silently compile the *old* file set while reporting
success. If the pbxproj changed outside Xcode, close and reopen the project
(or verify the build log actually compiled your files) before trusting any
build.
- Agents with Xcode automation (MCP `XcodeWrite`/`XcodeRM`): use it to
add/remove files — it updates Xcode's live model with real IDs. Otherwise,
ask the user to add the file in Xcode rather than editing the pbxproj by
hand.
- Prefer resolving pbxproj merge conflicts by replaying the add/remove
operations on a fresh branch instead of hand-merging conflict hunks.
## Pull request conventions
- One focused change per PR; branch off `main` (e.g.
`feature/modernization-…`, `bugfix/…`).
- PR titles carry a hashtag: `#added`, `#fixed`, `#changed`, `#removed`, or
`#infra` (most modernization work is `#infra`).
- Fill the PR template sections: Changes / Closes following issues / Tested
(processor, macOS version, Xcode version) / Screenshots / Additional notes.
- CodeRabbit reviews every PR — its "Major" findings are usually worth
addressing; note behavior-parity decisions in the PR body so reviewers know
a risk is pre-existing rather than introduced.
- UI-affecting changes (printing, help viewer, connection flow) need a manual
verification pass against a live MySQL/MariaDB server; say so in the PR if
it hasn't happened yet.