feat: add Cloudflare D1 database driver plugin#376
Conversation
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthroughAdds first-class Cloudflare D1 support: a new DriverPlugin and async HTTP driver/client, registry/dialect defaults, API-only connection mode and UI updates, plugin packaging and assets, docs (EN/VI/ZH), changelog/CI updates, and comprehensive unit tests for parsing and helper utilities. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as TablePro Client
participant Driver as CloudflareD1PluginDriver
participant HttpClient as D1HttpClient
participant D1API as Cloudflare D1 API
Client->>Driver: connect()
activate Driver
Driver->>HttpClient: listDatabases() / getDatabaseDetails()
activate HttpClient
HttpClient->>D1API: GET /accounts/:id/databases or /databases/:uuid
D1API-->>HttpClient: database list / details
HttpClient-->>Driver: parsed DatabaseInfo
deactivate HttpClient
Driver->>Driver: resolve/cache database UUID, set serverVersion
Driver-->>Client: connected
deactivate Driver
Client->>Driver: executeParameterized(sql, params)
activate Driver
Driver->>HttpClient: executeQuery(sql, params) (POST /query)
activate HttpClient
HttpClient->>D1API: POST /query { sql, params }
D1API-->>HttpClient: query result envelope (results, meta)
HttpClient-->>Driver: payload
deactivate HttpClient
Driver->>Driver: mapRawResult() -> PluginQueryResult
Driver-->>Client: query result (rows, columns, meta)
deactivate Driver
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (8)
TableProTests/Core/CloudflareD1/CloudflareD1PluginMetadataTests.swift (1)
33-172: Exercise the live registry lookup instead of the defaults factory.These tests call
registryPluginDefaults()directly, but the app reads metadata through the registry lookup path. A broken bootstrap/registration step could still pass this suite. Prefer asserting viasnapshot(forTypeId:).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TableProTests/Core/CloudflareD1/CloudflareD1PluginMetadataTests.swift` around lines 33 - 172, Tests are using registryPluginDefaults() directly which bypasses the registry lookup path; change each test to query PluginMetadataRegistry.shared.snapshot(forTypeId: "Cloudflare D1") (or equivalent lookup API) instead of calling registryPluginDefaults(), and update assertions to unwrap the returned snapshot (guard let snapshot = registry.snapshot(forTypeId: "Cloudflare D1") else { Issue.record(...) ; return }) then assert against snapshot properties (displayName, brandColorHex, isDownloadable, capabilities, supportsDatabaseSwitching, schema.databaseGroupingStrategy, editor.sqlDialect, explainVariants, connection.additionalConnectionFields, parameterStyle) so the live registration/bootstrap is exercised. Ensure you remove uses of defaults/d1Entry and reference snapshot consistently across tests.TableProTests/Core/CloudflareD1/D1ValueDecodingTests.swift (1)
14-60: Avoid a second hand-rolledD1Valuedecoder in the test target.This suite only proves the local copy can decode the fixtures. It will stay green even if the real D1 value decoder regresses, and it can also drift from the other local
D1Valuecopy inD1ResponseParsingTests.swift.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TableProTests/Core/CloudflareD1/D1ValueDecodingTests.swift` around lines 14 - 60, Tests define a duplicate D1Value enum (with stringValue and init(from:)) which duplicates production/other-test decoding logic; remove this local enum and instead reuse the canonical D1Value implementation by importing or referencing the shared type used in D1ResponseParsingTests.swift (or the production module) so the tests exercise the real decoder; update usages in D1ValueDecodingTests.swift to use the shared D1Value type and delete the enum and its stringValue/init(from:) implementation from this file.TablePro.xcodeproj/project.pbxproj (1)
781-782: Add an explicitTablePro → CloudflareD1DriverPlugintarget dependency.The app target's "Copy D1 Plugin" build phase relies on implicit dependency resolution. All other bundled plugins (OracleDriver, SQLiteDriver, ClickHouseDriver, MySQLDriver, PostgreSQLDriver, DuckDBDriver, CSVExport, JSONExport, SQLExport) have explicit
PBXTargetDependency+PBXContainerItemProxyentries. CloudflareD1DriverPlugin should follow the same pattern for consistency and build reliability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@TablePro.xcodeproj/project.pbxproj` around lines 781 - 782, Add an explicit PBXTargetDependency + PBXContainerItemProxy for CloudflareD1DriverPlugin so the TablePro target's "Copy D1 Plugin" build phase no longer relies on implicit resolution: create a PBXContainerItemProxy entry that points to the CloudflareD1DriverPlugin target and then add a corresponding PBXTargetDependency referencing that container item, and include the new PBXTargetDependency identifier in the TablePro target's dependencies (alongside the existing OracleDriver/SQLiteDriver/etc. entries) so CloudflareD1DriverPlugin is treated the same as the other bundled plugins.Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift (3)
219-225: Inconsistent filtering pattern for Cloudflare internal tables.Line 181 (
fetchTables) usesGLOB '_cf_*'while line 223 usesLIKE '_cf_%'. GLOB is case-sensitive in SQLite whereas LIKE is case-insensitive by default. For consistency, consider using the same pattern throughout:♻️ Suggested fix
WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%' AND m.name NOT LIKE '_cf_%' + -- Or alternatively, use GLOB for consistency with fetchTables: + -- WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%' AND m.name NOT GLOB '_cf_*'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift` around lines 219 - 225, fetchAllColumns uses "LIKE '_cf_%'" which is inconsistent with fetchTables that uses "GLOB '_cf_*'"; update the SQL in fetchAllColumns (the multiline query in function fetchAllColumns) to use the same GLOB '_cf_*' pattern to match Cloudflare internal table names the same way as fetchTables (ensuring consistent case-sensitive filtering across both methods).
572-574: Consider documenting or throwing for unsupported transaction methods.Since
supportsTransactionsreturnsfalse, these no-op implementations are technically correct. However, if a caller ignores the capability flag and calls these methods, the silent no-op could lead to data integrity issues (caller believes a transaction is active when it isn't).Consider either:
- Adding a comment explaining the intentional no-op behavior, or
- Throwing an error to fail fast if called incorrectly
💡 Alternative: fail-fast approach
- func beginTransaction() async throws {} - func commitTransaction() async throws {} - func rollbackTransaction() async throws {} + func beginTransaction() async throws { + throw CloudflareD1Error(message: String(localized: "Transactions are not supported by Cloudflare D1")) + } + func commitTransaction() async throws { + throw CloudflareD1Error(message: String(localized: "Transactions are not supported by Cloudflare D1")) + } + func rollbackTransaction() async throws { + throw CloudflareD1Error(message: String(localized: "Transactions are not supported by Cloudflare D1")) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift` around lines 572 - 574, The no-op transaction methods (beginTransaction, commitTransaction, rollbackTransaction) in CloudflareD1PluginDriver silently do nothing despite supportsTransactions returning false; update these methods to fail fast by throwing a descriptive error (e.g., “Transactions not supported by CloudflareD1PluginDriver”) when called, or at minimum add a clear comment above each method noting the intentional no-op and referencing supportsTransactions; ensure the thrown error type/message is consistent across beginTransaction, commitTransaction, and rollbackTransaction so callers immediately know transactions are unsupported.
142-145: Parameter mapping can be simplified.The explicit mapping from
[String?]to[Any?]is correct but verbose. Swift handles this coercion automatically.♻️ Suggested simplification
- let anyParams: [Any?] = parameters.map { param -> Any? in - guard let value = param else { return nil } - return value - } + let anyParams: [Any?] = parameters🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift` around lines 142 - 145, The local mapping that constructs anyParams from parameters is verbose; replace the explicit map (the block creating anyParams) with a direct type coercion by assigning parameters to anyParams (use parameters as [Any?] or pass parameters directly where [Any?] is expected), removing the unnecessary map; update references to anyParams in the surrounding code (in CloudflareD1PluginDriver.swift where anyParams and parameters are used) so the simpler cast is used consistently.Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift (2)
365-370: Minor: Rate limit message could be clearer when Retry-After is missing.When
Retry-Afterheader is absent, the message becomes "Rate limited by Cloudflare. Retry after unknown seconds." which reads awkwardly.💡 Suggested improvement
case 429: - let retryAfter = response.value(forHTTPHeaderField: "Retry-After") ?? "unknown" - Self.logger.warning("D1 rate limited. Retry-After: \(retryAfter)") - throw D1HttpError( - message: String(localized: "Rate limited by Cloudflare. Retry after \(retryAfter) seconds.") - ) + let retryAfter = response.value(forHTTPHeaderField: "Retry-After") + Self.logger.warning("D1 rate limited. Retry-After: \(retryAfter ?? "not specified")") + if let seconds = retryAfter { + throw D1HttpError( + message: String(localized: "Rate limited by Cloudflare. Retry after \(seconds) seconds.") + ) + } else { + throw D1HttpError( + message: String(localized: "Rate limited by Cloudflare. Please try again later.") + ) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift` around lines 365 - 370, The rate-limit error message becomes awkward when the Retry-After header is missing; update the handling in the case 429 block (the retryAfter variable and thrown D1HttpError) to produce a clearer message when retryAfter == "unknown" (e.g., log "D1 rate limited. Retry-After: unknown" and throw D1HttpError with text like "Rate limited by Cloudflare. Retry time unknown." or "Rate limited by Cloudflare. Retry after an unknown time."), so the logger and D1HttpError messages read naturally whether the header is present or not.
311-343: Concurrent requests could lose cancellation tracking.If multiple requests execute concurrently, storing the task in
currentTask(line 329) will overwrite any previously running task reference. This meanscancelCurrentTask()would only cancel the most recent request, not all in-flight requests.If concurrent requests are expected, consider using a collection to track all active tasks:
♻️ Suggested approach
- private var currentTask: URLSessionDataTask? + private var currentTasks: Set<URLSessionDataTask> = []Then add/remove tasks from the set instead of replacing a single reference.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift` around lines 311 - 343, The code overwrites a single currentTask for concurrent requests causing lost cancellation; replace currentTask with a thread-safe collection (e.g., Set<URLSessionTask> or [URLSessionTask]) named currentTasks, and update the logic in the dataTask creation and cancellation handler to add the task to currentTasks under lock before task.resume(), remove it from currentTasks under lock in the task completion closure (and in the continuation error/return paths), and in the onCancel block iterate over currentTasks to cancel each task and then clear the set; also update the final cleanup (where currentTask is set to nil) to remove the finished task from currentTasks under the same lock.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@TablePro/Views/Connection/ConnectionFormView.swift`:
- Around line 321-324: The SecureField is still bound to password even for
API-only connections, so testConnection() persists that value under the
temporary testConn.id and leaves the API token orphaned; change the UI and
persistence logic so API tokens are not stored to the regular password field or
left in Keychain after a test: introduce a separate state (e.g., apiToken) and
bind the SecureField to apiToken when PluginManager.shared.connectionMode(for:
type) == .apiOnly (instead of password), and update testConnection() to use that
apiToken and either avoid writing it to Keychain for unsaved connections (or
delete the temporary testConn.id entry after testing) so tokens are never
orphaned.
- Around line 875-877: The current basicValid check only enforces database for
.fileBased/.apiOnly modes but ignores the new cfAccountId and api token
requirements, so update the validation in ConnectionFormView.swift (around
PluginManager.shared.connectionMode(for: type), mode, requiresDatabase,
basicValid) to also require non-empty cfAccountId and apiToken when mode ==
.apiOnly (or when PluginManager indicates API-only). Concretely, add checks for
!cfAccountId.isEmpty and !apiToken.isEmpty into the boolean used for enabling
Save/Test so API-only connections are rejected until those fields are filled.
In `@TableProTests/Core/CloudflareD1/CloudflareD1DriverHelperTests.swift`:
- Around line 14-116: The test file currently reimplements helper functions
(quoteIdentifier, escapeStringLiteral, castColumnToText, isUuid,
stripLimitOffset, formatDDL) and SQL snippets; instead, update the tests to call
the real implementations on CloudflareD1PluginDriver (e.g.,
CloudflareD1PluginDriver.quoteIdentifier(...), .stripLimitOffset(...),
.formatDDL(...)) and remove the duplicated local helper functions so the
assertions validate the actual driver behavior; ensure any test data or calls
that used the local helpers now invoke the corresponding
CloudflareD1PluginDriver methods and adjust imports/visibility if needed so the
driver helpers are accessible to the test.
In `@TableProTests/Core/CloudflareD1/D1ResponseParsingTests.swift`:
- Around line 14-92: Tests are using locally redefined mirror models
(D1ApiResponse, D1Value, D1RawResultPayload, D1RawResults, D1QueryMeta,
D1DatabaseInfo, D1ListResponse) instead of the real decoder types used by
D1HttpClient, so they only validate the fixtures against the copies; change the
tests to import and decode into the actual parser types used by D1HttpClient (or
remove the duplicated structs and reference the shared types), ensuring the test
uses the real D1Value decoding/coercion and the real API response structs (e.g.,
D1ApiResponse and D1Value from the production module) so any future changes to
coding keys or optional handling in D1HttpClient will be caught by the tests.
---
Nitpick comments:
In `@Plugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swift`:
- Around line 219-225: fetchAllColumns uses "LIKE '_cf_%'" which is inconsistent
with fetchTables that uses "GLOB '_cf_*'"; update the SQL in fetchAllColumns
(the multiline query in function fetchAllColumns) to use the same GLOB '_cf_*'
pattern to match Cloudflare internal table names the same way as fetchTables
(ensuring consistent case-sensitive filtering across both methods).
- Around line 572-574: The no-op transaction methods (beginTransaction,
commitTransaction, rollbackTransaction) in CloudflareD1PluginDriver silently do
nothing despite supportsTransactions returning false; update these methods to
fail fast by throwing a descriptive error (e.g., “Transactions not supported by
CloudflareD1PluginDriver”) when called, or at minimum add a clear comment above
each method noting the intentional no-op and referencing supportsTransactions;
ensure the thrown error type/message is consistent across beginTransaction,
commitTransaction, and rollbackTransaction so callers immediately know
transactions are unsupported.
- Around line 142-145: The local mapping that constructs anyParams from
parameters is verbose; replace the explicit map (the block creating anyParams)
with a direct type coercion by assigning parameters to anyParams (use parameters
as [Any?] or pass parameters directly where [Any?] is expected), removing the
unnecessary map; update references to anyParams in the surrounding code (in
CloudflareD1PluginDriver.swift where anyParams and parameters are used) so the
simpler cast is used consistently.
In `@Plugins/CloudflareD1DriverPlugin/D1HttpClient.swift`:
- Around line 365-370: The rate-limit error message becomes awkward when the
Retry-After header is missing; update the handling in the case 429 block (the
retryAfter variable and thrown D1HttpError) to produce a clearer message when
retryAfter == "unknown" (e.g., log "D1 rate limited. Retry-After: unknown" and
throw D1HttpError with text like "Rate limited by Cloudflare. Retry time
unknown." or "Rate limited by Cloudflare. Retry after an unknown time."), so the
logger and D1HttpError messages read naturally whether the header is present or
not.
- Around line 311-343: The code overwrites a single currentTask for concurrent
requests causing lost cancellation; replace currentTask with a thread-safe
collection (e.g., Set<URLSessionTask> or [URLSessionTask]) named currentTasks,
and update the logic in the dataTask creation and cancellation handler to add
the task to currentTasks under lock before task.resume(), remove it from
currentTasks under lock in the task completion closure (and in the continuation
error/return paths), and in the onCancel block iterate over currentTasks to
cancel each task and then clear the set; also update the final cleanup (where
currentTask is set to nil) to remove the finished task from currentTasks under
the same lock.
In `@TablePro.xcodeproj/project.pbxproj`:
- Around line 781-782: Add an explicit PBXTargetDependency +
PBXContainerItemProxy for CloudflareD1DriverPlugin so the TablePro target's
"Copy D1 Plugin" build phase no longer relies on implicit resolution: create a
PBXContainerItemProxy entry that points to the CloudflareD1DriverPlugin target
and then add a corresponding PBXTargetDependency referencing that container
item, and include the new PBXTargetDependency identifier in the TablePro
target's dependencies (alongside the existing OracleDriver/SQLiteDriver/etc.
entries) so CloudflareD1DriverPlugin is treated the same as the other bundled
plugins.
In `@TableProTests/Core/CloudflareD1/CloudflareD1PluginMetadataTests.swift`:
- Around line 33-172: Tests are using registryPluginDefaults() directly which
bypasses the registry lookup path; change each test to query
PluginMetadataRegistry.shared.snapshot(forTypeId: "Cloudflare D1") (or
equivalent lookup API) instead of calling registryPluginDefaults(), and update
assertions to unwrap the returned snapshot (guard let snapshot =
registry.snapshot(forTypeId: "Cloudflare D1") else { Issue.record(...) ; return
}) then assert against snapshot properties (displayName, brandColorHex,
isDownloadable, capabilities, supportsDatabaseSwitching,
schema.databaseGroupingStrategy, editor.sqlDialect, explainVariants,
connection.additionalConnectionFields, parameterStyle) so the live
registration/bootstrap is exercised. Ensure you remove uses of defaults/d1Entry
and reference snapshot consistently across tests.
In `@TableProTests/Core/CloudflareD1/D1ValueDecodingTests.swift`:
- Around line 14-60: Tests define a duplicate D1Value enum (with stringValue and
init(from:)) which duplicates production/other-test decoding logic; remove this
local enum and instead reuse the canonical D1Value implementation by importing
or referencing the shared type used in D1ResponseParsingTests.swift (or the
production module) so the tests exercise the real decoder; update usages in
D1ValueDecodingTests.swift to use the shared D1Value type and delete the enum
and its stringValue/init(from:) implementation from this file.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 87ec9257-bc6c-4b5b-9559-f62aa6a7d191
⛔ Files ignored due to path filters (1)
TablePro/Assets.xcassets/cloudflare-d1-icon.imageset/cloudflare-d1.svgis excluded by!**/*.svg
📒 Files selected for processing (16)
CHANGELOG.mdPlugins/CloudflareD1DriverPlugin/CloudflareD1Plugin.swiftPlugins/CloudflareD1DriverPlugin/CloudflareD1PluginDriver.swiftPlugins/CloudflareD1DriverPlugin/D1HttpClient.swiftPlugins/CloudflareD1DriverPlugin/Info.plistPlugins/TableProPluginKit/ConnectionMode.swiftTablePro.xcodeproj/project.pbxprojTablePro/Assets.xcassets/cloudflare-d1-icon.imageset/Contents.jsonTablePro/Core/Plugins/PluginMetadataRegistry+RegistryDefaults.swiftTablePro/Models/Connection/DatabaseConnection.swiftTablePro/Resources/Localizable.xcstringsTablePro/Views/Connection/ConnectionFormView.swiftTableProTests/Core/CloudflareD1/CloudflareD1DriverHelperTests.swiftTableProTests/Core/CloudflareD1/CloudflareD1PluginMetadataTests.swiftTableProTests/Core/CloudflareD1/D1ResponseParsingTests.swiftTableProTests/Core/CloudflareD1/D1ValueDecodingTests.swift
There was a problem hiding this comment.
🧹 Nitpick comments (2)
.github/workflows/build-plugin.yml (1)
182-183: Fail fast on malformed plugin tags before extraction.If the tag does not match,
sedreturns the original string and the failure surfaces later as “Unknown plugin”. An explicit format guard here makes failures clearer.♻️ Proposed change
- PLUGIN_NAME=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\1/') - VERSION=$(echo "$TAG" | sed -E 's/^plugin-([a-z0-9-]+)-v([0-9].*)$/\2/') + if [[ ! "$TAG" =~ ^plugin-([a-z0-9-]+)-v([0-9].*)$ ]]; then + echo "::error::Invalid tag format: $TAG (expected plugin-<name>-v<version>)" + exit 1 + fi + PLUGIN_NAME="${BASH_REMATCH[1]}" + VERSION="${BASH_REMATCH[2]}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/build-plugin.yml around lines 182 - 183, Add an explicit validation step that checks TAG against the expected pattern before extracting PLUGIN_NAME and VERSION: test TAG with the regex used by the sed commands and if it does not match, print a clear error including the malformed TAG and exit non-zero. Keep using the same identifiers (TAG, PLUGIN_NAME, VERSION) so the guard runs just prior to the existing sed extractions and prevents later “Unknown plugin” failures by failing fast with a descriptive message.docs/databases/cloudflare-d1.mdx (1)
97-102: Consider adding a language identifier to the code block.The fenced code block lacks a language identifier. While this is an example configuration (not executable code), the coding guidelines recommend including language identifiers in all fenced code blocks for consistency.
📝 Suggested addition
-``` +```text Name: My D1 Database Database: my-app-db Account ID: abc123def456 API Token: (your Cloudflare API token)</details> As per coding guidelines: "Include language identifiers in fenced code blocks." <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@docs/databases/cloudflare-d1.mdxaround lines 97 - 102, The fenced code
block containing the Cloudflare D1 example lacks a language identifier; update
that block (the triple-backtick block showing "Name: My D1 Database / Database:
my-app-db / Account ID: abc123def456 / API Token: (your Cloudflare API token)")
to include a language tag such as "text" (e.g., ```text) so it follows the
repository guideline to always include language identifiers in fenced code
blocks.</details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In @.github/workflows/build-plugin.yml:
- Around line 182-183: Add an explicit validation step that checks TAG against
the expected pattern before extracting PLUGIN_NAME and VERSION: test TAG with
the regex used by the sed commands and if it does not match, print a clear error
including the malformed TAG and exit non-zero. Keep using the same identifiers
(TAG, PLUGIN_NAME, VERSION) so the guard runs just prior to the existing sed
extractions and prevents later “Unknown plugin” failures by failing fast with a
descriptive message.In
@docs/databases/cloudflare-d1.mdx:
- Around line 97-102: The fenced code block containing the Cloudflare D1 example
lacks a language identifier; update that block (the triple-backtick block
showing "Name: My D1 Database / Database: my-app-db / Account ID: abc123def456 /
API Token: (your Cloudflare API token)") to include a language tag such as
"text" (e.g., ```text) so it follows the repository guideline to always include
language identifiers in fenced code blocks.</details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `14a597aa-0633-4ba8-8ffd-c870f46da0f4` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 093935bbd936885695ac6257a8750f4c54dc3d01 and 535185a647ce4aff6c13cd881478b3e843e6e5c6. </details> <details> <summary>📒 Files selected for processing (5)</summary> * `.github/workflows/build-plugin.yml` * `TablePro.xcodeproj/project.pbxproj` * `docs/databases/cloudflare-d1.mdx` * `docs/vi/databases/cloudflare-d1.mdx` * `docs/zh/databases/cloudflare-d1.mdx` </details> <details> <summary>✅ Files skipped from review due to trivial changes (2)</summary> * docs/zh/databases/cloudflare-d1.mdx * docs/vi/databases/cloudflare-d1.mdx </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
Summary
Closes #206
ConnectionMode.apiOnly— new connection form mode that shows only Database, Account ID, and API Token (no Host/Port/Username/SSH/SSL)Plugin Features
/rawendpoint with stable column ordering?placeholder support (nil → JSON null)") and string escaping_cf_*)Files Changed
Plugins/CloudflareD1DriverPlugin/(4 files)TableProTests/Core/CloudflareD1/(4 files, 76 tests)cloudflare-d1-icon.imageset/ConnectionMode.swift(added.apiOnly),ConnectionFormView.swift(apiOnly form layout),PluginMetadataRegistry+RegistryDefaults.swift(D1 entry),DatabaseConnection.swift(.cloudflareD1constant),project.pbxproj(D1 target)Test plan
xcodebuild -scheme TablePro build -skipPackagePluginValidationSummary by CodeRabbit
New Features
Documentation
Tests