diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 553083429..4f35610b1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -51,3 +51,9 @@ jobs: - name: Check writing style run: bash docs/scripts/check-writing-style.sh + + # The Mintlify CLI needs a working native toolchain, so it is the step most likely + # to break on an environment change. This one is plain Python, and it also answers + # what the CLI does not: a page in no navigation group, which nothing links to. + - name: Check links and navigation coverage + run: python3 docs/scripts/check-links.py diff --git a/CLAUDE.md b/CLAUDE.md index 32753de1a..2d9f629a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ TablePro is a native macOS database client (SwiftUI + AppKit), a fast, lightweig scripts/download-libs.sh # static libraries, not in git scripts/generate-project.sh # generates both .xcodeproj bundles from project.yml (or pass macos|ios) -# Build (development), -skipPackagePluginValidation required for SwiftLint plugin in CodeEditSourceEditor +# Build (development). Every script and CI job passes -skipPackagePluginValidation; keep passing it xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation # Clean build diff --git a/docs/STYLE.md b/docs/STYLE.md new file mode 100644 index 000000000..fb4d8a524 --- /dev/null +++ b/docs/STYLE.md @@ -0,0 +1,341 @@ +# How the TablePro docs are written + +The rules a page has to meet. `docs/scripts/check-writing-style.sh` and +`docs/scripts/check-docs-against-source.py` enforce the mechanical half in CI; the rest is here +because no grep can settle it. + +Most of this was learned the hard way, from defects that shipped. Where a rule has a scar, the scar +is written down with it. That is deliberate: a rule with a reason survives, and a rule without one +gets argued away. + +--- + +## 1. What the docs are for + +A reader arrives with a task and leaves when they can do it. Everything else is cost. + +The single most useful test on any sentence: **would the reader do something different if this +sentence were deleted?** If not, delete it. + +### The corpus has one origin defect, and it explains most of the rest + +These docs grew one feature PR at a time. Each PR appended a paragraph to a page in the same voice +it used for its CHANGELOG entry, and no commit ever rewrote a page whole. `features/tabs.mdx` and +the v0.67.0 release notes still share the same `app.orders` / `staging.orders` sentence. + +Release notes answer *what is new*. Docs answer *how do I do this*. They are different genres, and +116 releases regrouped by topic produce exactly the exhaustive, product-centred, focus-free prose +this corpus is being rewritten out of. + +**So: when you touch a page, rewrite the section for someone doing a task. Never append a paragraph +describing your change, and never copy a sentence out of the changelog.** + +--- + +## 2. Voice + +Write like the developer who built the feature explaining it to the developer about to use it. + +- **Second person, imperative.** "Click **Test Connection**." Never "we", never "let's", never + "users can". +- **Present tense.** "The driver reconnects", not "will reconnect". +- **The product is not the subject.** 409 sentences in this corpus opened `TablePro `, and 26 + of 33 database pages opened with the same five words. Write from the reader's side, or from the + thing itself. "Indices are tables, documents are rows" beats "TablePro connects to Elasticsearch". +- **No design rationale.** 464 clauses in this corpus explain *why the app is built this way* to a + reader who only needs to know what to click. + + > The prefix test: if the clause still reads correctly after you put "we did it this way because" + > in front of it, cut it. + + ``` + no With a single tab there is no strip, so a window that behaves the way it + always did gains no chrome. + yes One tab, no strip. + ``` + + Consequence is not rationale. "Value filters run on the rows already loaded, so they cover the + current page only" is the point of the sentence; keep it. + +- **No internals on user-facing pages.** No class names, bundle identifiers, or activity types under + `features/`, `databases/`, `connections/`, `customization/`. A page once opened with + `NSUserActivity`. They are fine under `development/` and `external-api/`. +- **Repair a hedge with a measurement.** Not "large results may take some time". Say the number, or + name the condition. + +### Banned + +On top of the CLAUDE.md list (seamless, robust, comprehensive, leverage, and the rest): + +`simply` · `just` (meaning only) · `easy` · `in order to` · `please note` · `and/or` · `e.g.` · +`i.e.` · `above` / `below` as document position · `allows you to` · `enables you to` · `lets you` · +`helps you` · **`you can`** + +`you can` is the same sentence with the subject moved. "You can right-click a column header and +choose **Filter with column**" is "Right-click a column header and choose **Filter with column**". + +**No em dashes. Anywhere.** + +--- + +## 3. Rhythm, and the trap on the other side of it + +Sentence length in this corpus is already healthy: mean 11.8 words, a third under seven. Rhythm is +not the problem and does not need fixing. + +**The problem is that every page makes the same move.** That is what "reads like AI" means here, and +it is why the first rewrite pass failed: + +> It killed the old formula completely. Zero of 46 pages still opened with the product name. It +> replaced it with a new one: 13 of 46 opened with a numeral, four of those on consecutive database +> pages; six ended on a variant of "there is nothing to install"; three reached for the same +> aphoristic predicate ("is the floor", "is the whole life of the token", "is the way in"); and two +> pages had become the same paragraph with different nouns. + +Every one of those collisions was invisible from inside the file being edited and obvious from three +pages away. + +> **Check variety across the reading order, not per page.** Before shipping a batch, read its +> openings consecutively, in sidebar order. If two rhyme, rewrite one. + +The pass after that one failed the same way in a third costume. Told not to end on "nothing to +install", it moved the negation to the front instead: a third of the batch opened on *no*, *not*, +*never*, *nothing*, or *cannot*. Two adjacent transport pages came out as the same page with +different nouns, down to a byte-identical closing sentence, because two agents wrote them +independently and neither could see the other. + +So the rule is not "avoid last time's tic". It is this: + +> **If a sibling page already carries the sentence you are about to write, you do not need a +> different wording. You need a snippet.** Rewording a duplicated fact eleven ways is how the corpus +> got eleven templates for "this driver downloads on first use". `docs/snippets/` is where a shared +> fact lives; `registry-plugin.mdx` and `helper-port.mdx` are the two that exist. + +Vary the shape deliberately: an imperative, a plain declarative, a constraint, a number, a full +complex sentence. **At most one page in five may open on a sentence fragment.** + +--- + +## 4. Opening a page + +Mintlify prints the frontmatter `description` directly under the H1, so the first body sentence must +not restate it. The description is read by someone deciding whether to open the page. The first +sentence is read by someone who already did. **They carry different facts.** + +Lead with the entry point, the constraint, or the fact that changes a decision. + +``` +no Browse and edit columns, indexes, foreign keys, triggers, and DDL for any table. + (the description, minus four words) +yes This is a DDL editor with a grid in front of it. Rename a column, add an index, + change a key, and what you get is a pending ALTER TABLE you can read before it runs. +``` + +The model in this corpus is `switching.mdx`: *"Most of a migration is one dialog."* Six words, does +not restate the title, has a point of view. + +--- + +## 5. Structure + +| Content | Use | +|---|---| +| Two or more actions in sequence | ``. Never a bare numbered list, never numbered headings | +| Parallel items, no order | Bullets, 40 words maximum each | +| Items with three or more properties | A table | +| One item with properties | A sentence | +| A chain of reasoning | Prose. Bulleting an argument hides the causal link | +| Six or more failure modes | H3 per error string | +| Same content, different variant | `` or `` | + +**A `**Label**:` paragraph is never structure.** 454 lines in this corpus open that way. A label plus +a body is an H3, a table row, or a ``. `databases/postgresql.mdx` had a `## Features` +section built from eight of them, one 620 characters long, and none appeared in the table of +contents, so a reader could not find array editing from the sidebar. + +**Never restate a table in prose.** After a table, write only what the table cannot carry. +`features/safe-mode.mdx` had a six-row table followed by six subsections saying "Same as X, but Y". + +**Name the default.** Whenever a page enumerates three or more options, one sentence says which one a +normal reader should pick and who should pick differently. Exhaustiveness is not neutrality. + +### Headings + +Sentence case. Capitals only for acronyms, names with an interior capital (`MySQL`, `PGlite`), and +headings that are literally a control or menu item in the app. `docs/scripts/proper-nouns.txt` holds +the product names whose second word is an ordinary English word: Google **Cloud** SQL, DynamoDB +**Local**. + +Stop at H3; Mintlify's table of contents ignores H4. Never write an H1, the frontmatter `title` is +the H1. Never `## Features`: it fitted 17 pages, so it is not a heading. + +--- + +## 6. Page shapes + +### A database page + +Fixed order for the sections that exist. **Engine-specific H2s are allowed** between +`## Connection URL` and `## Limitations`, and this matters: an earlier draft of this guide froze the +section list, which would have destroyed `beancount.mdx` (Source locations, Includes, BQL) and +`etcd.mdx`, the two best pages in that directory. + +``` +Quick setup · Connection settings · Connection URL · Authentication · · +SSL/TLS · Limitations · Troubleshooting · Related +``` + +Every database page answers all of these, even when the answer is "none". A page missing one fails +review however well it reads. + +1. Minimum server version, or "any" +2. Where the driver comes from: bundled, or the registry, with the plugin's exact name +3. Default port, or "no port" and what replaces it +4. Whether the Database field is required to connect +5. Default SSL mode and what it falls back to +6. What a tab bound to a second database does +7. At least one limitation. If an engine genuinely has none, say "No driver-side limits" in one + line. Never omit the heading + +### A limitation + +Three slots, fixed order: **what you cannot do, what happens instead, what to do about it.** Never +why. + +``` +no Arrays of jsonb, bytea, or composite types keep the plain text editor, since + their quoting cannot round-trip through a per-element list. +yes The list editor covers arrays of simple types. jsonb[], bytea[], composite and + multi-dimensional arrays open the text editor instead: edit the {…} literal directly. +``` + +### An error entry + +The heading is the verbatim string the server or the app produced, casing and punctuation preserved, +variable parts collapsed to `…` inside the original quoting. The body is three lines, in order: what +it means, what to check with the file or setting named, what to change. + +Never open with "This error occurs when". The heading is the name. + +`**Auth failed**` is not searchable, because PostgreSQL has never emitted that string. +`FATAL: password authentication failed for user "…"` is what the reader has in their clipboard. + +### A capability that varies by engine + +Asserted in exactly one place: the feature page, as a table, one row per engine, no prose gloss. A +database page states what the capability does on **that** engine and links. It never lists which +other engines have it, because nobody will remember to edit it when the 27th engine gains the +feature. + +--- + +## 7. Naming what is on screen + +The shipped `String(localized:)` is the name. One term per element. + +| Element | Term | Source | +|---|---|---| +| Filter row area above the grid | filter bar | `ViewMenuBuilder.swift` "Show Filter Bar" | +| Narrow strip on the leading edge | connections strip | `ViewMenuBuilder.swift` "Show Connections" | +| Object list on the left | sidebar | | +| Right pane | inspector | | +| Launch window | welcome window | never "welcome screen" | +| Disabled control | dimmed | never "greyed out" | +| Pointer | pointer | "cursor" is the text insertion point only | + +Apple's nouns, not Microsoft's: **pane**, **dialog**, **sidebar**, **inspector**, **menu bar**, +**System Settings**. + +Verbs: **click** a button, **choose** a menu item, **select** a row or a checkbox, **press** a key, +**deselect** a checkbox. Never "click on", never "uncheck". + +**Menu paths** are one bold run with ` > `: `**File > Import from Other App…**`, never +`**File** > **X**`. Verify the leaf against `TablePro/Core/Menu/*MenuBuilder.swift` every time. Eight +paths were wrong at once because the Database menu shipped and nobody re-checked. + +**Shortcuts** in backticks, modifiers spelled out, joined with `+`: `` `Cmd+Option+F` ``. Never bold, +never a glyph. Apple's `Command-K` is deliberately rejected: the app's own menus render `Cmd`. + +**Labels** are bold and never in backticks. Drop a trailing ellipsis when instructing +(`Choose **Save as**`), keep it when the label alone is ambiguous (`**Create Connection…**`). + +--- + +## 8. Callouts + +By consequence, never by tone. A reader who learns the yellow box and the blue box mean the same +thing stops reading both. + +| | Means | +|---|---| +| `` | Irreversible. Data loss, dropped objects, corrupted files | +| `` | This will fail, cost money, or silently produce wrong output | +| `` | A support fact: which engines, which versions, which tier | +| `` | An aside the reader can skip without breaking anything | +| `` | An optional shortcut that makes the task faster | +| `` | What success looks like at the end of a step | + +**A callout is never load-bearing.** If deleting it leaves the page wrong, it is not a callout. + +Before the reclassification, a `` carried silent MongoDB round-trip corruption while a +`` carried which builds accepted a URL format in 0.38. + +--- + +## 9. Numbers, and the rule that keeps them true + +**A number that exists in the source code is owned by exactly one page. Every other page links to +it.** If linking reads badly, the sentence is wrong, not the rule. + +Every count contradiction this corpus has shipped came from one fact typed on two pages: the driver +count, the database count, the AI provider count, the Settings tab count, the PluginKit ABI version. + +Three claims rot faster than anything else and are checked in CI against the source that defines +them: menu paths, keyboard shortcuts, and the PluginKit ABI. All three were wrong at once in +August 2026. + +**Verify before you write.** A rewrite pass that trades a prose problem for an accuracy problem is +strictly worse than doing nothing. One turned "A filled yellow star marks a favorite" into "Starred +tables turn yellow"; the star turns yellow, not the table. Another asserted the MCP server grants +anonymous read access on stock defaults, which is the exact opposite of what +`MCPCompositeAuthenticator.swift` does, on the page whose entire job is to be trusted. + +--- + +## 10. Images + +Alt describes the picture. The caption adds what the prose does not carry. **They may never be +equal**; 21 pairs once were. If you cannot write an alt that differs from the caption, delete the +caption. + +Every `` holds either zero images or a light and dark pair at identical dimensions: + +```mdx + + What is actually visible + What is actually visible + +``` + +--- + +## 11. Before you open the pull request + +```bash +cd docs +bash scripts/check-writing-style.sh +python3 scripts/check-docs-against-source.py +mint validate +mint broken-links --check-anchors --check-redirects +mint a11y +``` + +Then the two things no script can do: + +1. **Read your batch's openings consecutively.** If two rhyme, rewrite one. +2. **Check every fact you wrote against the source**, not against the page you were editing. The + page you were editing is where the error came from. + +And when you add a check to CI, **test it by reintroducing the bug it exists to catch.** The first +version of the shortcut check joined table rows by title, and the docs word one row differently from +the action name, so it passed green on a live bug. Two of the first three checks were silent. diff --git a/docs/connections/aws-iam.mdx b/docs/connections/aws-iam.mdx index a3e78151b..b228155ea 100644 --- a/docs/connections/aws-iam.mdx +++ b/docs/connections/aws-iam.mdx @@ -3,11 +3,32 @@ title: AWS IAM Authentication description: Connect to Amazon RDS and Aurora with IAM database authentication instead of a static password --- -There is no password to save here. TablePro signs a short-lived token with your AWS credentials on every connect and gives the database that instead. The token expires after 15 minutes and is never stored. MySQL, MariaDB, and PostgreSQL on RDS and Aurora all authenticate this way. +Set up the AWS side first, or **Test Connection** fails against a user the database never created for IAM. + +## On the AWS side + +Enable IAM database authentication on the RDS instance or Aurora cluster. MySQL, MariaDB, and PostgreSQL all support it. Then create the database user: + + + + ```sql + CREATE USER 'app_user' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; + ``` + + + ```sql + GRANT rds_iam TO "app_user"; + ``` + + + + +A user is either password-authenticated or IAM-authenticated, never both. Connecting as a user that still has only a password fails. + ## Setting up -In the connection form, set **Authentication** to one of the AWS IAM options. The password field disappears and the AWS fields appear. +In the connection form, set **Authentication** to one of the AWS IAM options. The **Password** field gives way to the AWS fields, and **Username** takes the IAM database user. Connection form with Authentication set to AWS IAM (Profile) @@ -16,83 +37,66 @@ In the connection form, set **Authentication** to one of the AWS IAM options. Th | Option | Credentials come from | |--------|----------------------| -| **AWS IAM (Access Key)** | An access key ID, secret access key, and optional session token entered in the form | +| **AWS IAM (Access Key)** | An access key ID, secret access key, and optional session token typed into the form | | **AWS IAM (Profile)** | A named profile in `~/.aws/credentials` and `~/.aws/config` | | **AWS IAM (SSO)** | A profile backed by IAM Identity Center, using the token cache in `~/.aws/sso/cache` | -**AWS Region** is detected from `*.rds.amazonaws.com` hostnames. Fill it in only for a custom endpoint (a CNAME or a proxy), or to override the detected region. - -Set **Username** to the database user created for IAM auth (see per-engine setup below). - -### Tunnels and port forwards +Pick **AWS IAM (Profile)** if you already use the AWS CLI here: it reads the same files the same way. -The token is signed for the database endpoint, and RDS checks it against its own hostname and port. The address the client dials does not have to match, but the address the token was signed for does. +**AWS Region** is read from the endpoint when the hostname looks like `mydb.abc123.us-east-1.rds.amazonaws.com`. Fill it in for a CNAME or any other custom endpoint, or to override what was detected. -When TablePro opens the tunnel (SSH, Cloudflare, Cloud SQL Auth Proxy, or SOCKS), there is nothing to set. It signs the token for the host and port in the connection form, not the local forward it dials. +## Token lifetime -When you run the forward yourself, the connection form holds `127.0.0.1` and a local port, and TablePro cannot tell which database is on the other end. Set **RDS Endpoint** to the real endpoint: - -```text -mydb.abc123.us-east-1.rds.amazonaws.com:5432 -``` +Each connect signs a fresh token, valid for 15 minutes and never written to disk; automatic reconnects sign another. Nothing to paste, rotate, or refresh. -The port is optional and falls back to the engine default (5432 for PostgreSQL, 3306 for MySQL and MariaDB). The same field covers a CNAME or any other alias that is not the endpoint AWS issued. +IAM also requires encryption in transit, so an [SSL mode](/connections/ssl) of **Disabled** or **Preferred** is raised to **Required** for the connect. -### Profiles +## Profiles -**Profile Name** defaults to `default`. TablePro reads the profile the same way the AWS CLI does: +**Profile Name** lists the profiles found in `~/.aws/config` and `~/.aws/credentials` and accepts a typed name. Blank means `default`. Profiles resolve the way the AWS CLI resolves them: - Static `aws_access_key_id` / `aws_secret_access_key` pairs. -- `credential_process` commands, so a profile can be backed by SSO, 1Password, or any external credential helper. -- `role_arn` assume-role profiles, resolved through STS. The base credentials come from `source_profile` (chains up to 5 profiles deep) or `credential_source = Environment`. Profiles with `mfa_serial` are not supported. +- `credential_process` commands, so a profile can be backed by SSO, 1Password, or any other credential helper. +- `role_arn` assume-role profiles, resolved through STS. Base credentials come from `source_profile`, chaining up to five deep, or from `credential_source = Environment`. `mfa_serial` is not supported. -### SSO +For **AWS IAM (SSO)**, run `aws sso login --profile ` first. A connect that fails on an expired session offers the browser sign-in. -Sign in first with `aws sso login --profile `. If the SSO session has expired when you click **Test Connection**, TablePro offers to run the browser sign-in for you. +## Tunnels and port forwards -## Token lifetime and refresh +RDS checks the token against its own hostname and port, so the endpoint it was signed for is what matters, not the address dialed. -Each token is valid for 15 minutes and is never stored. TablePro generates a fresh one on every connect, and again on every automatic reconnect, so you never paste or refresh an expiring token yourself. +A tunnel the app opens needs nothing extra: the token is signed for the **Host** and **Port** in the form, not the loopback address the driver gets. -SSL is required for IAM. If the connection's SSL mode is Disabled or Preferred, TablePro raises it to Required for the connection. +A forward you run yourself leaves `127.0.0.1` in the form, which names no database. Set **RDS Endpoint** to the real one: -## Per-engine setup +```text +mydb.abc123.us-east-1.rds.amazonaws.com:5432 +``` -IAM database authentication must be enabled on the RDS instance or Aurora cluster, and the database user must be created for it. +The port is optional, falling back to 5432 for PostgreSQL and 3306 for MySQL and MariaDB. The field also covers a CNAME or any alias AWS did not issue, and is honored only when **Host** is loopback or already the same hostname. - - - Create the user with the AWS authentication plugin: +## Troubleshooting - ```sql - CREATE USER 'app_user' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'; - ``` - - - Grant the `rds_iam` role: +### Could not determine an AWS region for "…" - ```sql - GRANT rds_iam TO "app_user"; - ``` - - +The hostname is not a standard RDS endpoint. Fill in **AWS Region**. - -Connecting fails if the user only has a password. A user is either password-authenticated or IAM-authenticated, not both. - +### TablePro cannot sign an RDS token for "…" -## Troubleshooting +The connection points at a port forward the app did not open. Fill in **RDS Endpoint** with the endpoint from the AWS console. + +### PAM authentication failed -**Region could not be determined**: the hostname is not a standard RDS endpoint. Fill in **AWS Region**. +PostgreSQL reports this and MySQL reports `Access denied` when the token was signed for the wrong endpoint. Check **RDS Endpoint** against the console, including the port. -**TablePro cannot sign an RDS token for "127.0.0.1"**: the connection points at a port forward TablePro did not open. Fill in **RDS Endpoint** with the endpoint from the AWS console. +### Profile "…" was not found -**PAM authentication failed** (PostgreSQL) or **Access denied** (MySQL) while a token you generated by hand works: the token was signed for the wrong endpoint. Check that **RDS Endpoint** matches the console endpoint, including the port. +The profile has no static keys, no `credential_process`, and no `role_arn`. Check the name and the contents of `~/.aws/config` and `~/.aws/credentials`. -**SSO session expired**: run `aws sso login --profile `, or accept the sign-in prompt after a failed **Test Connection**. +### AWS SSO Sign-In Required -**Profile incomplete**: the profile has no static keys, no `credential_process`, and no `role_arn`. Check the profile name and the files in `~/.aws/`. +The cached SSO session expired. Accept the prompt, or run `aws sso login --profile `. ## Other AWS services -The same Authentication options connect to Amazon ElastiCache ([Redis](/databases/redis)) and Amazon Keyspaces ([Cassandra](/databases/cassandra)). Those pages cover the service-specific fields. +The same **Authentication** options reach Amazon ElastiCache ([Redis](/databases/redis)) and Amazon Keyspaces ([Cassandra](/databases/cassandra)), each with fields of its own on those pages. diff --git a/docs/connections/cloud-sql-proxy.mdx b/docs/connections/cloud-sql-proxy.mdx index 9a17c481b..269864846 100644 --- a/docs/connections/cloud-sql-proxy.mdx +++ b/docs/connections/cloud-sql-proxy.mdx @@ -3,91 +3,80 @@ title: Cloud SQL Auth Proxy description: Connect to Google Cloud SQL by letting TablePro manage the Cloud SQL Auth Proxy --- -The [Cloud SQL Auth Proxy](https://cloud.google.com/sql/docs/mysql/sql-proxy) carries an encrypted, IAM-authorized channel to your instance, so the database can stay off the public internet with no client certificates to hand out. Running it is part of the connection here, so the process comes up when you connect, goes away when you disconnect, and never needs a terminal window of its own, the same as an [SSH tunnel](/connections/ssh-tunneling) or a [Cloudflare tunnel](/connections/cloudflare-tunnel). +import HelperPort from "/snippets/helper-port.mdx"; -This works for Cloud SQL instances running MySQL, PostgreSQL, or SQL Server. +One field carries the whole setup: the instance connection name, `project:region:instance`, which is on the instance's overview page in the Google Cloud console. The proxy itself runs as a child process, started on connect and killed on disconnect. -## How it works + + Cloud SQL Auth Proxy pane + Cloud SQL Auth Proxy pane + -```mermaid -flowchart LR - subgraph mac ["Your Mac"] - TablePro["TablePro
127.0.0.1:auto"] - Proxy["cloud-sql-proxy"] - end +## Before you start - subgraph gcp ["Google Cloud"] - CloudSQL["Cloud SQL
instance"] - end +The pane appears for Cloud SQL instances running MySQL, PostgreSQL, or SQL Server. The account you connect with needs the **Cloud SQL Client** role (`roles/cloudsql.client`) on the project, and for Application Default Credentials, one run of: - TablePro -->|"loopback"| Proxy -->|"encrypted + IAM"| CloudSQL +```bash +gcloud auth application-default login ``` -TablePro picks a free loopback port, runs `cloud-sql-proxy --port --address 127.0.0.1 `, waits until the local port accepts connections, then points the database driver at it. When you disconnect, quit the app, or the process exits, the proxy is torn down. - -## Prerequisites - -Install the proxy, or let TablePro download it for you (see the binary options below). +Install the binary, or click **Download cloud-sql-proxy…** in the pane, which fetches 2.23.0 and checks its SHA-256 against the value pinned for your CPU architecture. ```bash brew install cloud-sql-proxy ``` -You also need credentials that can reach the instance, and the account needs the **Cloud SQL Client** role (`roles/cloudsql.client`). +Auto-detection covers your `PATH`, `/opt/homebrew/bin`, `/usr/local/bin`, and `~/google-cloud-sdk/bin`. Anywhere else, use **Choose…**. ## Setting up -Open the connection form, switch to the **Cloud SQL Auth Proxy** pane, toggle **Enable Cloud SQL Auth Proxy** on, enter the **instance connection name**, choose how to authenticate, then go back to **General** and click **Test Connection**. - -Keep SSL/TLS off in the SSL pane. The proxy already encrypts the connection to Cloud SQL, and the local endpoint is plain loopback. - -A connection uses one connection method at a time. If an SSH tunnel, Cloudflare tunnel, or SOCKS proxy is already enabled, the pane shows a button to disable it. + + + Select **Cloud SQL Auth Proxy** and turn **Enable Cloud SQL Auth Proxy** on. A connection carries one method at a time, so an SSH tunnel or SOCKS proxy already enabled has to be switched off here first. + + + Enter the **Instance connection name**, then choose **Application Default Credentials** or **Service Account Key**. + + + The proxy encrypts the leg to Cloud SQL and hands the driver plain loopback, so SSL/TLS stays off. + + + On **General**, click **Test Connection**. **Host** and **Port** there are never dialed: the instance connection name decides where the proxy lands. **Username** and **Database** work as usual. + + ## Options -### Cloud SQL instance - -| Field | Description | -|-------|-------------| -| **Instance connection name** | The instance's connection name in the form `project:region:instance`. Find it on the instance overview page in the Google Cloud console. | - -### Authentication - -| Option | Description | -|--------|-------------| -| **Application Default Credentials** | Uses the credentials already set up on your Mac. Run `gcloud auth application-default login` before connecting. Setting `GOOGLE_APPLICATION_CREDENTIALS` also works, but only if the variable is visible to TablePro: the proxy inherits the app's environment, and a GUI app doesn't see variables exported in your shell profile. | -| **Service Account Key** | Paste a service account key in JSON. TablePro stores it in the macOS Keychain and writes it to a temporary file, readable only by you, while the proxy runs. It is never passed on the command line. | -| **Use IAM database authentication** | Signs in to the database as an IAM principal instead of with a database password. Set the connection's username to the IAM principal (a user email, or `name@project.iam` for a service account). The database password is not used. | +| Option | What it does | Default | +|--------|--------------|---------| +| **Instance connection name** | `project:region:instance`. Three colon-separated parts, none empty, or the connect is refused outright. | - | +| **Credentials** | **Application Default Credentials**, or a **Service Account Key** pasted as JSON. | Application Default Credentials | +| **Use IAM database authentication** | Signs in as an IAM principal instead. Set **Username** to that principal, a user email or `name@project.iam` for a service account; the password goes unused. | Off | +| **Connect over private IP** | Reaches the instance on its private address instead of its public one. | Off | +| **Path** | The `cloud-sql-proxy` binary. Blank auto-detects. | Blank | -### Network +A pasted key is kept in the macOS Keychain and written to a temporary file readable only by you while the proxy runs, then deleted. It never reaches the command line. -| Option | Description | Default | -|--------|-------------|---------| -| **Connect over private IP** | Use the instance's private IP address instead of its public IP. | Off | + +`GOOGLE_APPLICATION_CREDENTIALS` works too, but the proxy inherits the app's environment, and a GUI app never sees variables exported by your shell profile. + -### Local listener + -| Option | Description | Default | -|--------|-------------|---------| -| **Choose port automatically** | TablePro picks a free loopback port. Avoids collisions between connections and with local databases. | On | -| **Local port** | Set a fixed port instead. | - | - -### cloud-sql-proxy binary +## Troubleshooting -Leave the path blank to auto-detect. TablePro looks on your `PATH`, in the common Homebrew locations (`/opt/homebrew/bin`, `/usr/local/bin`), and in `~/google-cloud-sdk/bin`. If it isn't found, use **Download cloud-sql-proxy…** or **Choose…** to point at a specific binary. +### cloud-sql-proxy was not found -The in-app download fetches cloud-sql-proxy 2.23.0 and checks its SHA-256 against the value pinned for your CPU architecture. A mismatch aborts the install. +Install it with `brew install cloud-sql-proxy`, download it from the pane, or set **Path**. -## Troubleshooting +### The proxy did not become ready in time -### cloud-sql-proxy not found +Run it by hand to see what it says: -Install it with `brew install cloud-sql-proxy`, download it from the pane, or set the binary path. A GUI app doesn't see your shell's `PATH`, so a custom install location may need to be set explicitly. +```bash +cloud-sql-proxy --port 5433 --address 127.0.0.1 project:region:instance +``` ### Permission or authentication errors -The proxy reports authentication problems on its own output, which TablePro shows when a connection fails. Common causes: the account is missing the **Cloud SQL Client** role, Application Default Credentials aren't set up, or, for IAM database authentication, the database user hasn't been created for the IAM principal. - -### Proxy didn't become ready - -TablePro waits up to 30 seconds for the local port to accept connections. If it times out, the last lines of the proxy's output are shown. Check the instance connection name and that the account can reach the instance. +The proxy reports these on its own output, which the failed connect shows. Usually the account is missing the **Cloud SQL Client** role, Application Default Credentials were never set up, or IAM database authentication is on with no database user for the principal. diff --git a/docs/connections/cloudflare-tunnel.mdx b/docs/connections/cloudflare-tunnel.mdx index 09bc3d807..4ee5dc1a0 100644 --- a/docs/connections/cloudflare-tunnel.mdx +++ b/docs/connections/cloudflare-tunnel.mdx @@ -3,95 +3,87 @@ title: Cloudflare Tunnel description: Reach a database behind Cloudflare Access by letting TablePro manage the cloudflared process --- -Install cloudflared first with `brew install cloudflared`. The tunnel is that binary, running `cloudflared access tcp` against your [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/) hostname on a loopback port, with no terminal session of your own to keep alive. +import HelperPort from "/snippets/helper-port.mdx"; -## How it works - -```mermaid -flowchart LR - subgraph mac ["Your Mac"] - TablePro["TablePro
127.0.0.1:auto"] - CFD["cloudflared"] - end - - subgraph edge ["Cloudflare"] - Access["Cloudflare
Access"] - end - - subgraph db ["Database Server"] - Database["PostgreSQL
MySQL
db:5432"] - end +The Access application is yours to create, in the Cloudflare dashboard. This pane takes the hostname of one that already fronts the database, and runs `cloudflared` against it for the life of the connection. - TablePro -->|"loopback"| CFD -->|"Access tunnel"| Access -->|"origin"| Database -``` - -TablePro picks a free loopback port, runs `cloudflared access tcp --hostname --url 127.0.0.1:`, waits until the local port accepts connections, then points the database driver at it. When you disconnect, quit the app, or the process exits, the tunnel is torn down. + + Cloudflare Tunnel pane + Cloudflare Tunnel pane + -## Prerequisites +## Before you start -Install cloudflared: +Create a self-hosted application in the [Zero Trust dashboard](https://developers.cloudflare.com/cloudflare-one/applications/non-http/) routed to the database, with a policy that admits you. Its public hostname is the only Cloudflare value this pane needs. ```bash brew install cloudflared ``` -TablePro looks for cloudflared on your `PATH` and in the common Homebrew locations (`/opt/homebrew/bin`, `/usr/local/bin`). If it lives somewhere else, set the path in the pane. +The pane looks on your `PATH` and in `/opt/homebrew/bin` and `/usr/local/bin`, and shows what it found. Installed elsewhere, set **Path** by hand: a GUI app never sees the `PATH` your shell profile exports. ## Setting up -The Cloudflare Tunnel pane appears for the same databases that support [SSH tunneling](/connections/ssh-tunneling), so file-based and HTTP-API databases such as SQLite, BigQuery, DynamoDB, and Snowflake don't show it. + + + Select **Cloudflare Tunnel** and turn **Enable Cloudflare Tunnel** on. One method per connection: any other tunnel or proxy already enabled gets a button to disable it. + + + **Hostname** is the Access application's public hostname, `db.example.com` in the example. With **Browser Sign-In** chosen, click **Sign In with Browser…** so the first connect does not stop to ask. + + + On **General**, click **Test Connection**. **Host** and **Port** there are never dialed while the tunnel is on: the Access application decides where it lands. + + -Open the connection form, switch to the **Cloudflare Tunnel** pane, toggle **Enable Cloudflare Tunnel** on, enter the Access **hostname**, choose how to authenticate, then go back to **General** and click **Test Connection**. +## How it works -A connection uses one connection method at a time. If an SSH tunnel, Cloud SQL Auth Proxy, or SOCKS proxy is already enabled, the pane shows a button to disable it. +A free port is picked, `cloudflared` starts listening on it, and the driver gets it once it accepts. Run the same command yourself to see the client's own output: + +```bash +cloudflared access tcp --hostname db.example.com --url 127.0.0.1:54321 +``` + +Disconnecting or quitting stops the process. One orphaned by a crash is reaped at the next launch. ## Options -### Access application +| Option | What it does | Default | +|--------|--------------|---------| +| **Hostname** | The Access application hostname `cloudflared` connects to. | - | +| **Expose to local network** | Binds `0.0.0.0` rather than `127.0.0.1`. | Off | -| Field | Description | -|-------|-------------| -| **Hostname** | The Access application hostname, for example `db.example.com`. This is the `--hostname` cloudflared connects to. | + +With **Expose to local network** on, anything that reaches the port is past Cloudflare Access on your session and faces only the database's own login. + ### Authentication - cloudflared signs in through your browser and caches a token under `~/.cloudflared`. Click **Sign In with Browser…** once so the login happens up front; after that, connecting uses the cached token without opening a browser. - - If you connect without a cached token, TablePro detects the sign-in prompt and asks you to sign in, rather than appearing to hang. + **Sign In with Browser…** runs `cloudflared access login` and caches the token under `~/.cloudflared`; later connects reuse it silently. A connect with no cached token asks you to sign in rather than appearing to hang. - For unattended connections, enter a Cloudflare Access service token (**Client ID** and **Client Secret**). TablePro stores them in the macOS Keychain and passes them to cloudflared as environment variables, never on the command line. + For unattended connections, enter a service token as **Client ID** and **Client Secret**. Both stay in the macOS Keychain and reach `cloudflared` as environment variables, never on the command line. - The Access application policy must use a **Service Auth** rule. If the policy only allows an identity provider, Cloudflare still prompts for a browser sign-in even when a service token is set. This is configured in your Cloudflare Zero Trust dashboard, not in TablePro. + The policy must use a **Service Auth** rule. Against an identity-provider-only policy, Cloudflare prompts for a browser sign-in even with a token set. -### Local listener - -| Option | Description | Default | -|--------|-------------|---------| -| **Choose port automatically** | TablePro picks a free loopback port. Avoids collisions between connections. | On | -| **Local port** | Set a fixed port instead. | - | -| **Expose to local network** | Bind `0.0.0.0` instead of `127.0.0.1`, so other machines on your network can reach the listener. Leave off unless you need it. | Off | - -### cloudflared binary - -Leave the path blank to auto-detect. The pane shows the detected path, or a hint to install cloudflared if it isn't found. Use **Choose…** to point at a specific binary. + ## Troubleshooting -### cloudflared not found +### cloudflared was not found -Install it with `brew install cloudflared`, or set the binary path in the pane. A GUI app doesn't see your shell's `PATH`, so a custom install location may need to be set explicitly. +Install it with `brew install cloudflared`, or set **Path** to the binary. -### A browser keeps opening on connect +### A browser opens every time you connect -The cached Access token expired (Access sessions are time-limited), or you're using a service token against a policy that isn't set to **Service Auth**. Sign in again, or fix the policy in your Cloudflare Zero Trust dashboard. +The cached Access token expired, or a service token is running against a policy that is not **Service Auth**. Sign in again, or change the policy. -### Tunnel didn't become ready +### The tunnel did not become ready in time -TablePro waits up to 30 seconds for the local port to accept connections. If it times out, the last lines of cloudflared's output are shown. Check the hostname and that the Access application is reachable. +An unknown hostname, a policy that is not **Service Auth**, or a fixed port already taken. diff --git a/docs/connections/connection-form.mdx b/docs/connections/connection-form.mdx new file mode 100644 index 000000000..dda3397bb --- /dev/null +++ b/docs/connections/connection-form.mdx @@ -0,0 +1,110 @@ +--- +title: Connection form +description: The nine panes of the connection editor, the fields on each, and which drivers get which +--- + +A driver only ever gets the panes it can use, so this form's sidebar is four items long for SQLite and nine for PostgreSQL. A warning triangle on one of them means a required field on that pane is empty. + + + Connection form + Connection form + + +| Pane | Contents | +|------|----------| +| **General** | Name, host, port, database, credentials, Test Connection | +| **SSH Tunnel** | Reach a database behind a bastion host. See [SSH Tunneling](/connections/ssh-tunneling) | +| **Cloudflare Tunnel** | Connect through `cloudflared`. See [Cloudflare Tunnel](/connections/cloudflare-tunnel) | +| **Cloud SQL Auth Proxy** | Google Cloud SQL, for MySQL, PostgreSQL, and SQL Server only. See [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy) | +| **SOCKS Proxy** | Route through a SOCKS5 proxy. See [SOCKS Proxy](/connections/socks-proxy) | +| **SSL/TLS** | Encryption mode and certificates. See [SSL/TLS](/connections/ssl) | +| **Customization** | Color, tags, group, Safe Mode | +| **Advanced** | Startup commands, pre-connect script, external access, plugin fields | +| **AI Rules** | Per-connection guidance the AI assistant sees on every chat turn. See [AI Assistant](/features/ai-assistant) | + +Only one of SSH Tunnel, Cloudflare Tunnel, Cloud SQL Auth Proxy, and SOCKS Proxy can be on at a time. Turning on a second offers a button to switch off the first. + +## General + +| Field | Description | +|-------|-------------| +| **Name** | Display name in the connection list | +| **Host** | Empty falls back to the driver's own default, usually `localhost` | +| **Port** | Pre-filled from the database type | +| **Database** | Optional on drivers that browse every database from one session. PostgreSQL and Redshift do not connect without one | +| **Username** | Not pre-filled. Empty means the driver's own default | +| **Password** | Stored in the macOS Keychain | +| **Prompt for password** | Stores nothing, asks on every connect. Reads **Prompt for API token** on API-only drivers | +| **Use Password File** | PostgreSQL, Redshift, and CockroachDB. Reads `~/.pgpass`, and reports underneath whether the file exists, has `chmod 0600`, and holds a matching line | +| **Socket Path** | Optional, and only with an SSH tunnel on. Forwards to a unix socket instead of Host and Port, which are then ignored. See [Forwarding to a unix socket](/connections/ssh-tunneling#forwarding-to-a-unix-socket) | + +SQLite, DuckDB, and Beancount replace the host section with a file path picker. + +## Advanced + +| Field | Description | +|-------|-------------| +| **Startup Commands** | SQL to run after every connect. See [Startup commands](#startup-commands) | +| **Pre-Connect Script** | Shell script run before connecting. A non-zero exit aborts the connect | +| **AI Policy** | Per-connection override for the in-app AI agents | +| **External Clients** | **Blocked**, **Read Only** (the default), or **Read & Write** for MCP clients such as Raycast, Cursor, and Claude Desktop. A token's own scope cannot raise it. See [External API](/external-api) | +| **Local only** | Keeps this connection off iCloud Sync. See [iCloud Sync](/features/icloud-sync) | +| Plugin fields | Driver-specific options, such as MongoDB's `replicaSet` | + +A pre-connect script never runs unprompted. A **Pre-Connect Script** alert shows the script itself and waits for **Run Script**, every time. At launch it is not prompted for at all: a restored window whose connection carries a script waits with a **Connect** button. + +## Startup commands + +Statements split on semicolons and newlines and run in order on the connection that just opened, after every connect including an automatic reconnect. They all run on one connection, so write one dialect. + + +```sql MySQL +SET time_zone = '+00:00'; +SET NAMES utf8mb4; +``` + +```sql PostgreSQL +SET search_path TO myschema, public; +``` + + +A statement that fails is logged and skipped, and the connection still opens. + +## Connection health + +Every active connection is pinged every 30 seconds, skipping the ping while one of your own queries is running. A failed ping starts a reconnect at 2s, then 4s, 8s, doubling to a 120-second ceiling, and keeps going until the connection comes back or you close it. An authentication failure stops the retries and leaves the session in error. A reconnect rebuilds the tunnel, restores the selected database and schema, and re-runs the startup commands; the session reads as connecting throughout. + +SQLite, DuckDB, Beancount, Snowflake, and Teradata are not monitored. + +## Which drivers get which panes + +| Database | Default port | SSH tunnel | SSL/TLS | Cloudflare Tunnel | Cloud SQL Proxy | SOCKS Proxy | +|----------|-------------|-----------|---------|-------------------|-----------------|-------------| +| [MySQL](/databases/mysql) | 3306 | Yes | Yes | Yes | Yes | Yes | +| [MariaDB](/databases/mariadb) | 3306 | Yes | Yes | Yes | No | Yes | +| [PostgreSQL](/databases/postgresql) | 5432 | Yes | Yes | Yes | Yes | Yes | +| [Amazon Redshift](/databases/redshift) | 5439 | Yes | Yes | Yes | No | Yes | +| [CockroachDB](/databases/cockroachdb) | 26257 | Yes | Yes | Yes | No | Yes | +| [PGlite](/databases/pglite) | 5432 | No | No | No | No | No | +| [Microsoft SQL Server](/databases/mssql) | 1433 | Yes | Yes | Yes | Yes | Yes | +| [Oracle](/databases/oracle) | 1521 | Yes | Yes | Yes | No | Yes | +| [Dameng DM8](/databases/dameng) | 5236 | Yes | No | Yes | No | Yes | +| [ClickHouse](/databases/clickhouse) | 8123 | Yes | Yes | Yes | No | Yes | +| [Teradata](/databases/teradata) | 1025 | Yes | Yes | Yes | No | Yes | +| [Trino](/databases/trino) | 8080 | Yes | Yes | Yes | No | Yes | +| [MongoDB](/databases/mongodb) | 27017 | Yes | Yes | Yes | No | Yes | +| [Redis](/databases/redis) | 6379 | Yes | Yes | Yes | No | Yes | +| [Cassandra / ScyllaDB](/databases/cassandra) | 9042 | Yes | Yes | Yes | No | Yes | +| [etcd](/databases/etcd) | 2379 | Yes | Yes | Yes | No | Yes | +| [SurrealDB](/databases/surrealdb) | 8000 | Yes | Yes | Yes | No | Yes | +| [Elasticsearch](/databases/elasticsearch) | 9200 | No | Yes | No | No | No | +| [Snowflake](/databases/snowflake) | 443 | No | No | No | No | No | +| [SQLite](/databases/sqlite) | File | No | No | No | No | No | +| [DuckDB](/databases/duckdb) | File | No | No | No | No | No | +| [Beancount](/databases/beancount) | File | No | No | No | No | No | +| [DynamoDB](/databases/dynamodb) | AWS API | No | No | No | No | No | +| [BigQuery](/databases/bigquery) | Cloud API | No | No | No | No | No | +| [Cloudflare D1](/databases/cloudflare-d1) | Cloud API | No | No | No | No | No | +| [libSQL / Turso](/databases/libsql) | URL | No | No | No | No | No | + +A driver with no SSL/TLS pane is either a local file or an HTTPS API that manages its own encryption. [SSL/TLS](/connections/ssl) has the per-driver defaults. diff --git a/docs/connections/index.mdx b/docs/connections/index.mdx index 9af67c62a..a803a5d07 100644 --- a/docs/connections/index.mdx +++ b/docs/connections/index.mdx @@ -1,68 +1,42 @@ --- title: Managing Connections -description: Create, organize, and switch database connections, with health monitoring and startup commands +description: Create, organize, and switch database connections --- -TablePro connects to 27 databases through its plugin system. This page covers creating and organizing connections. Driver-specific fields and quirks live on each database's own page. - -## Supported databases - -| Database | Default port | SSH tunnel | SSL/TLS | Cloudflare Tunnel | Cloud SQL Proxy | SOCKS Proxy | -|----------|-------------|-----------|---------|-------------------|-----------------|-------------| -| [MySQL](/databases/mysql) | 3306 | Yes | Yes | Yes | Yes | Yes | -| [MariaDB](/databases/mariadb) | 3306 | Yes | Yes | Yes | No | Yes | -| [PostgreSQL](/databases/postgresql) | 5432 | Yes | Yes | Yes | Yes | Yes | -| [Amazon Redshift](/databases/redshift) | 5439 | Yes | Yes | Yes | No | Yes | -| [CockroachDB](/databases/cockroachdb) | 26257 | Yes | Yes | Yes | No | Yes | -| [PGlite](/databases/pglite) | 5432 | No | No | No | No | No | -| [Microsoft SQL Server](/databases/mssql) | 1433 | Yes | Yes | Yes | Yes | Yes | -| [Oracle](/databases/oracle) | 1521 | Yes | Yes | Yes | No | Yes | -| [Dameng DM8](/databases/dameng) | 5236 | Yes | No | Yes | No | Yes | -| [ClickHouse](/databases/clickhouse) | 8123 | Yes | Yes | Yes | No | Yes | -| [Teradata](/databases/teradata) | 1025 | Yes | Yes | Yes | No | Yes | -| [Trino](/databases/trino) | 8080 | Yes | Yes | Yes | No | Yes | -| [MongoDB](/databases/mongodb) | 27017 | Yes | Yes | Yes | No | Yes | -| [Redis](/databases/redis) | 6379 | Yes | Yes | Yes | No | Yes | -| [Cassandra / ScyllaDB](/databases/cassandra) | 9042 | Yes | Yes | Yes | No | Yes | -| [etcd](/databases/etcd) | 2379 | Yes | Yes | Yes | No | Yes | -| [SurrealDB](/databases/surrealdb) | 8000 | Yes | Yes | Yes | No | Yes | -| [Elasticsearch](/databases/elasticsearch) | 9200 | No | Yes | No | No | No | -| [Snowflake](/databases/snowflake) | 443 | No | No | No | No | No | -| [SQLite](/databases/sqlite) | File | No | No | No | No | No | -| [DuckDB](/databases/duckdb) | File | No | No | No | No | No | -| [Beancount](/databases/beancount) | File | No | No | No | No | No | -| [DynamoDB](/databases/dynamodb) | AWS API | No | No | No | No | No | -| [BigQuery](/databases/bigquery) | Cloud API | No | No | No | No | No | -| [Cloudflare D1](/databases/cloudflare-d1) | Cloud API | No | No | No | No | No | -| [libSQL / Turso](/databases/libsql) | URL | No | No | No | No | No | - -Transport details: [SSH Tunneling](/connections/ssh-tunneling), [SSL/TLS](/connections/ssl), [Cloudflare Tunnel](/connections/cloudflare-tunnel), [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy), [SOCKS Proxy](/connections/socks-proxy). - -## Creating a connection - -The Welcome window appears on launch. The left panel has **Create Connection…** and an **Add from Existing** menu holding Import from URL, Import from Other App, Open Project Folder, and Import Connections. Saved connections are on the right, with a search field in the header (`Cmd+F` focuses it). With nothing saved yet, that list offers **Try Sample Database**. +Press `Cmd+N` anywhere in the app to open the connection form. **Create Connection…** on the welcome window does the same, and the **Add from Existing** menu under it takes one from a URL, [another client](/features/connection-sharing), a [project folder](/features/project-folder-import), or an exported file. Saved connections fill the right side of that window under a search field, focused by `Cmd+F`, and offer **Try Sample Database** until you save something of your own. Welcome window with actions panel and connection list Welcome window with actions panel and connection list -1. Click **Create Connection…** (or press `Cmd+N` anywhere) -2. Pick a database type from the chooser sheet -3. Fill in connection details -4. Click **Test Connection**. A green **Connected** pill confirms success -5. Click **Save & Connect** - -The chooser groups drivers by category: Relational, Document, Key-Value, Analytical, Wide-Column, Cloud Native, Coordination & Config, and Other. Drivers that aren't installed show a **Not Installed** badge; selecting one prompts to install the plugin first. See [Plugins](/features/plugins). +## Create a connection + + + + The chooser sheet groups drivers as Relational, Document, Key-Value, Analytical, Wide-Column, Cloud Native, Coordination & Config, and Other. Picking a driver badged **Not Installed** offers the download before the form opens. See [Plugins](/features/plugins). + + + Host, port, and credentials live on **General**. Everything else has a pane of its own; see [Connection form](/connections/connection-form). + + + On success the button becomes a green checkmark and reads **Connected**. On failure a **Connection Test Failed** sheet carries the driver's own message, and for errors the driver recognizes a diagnostic sheet appears instead, with suggested fixes and **Copy Diagnostic Info**. + + + The connection is saved and the window opens on it. **Save** alone keeps it without connecting. + + Database type chooser Database type chooser +Every driver's default port, and which of SSH tunnel, SSL/TLS, Cloudflare Tunnel, Cloud SQL Proxy, and SOCKS Proxy it accepts, is in the [connection form reference](/connections/connection-form#which-drivers-get-which-panes). + ### Import from URL -Paste a connection string and let TablePro fill in the form. In the chooser sheet footer, click **Import from URL…**, paste the URL, review the parsed preview, and click **Import**. The form opens pre-filled so you can review and save. See [Connection URL Reference](/connections/urls) for schemes and formats. +Paste a connection string instead of typing the fields. Click **Import from URL…** in the chooser sheet footer, paste, check the parsed preview, and click **Import**. The form opens filled in, ready to save. Schemes, query parameters, and the `+ssh` form are in the [Connection URL Reference](/connections/urls). Import from URL @@ -73,74 +47,17 @@ Paste a connection string and let TablePro fill in the form. In the chooser shee Special characters in passwords (`@`, `#`, `%`) need percent-encoding. `p@ssword` becomes `p%40ssword`.
-### Open a URL directly - -Opening a database URL from a browser or terminal skips the form: +### Open a URL without saving anything ```bash open "postgresql://user:pass@host:5432/dbname" ``` -TablePro registers these URL schemes with macOS: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `dm`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, and `surrealdb`. - -What happens on open: - -- A confirmation alert shows the connection target before anything connects. For loopback hosts you can pick **Always Allow**, which trusts that exact combination of type, host, database, username, and the URL's `name` parameter for future opens -- If a saved connection matches the host, port, database, and username, TablePro reuses it. Otherwise it creates a temporary session that is not added to your connection list -- The URL password stays in memory for the session. It is never written to the Keychain -- If the connection has a pre-connect script, TablePro shows the script and asks before running it -- URLs can target a table or apply a filter via query parameters; filters also require confirmation. See [Connection URL Reference](/connections/urls) - -## Connection form - -The form is a sidebar with up to nine panes. Tunnel and SSL panes appear only for drivers that support them (see the matrix above). A warning triangle on a sidebar item marks missing required fields. - -| Pane | Contents | -|------|----------| -| **General** | Name, host, port, database, credentials, Test Connection | -| **SSH Tunnel** | Reach databases behind a bastion host. See [SSH Tunneling](/connections/ssh-tunneling) | -| **Cloudflare Tunnel** | Connect through `cloudflared`. See [Cloudflare Tunnel](/connections/cloudflare-tunnel) | -| **Cloud SQL Auth Proxy** | Google Cloud SQL proxy for MySQL, PostgreSQL, SQL Server. See [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy) | -| **SOCKS Proxy** | Route the connection through a SOCKS5 proxy. See [SOCKS Proxy](/connections/socks-proxy) | -| **SSL/TLS** | Encryption mode and certificates. See [SSL/TLS](/connections/ssl) | -| **Customization** | Color, tags, group, Safe Mode | -| **Advanced** | Startup commands, pre-connect script, external access, plugin-specific fields | -| **AI Rules** | Per-connection guidance the AI assistant sees on every chat turn. See [AI Assistant](/features/ai-assistant) | - - - Connection form - Connection form - +A confirmation alert names the target first. Matching a saved connection on type, host, port, database, and username reuses it; otherwise the session is temporary and never joins your connection list, and the URL's password stays in memory rather than reaching the Keychain. [Confirmation and trusted links](/connections/urls#confirmation-and-trusted-links) covers **Always Allow** and how to revoke it. -### General +## Organize connections -| Field | Description | -|-------|-------------| -| **Name** | Display name in the connection list | -| **Host** | Server address. Defaults to `localhost` | -| **Port** | Pre-filled per database type | -| **Database** | Default database. Optional for service-level access | -| **Username** | Optional and not pre-filled. Empty means the driver's own default | -| **Password** | Stored in the macOS Keychain | -| **Prompt for password** | Skip saving. TablePro asks on every connect | -| **Use Password File** | PostgreSQL, Redshift, and CockroachDB. Reads credentials from `~/.pgpass` | - -File-based drivers (SQLite, DuckDB, Beancount) replace the host section with a file path picker. - -### Advanced - -| Field | Description | -|-------|-------------| -| **Startup Commands** | SQL that runs after every connect. See [Startup Commands](#startup-commands) | -| **Pre-Connect Script** | Shell script run before connecting. A non-zero exit aborts. TablePro shows the script and asks before running it, and never runs it when the app reopens a session at launch: that window waits with a **Connect** button | -| **AI Policy** | Per-connection override for in-app AI agents | -| **External Clients** | Access level for MCP clients: **Blocked**, **Read Only** (default), or **Read & Write**. See [External API](/external-api) | -| **Local only** | Excludes this connection from iCloud Sync. See [iCloud Sync](/features/icloud-sync) | -| **Plugin fields** | Driver-specific options like MongoDB `replicaSet` | - -## Organizing connections - -The **Customization** pane sets a color, tags, and a group per connection. The color tints the toolbar when connected, so production and development are easy to tell apart. +The **Customization** pane holds a color, tags, and a group. The color tints the toolbar while the connection is open. Customization pane @@ -151,73 +68,29 @@ The **Customization** pane sets a color, tags, and a group per connection. The c Red for production, green for development. Set Safe Mode to **Read-Only** on production to block accidental writes. See [Safe Mode](/features/safe-mode).
-### Groups - -Groups are folders in the connection list, nested up to 3 levels. Right-click empty space or a group to create, move, or delete one. Deleting a group removes its subgroups; the connections inside are ungrouped, not deleted. - -### Tags - -A connection can carry multiple tags, each with a name and color. When any connection has tags, a filter bar appears above the welcome list with one pill per tag. Click pills to filter; with two or more selected, a **Match Any** / **Match All** menu switches between OR and AND matching, and **Clear** resets the filter. - -### Favorites +| | How it works | +|---|---| +| **Groups** | Folders in the connection list, nested up to three levels. Right-click empty space for **New Group…**, or a group for **Rename**, **New Subgroup**, **Change Color**, **Move Group to…**, and **Delete Group**. Deleting a group deletes its subgroups too and leaves the connections inside ungrouped rather than deleted | +| **Tags** | Several per connection, each with a name and color. Once any connection is tagged, a row of pills appears above the list; click to filter, and with two or more selected a **Match Any** / **Match All** menu switches between OR and AND. **Clear** resets it | +| **Favorites** | Hover a row and click the star, or right-click and choose **Add to Favorites**. Favorites gather in a section at the top, sorted alphabetically, while the connection keeps its place in its group | -Hover a connection row and click the star, or right-click and choose **Add to Favorites**. Favorites gather in a section at the top of the list, sorted alphabetically, while the connection stays in its group below. Favorites sync through iCloud unless the connection is marked local only. +A connection's group, tags, and favorite star sync through iCloud unless it is marked local only. See [iCloud Sync](/features/icloud-sync). -## Switching connections and databases +## Switch connections and databases -- **Switch Connection** (`Ctrl+Cmd+C`): a toolbar popover lists active sessions and saved connections. Type to filter, arrow keys to move, Return to switch -- **Open Database** (`Cmd+K`): switch databases on the same server without reconnecting +**Switch Connection** (`Ctrl+Cmd+C`) opens a toolbar popover listing active sessions and saved connections: type to filter, arrow keys to move, Return to switch. **Open Database** (`Cmd+K`) moves to another database on the same server. Database switcher in toolbar Database switcher in toolbar -Leave the **Database** field empty when creating a connection to browse every database your user can access (MySQL, MariaDB, MongoDB, SQL Server, ClickHouse). PostgreSQL and Redshift need an initial database; connect to `postgres` (Redshift: `dev`) and switch with `Cmd+K`. - -When the sidebar is in tree layout, each database or schema groups its tables, views, materialized views, foreign tables, procedures, and functions into collapsible folders. View > Filter Databases limits the tree to checked databases, and the same command is in the sidebar right-click menu. The choice is saved per connection. - -## Dock menu - -Right-click the TablePro Dock icon and pick a saved connection under **Open Connection**. - -## While a connection is being established - -The window fills with the connection's name, its endpoint, and the step currently running: opening the tunnel, running the pre-connect script, negotiating encryption, authenticating, preparing the session. The sidebar and inspector stay out of the way until there is a session to put in them, and come back at the width you left them. - -PostgreSQL, CockroachDB, Redshift, ClickHouse, and Redis report steps from inside their own handshake. Other drivers report the steps around the driver, because their client libraries connect in a single call with nothing observable in between. If a step stalls, the window says so. **Cancel**, or Escape, stops waiting immediately. - -A connection that fails stays in its window and shows the database's own error, with **Try Again**, **Manage Connections…**, and **Copy Details**. Nothing is reported as an alert, so reopening several connections at launch cannot bury you in dialogs. - -## Connection health monitoring - -TablePro pings every active connection every 30 seconds and skips the ping while one of your queries is running. File-based drivers (SQLite, DuckDB, Beancount), Snowflake, and Teradata are not monitored. - -When a ping fails, TablePro reconnects with exponential backoff: 2s, 4s, 8s, doubling up to a 120-second cap, and keeps retrying until the connection recovers or you close it. Authentication failures stop the retries, since an expired credential never recovers on its own. Reconnecting rebuilds the SSH tunnel if there is one, restores the selected database and schema, and re-runs startup commands. The session shows as connecting while this happens. - -## Startup Commands - -SQL statements that run after every connect, including auto-reconnects. Configure them in the **Advanced** pane; statements are split on semicolons and newlines and run in order. - -All statements run on the same connection, so use one dialect. MySQL: - -```sql -SET time_zone = '+00:00'; -SET NAMES utf8mb4; -``` - -PostgreSQL: - -```sql -SET search_path TO myschema, public; -``` - -A failed statement is logged and skipped; the connection still opens. +Leaving **Database** empty on MySQL, MariaDB, MongoDB, SQL Server, and ClickHouse browses every database the user can reach. PostgreSQL and Redshift need one to connect at all: use `postgres` (Redshift: `dev`) and switch with `Cmd+K`. To hide the rest, choose **View > Filter Databases** and check the ones you want; the choice is saved per connection. -## Editing, deleting, and storage +## Edit, delete, and where it all lives -Right-click a connection to edit or delete it. Changes apply on the next connect; deleting removes the saved settings only. +Right-click a connection to edit or delete it, or swipe left on the row for the same two buttons. Swiping never deletes on its own. Edits apply on the next connect, and deleting removes the saved settings, nothing on the server. -Swiping left on a connection row reveals the same Edit and Delete buttons. The gesture needs two fingers on a trackpad, or one finger on a Magic Mouse, so the right-click menu stays the way to reach these with a plain mouse or the keyboard. Swiping alone never deletes anything: you still click Delete and confirm. +Connections are stored in `~/Library/Application Support/TablePro/connections.json` with their passwords in the macOS Keychain, so copying that file to another Mac restores the connections without the passwords. -Connections are stored in `~/Library/Application Support/TablePro/connections.json`. Passwords live in the macOS Keychain, so a copied file restores connections but not passwords. +Right-clicking the Dock icon lists saved connections under **Open Connection**. diff --git a/docs/connections/socks-proxy.mdx b/docs/connections/socks-proxy.mdx index 3f36358d1..b2e1a6aeb 100644 --- a/docs/connections/socks-proxy.mdx +++ b/docs/connections/socks-proxy.mdx @@ -3,7 +3,12 @@ title: SOCKS Proxy description: Route a database connection through a SOCKS5 proxy, with remote DNS (socks5h) so the proxy resolves the database hostname --- -When the database is only reachable through a corporate proxy, a bastion running `ssh -D`, or another SOCKS5 endpoint, the **SOCKS Proxy** pane in the connection form is the way in. +Leave the database's **Host** and **Port** on the General pane exactly as they are. The proxy resolves that name and dials it from its own side. A hostname that exists only inside the private network works, and no DNS query for the database leaves your Mac. + + + SOCKS Proxy pane + SOCKS Proxy pane + ## How it works @@ -25,41 +30,49 @@ flowchart LR Driver -->|"loopback"| Relay -->|"SOCKS5"| SOCKS -->|"TCP"| Database ``` -TablePro opens a loopback listener on a free port and relays every connection through the proxy to the database, then points the driver at the local port. Unlike Cloudflare Tunnel and Cloud SQL Auth Proxy, there is no external binary to install: TablePro's own networking stack (Network.framework) speaks SOCKS5 directly. - -The database hostname is sent to the proxy and resolved there, not on your Mac. This is remote DNS (what a `socks5h://` URL selects in other clients), and TablePro always does it, so there is no separate `socks5` mode that resolves locally. Hostnames that only resolve inside the private network behind the proxy work, and no DNS query for the database host leaves your machine. +No helper binary is involved: the relay is part of the app. It listens on a free loopback port and carries each connection through the proxy. If the relay dies mid-session, the connection reconnects and rebuilds it, up to ten attempts with a widening delay. ## Setting up -The SOCKS Proxy pane appears for the same databases that support SSH tunneling. SQLite and cloud-managed databases like BigQuery, DynamoDB, and Snowflake don't show it. + + + Select **SOCKS Proxy** and turn **Enable SOCKS Proxy** on. Only one method at a time: anything else already enabled has a button here to switch it off. + + + **Host** and **Port** under **Proxy Server**, plus **Username** and **Password** if the proxy authenticates. + + + On **General**, click **Test Connection**. + + -Open the connection form, switch to the **SOCKS Proxy** pane, toggle **Enable SOCKS Proxy** on, enter the proxy **host** and **port**, then go back to **General** and click **Test Connection**. - -A connection uses one connection method at a time. If the SSH Tunnel, Cloudflare Tunnel, or Cloud SQL Auth Proxy is enabled, the pane offers to turn it off. +The pane appears for the drivers that support SSH tunneling; the [transport matrix](/connections/connection-form#which-drivers-get-which-panes) says which. ## Options -| Field | Description | Default | -|-------|-------------|---------| -| **Host** | The SOCKS5 proxy server address. | - | -| **Port** | The proxy port. | 1080 | -| **Username** | Optional. Set it when the proxy requires username and password authentication. | - | -| **Password** | Optional. Stored in the macOS Keychain. | - | +| Field | What it is | Default | +|-------|-----------|---------| +| **Host** | The SOCKS5 proxy's address. | - | +| **Port** | The proxy's port. | 1080 | +| **Username** | Only for a proxy that requires username and password authentication. | - | +| **Password** | Stored in the macOS Keychain. Blank connects without authentication. | - | -An SSH dynamic port forward is a SOCKS5 proxy. Run `ssh -D 1080 user@bastion` in a terminal, then set the proxy host to `127.0.0.1` and the port to `1080`. +An SSH dynamic port forward is a SOCKS5 proxy. Run `ssh -D 1080 user@bastion`, then set the host to `127.0.0.1` and the port to `1080`. +[SSL/TLS](/connections/ssl) still applies, with one unavoidable adjustment: the driver dials a loopback port that no server certificate names, so **Verify CA** and **Verify Identity** fall back to **Required** and certificate paths are dropped. + ## Troubleshooting -### Timed out connecting through the proxy +### Timed out connecting through the SOCKS proxy -TablePro waits 15 seconds for the path through the proxy to come up. A timeout means the proxy did not answer, the credentials were rejected, or the proxy could not reach the database. Check the proxy host and port, verify the username and password, and confirm the database host and port are reachable from the proxy's network. +Fifteen seconds passed with no path to the database. The proxy did not answer, rejected the credentials, or could not reach the database. Check the proxy host and port, then that the database answers from the proxy's network. ### Local network permission prompt -On macOS 15 and later, connecting to a proxy on your local network (a `192.168.x.x` address, for example) triggers the one-time Local Network permission alert. Allow it, or the proxy is unreachable. Loopback proxies like `ssh -D` on `127.0.0.1` don't trigger the prompt. +On macOS 15 and later, a proxy on your local network (a `192.168.x.x` address) raises the one-time Local Network alert. Allow it, or the proxy stays unreachable. A loopback proxy such as `ssh -D` on `127.0.0.1` never triggers it. ### The database rejects the connection -The proxy path is working but the database refused the connection. The driver's own error is shown, the same as connecting directly. Check credentials, SSL settings, and that the database allows connections from the proxy's address. +The path works and the server refused you. The driver's own error is shown, exactly as on a direct connection: check credentials, SSL settings, and whether the database accepts connections from the proxy's address. diff --git a/docs/connections/ssh-profiles.mdx b/docs/connections/ssh-profiles.mdx index 0413a6b50..6fe7e0ef5 100644 --- a/docs/connections/ssh-profiles.mdx +++ b/docs/connections/ssh-profiles.mdx @@ -3,38 +3,39 @@ title: SSH Profiles description: Save SSH tunnel configurations as reusable profiles shared across connections --- -Define an SSH tunnel config once, then reuse it across multiple connections. A profile stores the server (host, port, username), the authentication method, optional two-factor (TOTP) settings, and jump hosts. For tunnel mechanics (auth methods, TOTP modes, jump hosts, `~/.ssh/config` resolution, troubleshooting), see [SSH Tunneling](/connections/ssh-tunneling). - -## Creating a profile - -1. Open any connection's **SSH Tunnel** pane -2. Click **Create New Profile…** -3. Fill in the server and authentication fields -4. Click **Create** - -Select the profile from the **Profile** picker in other connections. A **Profile Details** disclosure shows the selected profile's settings. Changes to a profile apply to all connections using it. +One bastion, many connections. A profile holds the server (host, port, username), the authentication method, the two-factor settings, and the jump hosts, so every connection through that bastion stops repeating them. How the tunnel itself behaves is on [SSH Tunneling](/connections/ssh-tunneling). + +## Create a profile + + + + **Create New Profile…** sits beside the **Profile** picker. + + + The same fields an inline config has, plus a name. + + + The profile now appears in the **Profile** picker of every connection. + + Creating an SSH profile Creating an SSH profile -To convert an existing inline SSH config into a profile, click **Save Current as Profile…**. - -## Profiles vs inline config - -Each connection can use either a shared profile or an inline (one-off) SSH config. Inline configs are stored with the connection and don't affect other connections. Profiles are stored globally and shared. +**Save Current as Profile…** converts a connection that already has an inline config. Picking a profile swaps the inline fields for a read-only **Profile Details** summary; **Inline Configuration** in the same picker brings them back for a tunnel that belongs to one connection alone. -Use a profile when multiple connections go through the same bastion host. Use inline for a one-off tunnel specific to a single connection. +## Test a profile -## Editing and deleting +**Test Connection** in the profile editor runs the handshake, verifies the host key, and authenticates, without touching a database. Success turns the button into a green checkmark reading **Connected**; failure marks it with a red X and prints the error beside it. An interactive prompt, a verification code for instance, appears here exactly as it would on connect. -Select a profile and click **Edit Profile…** to modify it. **Delete Profile** is at the bottom of the editor. +## Edit and delete -## Testing a profile +**Edit Profile…** opens the selected profile, and **Delete Profile** is at the bottom of that editor. The password, key passphrase, and TOTP secret are read from the profile at connect time, so correcting one there reaches every connection using it. -Click **Test Connection** in the profile editor to verify SSH settings without connecting to a database. TablePro performs the SSH handshake, verifies the host key, and authenticates. On success the button shows a green checkmark and **Connected** appears next to it. On failure the button shows a red X and the error appears in orange, cut to two lines. If the server asks for a verification code or another interactive response, the same prompt you'd see on Connect appears during the test. +Deleting a profile leaves the connections that used it pointing at nothing. Each shows **Selected SSH profile no longer exists.** on its SSH Tunnel pane, with a **Switch to Inline Configuration** button, until it is given a tunnel config again. ## iCloud Sync -SSH profiles sync across Macs when iCloud Sync is enabled with the **SSH Profiles** toggle on in **Settings > Account**. Passwords, key passphrases, and TOTP secrets stay in the local Keychain by default. Turning on the **Passwords** toggle (nested under **Connections**) syncs newly saved credentials via iCloud Keychain; re-save an existing password to include it. +Profiles sync across Macs with iCloud Sync on and the **SSH Profiles** toggle set in **Settings > Account**. Passwords, key passphrases, and TOTP secrets stay in the local Keychain unless the **Passwords** toggle, nested under **Connections**, is on too, and that covers newly saved credentials only: re-save an existing password to include it. diff --git a/docs/connections/ssh-tunneling.mdx b/docs/connections/ssh-tunneling.mdx index 5f0592761..36bb29e9e 100644 --- a/docs/connections/ssh-tunneling.mdx +++ b/docs/connections/ssh-tunneling.mdx @@ -3,112 +3,94 @@ title: SSH Tunneling description: Route database connections through an SSH tunnel to reach servers in private networks --- -A wrong host or a blocked forward fails with the real reason instead of a database timeout, because the destination is checked from the SSH side before the driver is handed the local port. That port sits in the 60000-65000 range, which is why macOS may show a network permission prompt. Nothing calls the `ssh` binary: TablePro carries its own libssh2 client, which opens the tunnel, sends a keep-alive every 30 seconds, and reconnects on its own if the tunnel dies. - -The **SSH Tunnel** pane appears only for databases whose driver supports it. SQLite, PGlite, libSQL, Beancount, BigQuery, Cloudflare D1, DynamoDB, Elasticsearch, and Snowflake do not show it: they are reached over a local file, a loopback socket, or a vendor HTTP API. +The database **Host** on the General pane is resolved from the SSH server, not from your Mac. A database on the SSH server itself is therefore `localhost`, not the server's public name, and a database elsewhere on the private network is whatever the SSH server calls it (an RDS endpoint, for instance). ```mermaid flowchart LR TablePro["TablePro
localhost:60000"] -->|"Encrypted tunnel"| SSH["SSH server"] -->|"Internal network"| DB["Database
db:3306"] ``` -## Setting up - -1. Open the connection form and switch to the **SSH Tunnel** pane -2. Toggle **Enable SSH Tunnel** on -3. Fill in **SSH Host**, **SSH Port** (default 22), and **SSH User** -4. Pick an authentication method -5. Back on **General**, click **Test Connection** - - -The database **Host** and **Port** on the General pane are what the SSH server uses to reach the database, not what your Mac would use. Use `localhost` if the database runs on the SSH server itself, or the internal hostname (for example an RDS endpoint reached through a bastion) if it runs elsewhere. If the database listens on a Unix socket rather than a port, use [Socket Path](#forwarding-to-a-unix-socket) instead. - - -A connection uses one connection method at a time. If a Cloudflare tunnel, Cloud SQL Auth Proxy, or SOCKS proxy is already enabled, the pane shows a button to disable it. - -To reuse one SSH config across several connections, save it as a profile with **Save Current as Profile…** or pick one from the **Profile** picker. A selected profile replaces the inline fields with a read-only **Profile Details** summary. Switch the picker back to **Inline Configuration** to edit the settings on this connection. See [SSH Profiles](/connections/ssh-profiles). +## Set up a tunnel + + + + In the connection form, open the **SSH Tunnel** pane and switch on **Enable SSH Tunnel**. A connection uses one transport at a time; if a Cloudflare tunnel, Cloud SQL Auth Proxy, or SOCKS proxy is already on, the pane offers a button to switch it off. + + + Fill in **SSH Host**, **SSH Port** (22 by default), and **SSH User**. With `~/.ssh/config` entries present, a **Config Host** picker appears above the host field instead. + + + Password, Private Key, SSH Agent, Keyboard Interactive, or None. [Authentication methods](#authentication-methods) has the fields for each. + + + Back on **General**, `localhost` reaches a database on the SSH server itself. One on a unix socket needs [Socket Path](#forwarding-to-a-unix-socket) instead. + + + A wrong host or a blocked forward names the real reason instead of timing out. [Troubleshooting](#troubleshooting) has the three the SSH side reports. + + + +To share one SSH config across connections, save it with **Save Current as Profile…** or pick an existing one from the **Profile** picker; see [SSH Profiles](/connections/ssh-profiles). To fill the whole pane from a string instead, paste a `scheme+ssh://` URL into the [Import from URL…](/connections/urls#ssh-tunnel-format) sheet. SSH Tunnel pane with a saved profile selected SSH Tunnel pane with a saved profile selected -## Authentication methods - - - - Enter your SSH password in the **Password** field. Prefer keys for production servers. - - - | Field | Description | - |-------|-------------| - | **Key File** | Path to your private key. Click **Browse** to pick one. | - | **Passphrase** | Key passphrase, if the key is encrypted | - - Leave **Key File** empty to auto-detect the key from `~/.ssh/config` and default key locations. - - If the server also requires a keyboard-interactive step such as a 2FA code, TablePro prompts for it after the key is accepted. - - - Signing is delegated to an agent process; TablePro never reads the key. The **Agent Socket** picker offers: +There is no **SSH Tunnel** pane on SQLite, PGlite, libSQL, Beancount, BigQuery, Cloudflare D1, DynamoDB, Elasticsearch, or Snowflake: each is reached over a local file, a loopback socket, or a vendor HTTP API. - - **SSH_AUTH_SOCK**: the system `SSH_AUTH_SOCK` environment variable - - **1Password**: 1Password's socket at `~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock` - - **Custom Path**: any other agent socket path (Secretive, custom `ssh-agent`) +## Authentication methods - If the server also requires a keyboard-interactive step such as a 2FA code, TablePro prompts for it after the agent authenticates. - - - Sends your password through SSH's keyboard-interactive challenge-response. Use this when the server rejects plain password auth, which is common with PAM-based setups. - - - Sends no password or key. Use this when the server authenticates the connection itself, such as a [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) host or a bastion configured for passwordless access. If the server still requires credentials, the connection fails with a message telling you to pick another method. - - +| Method | What to fill in | +|---|---| +| **Password** | The SSH password. Prefer a key on anything production | +| **Private Key** | **Key File**, with **Browse** to pick one, and **Passphrase** if the key is encrypted. Leaving **Key File** empty auto-detects from `~/.ssh/config` and the default key locations | +| **SSH Agent** | **Agent Socket**: **SSH_AUTH_SOCK**, **1Password** (its socket at `~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock`), or **Custom Path** for Secretive or your own `ssh-agent`. Signing stays in the agent; the key is never read | +| **Keyboard Interactive** | The SSH password, sent through SSH's challenge-response. Use it when the server rejects plain password auth, common with PAM | +| **None** | Nothing, for a server that authenticates the connection itself such as a [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh) host. A server that does want credentials fails the connect with a message naming the other methods | -### Verification prompts and two-factor authentication +**Private Key** is the one to pick unless the server only ever issued you a password. -If the SSH server issues a keyboard-interactive challenge during authentication, for example a verification code from `google-authenticator` or `duo_unix`, TablePro shows the server's prompt and takes your response. This works with every authentication method except **None**, including a private key or SSH agent followed by a second factor (`AuthenticationMethods publickey,keyboard-interactive`). +### Verification codes and two-factor authentication -Use the **Two-Factor Authentication** section to skip that prompt for TOTP codes: +A keyboard-interactive challenge partway through authentication, from `google-authenticator` or `duo_unix` for example, is shown as the server worded it and your answer sent back. Every method except **None** supports this, a key or agent followed by a second factor included (`AuthenticationMethods publickey,keyboard-interactive`). -- **None** or **Prompt at Connect**: TablePro asks for the code when the server requests it. -- **Auto Generate**: TablePro computes the code from your base32 **TOTP Secret** (the key from your authenticator enrollment). Algorithm (SHA1, SHA256, SHA512), digits (6 or 8), and period (30s or 60s) are configurable; defaults match most setups. +The **Two-Factor Authentication** section can answer TOTP codes instead. **None** and **Prompt at Connect** both ask when the server does; **Auto Generate** computes the code from the base32 **TOTP Secret** off your authenticator enrollment, with **Algorithm** (SHA1, SHA256, SHA512), **Digits** (6 or 8), and **Period** (30s or 60s) already set to what most servers use. ## Host keys -On first connection TablePro shows the server's key type and SHA-256 fingerprint (same format as `ssh-keygen -l`) and asks whether to trust it. Trusted keys are stored in `~/Library/Application Support/TablePro/known_hosts`. +A first connection shows the server's key type and SHA-256 fingerprint, in `ssh-keygen -l` format, and waits for **Trust**. Trusted keys go to `~/Library/Application Support/TablePro/known_hosts`. -If a trusted server's key changes, TablePro shows a warning with the old and new fingerprints. **Disconnect** is the default button; choose **Connect Anyway** only if you know the server was reinstalled. With jump hosts, every hop's key is verified. +If a trusted server's key later changes, an **SSH Host Key Changed** alert lists the previous and current fingerprints. **Connect Anyway** does not answer the Return key. Escape picks **Disconnect**, which is what to take unless you know the server was reinstalled. With jump hosts, every hop's key is verified the same way. ## Using ~/.ssh/config -If `~/.ssh/config` has Host entries, a **Config Host** picker appears above the SSH Host field. Pick an alias and TablePro resolves `HostName`, `User`, `Port`, `IdentityFile`, `IdentityAgent`, and `ProxyJump` from the config at connect time. Values you type in the form override the config. The file is re-read whenever it changes, including `Include`d files. +Pick an alias from the **Config Host** picker and `HostName`, `User`, `Port`, `IdentityFile`, `IdentityAgent`, and `ProxyJump` are resolved from the config at connect time. Anything typed into the form overrides the file, which is re-read whenever it or an `Include`d file changes. Config Host picker Config Host picker -## Jump Hosts +## Jump hosts -When the database sits behind one or more bastions, expand the **Jump Hosts** section and add each intermediate host in order. TablePro chains the hops in-process: each hop is an SSH session tunneled through the previous one, no `ssh` subprocess involved. +For a database behind more than one bastion, expand **Jump Hosts** and add each hop in order. Each hop is an SSH session tunneled through the previous one, with no `ssh` subprocess involved. | Field | Description | |-------|-------------| | **Host** | Hostname or IP of the jump host | -| **Port** | SSH port (default 22) | +| **Port** | SSH port, 22 by default | | **Username** | SSH username for this hop | -| **Auth** | **Private Key** or **SSH Agent** | -| **Key File** | Path to private key (Private Key auth only) | +| **Auth** | **Private Key** or **SSH Agent**. Password auth is not offered for a hop | +| **Key File** | Private key path, under Private Key auth | -Password auth is not available for jump hosts. If the jump host list is empty and the SSH host matches a config entry with a `ProxyJump` directive, TablePro follows it. +With the list left empty and an SSH host that matches a config entry carrying `ProxyJump`, that directive is followed instead. ## Forwarding to a unix socket -Some servers only listen on a Unix socket, with no TCP port open at all. A PostgreSQL box configured for `local` connections in `pg_hba.conf` is the common case. Fill in **Socket Path** on the General pane and TablePro forwards to that socket instead of a host and port, the same thing `ssh -L 5434:/var/run/postgresql/.s.PGSQL.5432 server` does by hand. **Host** and **Port** are ignored while a socket path is set. +Some servers listen on a unix socket with no TCP port open at all, a PostgreSQL box set up for `local` connections in `pg_hba.conf` being the usual case. Fill in **Socket Path** on the General pane and the forward targets that socket, the same thing `ssh -L 5434:/var/run/postgresql/.s.PGSQL.5432 server` does by hand. **Host** and **Port** are ignored while a socket path is set. Jump hosts still apply: the hops reach the SSH server, and the socket opens from there. -Give the path to the socket **file**, not the directory holding it: +Point at the socket file, not the directory holding it: | Database | Typical socket path | |----------|---------------------| @@ -116,32 +98,42 @@ Give the path to the socket **file**, not the directory holding it: | MySQL / MariaDB | `/var/run/mysqld/mysqld.sock` | | Redis | `/var/run/redis/redis.sock` | -This works through jump hosts: the hops get you to the SSH server, and the socket is opened from there. - - -A database listening on a Unix socket cannot negotiate TLS, so TablePro turns SSL off for the connection. Nothing is exposed by this: the whole path still runs inside the encrypted SSH tunnel. - +`peer` authentication works: the SSH server opens the socket as your SSH login user, so a `local all all peer` line authenticates you as the PostgreSQL role of that name. -`peer` authentication works, because the connection to the socket is made by the SSH server as your SSH login user. So a `local all all peer` line authenticates you as the PostgreSQL role matching your SSH username. +A database on a socket cannot negotiate TLS, so SSL is switched off for the connection. The SSH tunnel still encrypts the whole path. -The SSH server has to allow socket forwarding. It is on by default, but if `sshd_config` sets `AllowStreamLocalForwarding no`, TablePro says so instead of failing with a dropped connection. Note this is a separate setting from `AllowTcpForwarding`. +## If the tunnel drops -## Import from URL +A keep-alive goes out every 30 seconds. When one fails the session drops to connecting and the tunnel is rebuilt, retrying at 2s, 4s, 8s and doubling to a 120-second ceiling, ten attempts at most. A query in flight dies with the tunnel and is not replayed. After ten failures the session ends with **SSH tunnel disconnected. Click to reconnect.** -Paste a `scheme+ssh://` URL to create a connection with SSH pre-filled. The `+ssh` suffix works with any non-file-based scheme (`mysql+ssh`, `postgres+ssh`, `redis+ssh`, `mongodb+ssh`, …). TablePlus SSH URLs import directly. See the [Connection URL Reference](/connections/urls#ssh-tunnel-format) for the format and parameters. +If tunnels keep dropping on an idle network, the keep-alive is not the missing piece: check the server's `ClientAliveInterval` and the idle timeouts on any firewall or load balancer in between. ## Troubleshooting -**Tunnel connects but the database fails**: the database host is resolved from the SSH server, not your Mac. Verify it from the server: `ssh user@server "nc -z db-host 5432"`. Also check that the database credentials differ from the SSH ones where they should. +### "The SSH server could not reach …" + +SSH is fine and the forward is not. Almost always the **Host** field: that address is resolved from the SSH server, so a database bound to `127.0.0.1` (the MySQL and PostgreSQL default) needs **Host** set to `localhost`. Check what it is bound to with `ss -lntp` on the server. If the host is right, check `sshd_config` for `AllowTcpForwarding yes`. + +### "The SSH server did not open a forwarding channel to …" + +The destination took the connection and never answered, which usually means a firewall or security group dropping packets rather than refusing them. Test it from the SSH server: `ssh user@server "nc -zv db-host 3306"`. + +### "The SSH server would not forward the socket …" + +The socket path does not exist on the server, or `sshd_config` sets `AllowStreamLocalForwarding no`. That is a separate setting from `AllowTcpForwarding`. + +### The tunnel connects and the database refuses the login + +The database credentials are separate from the SSH ones. Check you did not carry one set into the other. -**"The SSH server could not reach \"**: the SSH connection is fine, but the SSH server could not open a connection to the database. Almost always the **Host** field. That address is resolved from the SSH server, not from your Mac, so if the database only listens on `127.0.0.1` (the default for MySQL and PostgreSQL), **Host** has to be `localhost`, not the server's public name or IP. Check what the database is bound to with `ss -lntp` on the server. If the host is right, check that `sshd_config` has `AllowTcpForwarding yes`. +### A firewall prompt on connect -**"The SSH server did not open a forwarding channel to \"**: the destination took the connection attempt and never answered, which usually means a firewall or security group is dropping packets rather than refusing them. Test it from the SSH server itself: `ssh user@server "nc -zv db-host 3306"`. +The local end of the tunnel listens on a port between 60000 and 65000. Allow it. -**Tunnel drops on idle networks**: TablePro already sends a libssh2 keep-alive every 30 seconds plus OS-level TCP keep-alives. If tunnels still drop, check the server's `ClientAliveInterval` and idle timeouts on firewalls or load balancers in between. +### SSH fails instantly against a server on your own network -**Firewall prompt on connect**: TablePro listens on a local port between 60000 and 65000 for the tunnel. Allow it. +macOS 15 and later put outbound connections to local network addresses behind a Local Network permission, and a denied app fails fast with "no route to host". Check TablePro under **System Settings > Privacy & Security > Local Network**. Servers over the internet, and anything on `127.0.0.1`, are unaffected. -**SSH fails right away against a server on your own network**: macOS 15 and later gate outbound connections to local network addresses behind a Local Network permission, and a denied app fails fast with "no route to host". Check TablePro under System Settings > Privacy & Security > Local Network. Servers reached over the internet, and anything on `127.0.0.1`, are not affected. +### SSH itself fails -**SSH itself fails**: test the same host, user, and key in Terminal with `ssh -v user@server`. If that fails, the problem is server-side, not TablePro. +Try the same host, user, and key in Terminal with `ssh -v user@server`. If that fails too, the problem is server-side. diff --git a/docs/connections/ssl.mdx b/docs/connections/ssl.mdx index 0d8ef6410..f033ce516 100644 --- a/docs/connections/ssl.mdx +++ b/docs/connections/ssl.mdx @@ -3,106 +3,103 @@ title: SSL/TLS description: Configure encrypted database connections, per-driver defaults, and certificate verification --- -TablePro supports five SSL modes that map to each driver's native TLS capabilities. New connections start with the mode that matches the driver's documented default. +Managed databases require TLS, every one of them, from RDS and Cloud SQL to Supabase, Neon, Atlas, and ClickHouse Cloud. Set **Required (skip verify)** and stop there, unless your provider handed you a CA certificate, in which case use **Verify Identity** and point at it. ## Modes | Mode | Behavior | |---|---| | Disabled | Plain TCP, no TLS negotiation | -| Preferred | Try TLS first, fall back to plain if the server doesn't support it (where the driver allows) | -| Required | Force TLS; fail if the server rejects encryption. No certificate validation. | -| Verify CA | Force TLS and validate the server certificate against the trust store. Hostname not checked. | -| Verify Identity | Force TLS, validate the certificate, and require the hostname to match the certificate subject | +| Preferred | TLS first, plain if the server will not, where the driver has a fallback at all | +| Required (skip verify) | TLS or nothing. The certificate is not checked | +| Verify CA | TLS, and the certificate is validated against the trust store. The hostname is not checked | +| Verify Identity | TLS, certificate validated, and the hostname must match the certificate subject | SSL mode and certificate settings in the connection form SSL mode and certificate settings in the connection form -## Per-driver defaults +## Certificate fields -New connections pick the mode that matches each driver's native behavior. Open the SSL/TLS pane on any connection to see the driver-specific guidance. +The certificate rows appear only once **SSL Mode** is anything but Disabled. Each takes a file path, with **Browse** to pick one. -| Driver | Default | Notes | +| Field | When it appears | What it takes | |---|---|---| -| PostgreSQL, Redshift, CockroachDB | Preferred | libpq `sslmode=prefer`. Matches `psql` and DataGrip. | -| MySQL, MariaDB | Preferred | 2-pass connect: try TLS first, fall back to plain on SSL handshake error | -| SQL Server | Preferred | FreeTDS `encryption=request`. Uses the system trust store; per-connection certificate paths are not supported. | -| MongoDB, Redis, Cassandra, ClickHouse, etcd, Elasticsearch, SurrealDB | Disabled | No TLS fallback: Preferred forces TLS, same as Required | -| Oracle | Disabled | Preferred connects in plain TCP. Use Required to enforce TCPS. | -| Teradata | Disabled | Preferred tries TLS and drops to plain TCP if the handshake fails | -| Trino | Disabled | Preferred switches the HTTP client to HTTPS with no fallback, same as Required | -| Snowflake, BigQuery, DynamoDB, Cloudflare D1, libSQL / Turso | Always encrypted | HTTPS-based drivers manage TLS themselves; no SSL/TLS pane | -| SQLite, DuckDB, Beancount | N/A | Local files, no network protocol | -| PGlite | N/A | No SSL support, so the connection form has no SSL/TLS pane | - -Managed services (AWS RDS and Aurora, Google Cloud SQL, Azure, Heroku, Supabase, Neon, PlanetScale, MongoDB Atlas, Redis Cloud, Upstash, ElastiCache, DataStax Astra, Oracle Autonomous Database, ClickHouse Cloud) require TLS out of the box. Pick **Required**, or **Preferred** where the driver falls back. - -## Preferred fallback behavior - -Preferred mode tries TLS first. What happens if the server doesn't support TLS depends on the driver: - -- **PostgreSQL, Redshift, CockroachDB**: libpq falls back to plain TCP natively -- **SQL Server**: FreeTDS `encryption=request` falls back to plain -- **MySQL, MariaDB**: 2-pass connect tries TLS, then plain on SSL-specific handshake errors (CR_SSL_CONNECTION_ERROR, CR_SERVER_HANDSHAKE_ERR, ER_HANDSHAKE_ERROR). Auth and network errors are not retried. -- **MongoDB, Redis, Cassandra, ClickHouse, etcd, Elasticsearch, SurrealDB**: no fallback. Preferred forces TLS, same as Required. The SSL/TLS pane shows a warning when you pick Preferred. -- **Oracle**: no opportunistic TLS. Preferred connects in plain TCP, so it behaves like Disabled. The SSL/TLS pane shows a red warning; use Required to enforce TCPS. -- **Teradata**: opens a TLS transport, then retries on a plain socket if that transport fails to come up -- **Trino**: no fallback. Preferred sends every request over HTTPS, same as Required. +| **Certificate**, under **CA Certificate** | Verify CA and Verify Identity only | The PEM the server's certificate chains to, for example `/path/to/ca-cert.pem`. Saving without it is blocked | +| **Client Certificate** | Any mode but Disabled | Your certificate, for a server that enforces mutual TLS. Optional otherwise | +| **Client Key** | Any mode but Disabled | The matching private key. Required once a client certificate is set | +| **Key Passphrase** | Once **Client Key** has a path, on drivers that support it (Cassandra and ScyllaDB) | The passphrase of an encrypted key. Stored in the Keychain | -## On iPhone and iPad +SQL Server shows no certificate fields at all. FreeTDS takes no per-connection paths and trusts the system store; a custom trust anchor goes in `freetds.conf` instead. + +## Per-driver defaults -MySQL, PostgreSQL and Redis connections have an SSL section with the same four modes. When the mode is anything but Disabled, three rows appear: CA Certificate, Client Certificate and Client Key. +A new connection starts on the mode that matches the driver's own default, and the pane prints that driver's guidance under the picker where there is any. -Each row takes a file, or pasted text if getting a file into the Files app is awkward. Client Certificate also accepts a PKCS#12 (`.p12` or `.pfx`) file, which fills in both the certificate and its key from one import. A `.p12` needs the password it was exported with; iOS cannot read one exported without a password. +| Driver | Default | What Preferred does | +|---|---|---| +| PostgreSQL, Redshift, CockroachDB | Preferred | libpq's own `sslmode=prefer` fallback to plain TCP. Same as `psql` and DataGrip | +| MySQL, MariaDB | Preferred | Tries TLS, then retries plain on an SSL handshake error. Auth and network errors are not retried | +| SQL Server | Preferred | FreeTDS `encryption=request`, falls back to plain | +| Teradata | Disabled | Opens a TLS transport, retries on a plain socket if it fails to come up | +| MongoDB, Redis, Cassandra, ClickHouse, Elasticsearch, SurrealDB | Disabled | Nothing. No fallback exists, so Preferred forces TLS exactly like Required | +| etcd | Disabled | Nothing. The driver never reads this pane. Set **TLS Mode** in the Advanced fields instead, and see [etcd](/databases/etcd) | +| Trino | Disabled | Sends every request over HTTPS with no fallback, again like Required | +| Oracle | Disabled | Connects in plain TCP, so it behaves like Disabled. The pane shows a red warning; use Required to enforce TCPS | +| Snowflake, BigQuery, DynamoDB, Cloudflare D1, libSQL / Turso | Always encrypted | These drivers are HTTPS and manage TLS themselves. No SSL/TLS pane | +| SQLite, DuckDB, Beancount, PGlite | None | Local files or an in-process engine. No SSL/TLS pane | -A private key protected by its own passphrase is rejected on import. Remove the passphrase, or export a `.p12` instead. +## Behind a tunnel -Certificates you import on iOS stay on that device. They are held in the keychain, marked so they never reach iCloud Keychain or a device backup, and are written to disk only while a connection is open. They are not synced, so a connection set up on the Mac needs its certificates imported again on each iPhone or iPad. The certificate paths configured on the Mac are left untouched. +Verify CA and Verify Identity do not survive a tunnel. An SSH tunnel, Cloudflare tunnel, Cloud SQL Auth Proxy, or SOCKS proxy has the driver connect to `127.0.0.1`, and no server certificate is issued for that, so the mode drops to Required for the tunneled connection and the certificate paths are cleared. TLS itself still runs the whole way to the database; only the certificate check goes. If you need the check, reach the server without a tunnel. -If a certificate goes missing, the connection reports it by name instead of connecting without it. +Forwarding to a unix socket drops TLS altogether, because a socket cannot negotiate it. The SSH tunnel encrypts that path instead. ## Troubleshooting +A TLS failure is reported with the cause, a mode to switch to, and the driver's own response underneath, password redacted: + +```text +The server requires an encrypted connection but TablePro is configured to connect in plain text. + +Open the connection editor, switch to the SSL tab, and set Mode to Required (or stricter). + +Server response: FATAL: no pg_hba.conf entry for host "…", user "…", database "…", no encryption +``` + ### "FATAL: no pg_hba.conf entry for host … no encryption" -PostgreSQL server requires SSL. Switch SSL Mode to **Preferred** or **Required**. +The PostgreSQL server requires SSL and the connection is set to Disabled. Switch to **Preferred** or **Required (skip verify)**. ### "Connections using insecure transport are prohibited" -MySQL server has `require_secure_transport=ON`. Switch SSL Mode to **Preferred** or **Required**. +The MySQL server has `require_secure_transport=ON`. Switch to **Preferred** or **Required (skip verify)**. ### "SSL handshake failed" / "tls handshake failed" -Driver and server can't agree on a TLS version or cipher. Update the server, or for development try **Required** instead of **Verify CA**/**Verify Identity** to skip certificate validation. +Driver and server share no TLS version or cipher. Update the server, or on a development box drop to **Required (skip verify)**. ### "certificate verify failed" / "self-signed certificate" -Server uses a certificate that isn't in your system trust store. Set SSL Mode to **Verify CA** and provide the CA certificate path, or use **Required** to skip certificate validation entirely. +The server's certificate chains to nothing in the system trust store. Set **Verify CA** and fill **Certificate** with the CA's PEM, or drop to **Required (skip verify)** to skip validation. ### "hostname does not match certificate" -The certificate's CN/SAN doesn't include the host you're connecting to. Switch from **Verify Identity** to **Verify CA** (validates the chain but skips hostname), or update the host field to match the certificate. +The certificate's CN or SAN does not cover the host you typed. Switch to **Verify CA**, which still validates the chain, or change **Host** to a name the certificate covers. ### "client certificate required" -Server requires mutual TLS. Fill in the **Client Certificate** and **Client Key** paths in the SSL/TLS pane. +The server enforces mutual TLS. Fill in **Client Certificate** and **Client Key**. ### "client private key is encrypted" / "passphrase is incorrect" -The client key is password-protected. Enter the **Key Passphrase** in the SSL/TLS pane. The field appears once a client key path is set, and the passphrase is stored in the Keychain. Currently supported by the Cassandra driver. +Enter the key's passphrase in **Key Passphrase**, which appears once **Client Key** has a path. -## Connection failures +## On iPhone and iPad -When a connection fails because of an SSL handshake problem, TablePro shows a structured message that names the cause and recommends a specific SSL Mode to switch to. The original driver error is shown below the suggestion. +Four modes, Preferred excluded. MySQL, MariaDB, PostgreSQL, Redshift, and Redis get **CA Certificate**, **Client Certificate**, and **Client Key** rows alongside them; Oracle gets the four modes with no certificate rows, and SQL Server only Disabled and Required. -For example, connecting to AWS RDS PostgreSQL with SSL Mode = Disabled produces: +Each row takes a file or pasted text. **Client Certificate** also accepts a PKCS#12 (`.p12` or `.pfx`) and fills in both the certificate and its key from it. iOS cannot read a `.p12` exported without a password, and rejects a private key carrying its own passphrase: strip the passphrase or export a `.p12`. -```text -The server requires an encrypted connection but TablePro is configured to connect in plain text. - -Open the connection editor, switch to the SSL tab, and set Mode to Required (or stricter). - -Server response: FATAL: no pg_hba.conf entry for host "…", user "…", database "…", no encryption -``` +Imported certificates never leave the device and never sync, so a connection set up on the Mac needs them imported again on each iPhone and iPad. See [TablePro for iPhone and iPad](/ios). diff --git a/docs/connections/urls.mdx b/docs/connections/urls.mdx index beeebc720..30bc21aaa 100644 --- a/docs/connections/urls.mdx +++ b/docs/connections/urls.mdx @@ -3,51 +3,54 @@ title: Connection URL Reference description: Every supported database URL scheme, format, and query parameter for imports and direct connections --- -TablePro parses standard database connection URLs for importing connections, opening them directly from a browser or terminal, and configuring SSH tunnels. +Two readers take these URLs, and not the same set. The **Import from URL…** sheet accepts every scheme in this reference, while `open` and browser links reach TablePro only through the ones it registers with macOS. + + + Import from URL sheet with a pasted connection URL + Import from URL sheet with a pasted connection URL + ## URL schemes -| Scheme | Database | -|--------|----------| -| `postgresql://` | PostgreSQL | -| `postgres://` | PostgreSQL (alias) | -| `mysql://` | MySQL | -| `mariadb://` | MariaDB | -| `sqlite://` | SQLite | -| `mongodb://` | MongoDB | -| `mongodb+srv://` | MongoDB (SRV) | -| `redis://` | Redis | -| `rediss://` | Redis with TLS | -| `redshift://` | Amazon Redshift | -| `cockroachdb://` | CockroachDB | -| `cockroach://` | CockroachDB (alias) | -| `pglite://` | PGlite | -| `mssql://` | Microsoft SQL Server | -| `sqlserver://` | Microsoft SQL Server (alias) | -| `jdbc:sqlserver://` | Microsoft SQL Server (JDBC) | -| `oracle://` | Oracle Database | -| `dm://` | Dameng DM8 | -| `jdbc:oracle:thin://` | Oracle Database (JDBC thin) | -| `cassandra://` | Cassandra | -| `cql://` | Cassandra (alias) | -| `scylladb://` | ScyllaDB | -| `scylla://` | ScyllaDB (alias) | -| `clickhouse://` | ClickHouse | -| `ch://` | ClickHouse (alias) | -| `teradata://` | Teradata | -| `trino://` | Trino | -| `duckdb://` | DuckDB file | -| `quack://` | DuckDB remote (Quack protocol) | -| `beancount://` | Beancount ledger file | -| `etcd://` | etcd | -| `etcds://` | etcd with TLS | -| `d1://` | Cloudflare D1 | -| `libsql://` | libSQL / Turso | -| `surrealdb://` | SurrealDB | - -Every scheme above works in the **Import from URL…** sheet. Only these are registered with macOS, so only these can be launched with `open` or a browser link: `postgresql`, `postgres`, `mysql`, `mariadb`, `sqlite`, `mongodb`, `mongodb+srv`, `redis`, `rediss`, `redshift`, `cockroachdb`, `cockroach`, `mssql`, `sqlserver`, `oracle`, `dm`, `clickhouse`, `ch`, `cassandra`, `cql`, `scylladb`, `scylla`, `duckdb`, `etcd`, `etcds`, `d1`, `libsql`, `surrealdb`. - -Append `+ssh` to any non-file-based scheme (not `sqlite`, `duckdb`, or `beancount`) to use an SSH tunnel: +| Scheme | Database | `open` | +|--------|----------|:---:| +| `postgresql://` | PostgreSQL | Yes | +| `postgres://` | PostgreSQL (alias) | Yes | +| `mysql://` | MySQL | Yes | +| `mariadb://` | MariaDB | Yes | +| `sqlite://` | SQLite | Yes | +| `mongodb://` | MongoDB | Yes | +| `mongodb+srv://` | MongoDB (SRV) | Yes | +| `redis://` | Redis | Yes | +| `rediss://` | Redis with TLS | Yes | +| `redshift://` | Amazon Redshift | Yes | +| `cockroachdb://` | CockroachDB | Yes | +| `cockroach://` | CockroachDB (alias) | Yes | +| `pglite://` | PGlite | No | +| `mssql://` | Microsoft SQL Server | Yes | +| `sqlserver://` | Microsoft SQL Server (alias) | Yes | +| `jdbc:sqlserver://` | Microsoft SQL Server (JDBC) | No | +| `oracle://` | Oracle Database | Yes | +| `dm://` | Dameng DM8 | Yes | +| `jdbc:oracle:thin://` | Oracle Database (JDBC thin) | No | +| `cassandra://` | Cassandra | Yes | +| `cql://` | Cassandra (alias) | Yes | +| `scylladb://` | ScyllaDB | Yes | +| `scylla://` | ScyllaDB (alias) | Yes | +| `clickhouse://` | ClickHouse | Yes | +| `ch://` | ClickHouse (alias) | Yes | +| `teradata://` | Teradata | No | +| `trino://` | Trino | No | +| `duckdb://` | DuckDB file | Yes | +| `quack://` | DuckDB remote (Quack protocol) | No | +| `beancount://` | Beancount ledger file | No | +| `etcd://` | etcd | Yes | +| `etcds://` | etcd with TLS | Yes | +| `d1://` | Cloudflare D1 | Yes | +| `libsql://` | libSQL / Turso | Yes | +| `surrealdb://` | SurrealDB | Yes | + +Append `+ssh` to any non-file scheme (so not `sqlite`, `duckdb`, or `beancount`) to carry SSH settings in the same string: ```text postgresql+ssh:// @@ -55,16 +58,14 @@ mysql+ssh:// mariadb+ssh:// ``` -## Standard format +No `+ssh` scheme is registered with macOS, so `open` and browser links cannot route one. Import those through the sheet. -If the URL you want is already in a project's `.env` file, [Open Project Folder](/features/project-folder-import) reads it for you instead. +## Standard format ```text scheme://[username[:password]@]host[:port][/database][?param=value&…] ``` -**Examples:** - ```text postgresql://alice:secret@db.example.com:5432/myapp mysql://root@localhost/shop @@ -73,46 +74,52 @@ redis://:password@cache.host:6379/1 sqlite:///Users/alice/data/local.db ``` +If the URL is already in a project's `.env` file, [Open Project Folder](/features/project-folder-import) reads it for you instead of asking you to paste it. + -Passwords containing special characters (`@`, `#`, `%`, `:`) must be percent-encoded. For example, `p@ss#word` becomes `p%40ss%23word`. +Passwords containing `@`, `#`, `%`, or `:` must be percent-encoded. `p@ss#word` becomes `p%40ss%23word`. +A URL the parser cannot read is reported as `Could not parse database URL: …`, with the password stripped out of the echoed string. + ## SSH tunnel format -SSH tunnel URLs encode both SSH and database credentials in a single string: +Both sets of credentials go in one string, SSH first: ```text scheme+ssh://[ssh_user@]ssh_host[:ssh_port]/[db_user[:db_pass]@]db_host[:db_port][/database][?params] ``` -**Examples:** - ```text postgresql+ssh://deploy@bastion.host:22/dbuser:dbpass@internal-pg:5432/mydb mysql+ssh://ec2-user@jump.host/root:secret@10.0.0.5/shop mariadb+ssh://admin@ssh.host/maria_user@db.internal/store?usePrivateKey=true ``` -If `db_host` is omitted, it defaults to `127.0.0.1`. +An omitted `db_host` defaults to `127.0.0.1`. ## Query parameters +Names are matched case-insensitively. Anything unrecognized is ignored, except on a MongoDB URL, where every unknown parameter is passed to the driver unchanged. + ### Connection | Parameter | Description | Example | |-----------|-------------|---------| -| `name` | Override the connection name shown in the sidebar | `?name=Production+DB` | -| `safeModeLevel` | Set [Safe Mode](/features/safe-mode) for the connection: 0=silent, 1=alert, 2=read-only | `?safeModeLevel=2` | +| `name` | Connection name shown in the sidebar, and part of what a trusted link is matched on | `?name=Production+DB` | +| `safeModeLevel` | [Safe Mode](/features/safe-mode) for this connection: 0=silent, 1=alert, 2=read-only | `?safeModeLevel=2` | +| `statusColor` | Connection color as hex, matched to the nearest palette color | `?statusColor=FF3B30` | +| `env` | Assign an existing tag by name. `environment` is accepted too | `?env=production` | -### SSL / TLS +### SSL/TLS | Parameter | Values | Description | |-----------|--------|-------------| -| `sslmode` | `disable`, `prefer`, `require`, `verify-ca`, `verify-full` | SSL mode (standard PostgreSQL names) | -| `tlsmode` | `0`-`4` | SSL mode as integer: 0=disable, 1=prefer, 2=require, 3=verify-ca, 4=verify-full | -| `tls` / `ssl` | `true` | Sets SSL mode to Required | +| `sslmode` | `disable`, `prefer`, `require`, `verify-ca`, `verify-full` | SSL mode under libpq's names | +| `tlsmode` | `0`-`4` | The same modes as integers: 0=disable, 1=prefer, 2=require, 3=verify-ca, 4=verify-full | +| `tls` / `ssl` | `true` | Sets the mode to Required | -**Example:** +`sslmode` wins if more than one of these is present. ```text postgresql://user:pass@host/db?sslmode=require @@ -121,17 +128,13 @@ postgresql://user:pass@host/db?tlsmode=2 ### Navigation -These parameters open a specific table, view, or schema immediately after connecting. - | Parameter | Description | Example | |-----------|-------------|---------| -| `table` | Open the named table | `?table=users` | -| `view` | Open the named view | `?view=active_orders` | -| `schema` | Switch to this schema before opening | `?schema=reporting` | +| `table` | Open this table once connected | `?table=users` | +| `view` | Open this view once connected | `?view=active_orders` | +| `schema` | Switch here first | `?schema=reporting` | -On databases that support schema switching (PostgreSQL, Redshift, SQL Server, Oracle), `schema` switches the schema. On databases that don't, the value is treated as a database name and switches the database instead. - -**Example:** +On PostgreSQL, Redshift, SQL Server, and Oracle, `schema` switches the schema. Everywhere else the value is read as a database name and switches the database. ```text postgresql://user:pass@host/mydb?schema=reporting&table=monthly_sales @@ -139,158 +142,78 @@ postgresql://user:pass@host/mydb?schema=reporting&table=monthly_sales ### Filtering -Apply a filter to the opened table automatically. +These apply a filter to the table the URL opened. Applying one needs its own confirmation. | Parameter | Description | Example | |-----------|-------------|---------| -| `column` | Column name to filter on | `?column=status` | -| `operation` | Filter operator (e.g. `=`, `!=`, `LIKE`) | `?operation=LIKE` | +| `column` | Column to filter on | `?column=status` | +| `operation` | Filter operator. Symbols and names both work (`=`, `LIKE`, `Equal`, `Contains`, `Is Null`); `operator` is an alias for the parameter | `?operation=LIKE` | | `value` | Filter value | `?value=active` | -| `condition` | Raw SQL WHERE condition (overrides column/operation/value) | `?condition=status%3D'active'+AND+role%3D'admin'` | - -`operator` is accepted as an alias for `operation`. `raw` and `query` are accepted as aliases for `condition`. - -**Example:** +| `condition` | Raw SQL WHERE condition, which overrides `column`, `operation`, and `value`. `raw` and `query` are aliases | `?condition=status%3D'active'` | ```text postgresql://user:pass@host/mydb?table=orders&column=status&operation==&value=pending postgresql://user:pass@host/mydb?table=orders&condition=total%3E1000 ``` -### Appearance - -| Parameter | Description | Example | -|-----------|-------------|---------| -| `statusColor` | Assign a color to the connection (hex, matched to nearest palette color) | `?statusColor=FF3B30` | -| `env` | Assign an existing tag by name (`environment` also accepted) | `?env=production` | +### SSH tunnel -**Example:** +| Parameter | Description | +|-----------|-------------| +| `usePrivateKey` | `true` for key-based SSH authentication | +| `useSSHAgent` | `true` to authenticate through an SSH agent | +| `sshNoAuth` | `true` to send no credentials at all, for servers like Tailscale SSH. See [Authentication methods](/connections/ssh-tunneling#authentication-methods) | +| `agentSocket` | Path to a non-default agent socket | ```text -postgresql://user:pass@host/db?statusColor=FF3B30&env=production +postgresql+ssh://ubuntu@bastion/dbuser@10.0.0.5/mydb?usePrivateKey=true ``` ### MongoDB | Parameter | Description | |-----------|-------------| -| `authSource` | Authentication database (defaults to `admin`) | -| `authMechanism` | Authentication mechanism (e.g. `SCRAM-SHA-256`) | +| `authSource` | Authentication database, `admin` by default | +| `authMechanism` | For example `SCRAM-SHA-256` | | `replicaSet` | Replica set name | -Any other query parameter on a MongoDB URL passes through to the driver unchanged. Multi-host URIs with comma-separated hosts are supported. - -**Example:** +Comma-separated hosts work for a replica set URI. ```text mongodb://user:pass@host:27017/mydb?authSource=admin mongodb://user:pass@host1:27017,host2:27017,host3:27017/mydb?replicaSet=rs0 ``` -### Redis +## What the path component means -The path component sets the database index (0-15). The `rediss://` scheme automatically enables TLS. +The part after the host is the database name on most schemes. Seven read it differently: -```text -redis://:password@host:6379/2 # database index 2 -rediss://:password@host:6380 # TLS, default index 0 -``` - -### Cassandra / ScyllaDB - -The path component sets the default keyspace. Both `cassandra://` and `scylladb://` schemes use the same format. +| Scheme | The path is | +|---|---| +| `redis://`, `rediss://` | The database index, 0 to 15 | +| `cassandra://`, `scylladb://` | The default keyspace. Omit it to connect with none | +| `sqlite://`, `duckdb://`, `beancount://` | An absolute file path, so the URL carries three slashes | +| `quack://` | The alias a remote DuckDB server attaches the database under. See [DuckDB](/databases/duckdb) | +| `d1://` | The D1 database name or UUID, with the host as the Cloudflare account ID | +| `libsql://` | Nothing. The host is the database URL, and the auth token goes in the connection form's password field | ```text +redis://:password@host:6379/2 cassandra://user:pass@host:9042/my_keyspace -scylladb://user:pass@host:9042/my_keyspace -cassandra://host:9042 # no auth, no default keyspace -``` - -### DuckDB - -With `duckdb://` the path after `://` is the file path, same as SQLite. `quack://` targets a remote DuckDB server instead: host, optional port, and the alias the remote attaches under. See [DuckDB](/databases/duckdb). - -```text duckdb:///Users/me/data/analytics.duckdb -duckdb:///path/to/database.db quack://host:9494/alias -``` - -### ClickHouse - -ClickHouse uses HTTP on port 8123 by default. `ch://` is accepted as an alias. - -```text -clickhouse://user:password@host:8123/mydb -ch://default@localhost/analytics -``` - -### etcd - -The `etcds://` scheme automatically enables TLS. Default port is 2379. - -```text -etcd://host:2379 -etcds://host:2379 -``` - -### libSQL / Turso - -The URL host maps to the database URL. The auth token goes in the password field of the connection form. - -```text -libsql://your-database.turso.io -``` - -### Cloudflare D1 - -The host maps to the Cloudflare Account ID. The path sets the database name or UUID. - -```text d1://account-id/database-name +libsql://your-database.turso.io ``` -### SSH Tunnel - -| Parameter | Description | -|-----------|-------------| -| `usePrivateKey` | `true` to use key-based SSH authentication instead of password | -| `useSSHAgent` | `true` to use SSH agent for authentication | -| `sshNoAuth` | `true` to authenticate with no password or key, for servers like Tailscale SSH. See [Authentication Methods](/connections/ssh-tunneling#authentication-methods) | -| `agentSocket` | Custom SSH agent socket path | - -**Example:** - -```text -postgresql+ssh://ubuntu@bastion/dbuser@10.0.0.5/mydb?usePrivateKey=true -``` - -## Opening URLs from browser or terminal - -Launch and connect from a browser or terminal: - -```bash -open "postgresql://user:pass@localhost:5432/mydb" -open "mysql://root@localhost/shop?table=products" -``` - -Uses a saved connection if it matches, otherwise creates a temporary session. To save permanently, click **Create Connection…** on the welcome window and pick **Import from URL…** in the chooser footer. - - - Import from URL sheet with a pasted connection URL - Import from URL sheet with a pasted connection URL - - -`+ssh` schemes are not registered with macOS, so `open` and browser links cannot route them to TablePro. Use the **Import from URL…** sheet for SSH tunnel URLs. - -To force TablePro when another client also handles the scheme, use `open -b com.TablePro "mysql://…"`. See [Terminal and DDEV](/external-api/terminal) for the `tablepro` command and a `ddev tablepro` host command. +When another client also handles the scheme, `open -b com.TablePro "mysql://…"` forces TablePro. See [Terminal and DDEV](/external-api/terminal) for the `tablepro` command and the `ddev tablepro` host command. ## Confirmation and trusted links -Every URL opened from outside the app shows a confirmation alert with the database type, host, user, and database before TablePro connects. Connect only if you trust the source of the link. +Every URL opened from outside the app raises an **Open External Database Connection?** alert naming the database type, host and port, user, and database. **Connect** does not answer the Return key. Escape is the only keyboard answer, and it cancels. -For connections to this machine (`localhost`, `127.x.x.x`, `::1`) the alert also offers **Always Allow**. A trusted link is matched by database type, host, database, username, and the URL's `name` parameter, never the port. Only loopback hosts can be trusted; remote hosts always prompt. +**Always Allow** appears only for a database on this machine (`localhost`, `127.x.x.x`, `::1`). A trusted link is matched on database type, host, database, username, and the URL's `name` parameter, never the port, so the same link on a different port still connects without asking again. Remote hosts always prompt. -Review trusted links under **Settings > General > Trusted Links**, where you can forget them one at a time or all at once. +Trusted links are listed under **Settings > General**, where **Forget** drops one and **Forget All** drops every one. diff --git a/docs/customization/appearance.mdx b/docs/customization/appearance.mdx index 5e33eaf96..c469d0606 100644 --- a/docs/customization/appearance.mdx +++ b/docs/customization/appearance.mdx @@ -3,7 +3,7 @@ title: Appearance description: Themes, per-theme fonts, color customization, and connection colors --- -Configure how TablePro looks in **Settings > Appearance** (`Cmd+,`). +Light and Dark each hold their own theme. The **Editing** control at the top of the tab says which of the two the theme list writes to, so setting your dark theme never means leaving Light mode; **Appearance** beside it picks the one in use, or **Auto** to follow the system. Appearance settings @@ -12,43 +12,32 @@ Configure how TablePro looks in **Settings > Appearance** (`Cmd+,`). ## Themes -Four themes ship with the app: Default Light, Default Dark, Dracula (dark), and Nord (dark). A theme controls editor and syntax colors, data grid colors, interface colors, and the editor and data grid fonts. Interface colors default to macOS system colors, so they follow the system appearance and accent color unless a theme overrides them. +Four ship with the app: Default Light, Default Dark, and two more dark ones, Dracula and Nord. One theme carries the editor and syntax colors, the data grid colors, the interface colors, and both fonts. -### Appearance mode +The list groups them into Built-in, Registry, and Custom, and shows the ones that suit the slot you are editing plus whatever that slot already holds. Click a row to assign it; it applies at once if that slot is the one in use. -Two segmented controls sit at the top of the tab. **Appearance** picks Light, Dark, or Auto; Auto follows the system and switches between the two. **Editing** picks which slot the theme list writes to, Light or Dark, so you can set your dark theme without leaving Light mode. It starts on the slot in use. Light and Dark each keep their own theme. +The editor on the right always edits the **active** theme, not the row you highlighted. Switch **Editing** to the slot in use before you change a font or a color, or the change lands on the other theme. -### Theme list - -Below the controls, the tab is a split view: - -- **Left**: theme list grouped into Built-in, Registry, and Custom. Each row shows a preview thumbnail, name, and source. Selecting a theme assigns it to the slot named in the **Editing** control, and it applies right away if that slot is the one in use. - - The list shows the themes that suit the slot you are editing, so a theme declared dark does not appear under **Light**. A theme that declares neither appears under both. The theme a slot already holds always stays listed, even when it contradicts the slot, so a choice you made earlier is never hidden from you and never rewritten on your behalf. -- **Right**: the editor, with **Fonts** and **Colors** tabs. It always edits the active theme, not the highlighted row, so switch **Editing** to the slot in use before you change fonts or colors. - -The action bar below the list: - -| Button | Action | +| Control | What it does | |--------|--------| -| **+** menu | New Theme (duplicates the active theme), Import… | -| **-** button | Delete the selected theme. Enabled only for custom themes. | -| **Gear** menu | Duplicate and Export… act on the active theme; Uninstall removes the selected registry theme | +| **+** menu | **New Theme** copies the active theme; **Import…** reads a theme JSON file | +| **-** button | Deletes the selected theme. Custom themes only | +| **Gear** menu | **Duplicate** and **Export…** act on the active theme; **Uninstall** removes the selected registry theme | + +A built-in or registry theme is read-only, so the first change forks it. Changing a font makes "Name (Custom)" and edits that; **Duplicate**, **New Theme**, and the **Duplicate Theme** button on the Colors tab make "Name (Copy)" for you to edit. ## Fonts -The **Fonts** tab sets the editor and data grid fonts per theme. Family pickers list the monospaced fonts installed on your Mac, and a preview renders a sample query with the theme's editor colors. +The pickers list the monospaced families installed on your Mac; the preview renders a sample query in the theme's own editor colors. | Setting | Options | Default | |---------|---------|---------| | Editor Font | Family + size, 11 to 18 pt | System Mono, 13 pt | | Data Grid Font | Family + size, 10 to 18 pt | System Mono, 13 pt | -Changing a font on a built-in theme does not modify the original: TablePro creates a copy named "Theme (Custom)" and applies the change there. - ## Colors -The **Colors** tab is editable only on custom themes. Built-in and registry themes show a lock and a **Duplicate Theme** button; the copy (named "Theme (Copy)") is fully editable. +Custom themes get color wells here. A built-in or registry theme shows a lock and a **Duplicate Theme** button instead. | Group | Controls | |-------|----------| @@ -61,22 +50,49 @@ The **Colors** tab is editable only on custom themes. Built-in and registry them | Sidebar | Background, text, selected item, hover, section header | | Toolbar | Secondary and tertiary text | -Interface, Sidebar, and Toolbar colors fall back to macOS system colors until you override them. Overridden values show a reset button that restores the system default. - -## Import, export & registry - -Export the active theme as JSON from the gear menu; import a JSON theme from the **+** menu. Community themes install from the [plugin registry](/features/plugins) (**Settings > Plugins > Browse**, Themes category). Colors on registry themes are locked, same as built-ins: duplicate the theme to edit them. Remove a registry theme with the gear menu's **Uninstall**. +Interface, Sidebar, and Toolbar colors sit on macOS system colors until you override one, which is why an untouched theme follows your system accent color. An overridden well grows a reset button that puts the system color back. + +## Theme files + +A theme is one JSON file. **Export…** in the gear menu writes the active theme with every key filled in, which is the shortest way to start a new one; **Import…** in the **+** menu reads one back into `~/Library/Application Support/TablePro/Themes/`. Community themes install from the [registry](/features/plugins) under **Settings > Plugins > Browse**, Themes category. + +| Key | Holds | +|-----|-------| +| `id`, `name`, `author` | Identity. An `id` that collides with a built-in, a registry theme, or one you already have is replaced on import | +| `version` | Format version, `1` | +| `appearance` | `light`, `dark`, or `auto`. Decides which slot lists the theme; `auto` lists in both | +| `editor` | Editor colors, with syntax colors nested under `syntax` | +| `dataGrid` | Grid colors | +| `ui` | Interface colors, with `status` and `badges` nested | +| `sidebar`, `toolbar` | Sidebar and toolbar colors | +| `fonts` | `editorFontFamily`, `editorFontSize`, `dataGridFontFamily`, `dataGridFontSize` | + +```json +{ + "id": "user.midnight", + "name": "Midnight", + "version": 1, + "appearance": "dark", + "author": "You", + "editor": { + "background": "#2E3440", + "text": "#D8DEE9", + "currentStatementHighlight": "#FFFFFF0F", + "syntax": { "keyword": "#81A1C1", "string": "#A3BE8C" } + }, + "fonts": { "editorFontFamily": "System Mono", "editorFontSize": 13 } +} +``` + +Colors are sRGB hex, `#RRGGBB` or `#RRGGBBAA` for a translucent overlay such as a selection band. A value that is neither renders as the system label color, which is how a typo shows up. + +Every key is optional. A group you leave out falls back to Default Light, and a color left out of `ui`, `sidebar`, or `toolbar` is the macOS system color rather than a fixed value. ## Connection colors -Assign a color to a connection in the connection form: **Customization > Appearance > Color**. Nine options: None, Red, Orange, Yellow, Green, Blue, Purple, Pink, Gray. With no color set, the connection uses its database type's brand color, supplied by the driver plugin. Connection groups take a color the same way, from their right-click menu on the welcome window. - -The color appears on: +Assign one in the connection form under **Customization > Appearance > Color**: None, Red, Orange, Yellow, Green, Blue, Purple, Pink, or Gray. With None, the connection takes its database type's brand color from the driver plugin. Connection groups take a color the same way, from their right-click menu on the welcome window. -- The toolbar connection status indicator -- The connection switcher popover -- The sidebar connection header -- The welcome window connection list +The color shows on the toolbar connection status indicator, the connection switcher popover, the sidebar connection header, and the welcome window list. Use red for production databases as a visual reminder to be careful with queries. diff --git a/docs/customization/data-settings.mdx b/docs/customization/data-settings.mdx index 5d15f3ef0..00ef473ea 100644 --- a/docs/customization/data-settings.mdx +++ b/docs/customization/data-settings.mdx @@ -3,9 +3,14 @@ title: Data Settings description: Data grid, pagination, query result cap, JSON viewer, and query history --- -Open **Settings > Data** (`Cmd+,`). Most visits here are about one of two limits: browsing a table pages 1,000 rows at a time, and a query returning more than 10,000 stops there and offers **Fetch All**. The result cap does not touch your SQL, which reaches the server exactly as you typed it; only the rows read back are limited. +Browsing a table pages 1,000 rows at a time, and a query that returns more than 10,000 rows stops there. Both numbers, and the rest of what the grid does before you touch anything, are on **Settings > Data**. -Data grid fonts are per-theme. Edit them on the theme's Fonts tab under [Appearance](/customization/appearance). +The tab is app-wide, so these values apply to every connection you open; filters, column widths, and the sort you clicked are remembered per table instead. Grid fonts are per-theme, on the theme's Fonts tab under [Appearance](/customization/appearance). + + + Data settings tab + Data settings tab + ## Data Grid @@ -36,7 +41,7 @@ Tables with more estimated rows than the threshold show an approximate count ins | Truncate query results | On | | | Row cap | 10,000 | 100; 1,000; 5,000; 10,000; 50,000; 100,000; 500,000 | -When on, `SELECT` queries without their own `LIMIT` run with the cap applied to the rows TablePro reads back. The SQL sent to the server is always the query you wrote. Capped results show a **Fetch All** button, and **Execute Without Limit** skips the cap for one run. +The cap limits the rows read back, not the statement: your SQL reaches the server exactly as you typed it, and a query carrying its own `LIMIT` is untouched. What a capped result offers instead is in [Query Results](/features/query-results#the-row-cap). ## JSON Viewer @@ -44,7 +49,7 @@ When on, `SELECT` queries without their own `LIMIT` run with the cap applied to |---------|---------|---------| | Default view | Text | Text, Tree | -The mode the JSON viewer opens in. Switching modes inside the viewer writes the choice back to this setting. See [JSON Viewer](/features/json-viewer). +Switching modes inside the viewer writes the choice back to this setting. See [JSON Viewer](/features/json-viewer). ## Query History @@ -52,7 +57,7 @@ The mode the JSON viewer opens in. Switching modes inside the viewer writes the |---------|---------|-------------| | **Maximum entries** | 10,000 | 100 to 10,000, or Unlimited | | **Keep entries for** | 90 days | 7 days to 1 year, or Forever | -| **Auto cleanup** | On | Applies the two limits above. Turn it off and nothing is pruned. | +| **Auto cleanup** | On | Applies the two limits in this table. Turn it off and nothing is pruned. | | **Clear History…** | - | Wipe all history, for every connection, after confirmation | To clear only what you are looking at, use the trash button in the history drawer instead: it respects the drawer's connection scope and date range. diff --git a/docs/customization/editor-settings.mdx b/docs/customization/editor-settings.mdx index 0e1321188..c4367eae8 100644 --- a/docs/customization/editor-settings.mdx +++ b/docs/customization/editor-settings.mdx @@ -3,7 +3,7 @@ title: Editor Settings description: SQL editor, Vim mode, line numbers, indentation, and query parameter settings --- -Open **Settings > Editor** (`Cmd+,`). This tab holds the SQL editor toggles. The data grid, pagination, query result cap, and JSON viewer are on the [Data](/customization/data-settings) tab. +**Settings > Editor** covers typing and how SQL is displayed. Grid, pagination, result cap and JSON viewer settings are on the [Data](/customization/data-settings) tab instead. Editor fonts are per-theme. Edit them on the theme's Fonts tab under [Appearance](/customization/appearance). @@ -18,8 +18,10 @@ Editor fonts are per-theme. Edit them on the theme's Fonts tab under [Appearance |---------|---------|-------| | Show line numbers | On | | | Highlight current line | On | | +| Highlight current statement | On | A faint band behind the statement the cursor is in. See [Statement markers](/features/sql-editor#statement-markers) | | Word wrap | Off | Off means long lines scroll horizontally | -| Code folding | On | Shows the fold ribbon in the gutter. See [Code Folding](/features/sql-editor#code-folding) | +| Code folding | On | Shows the fold ribbon in the gutter. See [Code Folding](/features/code-folding) | +| Run button beside each statement | On | Gutter run buttons, revealed when the pointer is over the gutter | | Tab width | 4 spaces | 2, 4, or 8 | | Auto-uppercase keywords | Off | Uppercases SQL keywords on word boundaries. Strings, comments, and quoted identifiers are untouched | | Query parameters (`:name` syntax) | On | Detects `:name` placeholders and shows the parameter panel. See [Query Parameters](/features/query-parameters) | diff --git a/docs/customization/general-settings.mdx b/docs/customization/general-settings.mdx new file mode 100644 index 000000000..01a9755bd --- /dev/null +++ b/docs/customization/general-settings.mdx @@ -0,0 +1,61 @@ +--- +title: General Settings +description: Language, startup behavior, sidebar layout, query timeout, the command line tool, updates, and reset +--- + +Most of this tab takes effect the moment you click. Language waits for a restart, and the query timeout reaches a connection only when that connection opens. + + + General settings tab + General settings tab + + +## Language + +System (the default), English, Tiếng Việt, 简体中文, 繁體中文, 한국어, Türkçe. The tab tells you to restart once you pick a different one. + +## Startup + +**When TablePro starts** takes **Reopen Last Session** (the default) or **Show Welcome Screen**. What a reopened session brings back, and what it does when a server is down, is on [Tabs](/features/tabs#what-survives-a-restart). + +## Tabs and sidebar + +| Setting | Default | What it does | +|---------|---------|--------------| +| **Enable preview tabs** | On | Single-click opens a temporary tab that the next click replaces; double-click or an edit makes it permanent | +| **Show connections** | On | The [connections strip](/features/workspace-rail) on the window's leading edge | +| **Show recent tables** | Off | A [Recent section](/features/favorites#recent-tables) at the top of the sidebar | +| **Show object icons** | On | A type icon before each object name. Off gives a plain list of names | +| **Show object comments** | On | Database object comments beside tables in the sidebar and in grid column headers | +| **Row size** | Match System | Match System follows **Sidebar icon size** in **System Settings > Appearance**. Small, Medium, or Large overrides it to fit more objects on screen | +| **Default layout for new connections** | List | List or Tree, on servers with a database tree. Switch the connection you are in from the **View** menu | + +Icons, comments, and row size are also under **View Options** in the sidebar's right-click menu, which every sidebar menu ends with. + +## Query timeout + +How many seconds a query runs before it is cancelled. The default is 60; the picker offers 10 to 600 seconds and **No limit**. The value is read when a connection opens, so a change reaches an open connection only after you reconnect. + +Where the engine can enforce it, the timeout becomes a server-side setting: `statement_timeout` on PostgreSQL, `max_execution_time` on MySQL and ClickHouse, `max_statement_time` on MariaDB. On SQLite it bounds how long a statement waits for a locked database instead. Drivers that talk HTTP (BigQuery, Cloudflare D1, LibSQL, Etcd, DynamoDB, Elasticsearch, and ClickHouse) bound the request at the timeout plus 30 seconds, and Oracle enforces it in the client: the connection is closed to unblock the call, and the next query reconnects and restores your current schema. + +**No limit** still caps an HTTP request at one hour, because the transport needs a ceiling. + +## Command line tool + +**Install** writes a `tablepro` command to `/usr/local/bin` that opens database URLs in the app. When it cannot write there, the section prints the command to run in Terminal yourself. See [Terminal and DDEV](/external-api/terminal). + +## Trusted links + +External links you chose to always allow, each with **Forget**, plus **Forget All**. Only a database on this machine can be trusted; a link to a remote host asks every time. + +## Software update + +**Automatically check for updates** is on and checks in the background. **Check for Updates…** checks now, here or as **TablePro > Check for Updates…** in the menu bar. Updates come through [Sparkle](https://sparkle-project.org/) and are signature-checked before they install. + +## Privacy + +**Share anonymous usage data** is on. It sends one heartbeat a day with no query text, hostname, or account in it. The full payload, field by field, is on [Privacy](/security/privacy#usage-heartbeat). + +## Reset + +**Reset All Settings to Defaults** is at the bottom of the tab. What it restores and what it leaves alone is under [Where settings live](/customization/settings#where-settings-live). diff --git a/docs/customization/notifications.mdx b/docs/customization/notifications.mdx new file mode 100644 index 000000000..50d8934bb --- /dev/null +++ b/docs/customization/notifications.mdx @@ -0,0 +1,38 @@ +--- +title: Notifications +description: Which finished operations notify you, how long work has to run first, and what the notification carries +--- + +You only get told when the result is somewhere you are not. That means TablePro behind another window or in another Space, the window minimized, or the tab that ran the work not the one on screen. Work you cancelled yourself never notifies, and neither does anything that finished faster than the threshold. + +**Settings > Notifications** changes any of it. + +| Setting | Default | Covers | +| --- | --- | --- | +| **Notify when long-running work finishes** | On | The master switch for every kind in this table | +| **Only after** | 20 seconds | How long work has to run to count: 5, 10, 20 or 30 seconds, or 1, 2, 5 or 10 minutes | +| **Queries** | On | Editor queries, including Explain | +| **Statement batches** | On | Execute All Statements, as one notification for the batch | +| **Row edits** | On | Saving edited rows | +| **Structure changes** | On | Applying table structure changes | +| **Imports** | On | File imports | +| **Exports** | On | Exports, including streaming exports | +| **Backups** | On | Database dumps | +| **Fetch all rows** | On | Loading every row of a truncated result | +| **AI and MCP queries** | On | Queries an AI assistant or MCP client runs | + +Keep **Only after** below the [query timeout](/customization/general-settings#query-timeout), or a query that times out finishes before its notification is due. + +## What a notification carries + +The connection name is the title, the database is the subtitle, and the body is the outcome with a duration: "1,204 rows in 3m 12s", or "Failed after 2m 45s" with the reason. A failure adds a **Copy Error** button, and anything that wrote a file adds **Show in Finder**. Clicking the notification brings TablePro forward on the tab that ran the work. + +Banners group by connection. Re-running the same kind of work in the same tab replaces that tab's earlier banner instead of stacking a second one. + +## The tab dot + +A tab whose work finished while you were looking elsewhere carries a dot beside its name until you select it. That dot is not a notification: it appears whatever the settings say, and whether or not you ever granted permission. + +## Permission + +macOS asks the first time there is actually something to tell you, not at launch. Refuse, or turn TablePro off in **System Settings > Notifications**, and this tab says so with a button that opens the right pane; TablePro bounces its Dock icon in place of the banner. Sound and alert style are per-app System Settings options, so this tab does not repeat them. diff --git a/docs/customization/settings.mdx b/docs/customization/settings.mdx index 278418eaa..5c03d1dc2 100644 --- a/docs/customization/settings.mdx +++ b/docs/customization/settings.mdx @@ -3,223 +3,104 @@ title: Settings description: Every settings tab, its controls, and their defaults --- -Open with `Cmd+,`. Settings are grouped into ten tabs. +The settings window is app-wide: a value on any of these ten tabs holds for every connection you open. `Cmd+,` opens it; what belongs to one connection is in that connection's form instead. - Language, startup, tabs, sidebar, query timeout, updates, reset. + Language, startup, tabs, sidebar, query timeout, updates, reset. Appearance mode, themes, per-theme fonts and colors. SQL editor, Vim mode, line numbers, query parameters. Data grid, pagination, result cap, JSON viewer, and query history. Custom shortcuts. - Which finished operations notify you when TablePro is in the background. - Providers, inline suggestions, context, slash commands. - MCP server, tokens, clients, activity. - Manage drivers, exporters, and themes. - License, iCloud sync, linked folders. + Which finished operations notify you when TablePro is in the background. + Providers, inline suggestions, context, slash commands. + MCP server, tokens, clients, activity. + Manage drivers, exporters, and themes. + License, iCloud sync, linked folders. - - General settings tab - General settings tab - - -## General - -### Language - -System (default), English, Tiếng Việt, 简体中文, 繁體中文, 한국어, Türkçe. Changing the language requires restarting TablePro. - -### Startup behavior - -| Option | Description | -|--------|-------------| -| **Reopen Last Session** (default) | Restore the windows, tabs, and view state from your last session | -| **Show Welcome Screen** | Show the welcome window with saved connections | - -Reopened tabs restore their SQL, cursor position, sort, filters, page, and column widths. The window you land on connects at launch; the others connect when you switch to them, so an unreachable database never blocks launch. A window that cannot reach its server shows the reason with a Try Again button and a way back to the connection list, and the other windows keep working. See [Tabs](/features/tabs). - -### Tabs - -| Setting | Default | Description | -|---------|---------|-------------| -| **Enable preview tabs** | On | Single-click opens a temporary tab that the next click replaces; double-click or interaction makes it permanent | - -### Sidebar - -| Setting | Default | Description | -|---------|---------|-------------| -| **Show connections** | On | Adds a narrow strip on the window's leading edge listing every connection and database you have open, so one click switches to it | -| **Show recent tables** | Off | Adds a Recent section at the top of the sidebar with the last 10 tables opened per connection and database | -| **Show object icons** | On | Shows a type icon before each object name in the sidebar. Turn it off for a plain list of names. Also on the **View** menu, and under **View Options** in the sidebar right-click menu | -| **Show object comments** | On | Shows database object comments next to tables in the sidebar and in grid column headers | -| **Row size** | Match System | How tall sidebar rows are. Match System follows **Sidebar icon size** in System Settings > Appearance, so the sidebar matches Finder and Mail. Pick Small, Medium or Large to override it and fit more objects on screen. Also under **View Options** in the sidebar right-click menu | -| **Default layout for new connections** | List | List or Tree, for servers that support a database tree. Switch the current connection from the **View** menu | - -### Query execution - -**Query timeout** sets the maximum seconds a query runs before cancellation. Default 60; choices from 10 to 600 seconds, plus **No limit**. Applies to new connections, so a change requires a reconnect. - -Enforcement happens at the database level where supported: - -| Database | Enforcement | -|----------|-------------| -| PostgreSQL | `statement_timeout` | -| MySQL | `max_execution_time` | -| MariaDB | `max_statement_time` | -| ClickHouse | `max_execution_time`, plus the HTTP bound below | -| SQLite | `sqlite3_busy_timeout` | -| BigQuery, Cloudflare D1, LibSQL, Etcd, DynamoDB, Elasticsearch | HTTP request timeout set to the query timeout plus a 30-second grace | -| Oracle | Client-side: the connection is closed, and the next query reconnects and restores the current schema | - -**No limit** raises the HTTP transport ceiling to 1 hour. - -### Command line - -**Install** writes a `tablepro` command to `/usr/local/bin` that opens database URLs in TablePro. If TablePro cannot write there, it shows a command for you to run in Terminal. See [Terminal and DDEV](/external-api/terminal). - -### Trusted links - -Lists external links you chose to always allow, with **Forget** per entry and **Forget All**. Only databases on this machine can be trusted; remote links ask for confirmation every time. - -### Software update - -| Setting | Default | Description | -|---------|---------|-------------| -| **Automatically check for updates** | On | Periodic background check | -| **Check for Updates…** | - | Check now | - -Powered by [Sparkle](https://sparkle-project.org/). Also available from the **TablePro** menu. - -### Privacy - -| Setting | Default | Description | -|---------|---------|-------------| -| **Share anonymous usage data** | On | Anonymous usage statistics, no personal data or queries | - -### Reset - -**Reset All Settings to Defaults** resets every settings tab after a confirmation. Saved connections and query history are not affected. - -## Notifications - -TablePro tells you when long-running work finishes while you are somewhere else. It only notifies when the result is not already in front of you: the app is in another Space or behind another window, the window is minimized, or the tab that ran the work is not the one on screen. Work you cancelled yourself never notifies. - -**Notify when long-running work finishes** is the master switch. **Only after** sets how long work has to run before it counts, from 5 seconds to 10 minutes; the default is 20 seconds. Set it below the query timeout in General, or a query that times out will finish before the notification is due. - -| Setting | Default | Description | -| --- | --- | --- | -| **Notify when long-running work finishes** | On | Master switch for every kind below | -| **Only after** | 20 seconds | How long work must run to count, from 5 seconds to 10 minutes | -| **Queries** | On | Editor queries, including Explain | -| **Statement batches** | On | Execute All, as one notification for the batch | -| **Row edits** | On | Saving edited rows | -| **Structure changes** | On | Applying table structure changes | -| **Imports** | On | File imports | -| **Exports** | On | Exports, including streaming exports | -| **Backups** | On | Database dumps | -| **Fetch all rows** | On | Loading every row of a truncated result | -| **AI and MCP queries** | On | Queries an AI assistant or MCP client runs | - -The notification says which connection and database, what happened, how much, and how long it took: "1,204 rows in 3m 12s", or "Failed after 2m 45s" with the reason. A failure offers Copy Error; an export or backup offers Show in Finder. Clicking it brings TablePro forward and selects the tab that ran the work. Notifications group by connection, and re-running the same tab replaces its previous notification instead of stacking a second one. - -Whatever the notification settings say, a tab whose work finished while you were looking elsewhere keeps a dot next to its name until you select it. That happens even if you never granted notification permission. - -macOS asks for notification permission the first time TablePro actually has something to tell you, not at launch. If you refuse, or turn TablePro's notifications off in System Settings, this section says so and links you there, and TablePro bounces its Dock icon instead. Notification sound and alert style are per-app System Settings options, so TablePro does not duplicate them. - -## AI - -**Enable AI Features** is the master switch; turning it off hides the other AI sections and disables chat, inline suggestions, and editor actions. **Active Provider** picks the default provider; the chat composer's model picker can override it per turn. - -**Add Provider…** lists GitHub Copilot, ChatGPT, Cursor, Claude, OpenAI, OpenRouter, Gemini, xAI, Ollama, llama.cpp, MLX, and OpenCode Zen. **Add Custom Provider…** below the divider takes any OpenAI-compatible endpoint. Click a provider row to open its detail sheet: - -- **Test Connection** verifies the key, sign-in, or endpoint. -- **Model** lists curated models plus models fetched from the provider; pick **Other…** to type a custom model ID. -- **Reasoning** sets the effort level on models that support it, or **Off**. - -API keys are stored in the macOS Keychain; removing a provider deletes its key. - -### Inline suggestions - -| Setting | Default | Description | -|---------|---------|-------------| -| **Enable inline suggestions while typing** | Off | Ghost-text SQL completions. Requires an active provider. Tab accepts, Escape dismisses | -| **Debounce** | 500 ms | Pause after typing before requesting a suggestion, 100 to 3,000 ms in 50 ms steps | - -### Agent - -| Setting | Default | Description | -|---------|---------|-------------| -| **Limit tool calls per reply** | On | Turn off to let the agent keep calling tools until it finishes | -| **Tool call limit** | 25 | Tool calls the AI may make in one reply before pausing, 5 to 200 in steps of 5 | - -Reaching the limit pauses the reply instead of failing it. Everything the AI found so far stays in the chat, and **Continue** picks up where it stopped with a fresh budget. GitHub Copilot manages its own tool loop, so this setting does not apply to it. - -### Context - -| Setting | Default | Description | -|---------|---------|-------------| -| **Include database schema** | On | Auto-attach the active connection's schema | -| **Include current query** | On | Auto-attach the active editor tab's text | -| **Include query results** | Off | Auto-attach the most recent query result snapshot | -| **Max schema tables** | 20 | Cap on tables in the schema attachment, 1 to 100 | - -The Privacy section sets the default **Connection policy** (Ask Each Time) applied to new connections. For custom slash commands, chat modes, and per-connection AI rules, see [AI Assistant](/features/ai-assistant). - -## Integrations - -The **Integrations** tab runs the MCP server. **Enable MCP Server** (default off) starts it and shows a live status. See [MCP Server](/features/mcp) for a walkthrough and the [External API](/external-api) reference for the tool catalog. The server binds `127.0.0.1` only. - - - Integrations settings - Integrations settings - - -### Server Configuration - -| Setting | Default | Description | -|---------|---------|-------------| -| **Port** | 23508 | Loopback port the server listens on. If it is taken, TablePro binds a free one instead and records it in the handshake file. | -| **Default row limit** | 500 | Rows returned when a tool call sets no limit, including `export_data` | -| **Maximum row limit** | 10,000 | Hard cap on rows per tool call, including `export_data`. Clamped to 1 to 500,000. | -| **Query timeout** | 30 seconds | Per-query limit for MCP-issued queries. Clamped to 1 to 300 seconds. | -| **Log MCP queries in history** | On | Record MCP queries in [query history](/features/query-history) | - -### Authentication - -**Require authentication** defaults to on. If you switch it on with no tokens saved, TablePro generates a full-access token named "Default token" and shows it once. Copy it before closing the sheet. Generate, revoke, and delete tokens in the list below the toggle. See [Tokens](/external-api/tokens). - -Switching it off accepts a local caller with no token, but only as a read-only one: it can read schema and run `SELECT`, and can never write, drop, or administer. - -### Clients and activity - -**Connect a Client…** shows setup steps and config snippets for Claude Code, Claude Desktop, Cursor, and Zed. **View Activity…** opens the Integrations Activity window with the activity log and the clients that have called in the last five minutes. **Disconnect** there revokes that client's token. - -## Plugins - -Two sub-tabs, each a split view with the plugin list on the left and details on the right. - -- **Installed**: bundled plugins (MySQL, PostgreSQL, SQLite, ClickHouse, and Redis drivers; CSV, JSON, SQL, XLSX, and MQL exporters; SQL, JSON, and CSV importers; CSV Inspector) plus anything you installed. Toggle a plugin on or off in the detail pane. Bundled plugins can be disabled but not uninstalled. To install from a file, click **+** below the list or drag a `.tableplugin` or `.zip` onto it. TablePro verifies the code signature before loading. -- **Browse**: install from the registry, with search and a category filter (Database Drivers, Export Formats, Import Formats, Themes, Other). Registry drivers: MongoDB, Oracle, DuckDB, SQL Server, Cassandra, Etcd, Cloudflare D1, DynamoDB, BigQuery, Snowflake, Elasticsearch, LibSQL, SurrealDB, Beancount, Teradata, and Trino. - -See [Plugins & Themes](/features/plugins) for installing, updating, and managing plugins. - - -Only install plugins from sources you trust. Code-signature checks do not guarantee plugin behavior. - - -## Account - -Activate, deactivate, or view your license here. TablePro checks the license signature locally, re-validates with the server every 7 days, and allows 30 days offline before validation fails. On a Team plan, paste an invite code in the activation field to join a team instead of entering a key. See [Team plan](/features/team) and [Licensing](/features/licensing). - -iCloud sync and linked folders live on the same tab. See [iCloud Sync](/features/icloud-sync). - -## Settings storage - -Settings live in `~/Library/Preferences/com.TablePro.plist`. Use **Reset All Settings to Defaults** on the General tab to reset them. - -Advanced flags not exposed in the UI: +## Stock values + +What a fresh install ships with, so one glance down this column says what you changed. Each tab page explains what a setting does and which other values it takes. + +| Tab | Setting | Default | +|-----|---------|---------| +| General | Language | System | +| General | When TablePro starts | Reopen Last Session | +| General | Enable preview tabs | On | +| General | Show connections | On | +| General | Show recent tables | Off | +| General | Show object icons | On | +| General | Show object comments | On | +| General | Row size | Match System | +| General | Default layout for new connections | List | +| General | Query timeout | 60 seconds | +| General | Automatically check for updates | On | +| General | Share anonymous usage data | On | +| Appearance | Appearance | Auto | +| Appearance | Light theme | Default Light | +| Appearance | Dark theme | Default Dark | +| Appearance | Editor font | System Mono, 13 pt | +| Appearance | Data grid font | System Mono, 13 pt | +| Editor | Show line numbers | On | +| Editor | Highlight current line | On | +| Editor | Highlight current statement | On | +| Editor | Word wrap | Off | +| Editor | Code folding | On | +| Editor | Run button beside each statement | On | +| Editor | Tab width | 4 spaces | +| Editor | Auto-uppercase keywords | Off | +| Editor | Query parameters | On | +| Editor | Vim mode | Off | +| Data | Row height | Normal | +| Data | Date format | ISO 8601 | +| Data | NULL display | `NULL` | +| Data | Show alternate row backgrounds | On | +| Data | Show row numbers | On | +| Data | Auto-show inspector on row select | Off | +| Data | Smart value detection | On | +| Data | Default row sort | No sorting (engine order) | +| Data | Default page size | 1,000 rows | +| Data | Count rows if estimate less than | 100,000 | +| Data | Truncate query results | On | +| Data | Row cap | 10,000 | +| Data | JSON viewer default view | Text | +| Data | Maximum history entries | 10,000 | +| Data | Keep history entries for | 90 days | +| Data | Auto cleanup | On | +| Notifications | Notify when long-running work finishes | On | +| Notifications | Only after | 20 seconds | +| Notifications | Every operation kind | On | +| AI | Enable AI Features | On | +| AI | Enable inline suggestions while typing | Off | +| AI | Debounce | 500 ms | +| AI | Limit tool calls per reply | On | +| AI | Tool call limit | 25 | +| AI | Include database schema | On | +| AI | Include current query | On | +| AI | Include query results | Off | +| AI | Max schema tables | 20 | +| AI | Connection policy | Ask Each Time | +| Integrations | Enable MCP Server | Off | +| Integrations | Port | 23508 | +| Integrations | Default row limit | 500 | +| Integrations | Maximum row limit | 10,000 | +| Integrations | Query timeout | 30 seconds | +| Integrations | Log MCP queries in history | On | +| Integrations | Require authentication | On | +| Account | iCloud Sync | Off | +| Account | Passwords (keychain sync) | Off | + +Two tabs are not in the table. Stock keyboard chords are on [Keyboard Shortcuts](/features/keyboard-shortcuts), and the Plugins tab holds no values of its own. + +## Where settings live + +The file is `~/Library/Preferences/com.TablePro.plist`. **Reset All Settings to Defaults** on the General tab puts every tab back to its stock value after a confirmation, and that includes emptying the AI provider list. Saved connections, query history, and MCP tokens survive it. + +Two settings have no control in the window: | Setting | Command | |---------|---------| | Custom plugin registry | `defaults write com.TablePro com.TablePro.customRegistryURL ` | | Clear custom registry | `defaults delete com.TablePro com.TablePro.customRegistryURL` | + +See [Plugin Registry](/development/plugin-registry#custom-registry-url) for what a private registry has to serve. diff --git a/docs/databases/beancount.mdx b/docs/databases/beancount.mdx index d206d90ef..8c56debb3 100644 --- a/docs/databases/beancount.mdx +++ b/docs/databases/beancount.mdx @@ -3,68 +3,48 @@ title: Beancount description: Backend requirements, the table layout, source locations, includes, and BQL support --- -Point TablePro at a `.beancount` file and you get a read-only SQL database over it. The driver projects the ledger into 17 tables: transactions, postings, accounts, prices, computed balances, balance assertions, the other directives, metadata, and the source files themselves. The plugin is not bundled with the app, so the first time you open a `.beancount` file you get a prompt to download it. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -The plugin uses either `rledger` (the rustledger project) or Python Beancount to parse ledgers. TablePro does not bundle either one, so install one yourself before opening a ledger. When both are available, TablePro uses `rledger`; Python Beancount is the fallback for browsing projected SQL tables. +Install `rledger` or Python Beancount before you connect. The plugin parses nothing itself: it shells out to one of those two and projects the answer into 17 read-only SQL tables. -The plugin also supports BQL queries through `rledger`. BQL requires the `rledger` executable even when the ledger was opened through Python Beancount. + ## Backend requirements -Install the Python backend with pip: +Either backend browses the tables, at any version. Install `rledger` if you are choosing: it wins when both are present, it is the only one that fills `diagnostics`, and BQL runs through it and nothing else. -```bash -pip3 install beancount -``` - -For `rledger`, build or download the rustledger project and put the executable on `PATH`, in `/opt/homebrew/bin`, or in `/usr/local/bin`. - -TablePro then discovers backends in this order: - -1. `rledger` from `TABLEPRO_RUSTLEDGER_BINARY`, then `PATH`, then `/opt/homebrew/bin` and `/usr/local/bin`. -2. A `python3` that can `import beancount`, from `TABLEPRO_BEANCOUNT_PYTHON`, then `PATH`, then `/opt/homebrew/bin/python3`, `/usr/local/bin/python3`, and `/usr/bin/python3`. - -The names are exact: an executable called `rledger`, and a `python3` that imports `beancount`. Any other Beancount-compatible parser has to be installed under one of those names, or pointed at with the environment variables below. - -To verify `rledger`: - -```bash + +```bash rledger +# build or download rustledger, put it on PATH, then: rledger --version ``` -To verify Python Beancount: - -```bash +```bash Python Beancount +pip3 install beancount python3 -c "import beancount" ``` + -If TablePro cannot find a supported backend, set the executable path before launching the app: +An app launched from Finder does not inherit your shell's `PATH`, so a backend that answers in Terminal can still be invisible. Name it outright, then relaunch: ```bash launchctl setenv TABLEPRO_RUSTLEDGER_BINARY /opt/homebrew/bin/rledger launchctl setenv TABLEPRO_BEANCOUNT_PYTHON /opt/homebrew/bin/python3 ``` -To force a backend: - -```bash -launchctl setenv TABLEPRO_BEANCOUNT_BACKEND python -``` +`TABLEPRO_BEANCOUNT_BACKEND` overrides which one wins. It takes `rledger` or `python`. -## Connecting to a Beancount ledger +## Quick setup - - Open TablePro and click **Create Connection…** or press `Cmd+N`. - - - Choose **Beancount** from the database type list. + + Click **Create Connection…** and choose **Beancount**. - - Click **Browse…** and select a `.beancount` file. + + Click **Browse…** and select a `.beancount` file. No host, no port, no password, no Database field: the ledger path is the whole connection. - Click **Save & Connect** to open the ledger. + Click **Save & Connect**. @@ -79,14 +59,14 @@ launchctl setenv TABLEPRO_BEANCOUNT_BACKEND python beancount:///path/to/main.beancount ``` -See [Connection URL Reference](/connections/urls) for all parameters. +Three slashes: the path is absolute. See [Connection URL Reference](/connections/urls). ## Tables | Table | Contents | |-------|----------| -| `transactions` | Transaction date, flag, payee, narration, and source location, including transactions with no postings | -| `postings` | Posting account, amount, commodity, resolved cost basis (`cost_number`, `cost_currency`), and source location | +| `transactions` | Date, flag, payee, narration, source location. Includes transactions with no postings | +| `postings` | Account, amount, commodity, resolved cost basis (`cost_number`, `cost_currency`), source location | | `accounts` | Opened accounts and declared currencies | | `prices` | Price directives | | `balances` | Computed account balances by commodity | @@ -96,62 +76,65 @@ See [Connection URL Reference](/connections/urls) for all parameters. | `notes` | Account notes | | `events` | Event type and description directives | | `closes` | Close directives | -| `transaction_metadata` | Transaction metadata key/value pairs | -| `posting_metadata` | Posting metadata key/value pairs | -| `transaction_tags` | Transaction tags | -| `transaction_links` | Transaction links | -| `diagnostics` | Validation diagnostics from `rledger check` | +| `transaction_metadata`, `posting_metadata` | Metadata key/value pairs | +| `transaction_tags`, `transaction_links` | Tags and links | +| `diagnostics` | Validation output from `rledger check` | | `source_files` | Parsed ledger and include files | -Amounts come from whichever backend parsed the ledger, so thousands separators, arithmetic, cost (`{}`), and price (`@`/`@@`) annotations arrive already resolved to their booked values. +`postings.transaction_id` points at `transactions.id`, and so do `transaction_metadata`, `transaction_tags`, and `transaction_links`. `posting_metadata.posting_id` points at `postings.id`. The rest join by name: an account column against `accounts.name`, a `source_file` column against `source_files.path`. -Metadata values are projected as text. Booleans keep their Beancount spelling, `TRUE` and `FALSE`. - -## Source locations +```sql +SELECT t.date, t.payee, p.account, p.amount, p.commodity +FROM postings p +JOIN transactions t ON t.id = p.transaction_id +WHERE p.account LIKE 'Expenses:%' +ORDER BY t.date DESC; +``` -`transactions` and `postings` carry `source_file`, `line`, and `source_location`. `source_location` is formatted as `path:line`, so it can be copied, filtered, or exported directly. A posting points at its own line rather than the line of the transaction that contains it. +Amounts arrive booked, so thousands separators, arithmetic, cost (`{}`) and price (`@`/`@@`) annotations are resolved before they reach a cell. Metadata values are text, and booleans keep their Beancount spelling of `TRUE` and `FALSE`. -Valid transactions with no postings remain in `transactions` with their metadata, tags, links, and source location. +Every ledger file, glob directory, and referenced document is watched by modification date, size, and directory listing. Change one and the next query rebuilds the projection first, so an edit in your text editor lands without reconnecting. -A transaction the backend generates rather than reads, such as the one inserted for a `pad`, has whatever location that backend gives it. `rledger` leaves these columns empty; Python Beancount points them at the `pad` directive. +## Source locations -## Diagnostics +`transactions` and `postings` carry `source_file`, `line`, and `source_location`, the last formatted `path:line` so it copies, filters, and exports as it stands. A posting points at its own line, not at its transaction's. -The `diagnostics` table holds structured validation output: `source_file`, `line`, `source_location`, `column_number`, `end_line`, `end_column`, `severity`, `phase`, `code`, and `message`. +The backends disagree about generated entries. For the transaction a `pad` inserts, `rledger` leaves these columns empty and Python Beancount points them at the `pad`. -The table is populated by `rledger check`. A ledger opened through `rledger` is projected even when it fails validation, so the entries the projection skipped are readable here rather than only on the command line. Ledgers opened on the Python Beancount backend leave the table empty. +## Diagnostics -TablePro refreshes the projection when a referenced document file is created or removed, so document existence diagnostics stay current without editing the ledger. +`diagnostics` carries `source_file`, `line`, `source_location`, `column_number`, `end_line`, `end_column`, `severity`, `phase`, `code`, and `message`. A ledger opened through `rledger` is projected even when validation fails, so whatever the projection skipped is readable here. The Python backend leaves the table empty. ## Includes -The parser follows Beancount `include` directives. Literal includes and glob patterns such as `include "imports/*.beancount"` and `include "imports/**/*.beancount"` are supported. +`include` directives are followed, both literal paths and glob patterns such as `include "imports/*.beancount"` and `include "imports/**/*.beancount"`. ## BQL -Prefix a query with `BQL:` to run it through your configured `rledger` executable: +Prefix a query with `BQL:` to send it to `rledger` instead of the projection. ```sql BQL: SELECT account FROM accounts ORDER BY account ``` -Table browsing, row counts, and pagination work for BQL results. BQL queries do not support SQL parameters. +Table browsing, row counts, and pagination work on a BQL result. SQL parameters do not. + +## Limitations + +- No writes. INSERT, UPDATE, DELETE, and every form of schema editing are rejected. Edit the ledger in a text editor; the next query picks the change up. +- No import, SSH, SSL, or ledger switching. One connection is one ledger file. +- BQL needs `rledger` even when the ledger opened on the Python backend. The query is refused. Install `rledger`, or drop the `BQL:` prefix and query the projected tables. +- `pad` directives get no table of their own. The balancing transaction they insert is projected; read the directive in the source file. +- Directives outside the 17 tables are not projected. They stay in the source files. ## Troubleshooting -**Beancount needs rledger or Python Beancount**: No backend was found. An app launched from Finder does not inherit your shell's `PATH`, so `rledger --version` working in Terminal is not enough. Set `TABLEPRO_RUSTLEDGER_BINARY` or `TABLEPRO_BEANCOUNT_PYTHON` with `launchctl setenv`, then relaunch TablePro. +**Beancount needs rledger or Python Beancount**: Neither was found on `PATH`, in `/opt/homebrew/bin`, or in `/usr/local/bin`. Set `TABLEPRO_RUSTLEDGER_BINARY` or `TABLEPRO_BEANCOUNT_PYTHON` with `launchctl setenv`, then relaunch. **TABLEPRO_BEANCOUNT_PYTHON points to a Python executable that cannot import beancount**: Run `pip3 install beancount` with that exact interpreter, or point the variable at one that already has the package. -**BQL queries need rledger**: BQL always runs through `rledger`, even when the ledger opened on the Python backend. Install `rledger`, or drop the `BQL:` prefix and query the projected tables instead. +**BQL queries need rledger**: Install rustledger so `rledger` lands on `PATH` or in Homebrew, or point `TABLEPRO_RUSTLEDGER_BINARY` at it. **File does not exist**: The ledger moved or was renamed. Re-pick it with **Browse…** in the connection form. **Beancount include cycle detected**: Two ledger files include each other. Break the loop in the source files. - -## Limitations - -- Beancount connections are read-only. -- Schema editing, imports, SSH, SSL, and database switching are not available for ledgers. -- `pad` directives are not projected as their own table. `rledger` does not expose the padded and source accounts, so the directive is only visible through the balancing transaction it inserts. The directive itself remains in the original source files. -- The SQL projection covers common ledger directives; unsupported directives remain available in the original source files. diff --git a/docs/databases/bigquery.mdx b/docs/databases/bigquery.mdx index fd31b264d..afcdefb44 100644 --- a/docs/databases/bigquery.mdx +++ b/docs/databases/bigquery.mdx @@ -3,117 +3,95 @@ title: Google BigQuery description: Connect to Google BigQuery with Service Account, ADC, or OAuth auth --- -TablePro connects to BigQuery via its REST API. Browse datasets and tables, run GoogleSQL queries, edit rows in the data grid. The plugin auto-installs when you pick BigQuery, or install it from **Settings > Plugins > Browse > BigQuery Driver**. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Quick setup - -Click **Create Connection…**, select **BigQuery**, pick an auth method, enter your Project ID, and click **Save & Connect**. - -## Authentication +Use Application Default Credentials if `gcloud` is already on the machine. The other two methods are for machines where it is not: a service account key file, or a browser sign-in against an OAuth client you create. Everything travels over the REST API, so there is no host, port, or tunnel. -**Service Account Key**: Point to a `.json` key file from Google Cloud Console (IAM > Service Accounts > Keys), or paste the raw JSON content directly into the field. + -**Application Default Credentials**: Uses cached credentials from gcloud CLI. Supports `authorized_user`, `service_account`, and `impersonated_service_account` credential types. Run this first: - -```bash -gcloud auth application-default login --project=my-project -``` - -**Google Account (OAuth 2.0)**: Sign in with your Google account via browser. Requires an OAuth Client ID from your GCP project: - -1. Go to [Google Cloud Console](https://console.cloud.google.com/) > APIs & Services > Credentials -2. Click "Create Credentials" > "OAuth client ID" -3. Select "Desktop app" as application type -4. Copy the Client ID and Client Secret into TablePro -5. On first connect, your browser opens for Google authorization -6. After approving, TablePro receives the token automatically +## Quick setup - -Without a saved refresh token, the browser authorization repeats on every connect. Paste a refresh token into the **OAuth Refresh Token** field and TablePro mints access tokens from it instead of opening the browser. Application Default Credentials also persist. - +Click **Create Connection…**, select **BigQuery**, pick an auth method, enter your Project ID, and click **Save & Connect**. ## Connection settings | Field | Required | Notes | |-------|----------|-------| -| **Auth Method** | Yes | Service Account Key, ADC, or Google Account (OAuth) | -| **Project ID** | Yes | e.g. `my-project-123456` | -| **Service Account Key** | SA only | Path to `.json` key file, or raw JSON content | -| **OAuth Client ID** | OAuth only | From GCP Console > APIs & Services > Credentials | -| **OAuth Client Secret** | OAuth only | From GCP Console > APIs & Services > Credentials | -| **OAuth Refresh Token** | No | Optional. When set, access tokens are minted from it and the browser flow is skipped | -| **Location** | No | Processing location: `US`, `EU`, `us-central1`, etc. | -| **Max Bytes Billed** | No | Cost cap per query in bytes (Advanced tab). Queries exceeding this limit will fail instead of billing. | - -## Features - -**Dataset Browsing**: The sidebar lists every dataset as an expandable node. Click a dataset to load its tables; they load the first time you open it. Search filters across the datasets you have open. +| **Auth Method** | Yes | Service Account Key, Application Default Credentials, or Google Account (OAuth) | +| **Project ID** | Yes | The ID (`my-project-123456`), not the name or number | +| **Service Account Key** | Service account only | A `.json` key file path, or the raw JSON | +| **OAuth Client ID**, **OAuth Client Secret** | OAuth only | From GCP Console > APIs & Services > Credentials | +| **OAuth Refresh Token** | No | Set it and the browser flow is skipped | +| **Location** | No | Processing location (`US`, `EU`, `us-central1`), sent with every job | +| **Max Bytes Billed** | No | Advanced tab. A query over the cap fails instead of billing | -**Dataset Switching**: Press `Cmd+K`, click the dataset chip in the toolbar, or use **Database > Open Database…** to jump to another dataset. The switcher also creates new datasets (`Cmd+N` inside the popover) and drops datasets from the row context menu. +There is no Database field and no URL scheme. Datasets stand in for databases: the sidebar lists them as expandable nodes, `Cmd+K` and **Database > Open Database…** move between them, and **Database > New Database…** runs `CREATE SCHEMA`. Switching does not move tabs you already have open; each stays on its own dataset. See [Tabs](/features/tabs#where-a-tab-points). -**Table Structure**: Columns with full BigQuery types: `STRUCT`, `ARRAY`, nullable status, field descriptions. Clustering and partitioning info in the Indexes tab. - -**GoogleSQL Queries** ([docs](https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax)): +## Authentication -```sql --- UNNEST arrays -SELECT id, tag -FROM `my_dataset.items`, UNNEST(tags) AS tag +### Application default credentials --- STRUCT access -SELECT address.city FROM `my_dataset.customers` +```bash +gcloud auth application-default login --project=my-project ``` - -Backtick-quote table names: `` `dataset.table` ``. Single-quote strings: `'value'`. - +The driver reads `~/.config/gcloud/application_default_credentials.json`, accepting the `authorized_user`, `service_account`, and `impersonated_service_account` types. The form then needs only a Project ID. -**Query Cost**: After running a query in the SQL editor, the status bar shows bytes processed, bytes billed, and estimated cost (e.g., `Processed: 1.5 MB | Billed: 10 MB | ~$0.0001`). Use the **Dry Run** option (Explain dropdown > "Dry Run (Cost)") to check cost before executing. +### Service account key - - Status bar showing bytes processed, bytes billed, and estimated query cost - Status bar showing bytes processed, bytes billed, and estimated query cost - +Point **Service Account Key** at a `.json` key file from Google Cloud Console (IAM > Service Accounts > Keys), or paste the JSON straight into the field. -**Data Types**: INT64, FLOAT64, NUMERIC, BIGNUMERIC, BOOL, STRING, BYTES, DATE, TIME, DATETIME, TIMESTAMP, GEOGRAPHY, JSON, STRUCT, ARRAY, RANGE. Complex types (STRUCT/ARRAY) display as JSON. +### Google account (OAuth 2.0) -**Data Editing**: Edit cells, insert rows, and delete rows in the data grid. + + + In [Google Cloud Console](https://console.cloud.google.com/), under APIs & Services > Credentials, click Create Credentials > OAuth client ID and pick Desktop app. + + + Paste the Client ID and Client Secret into the connection form. + + + On connect your browser opens Google's consent screen, and the token arrives on an ephemeral loopback redirect. The step times out after two minutes. + + -Partitioned tables require a partition filter for UPDATE/DELETE. Every query costs money (billed per bytes scanned). Set **Max Bytes Billed** in Advanced settings to cap costs. +The refresh token from a browser sign-in is held in memory only, so the browser opens again on every connect. Paste a refresh token into **OAuth Refresh Token** to mint access tokens from it instead. -**DDL**: Run DDL from the SQL editor: `CREATE SCHEMA` for datasets, `ALTER TABLE … ADD COLUMN` and `DROP COLUMN`, `CREATE OR REPLACE VIEW`. The DDL tab reads each table's definition from `INFORMATION_SCHEMA.TABLES`. The Structure tab is read-only for BigQuery. +## Cost -**Export**: CSV, JSON, SQL, XLSX formats. +Every query is billed on bytes scanned, so there are three places to control it. -## IAM permissions +- Before running, pick **Dry Run (Cost)** from the Explain dropdown. It asks BigQuery what the query would scan without executing it. +- After running, the status bar shows bytes processed, bytes billed, and an estimate at the on-demand rate of $6.25 per TB: `Processed: 1.5 MB | Billed: 10 MB | ~$0.0001`. A cache hit is marked `(cached)`. +- As a hard ceiling, set **Max Bytes Billed**: BigQuery rejects a job that would exceed it. -Minimum roles: + + Status bar showing bytes processed, bytes billed, and estimated query cost + Status bar showing bytes processed, bytes billed, and estimated query cost + -- `roles/bigquery.user`: run queries -- `roles/bigquery.dataViewer`: browse and read data -- `roles/bigquery.dataEditor`: INSERT, UPDATE, DELETE +## Querying and editing -## Troubleshooting +Completion, quoting, and formatting follow GoogleSQL: backticks around table names, single quotes around strings. `STRUCT` and `ARRAY` columns display as JSON. Cells, inserts, and deletes go through the grid. The Structure tab is read-only, but DDL runs from the editor: `CREATE SCHEMA`, `ALTER TABLE … ADD COLUMN` and `DROP COLUMN`, `CREATE OR REPLACE VIEW`. The DDL tab reads definitions from `INFORMATION_SCHEMA.TABLES`; clustering and partitioning appear under Indexes. -**Auth failed**: Check key file path or run `gcloud auth application-default login`. +Minimum IAM roles: `roles/bigquery.user` to run queries, `roles/bigquery.dataViewer` to read, `roles/bigquery.dataEditor` to write. -**Permission denied**: Ensure `bigquery.user` role is granted on the project. +## Limitations -**Project not found**: Use the Project ID (not name or number). +- `STRUCT` and `ARRAY` columns are left out of a generated UPDATE or DELETE's `WHERE` clause. A row identified only by one of them cannot be edited from the grid. +- A partitioned table needs a partition filter for UPDATE and DELETE, or the statement is rejected. Write it in the editor. +- No transactions, no streaming inserts, no SSH tunnel. +- Deep pagination rescans from the start of the table. Filter rather than paging far in. +- A job is polled until the [query timeout](/customization/general-settings#query-timeout), then cancelled. The floor is 30 seconds, so **No limit** gives BigQuery 30 seconds rather than none. -**Query timeout**: BigQuery runs jobs async. The plugin polls up to 5 minutes (configurable). +## Troubleshooting -**Cost**: Use `LIMIT`, select specific columns, prefer partitioned tables. Set **Max Bytes Billed** to prevent expensive queries. Table browsing caps rows automatically. +**Application default credentials not found at ~/.config/gcloud/application_default_credentials.json**: Run `gcloud auth application-default login`, or switch the connection to a service account key. -**No tables after connect**: Expand a dataset node in the sidebar to load its tables. Empty datasets stay empty; open another dataset that has tables. +**Permission denied**: Authenticated but unauthorized. Grant `roles/bigquery.user` on the project and a data role on the dataset. -## Limitations +**Project not found**: Enter the Project ID, not the display name and not the numeric project number. -- No SSH tunneling (HTTPS only to BigQuery API) -- No transactions -- STRUCT/ARRAY columns excluded from UPDATE/DELETE WHERE clauses -- Deep pagination (large OFFSET) scans from start. Use filters to narrow results -- No streaming inserts -- Browser OAuth re-authorizes on each connect unless a refresh token is saved in the connection +**No tables after connect**: Tables load when you expand the dataset node. An empty dataset stays empty; open another. diff --git a/docs/databases/cassandra.mdx b/docs/databases/cassandra.mdx index a6057f3eb..6b0d36434 100644 --- a/docs/databases/cassandra.mdx +++ b/docs/databases/cassandra.mdx @@ -3,13 +3,15 @@ title: Cassandra / ScyllaDB description: Connect to Cassandra and ScyllaDB clusters, browse keyspaces, and run CQL queries --- -Anything older than Cassandra 3.0 will not connect. The driver reads its schema from `system_schema`, which 2.x does not have. ScyllaDB is a separate entry in the database type list but the same driver, over the same CQL native protocol. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; + +Cassandra 3.0 is the floor here, and 2.x will not connect. The driver reads its schema from `system_schema`, which 2.x does not have. ScyllaDB is a separate entry in the database type list but the same driver, over the same CQL native protocol. ## Quick setup -Click **Create Connection…**, select **Cassandra** or **ScyllaDB** (both entries use the same driver), enter host, port, and credentials, and connect. +Click **Create Connection…**, select **Cassandra** or **ScyllaDB**, enter host, port, and credentials, and connect. -Cassandra is a registry plugin. Picking it in the **Choose a Database** sheet prompts to install; connecting to a saved Cassandra connection installs it without asking. You can also install **Cassandra Driver** from **Settings > Plugins > Browse**. + ## Connection settings @@ -17,10 +19,12 @@ Cassandra is a registry plugin. Picking it in the **Choose a Database** sheet pr |-------|---------|-------| | **Host** | `localhost` | CQL contact point | | **Port** | `9042` | CQL native port | -| **Username** | - | Only if authentication is enabled | +| **Username** | - | Only if the cluster has an authenticator configured | | **Password** | - | | -The form has no Keyspace field. Pick a keyspace in the sidebar after connecting, or put one in the URL path (`cassandra://user:pass@host:9042/keyspace`) and import that URL. +There is no Keyspace field. Pick one in the sidebar after connecting, or put it in the URL path and import that URL. Later switches happen in place, with no reconnect, and the system keyspaces are filtered out of the list. Tabs already open are bound to their own keyspace and do not follow. See [Tabs](/features/tabs#where-a-tab-points). + +There is no consistency level field either. The driver never sets one, so every statement runs at the DataStax C driver's default of `LOCAL_ONE`. ## Connection URLs @@ -29,81 +33,59 @@ cassandra://user:password@host:9042/keyspace scylladb://user:password@host:9042/keyspace ``` -`cql://` and `scylla://` work too. See [Connection URL Reference](/connections/urls#cassandra-/-scylladb). - -## Example configurations - -**Local**: host `localhost:9042`, no auth - -**Docker**: `cassandra:latest`, user/pass `cassandra:cassandra` - -**Remote**: use [SSH tunneling](/connections/ssh-tunneling) for production clusters - -## Amazon Keyspaces (IAM) - -Set **Authentication** to an AWS IAM mode (Access Key, Profile, or SSO) to connect to Amazon Keyspaces with SigV4 signing instead of a password. Enter the AWS region and enable TLS, which Keyspaces requires (use the `cassandra.{region}.amazonaws.com` endpoint on port 9142). Profiles resolve from `~/.aws/config` and `~/.aws/credentials`, including `credential_process`, SSO, and assumed roles. - -## SSL/TLS - -The Cassandra driver has no TLS fallback. **Preferred** behaves the same as **Required** (the SSL pane shows a warning). Use **Verify CA** with a CA certificate path for private PKI. For mutual TLS, set the client certificate and key paths; if the key is encrypted, enter its passphrase in the **Key Passphrase** field (stored in the Keychain). See [SSL/TLS](/connections/ssl) for details. +`cql://` and `scylla://` work too. See [Connection URL Reference](/connections/urls). -## Features +## Amazon Keyspaces -**Keyspace browsing**: The sidebar lists keyspaces, and each keyspace lists its tables. Click a table to view its data. +Keyspaces authenticates with SigV4, not a password, so set **Authentication** to an AWS IAM mode and leave Username and Password empty. -**Table structure**: The structure view shows each column's name, CQL type, and a primary key flag (set for both partition key and clustering columns), plus the table's secondary indexes. Structure editing supports adding and dropping columns; make other schema changes in the CQL editor. + + + Host `cassandra.{region}.amazonaws.com`, port `9142`. + + + Access Key, Profile, or SSO. Profiles resolve from `~/.aws/config` and `~/.aws/credentials`, including `credential_process` and assumed roles. + + + Keyspaces refuses a plaintext connection. See [AWS IAM Authentication](/connections/aws-iam). + + -**Data grid**: Paginated, with type-aware formatting: uuid/timeuuid formatted, timestamps configurable, map/set/list/tuple as formatted collections, blob as hex. +## Browsing and editing Cassandra keyspace and tables in the sidebar with table data in the grid Cassandra keyspace and tables in the sidebar with table data in the grid -### CQL editor +The structure view gives each column's name, CQL type, a primary key flag set for partition key and clustering columns alike, and the table's secondary indexes. Structure editing adds and drops columns; every other schema change goes in the CQL editor. In the grid, map, set, list, and tuple render as collections and blob as hex. -```sql -SELECT * FROM users WHERE user_id = 123e4567-e89b-12d3-a456-426614174000; +Row counts stop at 100,001. Cassandra has no cheap count, so the driver runs `SELECT COUNT(*) … LIMIT 100001` and a larger table reports that ceiling. -INSERT INTO users (user_id, name, email, created_at) -VALUES (uuid(), 'Alice', 'alice@example.com', toTimestamp(now())); +## SSL/TLS -UPDATE users USING TTL 86400 -SET email = 'new@example.com' -WHERE user_id = 123e4567-e89b-12d3-a456-426614174000; +The default is **Disabled**, and there is no opportunistic TLS here, so **Preferred** behaves exactly like **Required**. -CREATE TABLE IF NOT EXISTS events ( - event_id timeuuid, - user_id uuid, - event_type text, - payload text, - PRIMARY KEY ((user_id), event_id) -) WITH CLUSTERING ORDER BY (event_id DESC); +| Mode | Verifies | +|------|----------| +| **Preferred**, **Required** | Nothing. Encryption only | +| **Verify CA** | The chain, against the PEM file you supply | +| **Verify Identity** | The chain and the hostname | -CREATE INDEX ON users (email); -``` +Both verifying modes need a **CA Certificate** path, and refuse to connect without one rather than falling back to the system roots. For mutual TLS set the client certificate and key paths, with the key's passphrase in **Key Passphrase**. See [SSL/TLS](/connections/ssl). -## CQL notes +## Limitations -- No JOINs or subqueries: denormalize, or run sequential statements. -- Every SELECT needs the full partition key in WHERE, or `ALLOW FILTERING` (a cluster scan, slow in production). -- Lightweight transactions: conditional writes with `IF`, e.g. `INSERT INTO users (…) IF NOT EXISTS;`. -- TTL and writetime: `USING TTL 3600`; `SELECT TTL(value), WRITETIME(value) FROM cache WHERE key = 'k1';`. +- Counter columns cannot be set with a plain `UPDATE`. The grid will edit one and the server rejects the statement it generates. Use `UPDATE … SET c = c + 1` in the CQL editor. +- Materialized views, UDTs, UDFs, and UDAs do not appear in the sidebar. Query `system_schema` in the CQL editor to see them. +- The DDL tab reconstructs `CREATE TABLE` from the column list, so table options (compaction, compression, `gc_grace_seconds`), clustering order, and the split between partition and clustering key are all missing from it. Read the real definition with `DESCRIBE TABLE` in `cqlsh`. +- DataStax Astra DB does not connect. The Secure Connect Bundle it requires cannot be loaded, and there is no way around it. +- Connect and request timeouts are fixed at 10 and 30 seconds. Neither the app-wide query timeout nor a multi-DC policy reaches this driver. ## Troubleshooting -**Connection refused**: Check Cassandra is running (`nodetool status`), verify port 9042 in `cassandra.yaml`, check `rpc_address` and `listen_address`. +**Connection refused**: Check Cassandra is running with `nodetool status`, that port 9042 matches `cassandra.yaml`, and that `rpc_address` and `listen_address` are reachable from your Mac. -**Auth failed**: Verify credentials, check `authenticator: PasswordAuthenticator` in `cassandra.yaml`. Default superuser: `cassandra:cassandra`. - -**Timeout**: The driver uses a fixed 10 second connect timeout and 30 second request timeout. Neither is configurable, and the app-wide query timeout setting does not apply to Cassandra. Check host, port, firewall rules, and cloud IP allowlists. - -**Read timeout**: Include the full partition key in WHERE, add a `LIMIT`, check cluster health with `nodetool`. - -## Limitations +**Authentication fails**: Verify the credentials and check `authenticator: PasswordAuthenticator` in `cassandra.yaml`. The default superuser is `cassandra` / `cassandra`. -- Materialized views, UDTs, UDFs, and UDAs do not appear in the sidebar. Query `system_schema` tables in the CQL editor instead. -- Table options (compaction, compression, gc_grace_seconds) and clustering order are not shown in the structure view. -- DataStax Astra DB is not supported: the driver cannot load a Secure Connect Bundle. -- Counter columns cannot be set with a plain `UPDATE`. The grid will edit one, but the statement it generates is rejected. Use `UPDATE … SET c = c + 1` in the CQL editor instead. -- Multi-DC settings are not configurable. +**Read timeout**: Put the full partition key in the `WHERE` clause, add a `LIMIT`, and check cluster health with `nodetool`. diff --git a/docs/databases/clickhouse.mdx b/docs/databases/clickhouse.mdx index 1f758dd9d..ef8a1386c 100644 --- a/docs/databases/clickhouse.mdx +++ b/docs/databases/clickhouse.mdx @@ -3,26 +3,22 @@ title: ClickHouse description: Connection settings, the URL format, and the parts and partition tools for ClickHouse --- -Every query travels over HTTP to port 8123. The native TCP protocol on 9000 is not used, so the server's HTTP interface has to be on. The driver ships inside the app; there is nothing to install. +Every query travels over HTTP to port 8123. The native TCP protocol on 9000 is not used, so the server's HTTP interface has to be on. Any version connects, though data-skipping indices need 19.17 or newer to show up. The driver ships inside the app. ## Quick setup -Click **Create Connection…**, select **ClickHouse**, enter host, port, credentials, and database, then click **Save & Connect**. +Click **Create Connection…**, select **ClickHouse**, fill in host, port, and credentials, then click **Save & Connect**. ## Connection settings | Field | Default | Notes | |-------|---------|-------| | **Host** | `localhost` | | -| **Port** | `8123` | HTTP port (not 9000 native TCP) | -| **Username** | `default` | Leave empty and TablePro sends `default`. Set a password in production | -| **Database** | `default` | Leave empty and the server uses `default`. Switch with Cmd+K | +| **Port** | `8123` | HTTP port, not 9000 native TCP | +| **Username** | `default` | Left empty, `default` is sent. Set a password in production | +| **Database** | `default` | Optional. Leave empty and the server uses `default` | -**SSL/TLS**: Setting any non-Disabled SSL Mode switches the URL scheme to `https`. **Preferred** and **Required** skip certificate verification, **Verify CA** validates against the supplied CA file, **Verify Identity** uses system default HTTPS trust (certificate chain plus hostname). Use **Verify Identity** for ClickHouse Cloud (port 8443); its certificates chain to a public CA. Use **Required** only for a self-signed certificate you have no CA file for. See [SSL/TLS](/connections/ssl). - -The CA file may be PEM or DER. If **Verify CA** cannot read the file you point it at, the connection fails rather than falling back to the public root store. - -For unencrypted HTTP to a remote server, use [SSH tunneling](/connections/ssh-tunneling). +Switching database happens in place, with no reconnect, and the sidebar's choice only affects new tabs: an open tab stays on its own database. See [Tabs](/features/tabs#where-a-tab-points). For unencrypted HTTP to a remote server, use [SSH tunneling](/connections/ssh-tunneling). ## Connection URL @@ -32,69 +28,69 @@ clickhouse://user:password@host:8123/database See [Connection URL Reference](/connections/urls) for all parameters. -## Features - -### Database and table browsing - -The sidebar lists the current database's tables, views, and materialized views. Press Cmd+K or click the database name in the toolbar to switch databases. Choose **View > Sidebar as Tree** to list every database and expand them in place. +## Browsing -For each table, TablePro shows: +The sidebar lists the current database's tables, views, and materialized views. Press `Cmd+K` or click the database name in the toolbar to switch. Choose **View > Sidebar as Tree** to list every database and expand them in place. -- **Structure**: Columns with ClickHouse data types, default expressions, and comments -- **Indexes**: The sorting key, listed as `PRIMARY (sorting key)`, plus data-skipping indices (minmax, set, bloom_filter, and others) -- **DDL**: The full CREATE TABLE statement including engine and settings -- **Parts**: Partition and part details from `system.parts` (rows, disk size, active status), with Optimize Table, Drop Partition, and Detach Partition actions +| Tab | Contents | +|-----|----------| +| **Structure** | Columns with types, default expressions, and comments | +| **Indexes** | The sorting key, listed as `PRIMARY (sorting key)`, plus data-skipping indices (minmax, set, bloom_filter, and the rest) | +| **DDL** | The full `CREATE TABLE`, engine and settings included | +| **Parts** | Every part from `system.parts` with its partition, rows, disk size, modification time, and active flag, plus **Optimize**, **Drop Partition**, and **Detach Partition** | ClickHouse Parts tab showing partitions with Optimize, Drop, and Detach actions ClickHouse Parts tab showing partitions with Optimize, Drop, and Detach actions -### EXPLAIN variants +The [Server Dashboard](/features/server-dashboard) covers ClickHouse with active sessions, server metrics, and slow queries. + +## EXPLAIN variants Click the Explain dropdown in the query editor to choose: | Variant | Description | |---------|-------------| | **Plan** | Logical query plan (default) | -| **Pipeline** | Physical execution pipeline with thread/port info | +| **Pipeline** | Physical execution pipeline with thread and port info | | **AST** | Abstract syntax tree of the parsed query | | **Syntax** | Query after syntax optimizations | | **Estimate** | Estimated rows, marks, and parts to read | -### Server-side query cancellation - -Cancelling a running query sends `KILL QUERY` to the server, stopping the query itself, not just the HTTP connection. - -### Server Dashboard +## Running queries -The [Server Dashboard](/features/server-dashboard) supports ClickHouse: active sessions, server metrics, and slow queries. +Each request carries a query id and asks for progress in the HTTP headers, so a long query reports rows read as it goes. Cancelling sends `KILL QUERY` for that id, stopping the query on the server rather than only dropping the connection. The [query timeout](/customization/general-settings#query-timeout) bounds each request at the timeout plus 30 seconds. -### Data editing - -Edit cells, insert rows, and delete rows in the data grid. Edits are submitted as INSERT, `ALTER TABLE … UPDATE`, and `ALTER TABLE … DELETE` statements. +Grid edits go out as INSERT, `ALTER TABLE … UPDATE`, and `ALTER TABLE … DELETE`. -ClickHouse mutations (UPDATE and DELETE) are asynchronous. They run in the background and can take time on large tables. Check progress with `SELECT * FROM system.mutations WHERE is_done = 0`. +ClickHouse mutations run in the background, so the grid reports the statement as accepted rather than applied, and a large table can take a long time. Watch `SELECT * FROM system.mutations WHERE is_done = 0` for progress. -### Export and import +## SSL/TLS -Export query results or table data to CSV, JSON, SQL, and XLSX. Import SQL, JSON, and CSV files. See [Import and Export](/features/import-export). +The default is **Disabled**, which sends plain HTTP. Any other mode switches the URL scheme to `https`. -## Troubleshooting +| Mode | Verifies | Pick it for | +|------|----------|-------------| +| **Preferred**, **Required** | Nothing. Encryption only | A self-signed certificate you have no CA file for | +| **Verify CA** | The chain, against the CA file | Private PKI | +| **Verify Identity** | The chain and the hostname, against the system trust store | ClickHouse Cloud, on port 8443 | -**Connection refused**: Check ClickHouse is listening on the HTTP port: `curl http://localhost:8123/ping` should return `Ok.`. Common port mix-up: 8123 is HTTP, 8443 is HTTPS, 9000 is native TCP (not used by TablePro). If the HTTP interface is disabled, check `` in `/etc/clickhouse-server/config.xml`. +The CA file may be PEM or DER. If **Verify CA** cannot read it, the connection fails rather than falling back to the public root store. See [SSL/TLS](/connections/ssl). -**Authentication failed (HTTP 403)**: Verify the username and password, that the user exists (`SELECT name, auth_type FROM system.users`), and that it has access to the target database (`SHOW GRANTS FOR app_user`). +## Limitations -**Connection timeout**: Verify host, port, firewall rules, and, for cloud-hosted ClickHouse, that your IP is in the allowed list. +- No foreign keys and no multi-statement transactions. +- No auto-increment, and the primary key and sorting key are fixed at creation. Structure editing covers adding, modifying, and dropping columns and data-skipping indexes; recreate the table for anything else. +- A `SET` does not carry to the next statement. Every statement is its own HTTP request with no session id, and the setting is gone by the next one. Put it in a `SETTINGS` clause on the query itself. +- A query with its own `FORMAT` clause, such as `SELECT 1 FORMAT JSON`, shows the server's raw output in one column instead of a parsed table. Drop the clause to get a grid. -## Limitations +## Troubleshooting + +**Connection refused**: `curl http://localhost:8123/ping` should answer `Ok.`. The usual cause is the port: 8123 is HTTP, 8443 HTTPS, 9000 native TCP and unused here. If the HTTP interface is off, check `` in `/etc/clickhouse-server/config.xml`. + +**Authentication failed (HTTP 403)**: Check the user exists (`SELECT name, auth_type FROM system.users`) and reaches the target database (`SHOW GRANTS FOR app_user`). -- No foreign keys or multi-statement transactions -- No auto-increment; primary key and sorting key are immutable after creation -- Structure editing supports add, modify, and drop columns, plus data-skipping indexes. Foreign keys and primary key changes are not available -- UPDATE/DELETE run as asynchronous background mutations via `ALTER TABLE` -- ClickHouse is designed for batch inserts, not single-row writes -- A query with its own `FORMAT` clause (for example `SELECT 1 FORMAT JSON`) shows the server's raw output in a single column instead of a parsed table +**Connection timeout**: Check host, port, firewall rules, and, on ClickHouse Cloud, that your IP is in the allowed list. diff --git a/docs/databases/cloudflare-d1.mdx b/docs/databases/cloudflare-d1.mdx index ac43dc749..8730fc5d4 100644 --- a/docs/databases/cloudflare-d1.mdx +++ b/docs/databases/cloudflare-d1.mdx @@ -3,15 +3,21 @@ title: Cloudflare D1 description: Connect to Cloudflare D1 databases over the Cloudflare REST API --- -There is no host or port to fill in. A D1 connection is a database name, an account ID, and an API token, and every query goes out as an HTTPS request to Cloudflare. The dialect is SQLite. The driver is a downloadable plugin, offered the moment you pick **Cloudflare D1** in the connection form. See [Plugins](/features/plugins). +import RegistryPlugin from "/snippets/registry-plugin.mdx"; + +A D1 connection is three strings: the database name, your Cloudflare account ID, and an API token. Every query leaves as an HTTPS request to the Cloudflare API, and the dialect on the other end is SQLite. + + ## Connection settings -| Field | Description | -|-------|-------------| -| **Database** | D1 database name or UUID. Names resolve to the UUID automatically via the Cloudflare API. | -| **Account ID** | Your Cloudflare account ID | -| **API Token** | Cloudflare API token with D1 permissions, entered in the built-in password field (labeled **API Token** for this driver). Stored in the macOS Keychain. | +| Field | Required | Description | +|-------|----------|-------------| +| **Database** | Yes | D1 database name or UUID. A name resolves to its UUID through the API | +| **Account ID** | Yes | Your Cloudflare account ID | +| **API Token** | Yes | Token with D1 permissions, entered in the password field (labeled **API Token** here). Stored in the macOS Keychain | + +No host, no port, and no SSL pane: the endpoint is fixed and always HTTPS. Cloudflare D1 connection form with Database, Account ID, and API Token fields @@ -20,45 +26,58 @@ There is no host or port to fill in. A D1 connection is a database name, an acco Click **Test Connection** to verify, then **Save & Connect**. -## Getting your credentials +## Connection URL -**Account ID**: On the [Cloudflare dashboard](https://dash.cloudflare.com) right sidebar, or `npx wrangler whoami`. +```text +d1://account-id/database-name +``` -**API Token**: +The token is not in the URL. See [Connection URL Reference](/connections/urls). -1. Go to [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) -2. Click **Create Token** and pick the **Custom token** template -3. Add permission **Account > D1 > Edit** -4. Save and copy the token +## Getting your credentials + + + + It is in the right sidebar of the [Cloudflare dashboard](https://dash.cloudflare.com), or run `npx wrangler whoami`. + + + On [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens), click **Create Token**, pick the **Custom token** template, and add the permission **Account > D1 > Edit**. + + + Cloudflare shows the token once. Paste it into **API Token**. + + -The token grants access to all D1 databases in your account. Scope it to the account you need. +The token reaches every D1 database in its account, not only the one you connect to. -## Database management +## Databases and schema -Use the database switcher in the toolbar to list every D1 database in your account and switch between them without reconnecting. You can also create and drop D1 databases directly from TablePro, or with `wrangler d1 create `. +The toolbar's database switcher lists every D1 database in the account and moves between them without reconnecting. It creates and drops them too. A tab keeps the database it opened on for its whole life: see [Tabs](/features/tabs#where-a-tab-points). -## Schema and triggers +| In the structure editor | Supported | +|---|---| +| Create and drop tables, add and drop columns, create and drop indexes | Yes, as generated DDL | +| Create, edit, and drop triggers | Yes, under **Triggers** | +| Rename or retype a column, change its nullability or default | No | +| Add or edit a foreign key | No | -The structure editor creates tables, adds and drops columns with generated `ALTER TABLE` DDL, and creates and drops indexes. It cannot change an existing column's name, type, nullability, or default: recreate the table for that. Triggers appear under **Triggers** in the Structure tab, where you can create, edit, and drop them. +Browsing, `EXPLAIN QUERY PLAN`, data editing, and export behave as they do for [SQLite](/databases/sqlite). -## SQL dialect +## Limitations -D1 uses SQLite syntax; browsing, EXPLAIN QUERY PLAN, data editing, and export work as they do for SQLite. See [SQLite](/databases/sqlite). +- Nothing survives between statements. Each query is an independent HTTP request with no session state: `PRAGMA` settings and temp tables do not carry. Put what a statement needs into that statement. +- No multi-statement transactions. Every statement auto-commits alone, with no rollback. +- A column cannot be renamed or retyped, and a foreign key cannot be edited. Recreate the table and copy the rows across. +- No bulk import through the plugin. Run `wrangler d1 execute --file=dump.sql`, which needs `wrangler` installed. +- 10 GB per database, Cloudflare's limit. Shard across databases for more. +- Cloudflare's API rate limits apply. A limited request surfaces as an error carrying the retry delay Cloudflare returned. ## Troubleshooting -**Authentication failed**: Check the token has **D1 Edit** permission, the Account ID matches, and the token has not expired or been revoked. +**Authentication failed. Check your API token and Account ID.**: The token lacks **D1 Edit**, the Account ID belongs to a different account, or the token expired or was revoked. -**Database not found**: Verify the name or UUID with `wrangler d1 list` and confirm the token can access the account that owns the database. - -## Limitations +**Database '…' not found in account**: Confirm the name or UUID with `wrangler d1 list`, and confirm the token reaches the account that owns it. -- No persistent connections: each query is an independent HTTP request with no session state -- No multi-statement transactions: each SQL statement auto-commits independently -- No column rename, retype, or foreign key editing in the structure editor -- 10 GB limit per D1 database; shard for larger datasets -- Cloudflare API rate limits apply; TablePro surfaces rate limit errors with retry timing -- No bulk import through the plugin: use `wrangler d1 execute` with SQL files -- No custom SSL/SSH: D1 is HTTPS-only via the Cloudflare API +**Rate limited by Cloudflare. Retry after … seconds.**: Wait the stated interval. If it repeats, refresh less often and lower the page size. diff --git a/docs/databases/cockroachdb.mdx b/docs/databases/cockroachdb.mdx index 364ce093c..99ab3cd2d 100644 --- a/docs/databases/cockroachdb.mdx +++ b/docs/databases/cockroachdb.mdx @@ -3,62 +3,63 @@ title: CockroachDB description: Connect to CockroachDB clusters over the PostgreSQL wire protocol, including CockroachDB Cloud with cluster routing --- -TablePro connects to CockroachDB, a distributed SQL database that is wire-compatible with PostgreSQL, using the same libpq driver as [PostgreSQL](/databases/postgresql). Schema introspection reads `information_schema` and `pg_catalog`, and table and view DDL come from CockroachDB's native `SHOW CREATE`. The PostgreSQL page applies unless noted here. +Everything on the [PostgreSQL](/databases/postgresql) page holds here. The same libpq driver serves both, out of the PostgreSQL plugin the app already ships, and no minimum cluster version is enforced. This page is only the differences. + + + CockroachDB connection form + CockroachDB connection form + ## Connection settings | Field | Default | Notes | |-------|---------|-------| -| **Host** | - | Cluster host, or `localhost` for a local node | -| **Port** | `26257` | | -| **Database** | `defaultdb` | Default database in every cluster | -| **Username** | - | Not pre-filled; `root` for local insecure clusters | - -Fill in the form and click **Save & Connect**. For a local node started with `cockroach start-single-node --insecure`, set SSL Mode to **Disabled** and leave the password empty. +| **Port** | `26257` | Not 5432 | +| **Database** | `defaultdb` | Required. New clusters start with `defaultdb` and `postgres`, and only `system` is marked a system database | +| **Username** | - | Not pre-filled. `root` on a local insecure cluster | +| **SSL Mode** | **Preferred** | TLS first, plain text if the server refuses | -## CockroachDB Cloud +For a node started with `cockroach start-single-node --insecure`, set SSL Mode to **Disabled** and leave the password empty. Otherwise fill in the form and click **Save & Connect**. -CockroachDB Cloud connection strings include a cluster routing parameter. Copy the host, port, user, password, and database from the Cloud console, then put the routing value in the **Connection Options** field under the Advanced tab: +## Connection URL ```text ---cluster=my-cluster-1234 +cockroachdb://user:password@host:26257/defaultdb ``` -Set SSL Mode to **Verify Identity** for Cloud clusters. The Connection Options field is passed straight to libpq, so it also accepts other libpq options for self-hosted clusters. +`cockroach://` also works. See [Connection URL Reference](/connections/urls). -## Connection URL +## CockroachDB Cloud + +A Cloud connection string carries a cluster routing parameter that the form has no field for. Copy the host, port, user, password, and database from the Cloud console, then put the routing value in **Connection Options** on the Advanced tab: ```text -cockroachdb://user:password@host:26257/defaultdb +--cluster=my-cluster-1234 ``` -`cockroach://` also works. See [Connection URL Reference](/connections/urls) for all parameters. - -## Features - -**Schemas**: like PostgreSQL, default schema `public`. The toolbar shows the active database and schema side by side; click either to switch it, or press Cmd+K for the database list. Database > Schema does the same job from the menu bar. +Set SSL Mode to **Verify Identity** for Cloud. **Connection Options** goes straight to libpq, so it takes any other libpq option a self-hosted cluster needs. -**Databases**: `defaultdb` and `postgres`, the two databases a new cluster starts with, are listed like any other. Only `system` is marked as a system database. +## What is different -**DDL**: table and view definitions come from `SHOW CREATE`. Indexes come from `SHOW INDEXES`. Foreign keys are read from `information_schema`. +Table and view DDL comes from CockroachDB's own `SHOW CREATE`, so the DDL tab shows the statement the cluster itself would emit rather than a reconstruction. -**EXPLAIN**: `EXPLAIN` and `EXPLAIN ANALYZE` render as a visual plan tree. CockroachDB returns text plans, not JSON, and TablePro parses them into the tree. See [EXPLAIN Visualization](/features/explain-visualization). +`EXPLAIN` and `EXPLAIN ANALYZE` render as a visual plan tree. CockroachDB returns text plans rather than JSON, and the text is parsed into the tree. See [EXPLAIN Visualization](/features/explain-visualization). -**Import & Export**: export to CSV, JSON, SQL, or XLSX. Import from CSV, JSON, or SQL. See [Import & Export](/features/import-export). +Changing database means reconnecting, as on PostgreSQL. A tab bound to a database other than the connection's active one runs on a separate connection for it, sharing no temp tables, session variables, or open transaction with the query editor. See [Cross-database tabs](/databases/postgresql#cross-database-tabs). ## Limitations -- TablePro disables all structure editing for CockroachDB: no add, modify, or drop column, no index changes, no primary key changes from the structure tab. Run schema changes as SQL in the editor. -- No `pg_dump` / `pg_restore`. The **Backup Dump** and **Restore Dump** menu items are not available. Use CockroachDB's `BACKUP` and `RESTORE` SQL statements instead. -- No Maintenance menu. CockroachDB garbage-collects automatically, so it offers no `VACUUM` to run. -- Foreign key checks are not deferrable. +- No structure editing at all: no column changes, no index changes, no primary key changes. The Structure tab is read-only. Run schema changes as SQL in the editor. +- No `pg_dump` or `pg_restore`. **File > Backup Dump…** and **File > Restore Dump…** stay dimmed. Use CockroachDB's `BACKUP` and `RESTORE` statements. +- No Table Maintenance submenu. CockroachDB garbage-collects on its own and offers no `VACUUM` to run. +- Foreign key checks cannot be deferred. ## Troubleshooting -**Connection refused**: check the cluster is running and the port (default `26257`) is reachable. For local clusters, confirm `cockroach start` is still running. +**Connection refused**: Check the cluster is running and port 26257 is reachable. For a local cluster, confirm `cockroach start` is still running. -**Auth failed**: for local insecure clusters use user `root` with no password and SSL Mode **Disabled**. For Cloud, copy credentials from the console and use SSL Mode **Verify Identity**. +**Authentication fails**: On a local insecure cluster use `root`, no password, and SSL Mode **Disabled**. On Cloud, copy the credentials from the console and use **Verify Identity**. -**Cloud connection hangs or rejects**: make sure the cluster routing value is set in **Connection Options** (`--cluster=your-cluster-name`). +**Cloud connection hangs or is rejected**: The cluster routing value is missing. Put `--cluster=your-cluster-name` in **Connection Options**. -**EXPLAIN shows raw text**: plan output that does not match the expected tree shape falls back to raw text display. +**EXPLAIN shows raw text**: The plan did not match the shape the parser expects, so it fell back to the server's own text. The plan is complete; only the tree is missing. diff --git a/docs/databases/dameng.mdx b/docs/databases/dameng.mdx index f533a0a19..e552e2196 100644 --- a/docs/databases/dameng.mdx +++ b/docs/databases/dameng.mdx @@ -3,13 +3,24 @@ title: Dameng DM8 description: Dameng DM8 over its native wire protocol, with no DM client stack to install --- -The plugin implements the DM8 wire protocol itself. Give it a host, a port and a login, the same as any server connection, and no part of the DM client stack has to be on your Mac. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Install Plugin +Host, port, username, password. The plugin speaks the DM8 wire protocol itself, so no part of the DM client stack has to be on your Mac and no DM8 version is ruled out. -1. Open **Settings > Plugins > Browse** -2. Find **Dameng Driver** and click **Install** -3. Create a Dameng connection from the connection chooser + +A `CLOB` or `BLOB` that DM8 stored outside the row reads as an empty cell. Nothing marks it as truncated, so it looks exactly like an empty column. Cast `CLOB` values to `VARCHAR`, or wrap binary columns in `RAWTOHEX`, when you need the contents. + + + + Dameng DM8 connection form + Dameng DM8 connection form + + + + +## Quick setup + +Click **Create Connection…**, choose **Dameng DM8**, fill in host, port, and credentials, and click **Save & Connect**. ## Connection settings @@ -17,11 +28,11 @@ The plugin implements the DM8 wire protocol itself. Give it a host, a port and a |-------|---------|-------| | **Host** | `localhost` | DM8 server or tunnel endpoint | | **Port** | `5236` | Default DM8 listener port | -| **Database** | - | Optional initial schema | +| **Database** | - | Optional, and it picks a schema, not a database. Left empty, the driver uses your username uppercased | | **Username** | - | DM username, such as `SYSDBA` | | **Password** | - | Stored in the macOS Keychain | -The driver supports SSH, SOCKS, and Cloudflare tunnels. Native DM8 TLS is not yet available, so the SSL pane is hidden. Use a trusted tunnel when the database is not on a private network. +One DM8 instance holds one database, so there is nothing to switch between; use the schema picker instead, which switches without reconnecting. `SYS`, `SYSDBA`, `SYSAUDITOR`, `SYSSSO`, and `CTISYS` are marked system schemas. An open tab stays on the schema it started with. See [Tabs](/features/tabs#where-a-tab-points). ## Connection URL @@ -29,32 +40,27 @@ The driver supports SSH, SOCKS, and Cloudflare tunnels. Native DM8 TLS is not ye dm://user:password@host:5236/schema ``` -## Features +The path is a schema, not a database. See [Connection URL Reference](/connections/urls). + +## SSL/TLS -- Browse schemas, tables, views, columns, indexes, and foreign keys -- Complete DM8 statements, functions, data types, schemas, tables, views, and columns while typing -- Run SQL with Unicode text, parameterized binary writes, transactions, and row limits -- Switch schemas without reconnecting -- Edit rows with escaped parameter values -- Create and alter tables, indexes, primary keys, and foreign keys -- Inspect table metadata, generated DDL, view definitions, and visual `EXPLAIN` plans +None. Native DM8 TLS is not implemented, the SSL pane is hidden, and the connection is unencrypted. Reach a server outside a private network through an [SSH tunnel](/connections/ssh-tunneling), [SOCKS proxy](/connections/socks-proxy), or [Cloudflare tunnel](/connections/cloudflare-tunnel). ## Limitations -- Username and password authentication only -- No native TLS or trigger editor -- Stopping a query closes its connection. DM8 has no out-of-band cancel request, so TablePro stops - reading the reply and reconnects on your next query, back on the schema you were using. A query - timeout behaves the same way. A read runs again on the new connection. A statement that writes is - reported instead of being run twice, because the stopped attempt may already have been applied, - and an open transaction does not survive. -- The server keeps running a statement you stopped. TablePro stops waiting for it, but DM8 has no - way to be told to abandon the work, so a heavy query still finishes on the server. Stopping can - take a few seconds to take effect for the same reason. -- A `CLOB` or `BLOB` value DM8 stored outside the row reads as empty. Small values, which DM8 - keeps in the row, read normally. The column is listed either way and every other column in the - row is correct. Cast `CLOB` values to `VARCHAR`, or use `RAWTOHEX` for binary columns, when you - need the contents. -- The Database field selects an initial schema, and leaving it empty is fine. A DM8 instance holds one database, so there is no database to switch; use the schema picker instead. - -If a server value fails to decode, include the DM8 version and column type in a [GitHub issue](https://github.com/TableProApp/TablePro/issues). +- Stopping a query drops its connection. The driver reconnects on your next query, back on the schema you were using: a read reruns, an open transaction is gone, and a write is reported rather than retried. Check whether the write landed before running it again. A query timeout does the same. +- The server finishes the statement you stopped. A heavy query keeps running there, and a stop can take a few seconds to show up. +- A view definition longer than 8188 bytes is refused. The DDL tab reports the length instead of showing truncated text you could save back. Read the full definition on the server. +- Username and password are the only authentication, and there is no trigger editor. Write trigger DDL in the SQL editor. + +## Troubleshooting + +**The Dameng connection is closed.**: The server, a tunnel, or the network dropped the connection. The driver rebuilds it on the next query; rerun the statement. + +**The Dameng connection was lost, so the open transaction was rolled back.**: Nothing in that transaction committed. Run its statements again. + +**Dameng did not switch to the requested schema.**: The schema does not exist or your user cannot reach it. List the schemas visible to your user with `SELECT OBJECT_NAME FROM ALL_OBJECTS WHERE OBJECT_TYPE = 'SCH'`. + +**Switch away from a schema before dropping it.**: A schema cannot drop itself out from under the session. Move to another one first. + +If a server value fails to decode, include the DM8 version and the column type in a [GitHub issue](https://github.com/TableProApp/TablePro/issues). diff --git a/docs/databases/duckdb.mdx b/docs/databases/duckdb.mdx index f523c6416..a5cbbf3ea 100644 --- a/docs/databases/duckdb.mdx +++ b/docs/databases/duckdb.mdx @@ -3,23 +3,23 @@ title: DuckDB description: Open local DuckDB files or connect to a remote DuckDB server over Quack --- -DuckDB 1.5.2 ships inside the driver plugin, an embedded analytical engine built for OLAP workloads. Pick DuckDB in the connection form and you are prompted to install the plugin. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Connecting to a local DuckDB file +Pick a `.duckdb` file to open a database, or a `.parquet`, `.csv`, `.tsv`, `.json` or `.ndjson` file to read it where it sits. DuckDB 1.5.2 is compiled into the driver, so nothing runs as a server unless you point the connection at one. + + + +## Quick setup - + Click **Create Connection…** or press `Cmd+N`, then choose **DuckDB**. - - Click **Browse…** to select an existing `.duckdb` file, or enter a path to create a new database. - - Enter `:memory:` instead of a path for an in-memory database. It holds nothing on disk, so everything in it is gone when you disconnect. - - Browse also accepts a `.parquet`, `.csv`, `.tsv`, `.json` or `.ndjson` file. DuckDB opens it read-only: you get an in-memory database holding two views over the file, one named `file` and one named after the file, and the file on disk is never written to. Creating a database at a path that does not exist only works for `.duckdb` and `.ddb`, so a mistyped data-file path is reported instead of turning into an empty database. + + Leave **Connection Type** on **Local File** and click **Browse…**. Type `:memory:` in the field instead for a database that lasts as long as the session. - Click **Save & Connect** to open the database. + Click **Save & Connect**. @@ -28,88 +28,82 @@ DuckDB 1.5.2 ships inside the driver plugin, an embedded analytical engine built DuckDB connection form with Local File and Remote (Quack) connection types -## Connecting to a remote DuckDB server (Quack) +## What the file field accepts + +| Path | What you get | +|-------|-------------| +| `.duckdb`, `.ddb` | The database in that file. A path that does not exist yet is created | +| `.parquet`, `.csv`, `.tsv`, `.json`, `.ndjson` | An in-memory database holding two read-only views over the file, one named `file` and one named after the file. A path that does not exist is reported instead of created | +| `:memory:` | An empty database, discarded on disconnect | -Quack is DuckDB's client-server protocol. A DuckDB server exposes itself with `quack_serve`, and TablePro attaches to it as a remote database. Set **Connection Type** to **Remote (Quack, experimental)** and fill in: +In Local File mode the path is the whole connection, and it is required; the form shows no host, port or Database field. Double-click a `.duckdb` or `.ddb` file in Finder to open it in TablePro; the data formats are not registered with Finder. + +## Remote (Quack) + +Quack is DuckDB's client-server protocol, served by `quack_serve`. Set **Connection Type** to **Remote (Quack, experimental)**: | Field | Value | |-------|-------| -| **Host** | Server address (required) | +| **Host** | Server address. Required | | **Port** | `9494` by default | | **Token** | The token the server was started with | -| **Database Alias** | Name TablePro attaches the remote under. Pre-filled with `remotedb` | +| **Database Alias** | The name the remote is attached under. Pre-filled with `remotedb` | -Click **Save & Connect**. TablePro opens an in-memory DuckDB, registers the token as a secret, and attaches the remote server. +**Save & Connect** attaches the remote to an in-memory DuckDB. Reach it through the alias: `SELECT * FROM remotedb.main.orders`. -Remote (Quack) is experimental and matches the Quack beta: -- You can run SQL against the remote, for example `SELECT * FROM alias.main.your_table`. -- The sidebar cannot list remote tables yet; the Quack beta cannot enumerate a remote catalog. -- On macOS the quack extension downloads from the DuckDB extension registry on first use, so the first remote connection needs network access. +The `quack` extension is not linked into the macOS build. DuckDB downloads it from `extensions.duckdb.org` on the first remote connect and caches it under `~/.duckdb`, so that one connect needs to reach that host. ## Connection URL ```text -# Local file duckdb:///path/to/database.duckdb - -# Remote (Quack) quack://host:9494/alias ``` See [Connection URL Reference](/connections/urls) for all parameters. -## Opening DuckDB files from Finder - -Double-click a `.duckdb` or `.ddb` file in Finder to open it directly in TablePro. - ## Databases and schemas -A DuckDB connection opens one database, named after the file (`analytics.duckdb` becomes `analytics`). Each database holds one or more schemas, and the default is `main`. - -The sidebar shows both levels. Switch it to Tree layout from View > Sidebar Layout to get database > schema > object folders; in the default Flat layout it lists the current schema's objects and the active schema sits at the foot of that list. The toolbar shows the active database and schema side by side; click either to switch it, or press `Cmd+K` for the database list. - -`ATTACH` a second file and it appears as a sibling database after a sidebar refresh: +A connection opens one database named after the file: `analytics.duckdb` becomes `analytics`, schema `main`. `ATTACH` a second file and it joins the sidebar as a sibling database after a refresh: ```sql ATTACH 'warehouse.duckdb' AS warehouse; ``` -Attachments last for the session. DuckDB does not persist them, so they are gone after a reconnect. - -DuckDB's built-in `system` and `temp` catalogs are hidden, and with them `information_schema` and `pg_catalog`, which live inside `system`. - -## Querying files directly +Press `Cmd+K` to move between them, in place, with no reconnect. DuckDB does not persist attachments, so a reconnect starts again from the one file. `system` and `temp` are hidden, and with them `information_schema` and `pg_catalog`. -DuckDB can query CSV, Parquet, and JSON files with SQL: +Any connection can also read a file straight from SQL: ```sql SELECT * FROM 'data.csv'; SELECT * FROM read_parquet('analytics.parquet'); -SELECT * FROM read_json('config.json'); ``` -## DuckDB on iOS - -The iOS app supports DuckDB too. In the connection form, pick DuckDB and either turn on **In-Memory Database** or open a `.duckdb`/`.ddb` file through the Files app. Opened files keep working across launches through a security-scoped bookmark, so edits write back to the original file. +## Limitations -The iOS build statically links the `core_functions`, `json`, `parquet`, `icu`, `autocomplete`, `httpfs`, and `quack` extensions, so remote Quack connections work on iOS without a download. Runtime extension autoloading stays off, so other on-demand extensions are not available on iOS. Large in-memory databases are bounded by the app's memory budget. +- A DuckDB file takes one writer at a time. A connect against a file another process holds fails, naming that process. Quit the `duckdb` CLI, the Python process or the other TablePro window first. +- A data file opened through **Browse…** is read-only, and the file on disk is left byte for byte identical. To change its contents, open a `.duckdb` database and run `CREATE TABLE t AS SELECT * FROM 'data.parquet'`. +- Remote (Quack) lists no tables in the sidebar. Write them out by name through the alias. +- No SSL/TLS pane and no SSH tunnel. The engine is in-process; see [SSL/TLS](/connections/ssl). +- No creating or dropping a database. `ATTACH` is the way to reach a second file. ## Troubleshooting -**Connect fails on a local file**: DuckDB allows one writer per file. Quit anything else holding it, a `duckdb` CLI session, a Python process, or another TablePro window, then reconnect. The error names the process that holds the lock. +### No file at … -**Querying a Parquet, CSV or JSON file fails on a restricted network**: the macOS build links no DuckDB extensions, so `read_parquet` and friends come from `parquet`, which DuckDB downloads from `extensions.duckdb.org` the first time you use it. Browsing tables, columns and keys needs no download. Run one query from a network that can reach the registry and DuckDB caches the extension in `~/.duckdb` for later. +A data-file path that is not there. Only `.duckdb` and `.ddb` are created on demand. Re-pick the file with **Browse…**. -**Remote connection fails while attaching**: On macOS the `quack` extension downloads from the DuckDB extension registry the first time you connect. If that download is blocked, the attach step fails. Connect once from a network that can reach the registry. +### DuckDB opened the file but reported no catalog to browse -**Host is required for a remote DuckDB connection**: The **Host** field is empty. Fill it in before saving. +The file opened and exposed nothing to list. Check that it is a DuckDB database and not a file with a borrowed extension. -**Port must be a number between 1 and 65535**: The **Port** field holds something else. The Quack default is `9494`. +### Port must be a number between 1 and 65535 -## Limitations +**Port** holds something else. The Quack default is `9494`. + +## Related -- A local DuckDB file allows only one writer at a time. -- Remote (Quack) mode cannot list tables in the sidebar. Reference them by name in SQL. -- TablePro cannot switch databases on a DuckDB connection. Each connection targets one file or one attached remote. +- [TablePro for iPhone and iPad](/ios), where DuckDB opens a file picked through the Files app +- [Import & Export](/features/import-export) diff --git a/docs/databases/dynamodb.mdx b/docs/databases/dynamodb.mdx index 3f5a286ee..cde6485b9 100644 --- a/docs/databases/dynamodb.mdx +++ b/docs/databases/dynamodb.mdx @@ -3,35 +3,41 @@ title: Amazon DynamoDB description: Connect to Amazon DynamoDB with PartiQL queries, GSI/LSI browsing, and DynamoDB Local support --- -Sign in with IAM keys, a named AWS profile, or an SSO session. Every request after that is a separate HTTPS call signed with SigV4, so nothing is held open and there is no SSH tunnel to configure. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Quick setup - -Click **Create Connection…**, select **DynamoDB**, choose an auth method, enter your credentials and region, then click **Save & Connect**. The plugin auto-installs when you pick DynamoDB, or install it from **Settings > Plugins > Browse > DynamoDB Driver**. +Opening a table runs a Scan. A filter becomes a Query only when it pins the partition key to a single value with `=`; every other filter is applied to the items after they come back, so it narrows what you see and not what the table read. -## Authentication methods + -**Access Key + Secret Key**: IAM credentials. Add a session token if you are using temporary credentials from STS. +## Quick setup -**AWS Profile**: Reads the named profile from both `~/.aws/config` and `~/.aws/credentials`, the same way the AWS CLI does. A profile can supply static keys (`aws_access_key_id`, `aws_secret_access_key`, optional `aws_session_token`) or a `credential_process` command. Profiles defined only in `~/.aws/config` (as `[profile name]`) resolve too. +Click **Create Connection…**, select **DynamoDB**, choose an **Auth Method**, enter credentials and region, then click **Save & Connect**. -**AWS SSO**: Reads the cached SSO token from `~/.aws/sso/cache/`. Run `aws sso login --profile my-sso-profile` before connecting. Tokens expire; the connection re-resolves credentials automatically and tells you to re-run `aws sso login` when the session has ended. + + DynamoDB connection form + DynamoDB connection form + ## Connection settings +The form asks for no host, no port and no database, and offers no SSL/TLS pane. Every request is an HTTPS call to the AWS endpoint signed with SigV4, and one connection sees one region's tables. + | Field | Description | |-------|-------------| -| **Auth Method** | Access Key + Secret Key, AWS Profile, or AWS SSO | -| **Access Key ID** | IAM access key, e.g. `AKIA…`. Access Key auth only | -| **Secret Access Key** | The matching secret. Access Key auth only | -| **Session Token** | Optional token for temporary STS credentials. Access Key auth only | -| **Profile Name** | Profile to read from `~/.aws/config` or `~/.aws/credentials`. AWS Profile and AWS SSO only | -| **AWS Region** | Region your tables live in. Defaults to `us-east-1` | -| **Custom Endpoint** | Overrides the DynamoDB endpoint URL. Leave it empty unless you run DynamoDB Local | +| **AWS Region** | Region the tables live in. Defaults to `us-east-1` | +| **Custom Endpoint** | Overrides the endpoint URL. Leave it empty unless you run DynamoDB Local | -## DynamoDB Local +## Authentication + +| Auth Method | Fields | Credentials come from | +|-------------|--------|----------------------| +| **Access Key + Secret Key** | **Access Key ID**, **Secret Access Key**, and **Session Token** for STS | What you type | +| **AWS Profile** | **Profile Name** | That profile in `~/.aws/config` and `~/.aws/credentials`, read the way the AWS CLI reads it | +| **AWS SSO** | **Profile Name** | The IAM Identity Center token cached in `~/.aws/sso/cache`. Run `aws sso login --profile ` first | -Start a local instance: +Pick **AWS Profile** if the AWS CLI already works on this Mac. Static keys, `credential_process` helpers and `role_arn` chains all resolve; the rules are on [AWS IAM Authentication](/connections/aws-iam#profiles). + +## DynamoDB Local ```bash docker run -p 8000:8000 amazon/dynamodb-local @@ -39,49 +45,30 @@ docker run -p 8000:8000 amazon/dynamodb-local Set **Auth Method** to Access Key + Secret Key, put any non-empty string in **Access Key ID** and **Secret Access Key**, and set **Custom Endpoint** to `http://localhost:8000`. -## Features - -**Table Browsing**: The sidebar lists every table. Open one to see its data, its structure (attribute names and types), its indexes (primary key, GSI, LSI), and its DDL (key schema, billing mode, throughput, item count, and size). - -**Schemaless Discovery**: DynamoDB tables have no fixed schema, so TablePro reads the attribute names from the items it fetched and picks each column's type by majority vote. The Structure tab samples up to 100 items. Partition and sort keys come first, then the other attributes in alphabetical order. - -**PartiQL Queries** ([reference](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html)): +## Columns over schemaless items -```sql --- Select items -SELECT * FROM "Users" WHERE userId = 'user123' +A table declares only its key attributes, so the grid builds columns from the items it fetched: the union of their attribute names, partition key first, sort key next, the rest alphabetically. An attribute none of those items carries gets no column. Each column's type is a majority vote over the same items; the Structure tab votes on a sample of up to 100. --- Insert an item -INSERT INTO "Users" VALUE {'userId': 'user456', 'name': 'Alice', 'age': 30} +| Attribute type | In the cell | +|----------------|-------------| +| `S`, `N`, `BOOL`, `NULL` | The value | +| `B` | Base64 | +| `L`, `M` | DynamoDB-typed JSON, `[{"S":"a"}]` and `{"k":{"S":"a"}}` | +| `SS`, `NS`, `BS` | A plain JSON array | --- Update an item -UPDATE "Users" SET name = 'Bob' WHERE userId = 'user123' +An edit is parsed back through the same shape, so keep the typed envelopes when you change a list or a map. --- Delete an item -DELETE FROM "Users" WHERE userId = 'user123' +In [Table Structure](/features/table-structure), **Indexes** lists the primary key with every GSI and LSI, and **DDL** prints the key schema, billing mode, capacity, item count, table size and each index's projection. --- Query with conditions -SELECT * FROM "Orders" -WHERE customerId = 'cust001' AND orderDate BETWEEN '2024-01-01' AND '2024-12-31' -``` - - -Table names in PartiQL must be quoted with double quotes: `"TableName"`. String values use single quotes: `'value'`. - - -**Data Types**: S (string), N (number), B (binary), BOOL, NULL, L (JSON array), M (JSON object), SS (string set), NS (number set), BS (binary set). +## PartiQL -**Data Editing**: Edit cells, insert rows, and delete rows. TablePro generates PartiQL: `INSERT INTO "Table" VALUE { … }`, `UPDATE "Table" SET attr = 'val' WHERE pk = 'pkVal'`, and `DELETE FROM "Table" WHERE pk = 'pkVal'`. Key attributes cannot be changed, so delete the item and insert it again. +The editor runs PartiQL. Table names take double quotes, string values single quotes: `SELECT * FROM "Users" WHERE userId = 'user123'`. Amazon's [PartiQL reference](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html) has the grammar. -**Query Instead of Scan**: A filter that pins the partition key to a single value with `=` runs through the Query API instead of Scan. It returns faster and burns less read capacity. - -**Capacity and Metrics**: Billing mode (`PAY_PER_REQUEST`, or `PROVISIONED` with read and write capacity units), approximate item count, table size, and GSI/LSI details. - -**Export**: CSV, JSON, SQL, XLSX formats. +Grid edits become PartiQL as well, one `INSERT`, `UPDATE` or `DELETE` per row. A key attribute is never part of a `SET`: to change a key, delete the item and insert it again. ## IAM permissions -The IAM user or role needs these permissions at minimum: +The driver calls `ListTables`, `DescribeTable`, `Scan`, `Query` and `ExecuteStatement`, and nothing else. That is this policy: ```json { @@ -94,10 +81,6 @@ The IAM user or role needs these permissions at minimum: "dynamodb:DescribeTable", "dynamodb:Scan", "dynamodb:Query", - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:UpdateItem", - "dynamodb:DeleteItem", "dynamodb:PartiQLSelect", "dynamodb:PartiQLInsert", "dynamodb:PartiQLUpdate", @@ -109,23 +92,32 @@ The IAM user or role needs these permissions at minimum: } ``` -For read-only access, remove the write actions (`PutItem`, `UpdateItem`, `DeleteItem`, `PartiQLInsert`, `PartiQLUpdate`, `PartiQLDelete`). +For read-only access, drop `PartiQLInsert`, `PartiQLUpdate` and `PartiQLDelete`. + +## Limitations + +- Consumed capacity is never reported. Nothing in the app shows what a browse cost; read it in CloudWatch. +- Paging is cursor-based, so page 40 re-scans everything before it. +- A list or map cell is cut at 10,000 characters and ends in `...`. Saving an edit to a cut cell stores the fragment; change long nested values with PartiQL instead. +- Table structure is fixed at creation: no structure editing, no transactions, no import. +- Item counts come from DynamoDB and refresh roughly every six hours, so they lag. +- DAX endpoints are not supported. Leave **Custom Endpoint** empty or point it at a standard endpoint. ## Troubleshooting -**Auth failed**: Check the access key and secret, the profile name, or the SSO session. For SSO, run `aws sso login --profile my-sso-profile`. If you pasted a temporary session token, get a fresh one from STS. +### Authentication failed: … -**Access denied**: Check the IAM policy on the user or role, the resource ARNs it covers, and any service control policies or permission boundaries above it. +The credentials were rejected: an unrecognized key, a bad signature, or a policy that denies the call. Check the key and secret, the profile name, or the SSO session with `aws sso login --profile `. An aged-out STS session token needs replacing. -**Region mismatch**: Tables are region-specific. Make sure **AWS Region** matches the region the table was created in. +### DynamoDB error: [ResourceNotFoundException] … -**Throughput exceeded**: Wait and retry, switch the table to on-demand billing, or add filters so fewer items are scanned. +The table is not in this region. Tables are regional; set **AWS Region** to the one the table was created in. -## Limitations +### DynamoDB error: [ProvisionedThroughputExceededException] … + +The table's provisioned read capacity is used up. Retry, move the table to on-demand billing, or filter on the partition key so the browse runs as a Query. + +## Related -- Every request is an independent HTTPS call signed with SigV4. There are no persistent connections and no SSH tunneling. -- Table structure is fixed when the table is created. TablePro cannot edit it. -- Item counts come from DynamoDB and refresh about every 6 hours, so they lag. -- Pagination is cursor-based, so jumping to a far page re-scans from the start. -- No transactions. -- DAX endpoints are not supported. Connect to the standard DynamoDB endpoint. +- [AWS IAM Authentication](/connections/aws-iam) +- [Import & Export](/features/import-export) diff --git a/docs/databases/elasticsearch.mdx b/docs/databases/elasticsearch.mdx index 125968fe8..cc464f28a 100644 --- a/docs/databases/elasticsearch.mdx +++ b/docs/databases/elasticsearch.mdx @@ -3,47 +3,49 @@ title: Elasticsearch description: Connect to Elasticsearch with a Query DSL console, index browsing, and document editing --- -TablePro connects to Elasticsearch 7.x and 8.x over the REST API. Browse indices as tables, run Query DSL requests in a Kibana-style console, page through documents in the data grid, and edit documents inline. Authentication is HTTP Basic, an API key, or none, over plain HTTP or TLS. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Quick setup - -Click **Create Connection…**, select **Elasticsearch**, enter host and port (default 9200), pick an auth method, and connect. +Two things share this connection. A grid browses and edits documents by `_id`, and a console that takes a method, a path and an optional JSON body the way Kibana Dev Tools does. There is no SQL behind either. Elasticsearch 7.x and 8.x are supported, over the REST API. -Elasticsearch is a registry plugin. Picking it in the **Choose a Database** sheet prompts to install; connecting to a saved Elasticsearch connection installs it without asking. You can also install **Elasticsearch Driver** from **Settings > Plugins > Browse**. + -## Authentication methods - -**Username & Password**: HTTP Basic auth. Enter the username and password in the standard connection fields. +## Quick setup -**API Key**: Paste a base64-encoded API key (the `id:api_key` pair encoded as base64, or the `encoded` value returned by the create API key request). Sent as the `Authorization: ApiKey` header. +Click **Create Connection…**, select **Elasticsearch**, enter host and port, pick an **Auth Method**, then click **Save & Connect**. -**None**: No authentication, for clusters with security disabled. +The form shows no Database field: a connection reaches one cluster, and its indices are the objects. ## Connection settings -### Required fields - | Field | Description | |-------|-------------| -| **Host** | Cluster host (e.g. `localhost`) | -| **Port** | REST port, default `9200` | +| **Host** | Cluster host, `localhost` for a local node | +| **Port** | REST port, `9200` by default | | **Auth Method** | Username & Password, API Key, or None | +| **API Key** | Base64-encoded key. Shown when **Auth Method** is API Key | +| **Skip TLS Verification** | Advanced section. Trusts any certificate, even under **Verify CA** or **Verify Identity** | -### Optional fields +## Authentication -| Field | Description | -|-------|-------------| -| **SSL Mode** | In the **SSL/TLS** pane. Any mode other than Disabled connects over HTTPS. **Preferred** and **Required (skip verify)** accept a self-signed certificate | -| **Skip TLS Verification** | Advanced section. Trusts any certificate, even with **Verify CA** or **Verify Identity** set. Elasticsearch 8.x ships with TLS on by default | -| **API Key** | Base64-encoded key, shown when Auth Method is API Key | +| Auth Method | What is sent | +|-------------|--------------| +| **Username & Password** | HTTP Basic, from the standard credential fields | +| **API Key** | An `Authorization: ApiKey` header carrying the field verbatim. Paste the `encoded` value the create API key request returns, or your own base64 of `id:api_key` | +| **None** | Nothing. For a cluster with security off | -## Features +8.x ships with security and TLS on, so **None** is a 7.x or dev-cluster setting. -**Index Browsing**: The sidebar lists indices as tables. System and hidden indices (names starting with `.`) are hidden. Each index shows its documents in the grid, its fields in the structure view, and its mapping as DDL. +## Browsing indices -**Field Discovery**: Columns come from the index mapping. Nested `object` and `nested` fields flatten into dotted paths (`address.city`). Documents always include the `_id`, `_index`, and `_score` meta columns; `_id` is the primary key. Array and object values render as JSON in the cell. +The sidebar lists indices as tables, hiding names that begin with `.`. -**Query DSL Console**: The editor is a Kibana Dev Tools-style console. Write a method, a path, and an optional JSON body: +Columns come from the index mapping. `object` and `nested` fields flatten to dotted paths such as `address.city`, and array and object values render as JSON in the cell. Every document also carries `_id`, `_index` and `_score`; `_id` is the primary key and all three are read-only. + +Column filters translate to `term`, `range`, `wildcard`, `terms` and `exists`. The **Raw SQL** filter column is the exception: its text goes over as a `query_string`, so write Lucene there, `name:Widget` or `price:>10`, matched across all fields. Sorting a `text` field targets its `.keyword` subfield when the mapping has one. + +Grid edits become REST calls keyed by `_id`: `POST /index/_update/{id}`, `PUT /index/_doc/{id}` and `DELETE /index/_doc/{id}`. + +## Query DSL console Elasticsearch Query DSL console with a search result rendered as a grid @@ -59,46 +61,33 @@ GET /my-index/_search } ``` -```http -GET /_cat/indices?format=json -``` - -```http -POST /my-index/_doc -{ - "title": "New document", - "views": 0 -} -``` - -Search responses render as a grid; `_cat` and other array responses tabularize; everything else shows as formatted JSON. - -**Filtering and Sorting**: Column filters translate to Query DSL (`term`, `range`, `wildcard`, `terms`, `exists`). The **Raw SQL** entry in the filter column picker is the default. Its text goes to Elasticsearch as a `query_string`, so write Lucene syntax such as `name:Widget` or `price:>10`, run across all fields. Sorting a `text` field automatically targets its `.keyword` subfield when one exists, since `text` fields are not directly sortable. - -**Data Editing**: Edit cells, insert documents, and delete documents. Edits map to REST operations keyed by `_id`: `POST /index/_update/{id}`, `PUT /index/_doc/{id}`, and `DELETE /index/_doc/{id}`. The `_id`, `_index`, and `_score` columns are read-only. +Search responses render as a grid, `GET /_cat/indices?format=json` and other array responses tabularize, and anything else is shown as formatted JSON. -**Pagination**: The grid pages with `from`/`size` for the first 10,000 documents, then switches to `search_after` with a point-in-time (PIT) for deeper browsing. The 10,000 threshold is fixed in the driver, matching Elasticsearch's default `max_result_window`; the index's own setting is not read, so an index with a lowered window errors before the switch. +## SSL/TLS -## Example configurations +New connections start on **Disabled**, plain HTTP with no fallback to anything else. Every other mode goes over HTTPS: **Preferred** and **Required (skip verify)** accept a self-signed certificate, **Verify CA** and **Verify Identity** check it. See [SSL/TLS](/connections/ssl). -**Local cluster**: Host `localhost`, port `9200`, Auth Method None (security disabled) or Username & Password. - -**Elastic 8.x with TLS**: Set SSL Mode to **Required (skip verify)** for the default self-signed certificate, or **Verify CA** for a trusted one, plus Username & Password. +## Limitations -**API key**: Auth Method API Key, paste the base64-encoded key. +- The SQL endpoint (`_sql`) is not reachable. Use the console. +- The sidebar lists concrete indices only, and hides every name beginning with `.`. Aliases and data streams do not appear; address them by name in the console. +- Sorting on `_id`, or on a `text` field with no `.keyword` subfield, does nothing: the column is dropped from the sort and the rows keep the order they had. Add the subfield, or sort a keyword field. +- Paging past 10,000 documents switches to `search_after` over a point-in-time. That threshold is fixed, so an index with a lowered `max_result_window` errors before the switch; raise the index setting back to 10,000. +- An array or object cell is cut at 10,000 characters and ends in `...`. Saving an edit to a cut cell stores the fragment; change long values in the console. +- No mapping or schema editing, no transactions, no import, no [SSH tunnel](/connections/ssh-tunneling). +- OpenSearch is not supported. ## Troubleshooting -**Authentication failed**: Verify the username and password, or the API key. On 8.x, security and TLS are on by default. +### Authentication failed: … -**TLS errors**: Set an SSL Mode other than Disabled. For a self-signed certificate use **Required (skip verify)**, or turn on Skip TLS Verification in the Advanced section. +The username and password, or the API key, were rejected. On 8.x check the connection is on HTTPS at all: security is on by default there. -**Sort not allowed on a text field**: Sort on the field's `.keyword` subfield, or add one to the mapping. +### Connection failed: … -## Limitations +A TLS or network failure. For a self-signed certificate set **SSL Mode** to **Required (skip verify)**, or turn on **Skip TLS Verification**. -- The SQL endpoint (`_sql`) is not supported. Use the Query DSL console. -- No schema editing and no transactions. -- [SSH tunneling](/connections/ssh-tunneling) is not available for Elasticsearch connections. -- Deep paging past 10,000 documents uses `search_after`, which can drift under concurrent writes. -- OpenSearch is not supported. +## Related + +- [Import & Export](/features/import-export) +- [Filtering](/features/filtering) diff --git a/docs/databases/etcd.mdx b/docs/databases/etcd.mdx index ef545cdd9..f65936825 100644 --- a/docs/databases/etcd.mdx +++ b/docs/databases/etcd.mdx @@ -3,35 +3,32 @@ title: etcd description: Connect to etcd v3 clusters with prefix-based key browsing and an etcdctl-style command editor --- -The driver talks to the HTTP/JSON gRPC-gateway and never speaks raw gRPC. At connect it probes the `/v3/`, `/v3beta/` and `/v3alpha/` gateway prefixes in that order, so anything from 3.2 to a current release works with no configuration. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Quick setup +etcd 3.2 and later need nothing configured beyond a host and the client port, 2379. The peer port on 2380 serves no HTTP/JSON gateway, so a connection aimed there never opens. + + -Click **Create Connection…**, select **etcd**, enter host (default `localhost`) and port (default `2379`), and connect. +## Quick setup -etcd is a registry plugin. Picking it in the **Choose a Database** sheet prompts to install; connecting to a saved etcd connection installs it without asking. You can also install **etcd Driver** from **Settings > Plugins > Browse**. +Click **Create Connection…**, select **etcd**, enter host and port, then click **Save & Connect**. etcd has no databases, so there is no Database field and nothing to switch between. ## Connection settings | Field | Description | |-------|-------------| -| **Host** | etcd server hostname or IP (default `localhost`) | -| **Port** | Client port, default `2379` | -| **Username / Password** | Only if etcd authentication is enabled | - -With a username set, TablePro exchanges the credentials for a gateway token via the gateway's `auth/authenticate` endpoint and retries once with a fresh token when a request returns 401. +| **Host** | etcd server hostname or IP. `localhost` by default | +| **Port** | Client port, `2379` by default | +| **Username / Password** | Only when etcd authentication is enabled | ### Advanced fields | Field | Description | |-------|-------------| -| **Key Prefix Root** | Scope browsing to a subtree (e.g. `/app`). Empty browses the whole keyspace | +| **Key Prefix Root** | Scope browsing to a subtree, `/app`. Empty browses the whole keyspace | | **TLS Mode** | Disabled, Required (skip verify), Verify CA, or Verify Identity | -| **CA Certificate** | Path to the CA certificate (`ca.pem`) for Verify CA and Verify Identity | -| **Client Certificate** | Path to the client certificate for mutual TLS | -| **Client Key** | Path to the client private key for mutual TLS | - -For self-signed certificates, pick **Required (skip verify)** or set the **CA Certificate** path. +| **CA Certificate** | Path to `ca.pem`, for Verify CA and Verify Identity | +| **Client Certificate** / **Client Key** | Paths to the client certificate and private key, for mutual TLS | ## Connection URL @@ -40,15 +37,17 @@ etcd://127.0.0.1:2379 etcds://127.0.0.1:2379 ``` -Both schemes are accepted when importing a URL. The scheme alone does not switch TLS on; set **TLS Mode** in the Advanced fields. +Both schemes import. Neither switches TLS on by itself; set **TLS Mode** in the Advanced fields. ## Browsing keys -The sidebar groups keys by their first path segment under the Key Prefix Root; keys without a segment appear under **(root)**. The grid shows one row per key with the columns **Key**, **Value**, **Version**, **ModRevision**, **CreateRevision**, and **Lease**. Key is the primary key. Saving grid edits generates `put` and `del` commands. +The sidebar groups keys by their first path segment under the **Key Prefix Root**, with segment-less keys under **(root)**. The grid gives one row per key: **Key**, **Value**, **Version**, **ModRevision**, **CreateRevision** and **Lease**. Key is the primary key and the three revision columns are read-only. + +Saving edits generates commands. Changing **Value** or **Lease** re-`put`s the key. Changing **Key** is a `put` at the new name and a `del` of the old one, so the old key is deleted rather than moved. A new row whose key does not start with `/` gets the **Key Prefix Root** prepended. ## Command editor -The editor accepts etcdctl-style commands with autocomplete for commands and flags. +The editor takes etcdctl-style commands, with autocomplete over both commands and flags. etcd command editor with a get --prefix result grid @@ -57,55 +56,47 @@ The editor accepts etcdctl-style commands with autocomplete for commands and fla ```bash get /app/config --prefix --limit=50 --keys-only -put /app/config/timeout 30 put /app/session/abc hello --lease=694d77aa1e775e08 del /app/cache --prefix watch /app/config --prefix --timeout=60 -``` - -`get` supports `--prefix`, `--limit=N`, `--keys-only`, `--order` (ASCEND or DESCEND), and `--sort-by` (KEY, VERSION, CREATE, MOD, or VALUE). `watch` collects events for `--timeout` seconds (default 30), then returns them as a grid. - -Lease, cluster, and maintenance commands: - -```bash lease grant 300 -lease timetolive 694d77aa1e775e08 --keys -lease revoke 694d77aa1e775e08 -lease list -member list -endpoint status -endpoint health compaction 1000 --physical ``` -Lease IDs accept decimal or hex. Hex is detected by its letters, so prefix an all-digit hex ID with `0x` (`--lease=0x69407712`) or it is read as decimal and resolves to a different lease. +The whole vocabulary is `get`, `put`, `del` (or `delete`), `watch`, `lease grant|revoke|timetolive|list|keep-alive`, `member list`, `endpoint status|health`, `compaction`, `auth enable|disable`, `user add|delete|list|grant-role|revoke-role`, and `role add|delete|list`. Anything else, an etcdctl command with no entry here included, comes back as `Unknown command: …` without reaching the server. -Auth, user, and role management: +| Command | Flags | +|---------|-------| +| `get` | `--prefix`, `--limit=N`, `--keys-only`, `--order=ASCEND` or `DESCEND`, `--sort-by=KEY`, `VERSION`, `CREATE`, `MOD` or `VALUE`. Leave the last two out and the server's own ordering applies | +| `put` | `--lease=ID` | +| `del` | `--prefix` | +| `watch` | `--prefix`, `--timeout=SECONDS`, 30 by default. Events collect for that long, then arrive as a grid | +| `lease timetolive` | `--keys` | -```bash -auth enable -user add alice secretpw -user grant-role alice readonly -role add readonly -user list -role list -``` +Lease IDs take decimal or hex, and hex is detected by its letters. Prefix an all-digit hex ID with `0x` (`--lease=0x69407712`) or it reads as decimal and resolves to another lease. -## SSH Tunnel +## SSL/TLS -etcd connections support [SSH tunneling](/connections/ssh-tunneling) for reaching remote clusters through a bastion host. +TLS here is set by **TLS Mode** in the Advanced fields, not by the connection's SSL mode, and it starts **Disabled**. **Required (skip verify)** encrypts without checking the certificate, **Verify CA** checks it against the **CA Certificate** you name, and **Verify Identity** adds the hostname check. **Client Certificate** and **Client Key** cover mutual TLS under either verifying mode. See [SSL/TLS](/connections/ssl) for the concepts. + +## Limitations + +- No transactions. Each command, and each row you save, is its own gateway request. +- The form takes one endpoint and does not fail over to another member. Point it at a member that is up; `member list` shows the rest. +- A value that is not valid UTF-8 shows in the grid as `` wrapping its base64, and text you type is sent as UTF-8. Write binary values with `etcdctl`. +- No schema editing and no import. ## Troubleshooting -**Connection refused**: Verify etcd is running and the client port (default 2379) is reachable. +### No supported etcd API found -**No supported etcd API found**: TablePro requires the HTTP/JSON gateway, which etcd 3.x serves on the client port by default. Point the connection at the client port, not the peer port (2380). +The HTTP/JSON gateway answered on none of the v3 paths. Point the connection at the client port, 2379 by default, and not the peer port on 2380. -**Authentication failed**: Check the username and password, and that the user has the required roles. +### Authentication failed -**TLS errors**: Set **TLS Mode** and the certificate paths in the Advanced fields. For self-signed certificates use **Required (skip verify)** or provide the CA certificate. +The username and password were rejected, or the user holds no role covering the key. Check both, and that `auth enable` has been run. -## Limitations +## Related -- No transactions. Each command is a separate gateway request. -- No schema editing and no import. +- [SSH Tunneling](/connections/ssh-tunneling), for reaching a cluster through a bastion host +- [Import & Export](/features/import-export) diff --git a/docs/databases/index.mdx b/docs/databases/index.mdx new file mode 100644 index 000000000..9bda24907 --- /dev/null +++ b/docs/databases/index.mdx @@ -0,0 +1,70 @@ +--- +title: Supported Databases +description: All 27 engines TablePro connects to, their default ports, and which ones need a plugin +--- + +import DriverCounts from "/snippets/driver-counts.mdx"; + +Twenty-seven engines, and every one of them is free to use. What differs between them is where the +driver comes from, not what the license covers. + + + +## The full list + +| Database | Default port | Driver | +|---|---|---| +| [Amazon Redshift](/databases/redshift) | 5439 | Built-in | +| [Beancount](/databases/beancount) | File-based | Plugin | +| [BigQuery](/databases/bigquery) | API-based | Plugin | +| [Cassandra](/databases/cassandra) | 9042 | Plugin | +| [ClickHouse](/databases/clickhouse) | 8123 | Built-in | +| [Cloudflare D1](/databases/cloudflare-d1) | API-based | Plugin | +| [CockroachDB](/databases/cockroachdb) | 26257 | Built-in | +| [Dameng DM8](/databases/dameng) | 5236 | Plugin | +| [DuckDB](/databases/duckdb) | File, or 9494 remote | Plugin | +| [DynamoDB](/databases/dynamodb) | API-based | Plugin | +| [Elasticsearch](/databases/elasticsearch) | 9200 | Plugin | +| [etcd](/databases/etcd) | 2379 | Plugin | +| [libSQL / Turso](/databases/libsql) | API-based | Plugin | +| [MariaDB](/databases/mariadb) | 3306 | Built-in | +| [Microsoft SQL Server](/databases/mssql) | 1433 | Plugin | +| [MongoDB](/databases/mongodb) | 27017 | Plugin | +| [MySQL](/databases/mysql) | 3306 | Built-in | +| [Oracle Database](/databases/oracle) | 1521 | Plugin | +| [PGlite](/databases/pglite) | 5432 | Built-in | +| [PostgreSQL](/databases/postgresql) | 5432 | Built-in | +| [Redis](/databases/redis) | 6379 | Built-in | +| [ScyllaDB](/databases/cassandra) | 9042 | Plugin | +| [Snowflake](/databases/snowflake) | 443 | Plugin | +| [SQLite](/databases/sqlite) | File-based | Built-in | +| [SurrealDB](/databases/surrealdb) | 8000 | Plugin | +| [Teradata](/databases/teradata) | 1025 | Plugin | +| [Trino](/databases/trino) | 8080 | Plugin | + +Rows sharing a page share a driver. MariaDB reads as MySQL, ScyllaDB as Cassandra, and Redshift, +CockroachDB and PGlite all speak the PostgreSQL wire protocol. + +## Built-in against plugin + +A built-in driver is already in the app and connects with nothing else installed. A plugin is a +signed bundle TablePro fetches the first time you pick that engine, before the connection form +opens and without a restart. After that the two behave the same: same grid, same editor, same +export. + +The split is about size, not capability. Bundling every client library would put hundreds of +megabytes of Oracle and MongoDB code in an app most people use for one database. + +Plugins update on their own schedule, so a driver fix can reach you without waiting for an app +release. [Plugins & Themes](/features/plugins) covers installing, updating and removing them. + +## When yours is not here + +Two of these engines already stand in for others. Anything that speaks the PostgreSQL or MySQL wire +protocol will usually connect through those drivers, including managed services that never say so: +Neon, Supabase, Heroku, Aurora and Cloud SQL all work this way. Point the connection at the +host they give you. + +If it speaks neither, open a request on +[GitHub](https://github.com/TableProApp/TablePro/issues). Drivers are plugins, so a new one is a +bundle rather than a new version of the app. diff --git a/docs/databases/libsql.mdx b/docs/databases/libsql.mdx index 1765bd01b..2c73470c4 100644 --- a/docs/databases/libsql.mdx +++ b/docs/databases/libsql.mdx @@ -3,90 +3,97 @@ title: libSQL / Turso description: Connect to Turso cloud databases, self-hosted sqld, or local libSQL files --- -**Connection Mode** in the connection form decides everything else on it. Remote runs the Hrana HTTP protocol against Turso or a self-hosted sqld; Local opens a libSQL file from disk, and the fields change to match. The driver is a downloadable plugin; the install is offered the first time you select it. See [Plugins](/features/plugins). +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Connection settings +**Connection Mode** decides every other field on the form. Remote speaks the Hrana HTTP protocol to Turso or a self-hosted sqld; Local File opens a libSQL file through the system SQLite engine. -Pick a mode with the **Connection Mode** dropdown: + -| Mode | Description | -|------|-------------| -| **Remote (Turso)** | Connects over HTTP using the Hrana protocol. For Turso cloud and self-hosted sqld. | -| **Local File** | Opens a libSQL database file directly from disk using the system SQLite engine. Unencrypted libSQL files use the standard SQLite file format. | +## Quick setup -### Remote fields +Click **Create Connection…**, select **libSQL / Turso**, pick a **Connection Mode**, fill in the fields it leaves on screen, then click **Test Connection** and **Save & Connect**. + + + libSQL connection form showing Remote (Turso) and Local File modes + libSQL connection form showing Remote (Turso) and Local File modes + + +## Connection settings + +### Remote (Turso) | Field | Description | |-------|-------------| -| **Database URL** | Full URL to your database, e.g. `https://your-db-name.turso.io` or `http://localhost:8080`. `libsql://` URLs are rewritten to `https://` automatically. | -| **API Token** | Bearer token, entered in the built-in password field (labeled **API Token** for this driver). Optional for self-hosted sqld without auth. Stored in the macOS Keychain. | +| **Database URL** | `https://your-db-name.turso.io`, or `http://localhost:8080` for sqld. A `libsql://` URL is rewritten to `https://`, and trailing slashes are dropped | +| **API Token** | The bearer token, in the password field, labeled **API Token** for this driver. Leave it empty for a sqld started without `--auth-jwt-key-file`. Stored in the macOS Keychain | + +There is no host, no port and no Database field: the URL names one database and the connection stays on it. Any Turso database works, as does any sqld serving the Hrana v2 pipeline endpoint. -### Local file fields +### Local File | Field | Description | |-------|-------------| -| **Database File** | Path to the file. Use **Browse…** to pick it, or type a path (`~` is expanded). | +| **Database File** | Path to the file. **Browse…** picks one and `~` expands. A path that does not exist is created, along with any missing folders | -In Local File mode the API Token field is hidden; local files need no authentication. +The API Token field is hidden here; a local file needs no authentication. - - libSQL connection form showing Remote (Turso) and Local File modes - libSQL connection form showing Remote (Turso) and Local File modes - + +Do not open a file that is being synced as a Turso embedded replica while the owning app is running. Concurrent access to a syncing replica can corrupt it. + -Click **Test Connection** to verify, then **Save & Connect**. +## Connection URL -## Getting your credentials +```text +libsql://your-db-name.turso.io +https://your-db-name.turso.io +``` + +Importing either creates a Remote connection. See [Connection URL Reference](/connections/urls). -### Turso Cloud +## Getting a token -Find the database URL on the [Turso dashboard](https://turso.tech/app) or with the CLI: +Take the URL and a fresh token from the Turso [dashboard](https://turso.tech/app) or the CLI: ```bash turso db show --url turso db tokens create ``` -### Self-hosted sqld +A self-hosted sqld answers on `http://localhost:8080` by default. With JWT authentication on, generate a token matching the key the server started with. -The default sqld HTTP endpoint is `http://localhost:8080`. If you started sqld without `--auth-jwt-key-file`, leave the token empty. With JWT authentication enabled, generate a token matching your configured key. +## Schema and SQL - -Do not open a file that is being synced as a Turso embedded replica while the owning app is running. Concurrent access to a syncing replica can corrupt it. - +libSQL takes SQLite syntax, so browsing, `EXPLAIN QUERY PLAN`, data editing and export behave as they do on [SQLite](/databases/sqlite). The structure editor creates tables, adds and drops columns, and adds and drops indexes; **Triggers** in the Structure tab creates, edits and drops those. -## Schema and triggers +## SSL/TLS -The structure editor creates tables, adds and drops columns, and creates and drops indexes. It cannot change an existing column's name, type, nullability, or default: recreate the table for that. Triggers appear under **Triggers** in the Structure tab, where you can create, edit, and drop them. +There is no SSL/TLS pane. A Remote connection is encrypted when its **Database URL** starts with `https://` and is not when it starts with `http://`, and a local file is not networked at all. See [SSL/TLS](/connections/ssl). -## SQL dialect +## Limitations -libSQL uses SQLite syntax; browsing, EXPLAIN QUERY PLAN, data editing, and export work as they do for SQLite. See [SQLite](/databases/sqlite). +- Remote mode runs no transaction. Each statement is its own HTTP request and auto-commits, and `BEGIN` fails with `Transactions are not supported in this mode`. Use Local File mode for transactional work. +- Local File mode does not sync. The file opens standalone, with no embedded-replica sync to a remote Turso database. Reach the remote copy through Remote mode. +- libSQL-only SQL such as `ALTER TABLE ALTER COLUMN` does not run in Local File mode. The system SQLite engine gets the statement and rejects it. Rewrite it as standard SQLite DDL. +- Encrypted libSQL files do not open in Local File mode. +- The structure editor cannot rename a column, change its type, nullability or default, or edit a foreign key. Recreate the table instead. +- One connection, one database. A switch returns `Switching databases is not supported`, and there is no create or drop. +- No bulk import, no SSH tunnel, no custom TLS settings. Load data with the Turso CLI or a direct sqld import. ## Troubleshooting -**Authentication failed (HTTP 401)**: The token is wrong or expired. Generate a new one with `turso db tokens create `; for sqld, check the JWT matches the configured key. Watch for stray whitespace in the pasted token. +### Authentication failed -**Invalid URL**: Use `https://your-db.turso.io` for Turso or `http://localhost:8080` for sqld, without a trailing slash or path. `libsql://` URLs are accepted. Verify the database exists with `turso db list`. - -**File not found (local mode)**: Re-pick the file with **Browse…** if it was moved or renamed. Encrypted libSQL files cannot be opened in local mode. - -## Limitations +The full message is `Authentication failed. Check your auth token.` The server answered 401 or 403. Generate a new token with `turso db tokens create `; for sqld, check the JWT matches the configured key. Watch for whitespace picked up in the paste. -Remote mode: +### Server not found -- No persistent connections: each query is an independent HTTP request via the Hrana protocol -- No multi-statement transactions: each SQL statement auto-commits independently -- No custom SSL/SSH tunnels: connections use HTTPS (Turso Cloud) or HTTP (local sqld) +`Server not found. Check your database URL.` is a 404. The URL wants the bare origin, `https://your-db.turso.io` or `http://localhost:8080`, with no path. Confirm the database exists with `turso db list`. -Local mode: +### Rate limited -- No embedded replica sync: local files open standalone, without syncing to a remote Turso database -- No encrypted database files -- libSQL-only SQL extensions (e.g. `ALTER TABLE ALTER COLUMN`) are unavailable; local mode uses the system SQLite engine +`Rate limited. Retry after … seconds.` means Turso is throttling the connection. Wait out the interval it names, then retry. -Both modes: +## Related -- No column rename, retype, or foreign key editing in the structure editor -- No database creation or switching from TablePro; a connection targets a single database -- No bulk import through the plugin: use the Turso CLI or direct sqld import +- [SQLite](/databases/sqlite) +- [Import & Export](/features/import-export) diff --git a/docs/databases/mariadb.mdx b/docs/databases/mariadb.mdx index 219b6e3db..76574b095 100644 --- a/docs/databases/mariadb.mdx +++ b/docs/databases/mariadb.mdx @@ -3,7 +3,7 @@ title: MariaDB description: Connect to MariaDB 10.x and later through the bundled MySQL driver --- -Setup lives on the [MySQL](/databases/mysql) page. Picking **MySQL** or **MariaDB** in the connection form loads the same plugin, built on MariaDB Connector/C 3.4.8, so the fields, [AWS IAM](/connections/aws-iam), SSL/TLS and the rest of the feature set behave identically. Cloud SQL Auth Proxy is the one exception: that pane is offered for MySQL, PostgreSQL, and SQL Server only. +MySQL and MariaDB load the same bundled driver, so [MySQL](/databases/mysql) carries the setup. Port `3306`, an optional Database field, `Cmd+K` to change database in place, SSL/TLS starting on **Preferred**, [AWS IAM](/connections/aws-iam) and SSH tunnels. MariaDB 10.x and later are supported. The rest of this page is what differs. ## Connection URL @@ -17,15 +17,22 @@ See [Connection URL Reference](/connections/urls). ## Differences from MySQL -- The default authentication plugin is `mysql_native_password`; MySQL 8 defaults to `caching_sha2_password`. Both work. -- MariaDB stores JSON columns as LONGTEXT. TablePro reads MariaDB's extended field attributes (`MARIADB_FIELD_ATTR_FORMAT_NAME`) to recognize those columns as JSON, so values open in the JSON editor instead of plain text. +- The default authentication plugin is `mysql_native_password`, against `caching_sha2_password` on MySQL 8. Both connect. +- MariaDB stores JSON in a LONGTEXT column. The driver reads MariaDB's extended field attributes and types those columns as JSON, so their values open in the JSON editor instead of as plain text. +- Virtual and persistent columns are detected on every version, including the bare `VIRTUAL` and `PERSISTENT` spelling that 10.1 and older report, and are left out of generated INSERT and UPDATE statements. +- A query timeout goes in as `SET SESSION max_statement_time`, counted in seconds. MySQL takes `max_execution_time` in milliseconds. JSON cell editor popover JSON cell editor popover -## See also +## Limitations -- [MySQL](/databases/mysql): setup, connection fields, SSL/TLS, troubleshooting +- Cloud SQL Auth Proxy is not offered for MariaDB. That pane appears for MySQL, PostgreSQL and SQL Server only. Reach a server that is not directly routable through an [SSH tunnel](/connections/ssh-tunneling). +- [Users & Roles](/features/users-roles) reads `mysql.user` and reports every row there as a user, never as a role. Create and grant MariaDB roles with SQL in the editor. + +## Related + +- [MySQL](/databases/mysql), for connection fields, common setups, SSL/TLS and troubleshooting - [SSH Tunneling](/connections/ssh-tunneling) diff --git a/docs/databases/mongodb.mdx b/docs/databases/mongodb.mdx index bfb8e4819..7bc93a9f4 100644 --- a/docs/databases/mongodb.mdx +++ b/docs/databases/mongodb.mdx @@ -3,18 +3,20 @@ title: MongoDB description: Connect to MongoDB with MQL shell queries, collection browsing, and automatic Atlas SRV setup --- -Collections appear as tables in the sidebar. Documents display with top-level fields as columns and nested objects as formatted JSON. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -MongoDB is a registry plugin. Picking it in the **Choose a Database** sheet prompts to install; connecting to a saved MongoDB connection installs it without asking. You can also install **MongoDB Driver** from **Settings > Plugins > Browse**. +Filters and pipelines are parsed as JSON and Extended JSON, never as JavaScript, so every key needs quoting, operators included: `{age: {$gte: 18}}` fails with a parse error. Collections appear as tables in the sidebar, with top-level fields as columns and nested objects as formatted JSON. + + ## Quick setup - Click **Create Connection…**, select **MongoDB**, enter hosts and credentials, and click **Save & Connect** + Click **Create Connection…**, select **MongoDB**, and enter hosts and credentials - Click **Test Connection** to verify + Click **Test Connection** to verify, then **Save & Connect** @@ -22,46 +24,25 @@ MongoDB is a registry plugin. Picking it in the **Choose a Database** sheet prom | Field | Default | Notes | |-------|---------|-------| -| **Hosts** | `localhost:27017` | Add multiple `host:port` pairs for replica sets | +| **Hosts** | `localhost:27017` | Several `host:port` pairs for a replica set | | **Username** | - | Leave empty for local dev without auth | | **Password** | - | | -| **Database** | - | The database to open on connect. Leave empty to browse them all | -| **Auth Mechanism** | Default | SCRAM-SHA-1, SCRAM-SHA-256, X.509, or AWS IAM. In the Authentication section | - -Naming a **Database** opens straight into it and skips listing every database on the server, which is worth doing on a cluster with many of them. Leave it empty and TablePro opens the first non-system database it finds. `Cmd+K` switches to another either way. A database in a connection URL path (`mongodb://host:27017/mydb`) fills the same field. +| **Database** | - | Opens straight into that database. Leave it empty to browse them all | +| **Auth Database** | - | Advanced. Where your user account lives, usually `admin` | +| **Auth Mechanism** | Default | SCRAM-SHA-1, SCRAM-SHA-256, X.509, or AWS IAM, under Authentication | -**Database** is not the same as **Auth Database**: one says what you browse, the other says where your user account lives. They are linked in one direction. With **Auth Database** empty, a named **Database** is also what TablePro authenticates against, matching how a `/database` path works in a MongoDB connection string. So if your user lives in `admin` and you name a **Database**, set **Auth Database** to `admin` as well, or authentication fails. SRV connections are exempt and always authenticate against `admin` unless you say otherwise. +Naming a **Database** skips listing every database on the server, which is worth doing on a cluster with hundreds. Leave it empty and the first non-system database opens instead. `Cmd+K` switches either way, on the same connection, with no reconnect. -**Advanced**: **Auth Database** (usually `admin`), **Read Preference**, **Write Concern**, **Use SRV Record**, **Replica Set** name, and **Legacy UUID Encoding**. +**Auth Database** is a separate question: it says where your account is defined, not what you browse. Left empty, it follows the **Database** field. An account defined in `admin` needs **Auth Database** set to `admin` whenever **Database** names something else, or authentication fails. SRV connections authenticate against `admin` regardless, unless told otherwise. Switching databases in the app never changes it, so browsing a database your user has no account in is fine. -Leaving **Auth Database** empty authenticates against the database in the connection URL path, or `admin` when there is none. Switching databases in the app never changes it, so browsing a database your user has no account in does not break the connection. +Also in Advanced: **Read Preference**, **Write Concern**, **Use SRV Record**, **Replica Set** name, and **Legacy UUID Encoding**. There is no minimum server version; the driver adapts what it asks for to what the server answers. MongoDB connection form with the multi-host Hosts editor MongoDB connection form with the multi-host Hosts editor -## MongoDB Atlas (SRV) - -The **Use SRV Record** toggle in Advanced connects with the `mongodb+srv://` scheme. For hosts ending in `.mongodb.net`, TablePro enables SRV automatically and turns TLS on if the SSL mode is Disabled, since Atlas requires both. An Atlas connection needs only the cluster hostname, username, and password. - -## Replica sets - -The **Hosts** field accepts multiple `host:port` pairs separated by commas (for example `host1:27017,host2:27017,host3:27017`). TablePro discovers the primary automatically and routes writes there. Set the replica set name in **Advanced**. - -You can also paste a multi-host URI directly: - -```text -mongodb://user:pass@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0 -``` - - -Over an SSH tunnel TablePro connects to the first host only and drops the rest of the list. Replica set discovery and failover are off for that session. - - -## SSL/TLS - -The MongoDB driver has no TLS fallback. **Preferred** behaves the same as **Required** (the SSL pane shows a warning). For unencrypted local instances, use **Disabled** or [SSH tunneling](/connections/ssh-tunneling). See [SSL/TLS](/connections/ssl) for details. +On MongoDB 4.0 and later the database list is requested as authorized databases only, so an account without the `listDatabases` privilege still sees what it can read. On an older server that list comes back empty: name a **Database** on the connection instead. ## Connection URL @@ -70,99 +51,100 @@ mongodb://user:password@host:27017/database?authSource=admin mongodb+srv://user:password@cluster.mongodb.net/database ``` -The `mongodb+srv://` scheme resolves hosts through DNS SRV records and does not allow a port. If you paste an SRV URL that includes one (for example `cluster.mongodb.net:27017`), TablePro strips it before connecting. The plain `mongodb://` scheme keeps any port you provide. +`mongodb+srv://` resolves hosts through DNS SRV records and takes no port; one pasted into the field is stripped before connecting. Plain `mongodb://` keeps whatever port you give it. See [Connection URL Reference](/connections/urls). -See [Connection URL Reference](/connections/urls) for all parameters. +## MongoDB Atlas (SRV) -## Features +An Atlas connection needs the cluster hostname, a username, and a password. Atlas requires SRV and TLS, so a host ending in `.mongodb.net` gets both turned on for you, TLS only if the SSL mode was still Disabled. Elsewhere the **Use SRV Record** toggle in Advanced does the same job. Add your current IP to the cluster's access list in the Atlas console first: traffic from an address that is not on it times out rather than failing. -**Collection Browsing**: Sidebar shows all collections. Click to view documents in the data grid. Top-level fields render as columns, nested objects and arrays as formatted JSON, ObjectIds as strings. The grid schema is inferred by sampling documents. +## Replica sets -**Nested Field Filtering**: The filter bar's column picker lists the paths inside a nested object or an array of objects, so `customer.country` and `items.sku` can be filtered directly. A row on a field inside an array chooses between **any element** (dot notation, where two rows may be satisfied by two different array entries) and **same element**, which groups every row set to it into one `$elemMatch` so they must all match one entry. Filter values are compared as the type the field holds, so a range on a date, ObjectId or `NumberDecimal` field is sent as `$date`, `$oid` or `$numberDecimal` rather than as a string that MongoDB's type bracketing would never match. See [Filtering](/features/filtering#nested-fields). +The **Hosts** field takes comma-separated pairs (`host1:27017,host2:27017,host3:27017`), the primary is discovered, and writes are routed to it. Set the replica set name in Advanced. A multi-host URI pastes in directly: -A field whose name contains a literal dot is left out of the picker: MongoDB reads the dot in a query as a path separator, so filtering it would address a different field. Reach it with `$getField` inside `$expr` in a raw filter or a query tab. +```text +mongodb://user:pass@host1:27017,host2:27017,host3:27017/db?replicaSet=rs0 +``` -**Binary UUIDs**: A binary subtype 4 field renders as `UUID("8cd003eb-4a25-4324-9332-88fce2da0d1a")`. Subtype 3 is the older format the Java, C# and Python drivers each wrote with a different byte order, and nothing in the stored bytes says which one produced them. TablePro shows subtype 3 as `BinData(3, "…")` until you pick a byte order in **Legacy UUID Encoding** on the connection, then renders it as `LegacyJavaUUID("…")`, `LegacyCSharpUUID("…")`, or `LegacyPythonUUID("…")`. The names match `mongosh` 2.6 and Compass 1.49, so a copied value pastes into either. + + +Over an SSH tunnel only the first host is used and the rest of the list is dropped. Replica set discovery and failover are off for that session, with nothing on screen to say so. + + -The setting only changes how TablePro reads the bytes; it never rewrites what is stored. Picking the wrong encoding shows a valid-looking but wrong UUID, so match it to the driver that wrote the data. Filtering, editing and MQL export all use the same encoding, and a value you type back is written as binary with the subtype it came from. Pasting a connection URL that carries `uuidRepresentation=javaLegacy` sets it, and Copy URL includes it. Changing it takes effect on the next connect. +## Browsing collections -**New Database**: The dialog asks for a database name and the name of its first collection. Both are required. MongoDB stores a database only once it holds a collection, so a name on its own would disappear as soon as the sidebar refreshed. The collection is created empty. +Click a collection to page through its documents. Column layout is inferred by sampling documents, not read from a validator, so a field missing from the sample gets no column. ObjectIds render as strings, arrays and nested objects as formatted JSON. -**Views**: **New View** in the sidebar opens a query tab with a `db.createView("view_name", "source_collection", [pipeline])` template. Editing a view definition pre-fills a `db.runCommand({"collMod": …})` command. +The filter bar's column picker lists paths inside nested objects and arrays of objects, so `customer.country` and `items.sku` filter directly; a row on an array field chooses **any element** or **same element**, which makes one array entry satisfy every row set to it. See [Filtering](/features/filtering#nested-fields). A field name containing a literal dot is left out of the picker, since MongoDB reads a dot as a path separator; reach it with `$getField` inside `$expr`. -**Explain**: Press `Cmd+Option+E` on an MQL statement. TablePro converts `find`, `aggregate`, `countDocuments`, update, delete, and `findOneAnd*` calls into `db.runCommand({"explain": …, "verbosity": "executionStats"})`. +The Structure tab lists a collection's indexes; create and drop them from a query tab with `db.users.createIndex({"email": 1})` and `db.users.dropIndex("email_1")`. **New Database** asks for a database name and a first collection, both required. **New View** opens a query tab holding a `db.createView("view_name", "source_collection", [pipeline])` template, and editing a view pre-fills `db.runCommand({"collMod": …})`. -**MQL Shell Queries**: Filters and pipelines are parsed as JSON and Extended JSON, not JavaScript. Quote every key, operators included: `{age: {$gte: 18}}` fails with a parse error. The editor has no comment syntax, so a `//` line becomes part of the statement. +### Binary UUIDs -Value constructors are translated for you, so a value copied out of the grid can be pasted straight into a filter: `ObjectId`, `ISODate`, `Date`, `NumberInt`, `NumberLong`, `NumberDecimal`, `Timestamp`, `BinData`, `HexData`, `MinKey`, `MaxKey`, `UUID`, and the legacy UUID names. `db.users.find({_id: ObjectId("507f1f77bcf86cd799439011")})` works. A constructor inside a string is left alone, and a method name is never mistaken for one. Anything else JavaScript, including `new Date()`, arithmetic and regex literals like `/abc/i`, is still not evaluated. +A binary subtype 4 field renders as `UUID("8cd003eb-4a25-4324-9332-88fce2da0d1a")`. Subtype 3 is the legacy format, and its bytes do not say which driver wrote them, so it stays `BinData(3, "…")` until **Legacy UUID Encoding** on the connection is set to Java, C#, or Python. Match it to the driver that wrote the data: the wrong choice shows a valid-looking but wrong UUID. -Find with a filter: +Once set, the value renders as `LegacyJavaUUID("…")` and reads that way everywhere, filters and MQL export included. Nothing stored is rewritten, `uuidRepresentation=javaLegacy` in a pasted URL sets the same option, and a change takes effect on the next connect. -```javascript -db.users.find({"age": {"$gte": 18}, "active": true}) -``` +## Writing MQL -Find with a projection and a sort: +Every key is quoted, and the editor has no comment syntax, so a `//` line becomes part of the statement. Value constructors are translated, so a value copied out of the grid pastes straight into a filter: `ObjectId`, `ISODate`, `Date`, `NumberInt`, `NumberLong`, `NumberDecimal`, `Timestamp`, `BinData`, `HexData`, `MinKey`, `MaxKey`, `UUID`, and the legacy UUID names. Anything else JavaScript, including `new Date()`, arithmetic, and regex literals such as `/abc/i`, is not evaluated. ```javascript db.orders.find( {"status": "completed"}, - {"customerId": 1, "total": 1, "date": 1} + {"customerId": 1, "total": 1} ).sort({"date": -1}).limit(20) -``` - -Aggregation pipeline, with dates in Extended JSON: -```javascript db.sales.aggregate([ {"$match": {"date": {"$gte": {"$date": "2025-01-01T00:00:00Z"}}}}, - {"$group": {"_id": "$product", "totalSales": {"$sum": "$amount"}}}, - {"$sort": {"totalSales": -1}}, - {"$limit": 10} + {"$group": {"_id": "$product", "totalSales": {"$sum": "$amount"}}} ]) ``` -Count documents: +### Collection references -```javascript -db.users.countDocuments({"role": "admin"}) -``` +`db.users`, `db["users"]`, or `db.getCollection("users")`. The last two take the name exactly as written, so use them for names with dots or spaces, names starting with a digit, and names that collide with a database method such as `stats`. + +### Chained methods -**Collection references**: Write `db.users`, `db["users"]`, or `db.getCollection("users")`. The bracket and `getCollection` forms take the name exactly as written, so use them for names that contain dots or spaces, start with a digit, or match a database method such as `stats` or `version`. Single quotes and backticks work in place of double quotes. `db.getSiblingDB(…)` is not supported: a query always runs against the database the connection is using. +`.sort()`, `.limit()` and `.skip()` chain onto `find` and onto `aggregate`, where they become `$sort`, `$skip` and `$limit` stages appended in that order. Chaining onto something that returns no cursor, such as `insertOne`, is an error, as is a method the parser does not know. -**Chained methods**: `.sort()`, `.limit()` and `.skip()` chain onto `find` and onto `aggregate`. On an aggregation they become `$sort`, `$skip` and `$limit` stages appended to the pipeline, in that order, which is what the cursor methods mean. Chaining onto a call that returns no cursor, such as `insertOne`, is an error, and so is a method the parser does not recognize. Nothing is dropped silently. +### Write options -**Write options**: `updateOne`, `updateMany`, `replaceOne` and `findOneAndUpdate` take a third options document. `upsert`, `arrayFilters` and `hint` are passed to the server. The option used to be dropped without a word, so an upsert quietly did nothing. +`updateOne`, `updateMany`, `replaceOne` and `findOneAndUpdate` take a third options document, and `upsert`, `arrayFilters` and `hint` reach the server. The result reports `modifiedCount` and `upsertedCount`. ```javascript db.users.updateOne({_id: 1}, {"$set": {"active": true}}, {"upsert": true}) ``` -The result reports `modifiedCount` and `upsertedCount`. +### Supported methods -**Formatting**: `Cmd+Shift+F` lays out filters and pipelines by nesting depth. Short documents stay on one line; anything nested or long is expanded. It used to run the SQL formatter over MQL. +Collection-level `find`, `findOne`, `aggregate`, `countDocuments`/`count`, `insertOne`/`insertMany`, `updateOne`/`updateMany`, `replaceOne`, `deleteOne`/`deleteMany`, `findOneAndUpdate`/`findOneAndReplace`/`findOneAndDelete`, `createIndex`, `dropIndex`, `drop`; database-level `getCollectionNames`/`listCollections`, `createCollection`, `dropDatabase`, `version`, `stats`. Anything else goes through `db.runCommand({…})` or `db.adminCommand({…})`. An unlisted shell method such as `distinct` or `getUsers` returns an unsupported-method error. -**Autocomplete**: The editor suggests collections, methods, field names including nested paths such as `address.city`, and the `$` operators that are valid at the cursor. See [Autocomplete](/features/autocomplete#mongodb). +`Cmd+Option+E` explains a statement, converting `find`, `aggregate`, `countDocuments`, update, delete, and `findOneAnd*` calls into `db.runCommand({"explain": …, "verbosity": "executionStats"})`. `Cmd+Shift+F` reformats by nesting depth. Autocomplete offers collections, methods, nested field paths such as `address.city`, and the `$` operators valid at the cursor; see [Autocomplete](/features/autocomplete#mongodb). -**Supported methods**: collection-level `find`, `findOne`, `aggregate`, `countDocuments`/`count`, `insertOne`/`insertMany`, `updateOne`/`updateMany`, `replaceOne`, `deleteOne`/`deleteMany`, `findOneAndUpdate`/`findOneAndReplace`/`findOneAndDelete`, `createIndex`, `dropIndex`, `drop`; database-level `getCollectionNames`/`listCollections`, `createCollection`, `dropDatabase`, `version`, `stats`. Anything else goes through `db.runCommand({…})` or `db.adminCommand({…})`. Unlisted shell methods (for example `distinct` or `getUsers`) return an unsupported-method error. +## SSL/TLS -## Troubleshooting +New connections default to **Disabled**, and the driver has no TLS fallback: **Preferred** behaves exactly as **Required**, which is what the SSL pane warns about. For an unencrypted local instance use **Disabled** or [SSH tunneling](/connections/ssh-tunneling). See [SSL/TLS](/connections/ssl). + +## Limitations -**Connection refused**: Check MongoDB is running (`brew services start mongodb-community`), verify host/port in `mongod.conf`, check `bindIp` setting. +- A row with no `_id` cannot be updated or deleted. The save is skipped rather than matched on the remaining fields. Keep `_id` in the projection so every row carries one. +- `_id` is read-only in the grid, and left out of an insert entirely so the server generates it. To choose your own, insert with `db.collection.insertOne({…})`. +- Transactions are not exposed. Statements always run standalone, on any topology. +- Nested paths filter but do not sort. Sorting works on the grid's own columns. +- **same element** covers a field one array deep. A path through an array inside another array needs nested `$elemMatch`, so those filter with dot notation only. +- GridFS buckets are not browsable, and change streams are unsupported. +- `db.getSiblingDB(…)` is not supported. A query runs against the database the connection is on; switch with `Cmd+K`. -**Auth failed**: The error names the database TablePro authenticated against. If that is not where your user lives, set **Auth Database** in the Advanced section. Otherwise verify username, password, and auth mechanism. The MQL editor does not parse `db.getUsers()` or `db.createUser()`; inspect users with `db.runCommand({"usersInfo": 1})` or manage them in mongosh. +## Troubleshooting -**Timeout**: Verify host/port, check network and firewall, whitelist your IP in MongoDB Atlas. +**Connection refused**: check MongoDB is running (`brew services start mongodb-community`) and that the port and `bindIp` in `mongod.conf` match what you entered. -**A collection is slow to open**: A sort or a filter on a field with no index makes MongoDB read every document, even when you only ask for 20 rows, because it has to look at all of them to find which 20 come first. Check the Structure tab for an index on the field you sorted or filtered by, and add one if it is missing. `Cmd+.` stops the query on the server. +**Authentication fails on connect**: the error names the database that was authenticated against. If your user does not live there, set **Auth Database** in Advanced; otherwise check the username, password, and auth mechanism. The MQL editor does not parse `db.getUsers()` or `db.createUser()`; read users with `db.runCommand({"usersInfo": 1})`. -**The row total shows `~`**: That is an estimate read from collection metadata, which is instant. TablePro caps the automatic count at 5 seconds and keeps the estimate if the server takes longer, so browsing is never held up by counting. **Count Exactly** runs a real count with your configured query timeout. Views and time-series collections have no metadata count, so their estimate can be missing. +**Timeout**: for Atlas, add your IP to the cluster's access list first. Otherwise verify host and port and check the network and firewall. -## Limitations +**A collection is slow to open**: a sort or filter on an unindexed field makes MongoDB read every document, even for 20 rows. Check the Structure tab for an index on that field. `Cmd+.` stops the query on the server. -- Transactions are not exposed. Statements always run standalone, on any topology. -- The grid schema is inferred by sampling documents, not read from a validator. -- Nested paths can be filtered but not sorted. Sorting works on the grid's own columns. -- **same element** is offered for a field one array deep. A field inside an array inside another array needs nested `$elemMatch` operators, so those paths filter with dot notation only. -- GridFS buckets are not browsable. -- Change streams are unsupported. +**The row total shows `~`**: that is the instant estimate from collection metadata. The automatic count is capped at 5 seconds and keeps the estimate if the server is slower; **Count Exactly** runs a real count against your query timeout. Views and time-series collections have no metadata count, so their estimate can be missing altogether. diff --git a/docs/databases/mssql.mdx b/docs/databases/mssql.mdx index 98d071a21..e96233be1 100644 --- a/docs/databases/mssql.mdx +++ b/docs/databases/mssql.mdx @@ -3,185 +3,148 @@ title: Microsoft SQL Server description: Connect to SQL Server and Azure SQL Database with the FreeTDS-based driver plugin --- -Named instances are not supported. Give the host and the TCP port instead, `1433` by default. The wire protocol underneath is TDS 7.4, which every server from 2012 on speaks, so instances on Windows, Linux and Docker all work, and so does Azure SQL Database. FreeTDS is linked statically, so there is nothing to build or install besides the plugin itself. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Install Plugin +Named instances are not supported. Give the host and the TCP port, `1433` by default, and let the **Authentication** dropdown decide the rest of the form: a SQL Server login, a Kerberos ticket from your domain, or a Microsoft Entra ID work account. The protocol underneath is TDS 7.4, which every server from 2012 on speaks, so Windows, Linux, Docker and Azure SQL Database are all reachable. -The SQL Server driver is a downloadable plugin. When you select SQL Server in the connection form, TablePro prompts to install it. To install manually: + -1. Open **Settings > Plugins > Browse** -2. Find **MSSQL Driver** and click **Install** -3. The plugin downloads and loads immediately, no restart needed +## Quick setup -See [Plugins](/features/plugins) for how plugin installs and updates work. - -## Connection settings - -Click **Create Connection…**, select **SQL Server**, fill in the fields, and click **Save & Connect**. +Click **Create Connection…**, select **SQL Server**, fill in the fields, then click **Save & Connect**. For Azure SQL Database, use `yourserver.database.windows.net` and an SSL mode of **Required (skip verify)** or stricter. | Field | Default | Notes | |-------|---------|-------| -| **Host** | `localhost` | Named instances are not supported; use the host and TCP port | +| **Host** | `localhost` | A hostname, or an IP for anything but Kerberos | | **Port** | `1433` | | -| **Authentication** | `SQL Server Authentication` | Or **Windows Authentication (Kerberos)**, macOS only, or **Microsoft Entra ID**. See [Windows Authentication](#windows-authentication-kerberos) and [Microsoft Entra ID](#microsoft-entra-id) below | -| **Username** | - | SQL Server login name. Hidden when Windows Authentication is selected | -| **Database** | - | Optional. Sent during login when set, which is required for logins scoped to one database, such as Azure SQL contained users | -| **Schema** | `dbo` | Active schema after connecting. Clear it to use the login's default schema, which the driver reads with `SELECT SCHEMA_NAME()` | +| **Authentication** | SQL Server Authentication | Or Windows Authentication (Kerberos), or Microsoft Entra ID | +| **Username** | - | The SQL Server login. Hidden under the other two methods | +| **Database** | - | Optional, sent during login when set. Required for a login scoped to one database, such as an Azure SQL contained user | +| **Schema** | `dbo` | Selected after connecting. Clear it to take the login's own default | SQL Server connection form with host, port, credentials, database, and schema fields SQL Server connection form with host, port, credentials, database, and schema fields -For Azure SQL Database, set the host to `yourserver.database.windows.net` and pick SSL mode **Required** or stricter. - -## Windows Authentication (Kerberos) - -Available on macOS. Set **Authentication** to **Windows Authentication (Kerberos)** to sign in with your domain identity instead of a SQL Server login. On macOS, Windows Authentication means Kerberos; NTLM is not supported. - -There are two ways to sign in: +## Connection URL -- **Use your existing ticket (single sign-on).** Leave the Kerberos principal and password blank. TablePro uses the ticket already in your credential cache. Get one first with `kinit`: +```text +sqlserver://user:password@host:1433/database +mssql://user:password@host:1433/database +``` - ```bash - kinit user@REALM.COM - klist # confirm a krbtgt/REALM.COM@REALM.COM ticket exists - ``` +See [Connection URL Reference](/connections/urls) for all parameters. -- **Sign in with a principal and password.** Enter the Kerberos principal (`user@REALM.COM`) and your domain password. TablePro requests a ticket for that principal, then connects. +## Windows Authentication (Kerberos) -Requirements: +On macOS, Windows Authentication means Kerberos. Set **Authentication** to **Windows Authentication (Kerberos)**, then either leave **Kerberos Principal** and **Password** blank to use the ticket in your credential cache, or enter `user@REALM.COM` and your domain password to have one requested at connect. Get a cached ticket with `kinit user@REALM.COM`; `klist` shows it. -- **Connect by hostname, not IP address.** Kerberos targets the service principal name `MSSQLSvc/host.domain.com:1433`, which is registered against the host name. -- **Uppercase realm.** The realm is usually the DNS domain in uppercase, for example `CONTOSO.COM`. -- **Realm resolution.** Your Mac must be able to find the domain's KDC, either through DNS SRV records or an `/etc/krb5.conf` entry. -- **A registered SPN.** The SQL Server service account must have an SPN such as `MSSQLSvc/host.domain.com:1433`. Ask your administrator if connections fail with an SSPI or service-principal error. +Four things have to hold: -### Servers in another realm +- Connect by hostname, never an IP address. Kerberos targets the service principal name `MSSQLSvc/host.domain.com:1433`, registered against the host name. +- Write the realm in uppercase, usually the DNS domain: `CONTOSO.COM`. +- This Mac has to reach the domain's KDC, through DNS SRV records or `/etc/krb5.conf`. +- The SQL Server service account has to own that SPN. Your administrator registers it. -If the SQL Server lives in a different Kerberos realm than your Mac's `default_realm`, map its DNS domain to that realm in `/etc/krb5.conf`: +For a server in a realm other than your `default_realm`, map its DNS domain and the ticket is requested there instead: ```ini [domain_realm] .sql.example.com = RESOURCE.REALM.COM ``` -TablePro reads that mapping and asks the KDC for a ticket in `RESOURCE.REALM.COM`, so a cross-realm trust works without changing `default_realm`. This is the same mapping the JDBC driver uses, so a server that already works in DataGrip works here. - -With no `[domain_realm]` entry for the host, TablePro falls back to `default_realm`, which is what a single-realm domain needs. - -TablePro does not run `kinit` or edit `/etc/krb5.conf` for you; those are set up once per machine. +A host with no `[domain_realm]` entry falls back to `default_realm`. TablePro never runs `kinit` or edits `/etc/krb5.conf`. ## Microsoft Entra ID -Set **Authentication** to **Microsoft Entra ID** to sign in with your work account instead of a SQL -Server login. This covers Azure SQL Database, Azure SQL Managed Instance, and SQL Server 2022 with -Entra authentication enabled. - -Available on macOS and iOS. The connection form is on the Mac; iPhone and iPad pick up Entra -connections through sync and prompt to sign in when you open one. - -Sign-in uses the device code flow, so it honors multifactor authentication and Conditional Access: - -1. Fill in **Application (Client) ID**, and **Directory (Tenant) ID** if your directory is single-tenant. -2. Click **Test Connection**. TablePro asks whether to sign in, opens your browser, and copies a - one-time code to the clipboard. -3. Paste the code in the browser and approve the sign-in, then test the connection again. - -TablePro stores the resulting tokens in your login keychain, marked device-only so they never travel -through iCloud Keychain. The access token is refreshed automatically in the background; you only sign -in again when the refresh token is revoked or expires. +Set **Authentication** to **Microsoft Entra ID** to sign in with a work account instead of a SQL Server login: Azure SQL Database, Azure SQL Managed Instance, and SQL Server 2022 with Entra authentication enabled. | Field | Notes | |-------|-------| | **Application (Client) ID** | The application registration TablePro signs in as. Required | -| **Directory (Tenant) ID** | Your directory ID or domain. Leave blank for the multi-tenant `organizations` authority | +| **Directory (Tenant) ID** | Your directory ID or domain. Blank uses the multi-tenant `organizations` authority | + + + + In the Azure portal, under **App registrations**, add one with platform **Mobile and desktop applications**, **Allow public client flows** set to **Yes**, and the delegated permission **Azure SQL Database → user_impersonation**. + + + ```sql + CREATE USER [you@contoso.com] FROM EXTERNAL PROVIDER; + ALTER ROLE db_datareader ADD MEMBER [you@contoso.com]; + ``` + + + Fill in the two fields and click **Test Connection**. A one-time code goes to the clipboard and your browser opens on the sign-in page. Paste it, approve, then test again. + + + +The device code flow honors multifactor authentication and Conditional Access. Tokens land in your login keychain marked device-only, so they never travel through iCloud Keychain, and the access token refreshes in the background. You sign in again only when the refresh token expires or is revoked. + +A sign-in that succeeds and is then refused by the database means no database user matches the token. Run `CREATE USER … FROM EXTERNAL PROVIDER` against the database you are opening. -### Registering the application - -TablePro signs in as an application registered in Microsoft Entra ID. Register one in the Azure -portal under **App registrations**: - -- Platform **Mobile and desktop applications** -- **Allow public client flows** set to **Yes**, which the device code flow requires -- Delegated permission **Azure SQL Database → user_impersonation** - -Then create the matching database user and grant it rights: - -```sql -CREATE USER [you@contoso.com] FROM EXTERNAL PROVIDER; -ALTER ROLE db_datareader ADD MEMBER [you@contoso.com]; -``` - -Using your own registration is also what tenants that restrict application consent require, and it -keeps TablePro identifiable in your sign-in logs and Conditional Access policies. +## Databases and schemas -### Troubleshooting +The sidebar nests tables under their schema, hides the built-in role schemas (`db_owner`, `guest`, and the rest), and marks `master`, `tempdb`, `model` and `msdb` as system databases. Press `Cmd+K` to change database: the switch happens in place, with no reconnect. Opening a table queries it in the schema it is listed under, so a table outside `dbo` needs no switch first. -**"Set the Microsoft Entra ID application (client) ID"**: the connection has no client ID. Fill in the -field. +INSERTs from the data grid leave IDENTITY columns out, so the server assigns them, and `TEXTSIZE` is raised at connect, so `nvarchar(max)` and `text` values arrive whole rather than cut to FreeTDS's 2048-byte default. -**Sign-in never completes**: the code expires after about 15 minutes. Test the connection again to -start a new one. +## SSL/TLS -**`AADSTS7000218`**: the registration does not allow public client flows. Turn on **Allow public -client flows**. +New connections start on **Preferred**. -**Login succeeds but the database rejects you**: the token is valid but no database user matches it. -Run the `CREATE USER … FROM EXTERNAL PROVIDER` above. +| Mode | Behavior | +|---|---| +| **Disabled** | Plain TCP | +| **Preferred** | TLS, falling back to plain when the server cannot | +| **Required (skip verify)** | TLS, certificate unchecked | +| **Verify CA** | TLS, certificate chained to a trusted authority | +| **Verify Identity** | TLS, chain checked, and the certificate names the host you dialled | -## Connection URL +Verification runs against the macOS system roots at `/etc/ssl/cert.pem`, which covers Azure SQL and anything else publicly trusted. See [SSL/TLS](/connections/ssl). -```text -sqlserver://user:password@host:1433/database -mssql://user:password@host:1433/database -``` +SQL Server connections can also run through the [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy), which TablePro starts and stops for you. -See [Connection URL Reference](/connections/urls) for all parameters. +## Limitations -## Databases and schemas +- Named instances are not supported. Give the host and the TCP port that instance listens on. +- NTLM is not supported: Windows Authentication on macOS is Kerberos only. +- Entra ID connections are created on the Mac only, through the device code flow only. iPhone and iPad take them over sync and prompt to sign in when you open one. +- The SSL pane offers no certificate fields here. Verification runs against the system trust store; a private trust anchor goes in your own `freetds.conf`. -The sidebar nests tables under their schema and hides the built-in role schemas (`db_owner`, `guest`, and the rest). Switch the active database with `Cmd+K`; switches happen in place, no reconnect. The toolbar shows the active database and schema side by side, and clicking the schema opens the schema list. You can also click a schema in the sidebar to make it active, or set a starting schema in the connection's **Schema** field. Opening a table always queries it in the schema it is listed under, so tables outside `dbo` work without switching first. `master`, `tempdb`, `model`, and `msdb` are marked as system databases. +## Troubleshooting -Select more than one database or schema to act on them together: Shift-click for a range, Cmd-click to add or remove one. The right-click menu then covers the whole selection. See [Work on Several Databases at Once](/features/table-operations#work-on-several-databases-at-once). +### Failed to connect to host:1433: … -**Drop Schema**: right-click a schema and choose **Drop Schema…**. T-SQL has no cascading drop, so SQL Server refuses to drop a schema that still owns objects; drop or move those first. The active schema and the built-in role schemas are left out. +The tail is FreeTDS's own message. Nothing listening means TCP/IP is off in SQL Server Configuration Manager, the service is down, or port 1433 is closed. A rejected login means the credentials, or a server that takes Windows Authentication only: `SELECT SERVERPROPERTY('IsIntegratedSecurityOnly')` returns `1` for that, and mixed mode goes on in SSMS under Server Properties > Security. A login scoped to one database, an Azure SQL contained user for instance, needs **Database** filled in; without it the server authenticates against `master` and refuses. -## Features +### No Kerberos ticket was found. -- **Table structure**: columns, indexes, foreign keys, triggers, and generated CREATE TABLE DDL. -- **IDENTITY columns**: INSERTs generated by the data grid omit IDENTITY columns, so the server assigns the value. -- **Pagination**: browse queries use `OFFSET … FETCH`. -- **Large text**: `TEXTSIZE` is raised at connect, so `nvarchar(max)` and `text` values are not truncated to the 2048-byte FreeTDS default. -- **Query control**: running queries can be cancelled; the query timeout maps to `SET LOCK_TIMEOUT`. -- **View templates**: use `CREATE OR ALTER VIEW` on SQL Server 2016 and later, plain `CREATE VIEW` (drop first when editing) on older versions. +There is no ticket in the credential cache. Run `kinit user@REALM.COM`, or fill in **Kerberos Principal** and **Password**, then reconnect. -## SSL/TLS +### Server not found in Kerberos database -TablePro maps the SSL mode to FreeTDS `DBSETENCRYPT`. New connections default to **Preferred**. +Reported as `The SQL Server Kerberos service principal name is not registered.` The server has no SPN, or has one in a realm this Mac does not map the host to. Ask your administrator to register `MSSQLSvc/host.domain.com:1433`, connect by hostname rather than IP, and add a `[domain_realm]` entry for a cross-realm server. -| Mode | FreeTDS value | Behavior | -|---|---|---| -| **Disabled** | `off` | Plain TCP | -| **Preferred** | `request` | Try TLS, fall back to plain if the server cannot | -| **Required** | `require` | TLS required | -| **Verify CA** | `require` | TLS required, and the certificate must chain to a trusted authority | -| **Verify Identity** | `require` | TLS required, chain checked, and the certificate must name the host you dialled | +### This Mac's clock is too far out of sync with the domain controller. -FreeTDS reads certificate settings from a config file rather than from the connection, so the two verifying modes make TablePro write a one-entry config for that connection and point FreeTDS at it. Nothing on your machine changes: your own `freetds.conf` is used for every other connection. +Turn on **Set time automatically** in System Settings > General > Date & Time, then reconnect. -Verification uses the macOS system roots at `/etc/ssl/cert.pem`, which covers Azure SQL and any server with a publicly trusted certificate. For a private authority, set the CA certificate on the SSL tab and that file is used instead. See [SSL/TLS](/connections/ssl) for concepts. +### Set the Microsoft Entra ID application (client) ID for this connection. -## Google Cloud SQL +The connection carries no client ID. Fill in **Application (Client) ID**. -SQL Server connections can run through the Cloud SQL Auth Proxy; TablePro starts and stops the proxy for you. See [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy). +### The Microsoft Entra ID sign-in timed out before it was approved. -## Troubleshooting +The one-time code expired first. Click **Test Connection** to start a new one. -**Connection refused**: enable TCP/IP in SQL Server Configuration Manager, confirm the service is running, check firewall port 1433, and if using Docker confirm the container is up. +### AADSTS7000218 -**Login failed**: verify credentials, then check the server allows SQL Server Authentication: `SELECT SERVERPROPERTY('IsIntegratedSecurityOnly')` returns `1` when only Windows Authentication is allowed. Enable mixed mode in SSMS under Server Properties > Security, then restart the service. If the login only has access to one database (an Azure SQL contained user), set the **Database** field; TablePro sends it during login. Without it the server authenticates against `master` and rejects the login. +The app registration does not allow public client flows, which the device code flow needs. Turn it on in the Azure portal. -**Windows Authentication fails**: run `klist` to confirm you have a ticket, and `kinit user@REALM.COM` if you do not. Connect by hostname, not IP address. An SSPI or "server not found in Kerberos database" error means the server's SPN is not registered, or it is registered in a realm your Mac does not map the host to; ask your administrator to register `MSSQLSvc/host.domain.com:1433`, and if the server is in another realm add a `[domain_realm]` entry as shown in [Servers in another realm](#servers-in-another-realm). A clock-skew error means this Mac's clock is too far from the domain controller; turn on **Set time automatically** in System Settings. +## Related -**Limitations**: Windows Authentication (Kerberos) is macOS only, Entra ID connections can only be created on macOS and sign in with the device code flow only, named instances unsupported (use host and port), Verify CA and Verify Identity behave like Required. +- [SSL/TLS](/connections/ssl) +- [SSH Tunneling](/connections/ssh-tunneling) +- [Work on Several Databases at Once](/features/table-operations#databases-and-schemas) diff --git a/docs/databases/mysql.mdx b/docs/databases/mysql.mdx index 3e96da158..1fcc4c422 100644 --- a/docs/databases/mysql.mdx +++ b/docs/databases/mysql.mdx @@ -3,16 +3,16 @@ title: MySQL description: Connect to MySQL 5.7 and later with the bundled MariaDB Connector/C driver --- -MySQL 8 accounts on `caching_sha2_password` connect on the first try, with no auth plugin to switch over. The same driver covers 5.7 and later, and MariaDB as well; MariaDB-specific notes are on [MariaDB](/databases/mariadb). +MySQL 8 accounts on `caching_sha2_password` connect on the first try, with no auth plugin to switch over. The same bundled driver covers 5.7 and later, and MariaDB as well; MariaDB-specific notes are on [MariaDB](/databases/mariadb). ## Quick setup - Click **Create Connection…** on the welcome window, pick **MySQL**, and fill in host, port, and credentials + Click **Create Connection…** on the welcome window and pick **MySQL** - Click **Test Connection** to verify, then **Save & Connect** + Fill in host, port, and credentials, click **Test Connection**, then **Save & Connect** @@ -20,10 +20,13 @@ MySQL 8 accounts on `caching_sha2_password` connect on the first try, with no au | Field | Default | Notes | |-------|---------|-------| -| **Host** | `localhost` | | +| **Host** | `localhost` | TCP only, never a Unix socket | | **Port** | `3306` | | | **Username** | empty | An empty field connects as your macOS login name | -| **Database** | empty | Optional. Leave empty to browse all databases | +| **Password** | empty | Stored in the macOS Keychain | +| **Database** | empty | Optional. Leave it empty to browse every database | + +The session character set is `utf8mb4`, so emoji and non-Latin text round-trip untouched. A connect attempt gives up after 10 seconds. ## Connection URL @@ -40,25 +43,17 @@ Opening a URL connects directly. See [Connection URL Reference](/connections/url | Homebrew | `localhost:3306` | | Docker | `localhost` with the mapped port, password from `MYSQL_ROOT_PASSWORD` | | MAMP Pro | `localhost:8889`, user and password `root` | -| AWS RDS / Aurora | Endpoint hostname, password or [AWS IAM](/connections/aws-iam) | -| Google Cloud SQL | [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy) | +| AWS RDS / Aurora | Endpoint hostname, password or [AWS IAM](/connections/aws-iam), which signs a fresh 15-minute token on each connect | +| Google Cloud SQL | **Enable Cloud SQL Auth Proxy** and the instance name, in the [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy) pane | | Remote production | [SSH tunnel](/connections/ssh-tunneling) | -## AWS IAM authentication - -RDS and Aurora connections can use your AWS identity instead of a static password: TablePro signs a fresh 15-minute token on every connect and reconnect. Setup and troubleshooting are on [AWS IAM Authentication](/connections/aws-iam). - -## Google Cloud SQL - -Turn on **Cloud SQL Auth Proxy** in the connection form and enter the instance connection name; TablePro starts and stops the proxy for you. See [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy). - ## Users & Roles -**Database > Users & Roles** manages users, roles, and privileges on MySQL servers. Changes are staged, undoable, and shown as SQL before they run. See [Users & Roles](/features/users-roles). +**Database > Users & Roles** manages users, roles, and privileges. Changes are staged and shown as SQL before they run. See [Users & Roles](/features/users-roles). ## Query plans -**EXPLAIN FORMAT=JSON**, **EXPLAIN FORMAT=TREE**, and **EXPLAIN ANALYZE** results render as a visual plan diagram or tree. Plain multi-column **EXPLAIN** output stays in the results grid. See [EXPLAIN Visualization](/features/explain-visualization). +`EXPLAIN FORMAT=JSON`, `EXPLAIN FORMAT=TREE`, and `EXPLAIN ANALYZE` render as a plan diagram or tree; plain multi-column `EXPLAIN` stays in the results grid. See [EXPLAIN Visualization](/features/explain-visualization). Visual EXPLAIN plan diagram @@ -67,20 +62,23 @@ Turn on **Cloud SQL Auth Proxy** in the connection form and enter the instance c ## Browsing -The sidebar lists all accessible databases with each table's structure (columns, indexes, foreign keys) and DDL. Switch databases with `Cmd+K`. JSON columns, window functions, and CTEs run and display like any other query. - -Table and column comments show in the UI: the sidebar dims a table's comment after its name, and the data grid header tooltip includes the column comment. Toggle this by right-clicking the sidebar and choosing **View Options > Comments**. +The sidebar lists every accessible database with each table's structure and DDL. Switch databases with `Cmd+K`. A tab keeps the database it was opened on; MySQL switches database in place, so that tab runs on the same connection rather than a second one. -Set session variables (timezone, encoding) on connect with [Startup Commands](/connections#startup-commands). +Table and column comments show in the UI: dimmed after a table's name in the sidebar, and in the grid header tooltip. Right-click the sidebar and choose **View Options > Comments** to turn that off. ## SSL/TLS -New connections default to **Preferred**: the driver tries TLS first and retries in plain text only after an SSL handshake error (auth and network errors are not retried). This works for Cloud SQL, Azure MySQL, and local Docker alike. Use **Verify CA** with the provider's certificate for strict validation. See [SSL/TLS](/connections/ssl). +New connections default to **Preferred**: TLS first, dropping to plain text only after an SSL handshake error. Pick **Verify CA** with the provider's certificate for strict validation. See [SSL/TLS](/connections/ssl). + +## Limitations + +- No Unix socket connections. Give the connection a host and a port, and leave networking on in the server. +- `LOAD DATA LOCAL INFILE` is refused by the driver. Load the file with **File > Import > Import Data…** instead. ## Troubleshooting **Connection refused**: check the server is running (`brew services start mysql`), the port is right, and `skip-networking` is not set. -**Access denied**: verify credentials and privileges with `SHOW GRANTS FOR 'user'@'host';`. +**Access denied for user '…'@'…' (using password: YES)**: the password is wrong, or the grant does not cover the host you are connecting from. Check both with `SHOW GRANTS FOR 'user'@'host';`. -**Auth plugin errors**: `caching_sha2_password` is supported natively. If a plugin error persists, check the user's plugin with `SELECT user, plugin FROM mysql.user;`. +**Auth plugin errors**: `caching_sha2_password` needs no configuration. For any other plugin error, read the account's plugin with `SELECT user, plugin FROM mysql.user;`. diff --git a/docs/databases/oracle.mdx b/docs/databases/oracle.mdx index 2b48adbbe..7b5c122c8 100644 --- a/docs/databases/oracle.mdx +++ b/docs/databases/oracle.mdx @@ -3,63 +3,36 @@ title: Oracle Database description: Connection settings, SSL modes, auth compatibility, and column type support for Oracle --- -This driver speaks TNS, Oracle's own wire protocol, in pure Swift. Oracle Instant Client and the rest of Oracle's client stack stay off your Mac. Logins work against Oracle Database 11.1 and later, whether the instance runs on-premises, in Docker, or in Oracle Cloud. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; + +The driver speaks TNS in pure Swift, so Instant Client and the rest of Oracle's stack stay off your Mac. Logins work against Oracle Database 11.1 and later, on-premises, in Docker, or in Oracle Cloud. -Oracle 10g and earlier are not supported: they use the older O3LOGON handshake that the pure-Swift driver does not implement. +Oracle 10g and earlier cannot connect. Their O3LOGON handshake is not implemented, and there is no setting that works around it. -Servers behind a RAC SCAN listener, shared server, or a load balancer are supported: TablePro follows the listener's redirect to the instance that serves the session. Servers that require Native Network Encryption (`SQLNET.ENCRYPTION_SERVER` or `SQLNET.CRYPTO_CHECKSUM_SERVER` set to REQUIRED) are also supported: TablePro offers AES encryption with a SHA crypto-checksum to every server and the server turns it on when required, the same as SQL Developer and DBeaver. There is no option to configure. - -## On iPhone and iPad - -TablePro Mobile connects to Oracle too. The driver is built into the app, so there is no plugin to install. Enter host, port, username, password, and either a service name or a SID, and pick an SSL mode. You can browse schemas, tables, columns, indexes, and foreign keys, run queries, and edit rows. - -Two things stay Mac-only: creating or altering tables and triggers, and certificate files. Certificates live on the device that created the connection and do not sync, so a connection set up on your Mac with a CA or client certificate reports that the file is missing when you open it on iPhone. Clear the certificate path and the verify modes fall back to the system trust store, which is all a publicly trusted server such as Oracle Autonomous Database needs. +A RAC SCAN listener, a shared server, or a load balancer in front of the database needs no configuration. Neither does a server that requires Native Network Encryption: AES with a SHA-2 checksum is offered on every connect, and the server turns it on when it wants it. -## Install Plugin - -The Oracle driver is a downloadable plugin on macOS. When you select Oracle in the connection form, TablePro prompts you to install it. You can also install it manually: - -1. Open **Settings > Plugins > Browse** -2. Find **Oracle Driver** and click **Install** -3. The plugin downloads and loads immediately, no restart needed + ## Connection settings -Enter host, port, username, password, and service name, then click **Save & Connect**. - | Field | Default | Notes | |-------|---------|-------| | **Host** | `localhost` | | | **Port** | `1521` | Listener port | -| **Service Name** | - | **Required**. Check `tnsnames.ora` if unclear. To connect by SID instead, set **Connection Type** to SID and enter the SID. | -| **Username** | - | Requires username/password auth (no OS auth) | -| **Role** | `Normal` | Set to SYSDBA or SYSOPER for an administrative logon. The account needs the matching privilege, and Oracle treats the session as `SYS` rather than as the user you signed in with. | +| **Connection Type** | Service Name | Switch to SID for an instance that has no service | +| **Service Name** | - | **Required**. `lsnrctl services` on the server lists what the listener answers for | +| **Username** | - | Username and password only. There is no OS auth | +| **Role** | Normal | SYSDBA or SYSOPER for an administrative logon. The account needs the matching privilege, and the session runs as `SYS` rather than as the user you signed in with | + +There is no Database field. Each user is a schema, the sidebar lists them all, and `Cmd+K` or the **Schema** toolbar button switches the active one. A tab binds to a schema rather than a database, so nothing reconnects when you switch. Oracle connection form with Connection Type and Service Name fields Oracle connection form with Connection Type and Service Name fields -## Example configurations - -**Local (Oracle XE)**: host `localhost:1521`, user `system`, service `XEPDB1` - -**Docker**: `gvenzl/oracle-xe:21-slim` image, same config as local - -**Remote**: Standard host/port/credentials, service name from DBA - -**Oracle Cloud (ADB)**: Copy host, port, and service name (format `mydb_tp`) from the TLS connection string in the Oracle Cloud Console, then set SSL mode to **Required** (or **Verify CA** with the CA certificate). TablePro connects with one-way TLS and does not read Oracle wallet files. - -This needs the database to allow TLS without mutual TLS, which means setting **Mutual TLS (mTLS) authentication** to *Not Required* and giving the database an access control list or a private endpoint. Oracle rejects the connection otherwise. Use the TLS connection string, not the mTLS one: mTLS is served on port 1522 only, while TLS is on 1521 or 1522. - -If you have an older wallet that stopped working, this is the path to switch to. Oracle's DigiCert G1 roots were distrusted in April 2026, so ADB wallets generated on or before 28 January 2026 no longer connect. One-way TLS is unaffected. - -## SSL/TLS - -OracleNIO has no TLS fallback. **Preferred** connects in plain TCP (the SSL pane shows a warning). Use **Required** for TCPS, **Verify CA** with a CA certificate path for strict validation. See [SSL/TLS](/connections/ssl) for details. - ## Connection URL ```text @@ -68,49 +41,38 @@ oracle://user:password@host:1521/service_name See [Connection URL Reference](/connections/urls) for all parameters. -## Features - -**Schema Selection**: Each user is a schema. The sidebar lists every schema and loads a schema's tables when you expand it. Switch the active schema with `Cmd+K` or the **Schema** toolbar button. +## Authentication -**Table Info**: Structure (columns, types, nullability, primary keys), indexes, foreign keys, and CREATE TABLE DDL. - -**Query Editor**: SQL and PL/SQL (anonymous blocks). Pagination uses OFFSET/FETCH syntax: +Username and password are the only credentials. Every verifier in `dba_users.password_versions` works, `10G` included; external, Kerberos, and LDAP-managed accounts do not connect at all. A `10G`-only account still connects, on DES-based hashing Oracle has deprecated; rotate it with `ALTER USER … IDENTIFIED BY` and check that `password_versions` then holds `11G` or `12C`. ```sql -SELECT * -FROM hr.employees -ORDER BY employee_id -OFFSET 0 ROWS FETCH NEXT 50 ROWS ONLY; +CREATE USER app_user IDENTIFIED BY "Password1!"; +GRANT CREATE SESSION, SELECT ANY TABLE TO app_user; +SELECT username, password_versions FROM dba_users WHERE username = 'APP_USER'; ``` -**Schema Editing**: ALTER TABLE with double quotes for case-sensitive names. Supports ADD, RENAME COLUMN, MODIFY, DROP, CREATE INDEX. +## Common setups -## Auth compatibility +| Where | Settings | +|-------|----------| +| Oracle XE, local | `localhost:1521`, user `system`, service `XEPDB1` | +| Docker | `gvenzl/oracle-xe:21-slim`, same as local | +| Remote | Host, port, and credentials as usual; service name from the DBA | +| Oracle Cloud ADB | One-way TLS, no wallet. See [Oracle Cloud (ADB)](#oracle-cloud-adb) | -TablePro authenticates via `OracleNIO`, a pure-Swift implementation of the Oracle TNS wire protocol (no Oracle Instant Client needed). Username and password are the only credentials it accepts; OS auth and Oracle wallets are not supported. Create a user with: +A pluggable database has its own service name, and it is not the container's: `XEPDB1` is a PDB inside `XE`, and connecting to the container name gets you the CDB with none of the application schemas. `lsnrctl services` lists both. -```sql -CREATE USER app_user IDENTIFIED BY "Password1!"; -GRANT CREATE SESSION, SELECT ANY TABLE TO app_user; -``` +## Oracle Cloud (ADB) -| `dba_users.password_versions` | Status | -|---|---| -| `12C` only (recommended) | ✓ Supported | -| `11G 12C` | ✓ Supported | -| `10G 11G 12C` | ✓ Supported | -| `10G 11G` (legacy migration) | ✓ Supported | -| `10G` only (deprecated) | ✓ Supported, but consider rotation | -| External / Kerberos / LDAP-managed | ✗ Not supported. File an issue. | +Copy host, port, and service name (of the form `mydb_tp`) from the **TLS** connection string in the Oracle Cloud Console, not the mTLS one, then set SSL mode to **Required**, or **Verify CA** with the CA certificate. Wallet files are never read, which also makes this the way out of a wallet that has stopped working. -10G hash is supported for compatibility with legacy environments. It uses DES-based hashing without modern salting and is deprecated by Oracle. Rotate affected accounts under modern auth so `password_versions` contains only `11G` or `12C`: +The database has to accept TLS without mutual TLS: set **Mutual TLS (mTLS) authentication** to *Not Required* and give the database an access control list or a private endpoint. Oracle rejects the connection otherwise. -```sql -ALTER USER IDENTIFIED BY ; -SELECT username, password_versions FROM dba_users WHERE username = ''; -``` +## Editing and queries -If the connection fails with "Unsupported Password Verifier", check `password_versions` first. "Server Version Not Supported" means the database is older than 11.1 (10g or earlier), which the driver cannot connect to. +Table info carries columns, types, nullability, primary keys, indexes, foreign keys, and CREATE TABLE DDL. The structure editor writes `ALTER TABLE` with double-quoted identifiers for case-sensitive names: ADD, RENAME COLUMN, MODIFY, DROP, and CREATE INDEX. + +The editor runs SQL and anonymous PL/SQL blocks, and grid paging is written as `OFFSET … ROWS FETCH NEXT … ROWS ONLY`. Import and export work as they do elsewhere, and a remote server reaches through an [SSH tunnel](/connections/ssh-tunneling). ## Column type support @@ -130,22 +92,35 @@ If the connection fails with "Unsupported Password Verifier", check `password_ve | BOOLEAN | `true` / `false` | | BFILE | `` placeholder (locator only) | -Columns with types not yet supported render as `` rather than crashing or corrupting data. Report unsupported types via [GitHub Issues](https://github.com/TableProApp/TablePro/issues). +Anything else renders as ``. Report those through [GitHub Issues](https://github.com/TableProApp/TablePro/issues). + +## SSL/TLS + +New connections default to **Disabled**, and there is no opportunistic upgrade: **Preferred** connects in plain TCP, which is why the SSL pane flags it in red. **Required** is TCPS; **Verify CA** adds a CA certificate path for strict validation. See [SSL/TLS](/connections/ssl). + +## On iPhone and iPad + +The Oracle driver is compiled into TablePro Mobile, with no plugin to install. Browsing, queries, and row editing work; creating or altering tables and triggers stays on the Mac. See [iPhone and iPad](/ios). ## Limitations -- Username and password auth only. No OS auth, wallets, Kerberos, or LDAP. -- BFILE columns show the locator only. Fetching content through `DBMS_LOB` is not supported. -- PL/SQL runs as anonymous blocks only. +- No OS auth, wallets, Kerberos, or LDAP. Create a database user with a password and connect as that. +- BFILE columns show the locator, never the file. There is no path to the contents from the app. +- PL/SQL runs as anonymous blocks. Create a procedure or package with a `CREATE OR REPLACE` statement in the editor. +- No Users & Roles pane. Manage accounts with `CREATE USER` and `GRANT` in the editor. ## Troubleshooting -**Connection refused**: check the listener is running with `lsnrctl status`, confirm port 1521 is open, and on Docker confirm the container is up (`docker start oracle-xe`). +**Connection refused**: check the listener is up with `lsnrctl status`, that port 1521 is open, and, on Docker, that the container is running (`docker start oracle-xe`). + +**Invalid service name**: the listener does not answer for that name. List what it does answer for with `lsnrctl services`. + +**Unsupported Password Verifier**: the account's verifier is one the driver cannot use, usually a new one such as 23ai. Read `password_versions` for that user and file an issue with the verifier flag the dialog shows. -**Invalid service name**: confirm the service exists with `SELECT value FROM v$parameter WHERE name = 'service_names';`, or list what the listener serves with `lsnrctl services`. +**Server Version Not Supported**: the server is 10g or earlier. Nothing on this side helps; the database has to be 11.1 or later. -**Connection dropped during handshake**: the server closed the connection mid-login. The error dialog shows the handshake phase it stopped at (for example `advancedNegotiation`, `dataTypeNegotiation`, or `authentication`). Check for a firewall, VPN, or proxy that resets traffic, and confirm the host and port reach the listener directly. +**Login Handshake Timed Out**: TCP connected but the login never finished, so the attempt stops after 30 seconds. Look for a firewall, VPN, or proxy that stalls traffic once the TCP handshake is done. -**Native network encryption not completed**: the server requires native network encryption and the negotiation did not finish. The server likely requires an encryption or checksum algorithm the driver does not support; the error dialog shows the reason. File an issue with your Oracle version and that reason. For encryption in transit you can also use TLS by setting an SSL mode in the SSL settings. +**Connection Dropped During Handshake**: the server closed the connection mid-login, and the dialog names the phase it stopped at (`advancedNegotiation`, `dataTypeNegotiation`, `authentication`). Look for a firewall, VPN, or proxy that resets traffic, and confirm the host and port reach the listener itself. -**Login handshake timed out**: the TCP connection is accepted but login never finishes, so the connect stops after 30 seconds instead of waiting for the server to reset. Check for a firewall, VPN, or proxy that stalls traffic after the TCP handshake, and confirm the host and port reach the listener directly. +**Native Network Encryption Not Completed**: the server requires an encryption or checksum algorithm the driver does not offer. Ask the DBA which ones are set and file an issue with the reason the dialog shows. TLS is the alternative: set an SSL mode instead. diff --git a/docs/databases/pglite.mdx b/docs/databases/pglite.mdx index 61eb6f80d..74cb06957 100644 --- a/docs/databases/pglite.mdx +++ b/docs/databases/pglite.mdx @@ -3,11 +3,16 @@ title: PGlite description: Connect to a PGlite database over its socket server using the PostgreSQL wire protocol --- -[PGlite](https://pglite.dev) is PostgreSQL 17 compiled to WebAssembly, and it runs inside a JavaScript process rather than listening on a socket of its own. A network client reaches it only through `@electric-sql/pglite-socket`, which you start yourself before opening the connection form. Once that is up, the connection behaves like [PostgreSQL](/databases/postgresql), down to the same libpq driver, schema introspection, DDL, and EXPLAIN. +[PGlite](https://pglite.dev) is PostgreSQL 17 compiled to WebAssembly, and it runs inside a JavaScript process instead of listening on a socket of its own. Nothing can reach it until you start `@electric-sql/pglite-socket` yourself. After that the connection is ordinary [PostgreSQL](/databases/postgresql), served by the same driver, with the same introspection, DDL, and EXPLAIN, and nothing extra to install. + + + PGlite connection form + PGlite connection form + ## Start the socket server -PGlite has no server of its own, so start one first. It listens on `127.0.0.1:5432` by default: +It listens on `127.0.0.1:5432` unless `--port` says otherwise. Leave it running for as long as the connection is open. ```bash # In-memory database, gone when the server stops @@ -17,8 +22,6 @@ npx @electric-sql/pglite-socket npx @electric-sql/pglite-socket --db=./my-pgdata ``` -Leave it running while you use the connection. TablePro opens a single connection to PGlite and serializes all of its own background work (schema browsing, autocomplete, row counts) over that one connection. - ## Connection settings | Field | Default | Notes | @@ -26,9 +29,9 @@ Leave it running while you use the connection. TablePro opens a single connectio | **Host** | `127.0.0.1` | The socket server binds loopback IPv4, not `localhost` | | **Port** | `5432` | Match the server's `--port` | | **Database** | `postgres` | The single database PGlite exposes | -| **Username** | `postgres` | PGlite uses trust auth; any username connects | +| **Username** | `postgres` | Trust auth; any username connects | -The socket server never checks a password, so the form has no password field. SSL is off and cannot be enabled: the socket server has no TLS. Fill in the form and click **Save & Connect**. +The socket server never checks a password, so the form has no password field, and it has no TLS, so SSL Mode is fixed to Disabled. Fill in the form and click **Save & Connect**. ## Connection URL @@ -38,22 +41,19 @@ pglite://postgres@127.0.0.1:5432/postgres See [Connection URL Reference](/connections/urls) for all parameters. -## Features - -**Schemas**: like PostgreSQL, default schema `public`. The toolbar shows the active database and schema side by side; click the schema to switch it, or use Database > Schema. Cmd+K opens the database switcher, which on PGlite lists only `postgres`. - -**DDL, EXPLAIN, editing**: PGlite is real PostgreSQL 17, so `pg_catalog` and `information_schema` are complete. Table and view definitions, indexes, foreign keys, structure editing, and the visual `EXPLAIN` / `EXPLAIN ANALYZE` plan tree all work as they do for PostgreSQL. +## What you get -**Import & Export**: export to CSV, JSON, SQL, or XLSX. Import from CSV, JSON, or SQL. See [Import & Export](/features/import-export). +Real PostgreSQL 17, so `pg_catalog` and `information_schema` are complete: table and view definitions, indexes, foreign keys, structure editing, and the visual `EXPLAIN` and `EXPLAIN ANALYZE` plan tree all behave as they do on a server. The default schema is `public`, and `postgres` is the only entry in the `Cmd+K` database list. ## Limitations -- **Single connection.** PGlite serves one connection at a time. TablePro is built for this and keeps to one connection, but other tools that open connections in parallel will fail against the same server. This also means PGlite cannot back a [cross-database tab](/databases/postgresql#cross-database-tabs) the way PostgreSQL, Redshift, and CockroachDB do: a PGlite tab bound to a database the connection is not on reports an error naming that database instead of running the query against the wrong one. -- **No TLS.** The socket server rejects SSL, so SSL Mode is fixed to Disabled and the SSH, Cloudflare Tunnel, and SOCKS panes do not apply. -- **Cancel does nothing.** PGlite has no backend process to signal, so cancelling a running query has no effect at the protocol level. Let long queries finish. +- One connection at a time. A second client, or a second TablePro window on the same server, is refused: close the other one first. +- No cross-database tabs. A tab bound to a database the connection is not on refuses to run and names that database instead of guessing. +- Cancel has no effect. `Cmd+.` returns without stopping anything and the query runs to completion, so keep exploratory queries small. +- No TLS. SSL Mode is fixed to Disabled, and the SSH, Cloudflare Tunnel, and SOCKS panes do not apply. ## Troubleshooting -**Can't reach a PGlite socket server**: the server is not running or is on a different address. Start it with `npx @electric-sql/pglite-socket` and confirm the host and port match. TablePro needs loopback `127.0.0.1`, not `localhost`, which can resolve to IPv6 first. +**Can't reach a PGlite socket server**: nothing is listening at that address. Start it with `npx @electric-sql/pglite-socket`, and check the host is `127.0.0.1` rather than `localhost`, which can resolve to IPv6 first. -**Too many connections**: something else is already connected to the socket server, which serves one client at a time. Close the other client and reconnect. +**Too many connections**: something else is already talking to the socket server. Close that client and connect again. diff --git a/docs/databases/postgresql.mdx b/docs/databases/postgresql.mdx index 48f417fb3..8cca499cd 100644 --- a/docs/databases/postgresql.mdx +++ b/docs/databases/postgresql.mdx @@ -3,7 +3,7 @@ title: PostgreSQL description: Connect to PostgreSQL 12 and later with the libpq driver, including PostGIS display, AWS IAM auth, and pg_dump backups --- -TablePro supports PostgreSQL 12 and later via the libpq C connector. The same driver powers [Amazon Redshift](/databases/redshift), [CockroachDB](/databases/cockroachdb), and [PGlite](/databases/pglite) connections. +Unlike MySQL, PostgreSQL will not connect without a **Database**, and it changes database only by reconnecting. Everything else on the form is ordinary. The libpq driver ships inside the app and also serves [Amazon Redshift](/databases/redshift), [CockroachDB](/databases/cockroachdb), and [PGlite](/databases/pglite). ## Connection settings @@ -12,10 +12,12 @@ TablePro supports PostgreSQL 12 and later via the libpq C connector. The same dr | **Host** | `localhost` | | | **Port** | `5432` | | | **Username** | - | Not pre-filled; most servers use `postgres` | -| **Database** | - | **Required** (unlike MySQL) | +| **Database** | - | **Required** | Fill in the form and click **Save & Connect**. For remote servers use [SSH tunneling](/connections/ssh-tunneling); Google Cloud SQL instances can connect through the [Cloud SQL Auth Proxy](/connections/cloud-sql-proxy). +Support starts at 12. An older server still connects, with whatever its catalogs carry: generated columns need 12, declarative partitioning and identity columns need 10. + PostgreSQL connection form PostgreSQL connection form @@ -29,54 +31,59 @@ postgresql://user:password@host:5432/database `postgres://` also works. Opening a URL connects directly. See [Connection URL Reference](/connections/urls). -## AWS IAM authentication +## Authentication -Connect to RDS or Aurora with your AWS identity instead of a static password: set **Authentication** to an AWS IAM option and set **Username** to a database role granted `rds_iam`. See [AWS IAM Authentication](/connections/aws-iam) for setup, credential sources, and troubleshooting. +### AWS IAM -## Features +Set **Authentication** to an AWS IAM option and set **Username** to a database role granted `rds_iam`. See [AWS IAM Authentication](/connections/aws-iam). -**Schemas**: The sidebar shows all accessible schemas and tables. The toolbar shows the active database and schema side by side; click either to switch it, or press Cmd+K for the database list. In Flat sidebar layout the active schema is also at the foot of the object list, and Database > Schema does the same job from the menu bar. Table info shows columns, indexes, constraints, and DDL. Right-click a schema for **Drop Schema…**, which runs `DROP SCHEMA … CASCADE`, so objects in other schemas that depend on it are dropped too. Select several databases or schemas with Shift-click or Cmd-click and the menu covers all of them. See [Work on Several Databases at Once](/features/table-operations#work-on-several-databases-at-once). +### Password file -**Databases**: Every database on the server is listed, including `postgres`. It is an ordinary database that `initdb` creates for users and applications, not a system database. `template0` and `template1` are not listed. +Turn on **Use Password File** to read the password from `~/.pgpass` instead of typing one. Entries are `hostname:port:database:username:password`, `*` matches any field, first match wins. The form reports what it found, including `~/.pgpass has incorrect permissions (needs chmod 0600)`, the one that silently blocks a connect. -**Partitioned tables**: A partitioned table is listed once, under its own icon. Its partitions are not listed beside it; expand the table in the sidebar to see them, and expand a partition again if it is subpartitioned. Opening a partition works like opening any other table. Tables that use the older `INHERITS` inheritance are listed normally, since a child there is a table in its own right. Requires PostgreSQL 10 or later, which is where declarative partitioning was added. +## Databases and schemas -**Types**: `jsonb` renders as formatted JSON. `uuid`, `inet`, `timestamp with time zone`, `interval`, and `bytea` display natively. +Every database on the server is listed, `postgres` included; `template0` and `template1` are not. The sidebar shows every schema you have access to, and the toolbar carries the active database and schema side by side: click either to switch, or press `Cmd+K` for the database list. -**Arrays**: An array column whose elements are a simple type (`text[]`, `integer[]`, `uuid[]`, `timestamptz[]`, and enum arrays such as `mood[]`) opens a list editor, one row per element. Reorder rows with the arrows, add and remove elements, and set any single element to NULL. An empty array and a NULL column are separate values and the editor keeps them apart. Elements of an enum array pick from the labels the type declares, in declaration order; a value the type no longer lists stays selectable and is flagged. Switch to **Edit as Text** for the raw `{…}` literal. Arrays of `jsonb`, `bytea`, or composite types, and multi-dimensional values, keep the plain text editor, since their quoting cannot round-trip through a per-element list. +Right-click a schema to drop it. The statement is `DROP SCHEMA … CASCADE`, so views and functions in *other* schemas that depend on it go too; the confirmation says so before it runs. -**EXPLAIN**: `EXPLAIN` and `EXPLAIN ANALYZE` run with `FORMAT JSON` and render as a plan diagram or tree. See [EXPLAIN Visualization](/features/explain-visualization). +A partitioned table is listed once, under its own icon. Expand it for its partitions, and expand a partition again if it is subpartitioned; opening one works like opening any other table. Tables using the older `INHERITS` inheritance are listed normally, each child in its own right. -**PostGIS**: `geometry` and `geography` columns render as EWKT with the SRID preserved (`SRID=4326;POINT(-73 40.7237)`) instead of the raw EWKB hex libpq returns. TablePro detects spatial columns from a one-time `pg_type` lookup at connect time and converts fetched values with `ST_AsEWKT(…)`. Your query is never re-run, so parameterized and multi-statement queries render EWKT too. If conversion fails, the raw hex is kept without an error. +## Column types -**Users & Roles**: Manage database users, roles, and privileges under **Database > Users & Roles**: see where each privilege comes from, then grant or revoke it. See [Users & Roles](/features/users-roles). +`jsonb` renders as formatted JSON, and `uuid`, `inet`, `timestamp with time zone`, `interval` and `bytea` display natively. PostGIS `geometry` and `geography` render as EWKT with the SRID kept, `SRID=4326;POINT(-73 40.7237)`, rather than raw EWKB hex; a value that fails to convert stays hex. -**Backup & Restore**: **Backup Dump** and **Restore Dump** run `pg_dump` and `pg_restore`. Among TablePro's databases this is available for PostgreSQL and Redshift only. See [Backup & Restore](/features/backup-restore). +An array column opens one of two editors, decided by its element type: -## Cross-database tabs +| Element type | Editor | +|---|---| +| `text[]`, `integer[]`, `numeric[]`, `uuid[]`, `boolean[]`, `timestamptz[]`, enum arrays such as `mood[]` | A list, one row per element | +| `jsonb[]`, `bytea[]`, composite arrays, and any multi-dimensional array | The plain text editor over the `{…}` literal | -A tab stays on the database and schema it was opened with for its whole life, even if the sidebar or another tab switches to a different one. See [Tabs](/features/tabs#database-binding). PostgreSQL, like Redshift and CockroachDB, changes database only by reconnecting: there is no in-place `USE` statement. So a tab bound to a database other than the connection's active one runs on a separate connection for that database instead. +In the list editor, reorder rows with the arrows, add and remove elements, and set a single element to NULL; an empty array and a NULL column stay distinct. Enum elements pick from the labels the type declares, and a label the type no longer lists stays selectable and is flagged. **Edit as Text** switches to the raw literal at any time. -That separate connection means the tab does not share temp tables, session variables, or an open transaction with the query editor on the main connection. Keep session-scoped work, such as a multi-statement transaction or a `CREATE TEMP TABLE`, on tabs bound to the same database if they need to see each other's state. +## Cross-database tabs -[PGlite](/databases/pglite) cannot open a second connection at all, so a PGlite tab bound to a database the connection is not on reports an error naming that database instead of running the query against the wrong one. +PostgreSQL has no in-place `USE`, so a tab bound to a database other than the connection's active one runs on a second connection opened for that database. It shares no temp tables, session variables, or open transaction with the query editor on the main connection: keep a multi-statement transaction or a `CREATE TEMP TABLE` on tabs bound to one database. Binding itself is on [Tabs](/features/tabs#where-a-tab-points). -## Advanced +## Tools -**~/.pgpass**: format `hostname:port:database:username:password`, wildcards (`*`) allowed. libpq silently ignores the file unless its permissions are `0600`. +`EXPLAIN` and `EXPLAIN ANALYZE` run with `FORMAT JSON` and render as a plan diagram or tree. See [EXPLAIN Visualization](/features/explain-visualization). **Database > Users & Roles** shows where each privilege comes from before you grant or revoke it. **Backup Dump** and **Restore Dump** shell out to your local `pg_dump` and `pg_restore`; see [Backup & Restore](/features/backup-restore). -**Startup Commands** (Advanced tab): SQL that runs on every connect, e.g. `SET timezone = 'UTC'; SET search_path TO myschema, public;`. +## SSL/TLS -**Pre-Connect Script** (Advanced tab): a shell script that runs before connecting, e.g. to refresh credentials from a secrets manager. 10-second timeout. +New connections default to **Preferred** (libpq `sslmode=prefer`): TLS first, plain text if the server refuses it. Pick **Verify CA** to validate the server certificate. See [SSL/TLS](/connections/ssl). -## SSL/TLS +## Limitations -New connections default to **Preferred** (libpq `sslmode=prefer`), which tries TLS first and falls back to plain. Pick **Verify CA** to validate the server certificate. See [SSL/TLS](/connections/ssl). +- Columns cannot be reordered. The structure editor adds, renames, retypes, and drops; changing the order of existing columns means recreating the table. +- A cross-database tab cannot share session state with the main connection. Statements that depend on a temp table or an open transaction have to run on one database. +- Backup and restore need `pg_dump` and `pg_restore` on your Mac. Neither is bundled; install them with Homebrew. ## Troubleshooting -**Connection refused**: check the server is running, `listen_addresses` in `postgresql.conf` allows remote connections, and the firewall allows port 5432. +**Connection refused**: check the server is running, that `listen_addresses` in `postgresql.conf` covers remote connections, and that the firewall allows port 5432. -**Auth failed**: check `pg_hba.conf` for the auth method (`scram-sha-256` or `md5` for passwords, `trust` for local dev). +**FATAL: password authentication failed for user "…"**: the role and password are checked against `pg_hba.conf`. Confirm the method on the matching line (`scram-sha-256` or `md5` for passwords, `trust` for local dev), and that the line matches the host you are connecting from. -**Postgres-compatible engines**: connect wire-compatible engines using the PostgreSQL type. TablePro probes which system catalogs the server provides, so engines that omit catalogs like `pg_matviews` still load their tables; object kinds the server does not expose just won't appear. +**A Postgres-compatible engine loads no tables**: wire-compatible engines connect under the PostgreSQL type, and the catalogs they omit are probed for rather than assumed. An engine without `pg_matviews` still lists its tables; object kinds it does not expose will not appear at all. diff --git a/docs/databases/redis.mdx b/docs/databases/redis.mdx index 888880d40..fd3b5f70b 100644 --- a/docs/databases/redis.mdx +++ b/docs/databases/redis.mdx @@ -3,16 +3,16 @@ title: Redis description: Browse keys by namespace, manage TTLs, and run Redis commands directly from the editor --- -TablePro connects to standalone Redis servers, Sentinel-managed primaries and Redis Cluster, all through hiredis. Keys are grouped by colon-separated namespaces in the sidebar. Values display with type-aware formatting in the data grid (strings, hashes, lists, sets, sorted sets, streams). +The sidebar splits keys into folders at every `:`, and each key gets a grid row with its type, TTL, length, and value. The editor takes Redis commands, one per statement, and passes them through as written. Redis needs no plugin: its hiredis-based driver is part of the app, and it reaches standalone servers, Sentinel-managed primaries, and Redis Cluster. ## Quick setup - Click **Create Connection…**, select **Redis**, enter host/port/password/database, and click **Save & Connect** + Click **Create Connection…**, select **Redis**, and enter host, port, password, and database index - Click **Test Connection** to verify + Click **Test Connection**, then **Save & Connect** @@ -26,148 +26,133 @@ TablePro connects to standalone Redis servers, Sentinel-managed primaries and Re | **Username** | - | Redis 6 ACL user, sent as `AUTH username password`. Leave empty for `requirepass` auth | | **Password** | - | Leave empty for local dev | | **Database Index** | `0` | 0-15 stepper, in the Advanced section | -| **Key Separator** | `:` | Groups keys by prefix in sidebar (Advanced) | +| **Key Separator** | `:` | What the sidebar splits key names on (Advanced) | - -Open URLs like `redis://:password@host:6379/0` or `rediss://` (TLS) from your browser. See [Connection URL Reference](/connections/urls#redis). - +No minimum server version. ACL users need Redis 6, a Sentinel ACL user needs 6.2, and Cluster mode reads routing tips from Redis 7 where they exist. + +The sidebar then lists one entry per database, `db0` upward, counted from the server's own `CONFIG GET databases` (16 if it does not answer). Click one to browse it in place, which is also how the database index changes after connecting. + +## Connection URL + +```text +redis://:password@host:6379/0 +rediss://:password@host:6380/0 +``` + +`rediss://` connects with TLS. See [Connection URL Reference](/connections/urls). ## Connection modes ### Standalone -The default. Connects to the one server named by Host and Port. +The default. One server, named by Host and Port. ### Sentinel -For a deployment fronted by [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/). TablePro asks the quorum which node is currently the primary, then opens the data connection to it. +For a deployment fronted by [Redis Sentinel](https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/). The quorum says which node is primary and the data connection opens there; a failover is read from the quorum on the next health check and the connection moves itself. A demoted primary keeps answering `role:master` and keeps accepting writes for several seconds, and those writes are lost, so the quorum is the only signal worth trusting. | Field | Notes | |-------|-------| -| **Sentinel Nodes** | One or more `host:port` entries. Port defaults to `26379`. | -| **Primary Group Name** | The name after `sentinel monitor` in `sentinel.conf`, usually `mymaster`. | -| **Sentinel Username** | Under Authentication. Only needed when Sentinel has its own ACL user (Redis 6.2+). | -| **Sentinel Password** | Under Authentication. Only needed when Sentinel has its own password. Stored in the Keychain. | +| **Sentinel Nodes** | One or more `host:port` entries. Port defaults to `26379` | +| **Primary Group Name** | The name after `sentinel monitor` in `sentinel.conf`, usually `mymaster` | +| **Sentinel Username** | Under Authentication. Only when Sentinel has its own ACL user | +| **Sentinel Password** | Under Authentication. Only when Sentinel has its own password. Kept in the Keychain | -Host and Port are ignored in Sentinel mode. The Username and Password under Authentication are the data-plane credentials, used for the primary itself; Sentinel authenticates separately because it is a different plane. - -Once connected, TablePro asks one Sentinel for the other Sentinels in the quorum and remembers them, so a later lookup still works when the node you listed is down. It re-checks the primary's address on every health check and reconnects when the quorum reports a new one. It does not infer failover from the data connection: a demoted primary keeps reporting `role:master` and keeps accepting writes for several seconds, and those writes are lost, so the quorum's answer is the only signal worth trusting. - -If no Sentinel can be reached, or none of them monitors the group you named, the error says which addresses were tried and which groups they do monitor. +Host and Port are ignored here. Username and Password under Authentication belong to the primary; Sentinel is a separate plane with its own. The rest of the quorum is discovered at connect and remembered, so a lookup still works once the node you listed goes down. If none can be reached, or none monitors the group you named, the error lists the addresses tried and the groups they do monitor. ### Cluster -For a sharded [Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/). TablePro reads the shard map with `CLUSTER SHARDS` (or `CLUSTER SLOTS` on Redis 6), opens a connection to every primary, and sends each command to the shard that owns its key. +For a sharded [Redis Cluster](https://redis.io/docs/latest/operate/oss_and_stack/management/scaling/). The shard map is read with `CLUSTER SHARDS`, or `CLUSTER SLOTS` on Redis 6; a connection opens to every primary and each command goes to the shard that owns its key. | Field | Notes | |-------|-------| -| **Cluster Seed Nodes** | One or more `host:port` entries. Any reachable member is enough; TablePro discovers the rest. Port defaults to `6379`. | - -What changes in Cluster mode: +| **Cluster Seed Nodes** | One or more `host:port` entries. Any reachable member is enough, the rest are discovered. Port defaults to `6379` | -- **One database.** Redis Cluster serves database 0 only, so the Database Index field is hidden and the sidebar shows a single `db0`. -- **Key counts are cluster-wide.** `DBSIZE` is summed across every shard, and browsing walks each shard in turn and merges the keys into one tree. -- **Redirects are followed.** A `MOVED` re-points the slot and retries; an `ASK` retries against the importing node. TablePro follows at most five redirects for one command. -- **Multi-key commands are split by slot.** `MGET`, `MSET`, `DEL`, `EXISTS`, `TOUCH` and `UNLINK` are sent per shard and recombined, so they work across shards. -- **Cross-slot commands are refused before they are sent.** `RENAME`, `SMOVE` and the `*STORE` commands need every key in one hash slot. Give the keys a shared hash tag, like `{user}:1` and `{user}:2`. -- **No transactions.** `MULTI` carries no key, so the client cannot know which shard to open it on, and Redis rejects a queued command that hashes elsewhere. Saving grid edits runs the statements one at a time instead. +`DBSIZE` is summed across shards, browsing merges their keys into one tree, and `MGET`, `MSET`, `DEL`, `EXISTS`, `TOUCH` and `UNLINK` are split per shard and recombined. A `MOVED` re-points the slot and retries, an `ASK` retries against the importing node, and one command follows at most five redirects. What Cluster mode cannot do is under [Limitations](#limitations). -If the mode does not match the server, TablePro says so at connect rather than failing later: pointing Standalone at a cluster member, or any data mode at a Sentinel port, names the field to change. +The wrong mode is caught at connect: Standalone against a cluster member, or any data mode against a Sentinel port, names the field to change. ## Amazon ElastiCache (IAM) -Set **Authentication** to an AWS IAM mode (Access Key, Profile, or SSO) to connect to an IAM-enabled ElastiCache cache. TablePro generates a short-lived IAM auth token and uses it as the Redis password; the username is your IAM-enabled Redis user. Enter the AWS region and the cache name (replication group ID), and enable TLS, which ElastiCache IAM requires. Profiles resolve from `~/.aws/config` and `~/.aws/credentials`, including `credential_process`, SSO, and assumed roles. +Set **Authentication** to an AWS IAM mode (Access Key, Profile, or SSO). A short-lived IAM token becomes the Redis password and the username is your IAM-enabled Redis user. Enter the AWS region and the cache name (replication group ID), and turn TLS on, which ElastiCache IAM requires. Profiles resolve from `~/.aws/config` and `~/.aws/credentials`, `credential_process`, SSO, and assumed roles included. -## SSL/TLS +## Browsing keys -Configure in the **SSL/TLS** pane of the connection form. Managed services like Upstash and Redis Cloud require TLS. Use `rediss://` for URL imports. - -| Mode | hiredis behavior | Use for | -|---|---|---| -| **Disabled** | Plain TCP | Local dev | -| **Preferred** | TLS, do not verify peer | Same as Required on Redis (there is no fallback) | -| **Required (skip verify)** | TLS, do not verify peer | Self-signed certs, Upstash, untrusted CAs | -| **Verify CA** | TLS, verify peer with your CA cert | Private PKI; supply CA path | -| **Verify Identity** | Same as Verify CA | No separate hostname check on Redis | - -For untrusted-CA endpoints (Upstash, internal load balancers), pick **Required (skip verify)**. For private CA setups, pick **Verify CA** and provide the CA certificate path. SNI is sent for every TLS mode; Verify Identity adds nothing over Verify CA on Redis. - -## Features - -**Namespace Browsing**: The sidebar groups keys by separator (default `:`). `user:1` and `user:2` appear under a `user` folder, and nesting goes as deep as the key does (`app:cache:session:1`). Change the separator in Advanced settings. +`user:1` and `user:2` sit under a `user` folder, nested as deep as the key goes (`app:cache:session:1`). Change the separator in Advanced settings. Redis keys grouped by namespace in the sidebar with values in the data grid Redis keys grouped by namespace in the sidebar with values in the data grid -**Key-Value Viewing**: The grid has **Key**, **Type**, **TTL**, **Length**, and **Value** columns. A string key shows its complete value, however long it is. The other types show the first 100 elements as JSON: hashes as an object, lists and sets as an array, sorted sets as `[member, score]` pairs, streams as `[id, fields]` for the newest 5 entries. The Length column is what Redis reports for the key, so you can tell how much a preview leaves out: bytes for a string (`STRLEN`), element count for everything else (`LLEN`, `HLEN`, `SCARD`, `ZCARD`, `XLEN`). Only a string Value cell can be edited in the grid; change the other types in the CLI. A value that is not valid UTF-8, such as a gzip or MessagePack payload, shows as binary and opens in the hex editor rather than as text. +The grid columns are **Key**, **Type**, **TTL**, **Length**, and **Value**. Value is a preview for everything but a string; Length is what Redis reports, so the two together say how much is missing. -**TTL Management**: The TTL column shows seconds to expiry. `-1` means no expiration, `-2` means the key does not exist. Edit the cell to run `EXPIRE`, or set it to `-1` to run `PERSIST`. +| Type | Value column | Length column | +|---|---|---| +| String | The entire value, however long | `STRLEN`, in bytes | +| Hash | First 100 fields, as a JSON object | `HLEN` | +| List | First 100 elements, as a JSON array | `LLEN` | +| Set | First 100 members, as a JSON array | `SCARD` | +| Sorted set | First 100 `[member, score]` pairs | `ZCARD` | +| Stream | Newest 5 entries, as `[id, fields]` | `XLEN` | -**Key Filtering**: Toggle the filter bar to search keys by pattern. Patterns use Redis glob syntax (`*` matches any sequence, `?` matches one character, `[ae]` matches a character set) and are case-sensitive, the same as `redis-cli`. +A value that is not valid UTF-8, such as a gzip or MessagePack payload, opens in the hex editor instead of as text. -- The type scope narrows results to String, Hash, List, Set, Sorted Set, or Stream. -- Matching runs server-side with `SCAN MATCH` and `SCAN TYPE`, and stops after 10,000 matching keys. -- Filtering by key value or TTL is not supported, since Redis has no server-side primitive for it. +### Editing -**Redis CLI**: Each statement is one Redis command. To run several in one go, separate them with `;`. TablePro passes each command through as-is, including commands it does not recognize; only the result formatting is type-aware. Arguments are quoted the same way `redis-cli` quotes them, so `"` and `'` both work and `\xHH` writes a raw byte. Unbalanced quotes are rejected instead of being guessed at. Redis has no comment syntax, and TablePro does not strip comments, so a `--` or `#` line inside a statement is sent to the server and fails. +Editing a **Key** cell runs `RENAME`. Editing a **TTL** cell runs `EXPIRE`, or `PERSIST` when you set it to `-1`; in that column `-1` means no expiry and `-2` means the key is gone. Editing a **Value** cell runs `SET`, and only on a string, since a preview of a hash or list is not the whole structure. -The blocks below list one command per line for reference. +Change the other types with a command: `HSET myhash field1 "value1"` rewrites one field and leaves the rest alone. Adding a row does follow the type you pick, generating `HSET`, `RPUSH`, `SADD`, `ZADD`, or `SET`. -Key operations: +### Filtering -```redis -GET mykey -SET mykey "hello" EX 60 -DEL mykey key2 key3 -KEYS user:* -``` - -Hash, list, set, and sorted set operations: +Toggle the filter bar to search keys by pattern. Patterns are Redis glob (`*` any sequence, `?` one character, `[ae]` a character set) and case-sensitive, as in `redis-cli`. The type scope narrows to String, Hash, List, Set, Sorted Set, or Stream. Matching runs server-side with `SCAN MATCH` and `SCAN TYPE`. -```redis -HGETALL myhash -HSET myhash field1 "value1" -LRANGE mylist 0 -1 -LPUSH mylist "item1" "item2" -SMEMBERS myset -SADD myset "member1" "member2" -ZRANGE myzset 0 -1 WITHSCORES -ZADD myzset 1 "one" 2 "two" -``` +## Redis CLI -Scanning and server info: +Each statement is one command; separate several with `;`. Commands the driver does not recognize go through too, and only the result formatting is type-aware. Arguments are quoted the way `redis-cli` quotes them, so `"` and `'` both work and `\xHH` writes a raw byte; unbalanced quotes are rejected rather than guessed at. Redis has no comment syntax and none is stripped, so a `--` or `#` line is sent to the server and fails. ```redis +SET mykey "hello" EX 60 +HGETALL myhash; LRANGE mylist 0 -1 SCAN 0 MATCH user:* COUNT 100 -PING -INFO -DBSIZE ``` -## Troubleshooting +## SSL/TLS + +Set this in the **SSL/TLS** pane. Upstash, Redis Cloud and the like require TLS; `rediss://` turns it on when importing a URL. -**Connection refused**: Check Redis is running (`brew services start redis`), verify correct port in `redis.conf`, check `bind` directive. +| Mode | hiredis behavior | Use for | +|---|---|---| +| **Disabled** | Plain TCP | Local dev | +| **Preferred** | TLS, do not verify peer | Same as Required here; there is no fallback | +| **Required (skip verify)** | TLS, do not verify peer | Self-signed certs, Upstash, untrusted CAs | +| **Verify CA** | TLS, verify peer with your CA cert | Private PKI; supply the CA path | +| **Verify Identity** | Same as Verify CA | Redis does no separate hostname check | -**Auth failed**: Verify password matches `requirepass` in `redis.conf`. For Redis 6.0+ ACL: `ACL SETUSER myuser on >password ~* +@all` +New connections default to **Disabled**. SNI is sent in every TLS mode. -With the Username field empty, TablePro sends `AUTH password`, which Redis and Valkey always check against the `default` user. An ACL user's password fails that check with `WRONGPASS invalid username-password pair or user is disabled.` Fill in Username to authenticate as that user. +## Limitations -Note that `>password` sets a password and `#hash` sets a SHA-256 hash. A 64-character hex string is valid for both, so `ACL SETUSER myuser on #<64-hex>` is accepted where `>` was meant, and every later login fails with `WRONGPASS`. `ACL LIST` shows stored passwords as hashes, so a value copied from there is a hash, not a password. +- No transactions in Cluster mode. Grid saves run their statements one at a time. Group keys under one hash tag if they must move together. +- Cluster mode serves database 0 only. The Database Index field is hidden and the sidebar shows a single `db0`. +- A command whose keys span hash slots, such as `RENAME`, `SMOVE`, or the `*STORE` commands, is refused in Cluster mode before it is sent. Give the keys a shared hash tag, like `{user}:1` and `{user}:2`. +- A key that is not valid UTF-8 never appears in the grid or the tree. Reach it from the CLI; values have no such limit. +- Pub/Sub has no grid support. `PUBLISH` runs in the CLI, and there is no subscriber view. +- The sidebar tree loads at most 50,000 keys and a filter scans at most 10,000 matches. Narrow the pattern, or `SCAN` from the CLI. +- Values and TTLs cannot be filtered on. Redis has no server-side primitive for either; filter by key pattern and type only. +- No import. Export works as it does elsewhere. -**Timeout**: Verify host/port, check network and firewall, whitelist IP for cloud-hosted Redis. +## Troubleshooting -**Sentinel or Cluster over SSH**: a tunnel forwards one local port to one remote address, and the addresses Sentinel returns are the server's own. Over a tunnel TablePro connects straight to the first host in the list as a standalone node, so failover and shard routing are not available. +**Connection refused**: check Redis is running (`brew services start redis`), the port matches `redis.conf`, and the `bind` directive covers the address you are using. -**Slow key list**: `KEYS` blocks the server on a large keyspace. Browse by namespace instead, and use `SCAN` when you need a pattern match in the CLI. Check memory pressure with `INFO memory`. +**WRONGPASS invalid username-password pair or user is disabled.**: with Username empty, `AUTH password` is checked against the `default` user, and an ACL user's password fails that. Fill in Username to authenticate as that user. -## Limitations +One ACL trap produces the same error: `>password` sets a password, `#hash` sets a SHA-256 hash, and a 64-character hex string is valid for both, so `ACL SETUSER myuser on #<64-hex>` is accepted where `>` was meant. `ACL LIST` prints passwords as hashes, so a value copied from there is a hash, not a password. + +**Timeout**: verify host and port, check the network and firewall, and allow your IP on a cloud-hosted Redis. + +**Sentinel or Cluster over SSH**: a tunnel forwards one local port to one remote address, and the addresses Sentinel and Cluster hand back are the server's own. Over a tunnel only the first host in the list is used, as a standalone node, so failover and shard routing are unavailable. -- Cluster mode has no transactions. `MULTI` carries no key, so a client cannot know which shard to open it on, and Redis rejects a queued command that hashes elsewhere. Grid saves run their statements one at a time instead. -- Cluster mode serves database 0 only, so the Database Index field is hidden and the sidebar shows a single `db0`. -- A command whose keys span hash slots, such as `RENAME`, `SMOVE` or the `*STORE` commands, is refused in Cluster mode. Give the keys a shared hash tag, like `{user}:1` and `{user}:2`. -- Keys must be valid UTF-8 to appear in the grid or the sidebar tree. Values have no such limit. -- Pub/Sub has no grid support. `PUBLISH` runs in the CLI, but there is no subscriber view. -- The grid previews the first 100 elements of a hash, list, set, or sorted set, and a stream's newest 5 entries. The Length column shows the real size. Use `HGETALL`, `LRANGE`, or `XRANGE` in the CLI for the rest. -- The sidebar key tree loads at most 50,000 keys, and key filtering scans at most 10,000 matching keys. +**Slow key list**: `KEYS` blocks the server on a large keyspace. Browse by namespace instead, and use `SCAN` in the CLI when you need a pattern. Check memory pressure with `INFO memory`. diff --git a/docs/databases/redshift.mdx b/docs/databases/redshift.mdx index 2d65c9d76..b57615930 100644 --- a/docs/databases/redshift.mdx +++ b/docs/databases/redshift.mdx @@ -3,20 +3,20 @@ title: Amazon Redshift description: Connect to Redshift clusters and Serverless over the PostgreSQL wire protocol, with DISTKEY and SORTKEY metadata display --- -TablePro connects to Amazon Redshift using the same libpq driver as [PostgreSQL](/databases/postgresql), plus Redshift-specific metadata queries for distribution style, sort keys, and table size. The PostgreSQL page applies unless noted here. +Redshift speaks the PostgreSQL wire protocol and uses the same bundled libpq driver, so the [PostgreSQL](/databases/postgresql) page covers the form and the URL. This page covers where a columnar warehouse differs: distribution style and sort keys in table info, external schemas, and a read-only structure tab. ## Connection settings | Field | Default | Notes | |-------|---------|-------| -| **Host** | - | Cluster endpoint, e.g. `my-cluster.abc123xyz.us-east-1.redshift.amazonaws.com` | -| **Port** | `5439` | | -| **Database** | `dev` | **Required**; every cluster has a `dev` database | +| **Host** | - | The cluster endpoint, `my-cluster.abc123xyz.us-east-1.redshift.amazonaws.com` in shape, or the workgroup endpoint on Serverless | +| **Port** | `5439` | Same for a provisioned cluster and for Serverless | +| **Database** | `dev` | **Required**; every cluster is created with a `dev` database | -Fill in the form and click **Save & Connect**. +Fill in the form and click **Save & Connect**. New connections default to SSL mode **Preferred**, and there is no version gate: any cluster AWS still runs will connect. -AWS IAM authentication is not available for Redshift connections (unlike MySQL and PostgreSQL). Use the cluster's database username and password. +AWS IAM authentication is not available for Redshift connections, unlike MySQL and PostgreSQL. Use a database username and password. For a cluster that is not publicly reachable, put it behind an [SSH tunnel](/connections/ssh-tunneling) on a bastion in the VPC. ## Connection URL @@ -27,45 +27,39 @@ redshift://user:password@my-cluster.abc123xyz.us-east-1.redshift.amazonaws.com:5 See [Connection URL Reference](/connections/urls). -## Features +## Schemas and metadata -**Schemas**: like PostgreSQL, default schema `public`. Table metadata comes from `svv_table_info`: distribution style, sort keys, and table size. +The default schema is `public`, and table metadata comes from `svv_table_info`: distribution style, sort keys, and table size. `dev` is listed like any other database; only `padb_harvest` is marked as a system database. A tab bound to a database other than the connection's active one opens a second connection for it, exactly as on [PostgreSQL](/databases/postgresql#cross-database-tabs). -**External schemas**: schemas created with `CREATE EXTERNAL SCHEMA` are listed alongside regular ones and marked **External**. This covers Redshift Spectrum over S3, federated query to Aurora, RDS, and MySQL, cross-database references, and datashare consumers. Their tables appear under the schema with an external icon. - -External tables are read-only. Redshift does not support `UPDATE` or `DELETE` on them and they have no primary key, so the data grid disables cell editing, adding rows, and deleting rows. Reading works as usual: browse, filter, sort, and export. Row counts are not shown, because `svv_table_info` does not cover external tables. - -**Databases**: `dev`, the database created with every cluster, is listed like any other. Only `padb_harvest` is marked as a system database. - -**DDL**: table definitions include DISTKEY, SORTKEY, DISTSTYLE, and ENCODE directives. Foreign keys display as informational; Redshift does not enforce them. +Table DDL includes DISTKEY, SORTKEY, DISTSTYLE, and ENCODE directives. Foreign keys are shown but not enforced by the server. Redshift table info showing distribution style and sort keys Redshift table info showing distribution style and sort keys -**Backup & Restore**: **Backup Dump** and **Restore Dump** run `pg_dump` and `pg_restore` for Redshift. See [Backup & Restore](/features/backup-restore). +## External schemas -**Import & Export**: export to CSV, JSON, SQL, or XLSX. Import from CSV, JSON, or SQL. See [Import & Export](/features/import-export). +Schemas created with `CREATE EXTERNAL SCHEMA` are listed beside regular ones and marked **External**, with an external icon on their tables. Spectrum over S3, federated query, cross-database references, and datashare consumers all land here. -## Structure editing +Their tables are read-only: Redshift takes no `UPDATE` or `DELETE` on them and they carry no primary key, so cell editing, adding rows, and deleting rows are off. Browsing, filtering, sorting, and export work as usual. -TablePro disables the structure-editing UI for Redshift: no column, index, or key changes from the structure tab. Run `ALTER TABLE` statements in the SQL editor instead. +## Backup -## Limitations +**Backup Dump** and **Restore Dump** run `pg_dump` and `pg_restore` against Redshift, the only engine besides PostgreSQL where they are offered. See [Backup & Restore](/features/backup-restore). -Redshift is a columnar warehouse, not a general-purpose RDBMS. +## Limitations -- No traditional indexes. Redshift uses DISTKEY and SORTKEY instead. -- No enums, sequences, or triggers. -- Foreign keys are informational. Redshift does not enforce them. +- No structure editing. The structure tab is read-only for Redshift; run `ALTER TABLE` in the SQL editor. +- No indexes, enums, sequences, or triggers. DISTKEY and SORTKEY do the work indexes would. +- Foreign keys are informational only. Nothing stops a row that violates one; validate in the load job. - No Maintenance menu. Run `VACUUM` and `ANALYZE` from the SQL editor. -- External tables cannot be edited, and have no row count. Column types show as the external catalog reports them, such as `varchar(65535)` or `array`. +- External tables show no row count, and their column types come through as the external catalog reports them, such as `varchar(65535)` or `array`. ## Troubleshooting -**Connection refused**: check the security group allows port 5439, the cluster is publicly accessible (or use an [SSH tunnel](/connections/ssh-tunneling)), and the cluster is not paused. +**Connection refused**: check the security group allows port 5439, the cluster is publicly accessible or reachable through a tunnel, and it is not paused. -**Auth failed**: verify the master username and password in the AWS Console. +**FATAL: password authentication failed for user "…"**: the cluster rejected the credentials. Verify the master username and password in the AWS Console, and that the user exists in this database. -**Slow queries**: check distribution with `SELECT * FROM svv_table_info WHERE "table" = 'your_table'`, run `ANALYZE` to update stats, and match SORTKEY to your WHERE/JOIN predicates. +**Slow queries**: check distribution with `SELECT * FROM svv_table_info WHERE "table" = 'your_table'`, run `ANALYZE` to refresh stats, and match SORTKEY to the predicates you filter and join on. diff --git a/docs/databases/snowflake.mdx b/docs/databases/snowflake.mdx index 5ab0794ee..8fc01e932 100644 --- a/docs/databases/snowflake.mdx +++ b/docs/databases/snowflake.mdx @@ -3,97 +3,109 @@ title: Snowflake description: Connect to Snowflake with password, key-pair, SSO, or OAuth auth --- -TablePro connects to Snowflake over its connector REST protocol (the same API used by the official drivers). Browse databases, schemas, and tables, run Snowflake SQL, and edit rows in the data grid. The plugin auto-installs when you pick Snowflake, or install it from **Settings > Plugins > Browse > Snowflake Driver**. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; + +The account identifier is the whole address: `myorg-myaccount`, or the legacy locator `xy12345.us-east-1`, whatever sits in front of `.snowflakecomputing.com`. The form has no host or port field at all. + + + Snowflake connection form + Snowflake connection form + ## Quick setup -Click **Create Connection…**, select **Snowflake**, enter your **Account Identifier**, pick an auth method, and click **Save & Connect**. +Click **Create Connection…**, select **Snowflake**, enter the **Account Identifier**, set **Auth Method**, and click **Save & Connect**. The form shows only the fields that method needs. Any account works: the driver speaks the same connector REST protocol as Snowflake's own drivers, so there is no edition or version to match. -The account identifier is the part before `.snowflakecomputing.com`: + -- Org-account format: `myorg-myaccount` -- Legacy locator format: `xy12345.us-east-1` (locator + region) +## Connection settings + +| Field | Required | Notes | +|-------|----------|-------| +| **Account Identifier** | Yes | `myorg-myaccount` or `xy12345.us-east-1` | +| **Auth Method** | Yes | Password, Key Pair, SSO (Browser), or OAuth Token | +| **Username** | Most | Password, key-pair, and SSO need it | +| Credentials | Per method | The form reveals what the method needs: **Password** with **MFA Passcode (TOTP)**, **Private Key File** with **Private Key Passphrase**, or **OAuth Token** | +| **Warehouse** | No | Compute warehouse such as `COMPUTE_WH` | +| **Database** | No | Default database. Empty browses all of them | +| **Schema** | No | Default schema, `PUBLIC` if empty | +| **Role** | No | Session role, Advanced pane | +| **CLI Connection Name** | No | Section in `~/.snowflake/connections.toml`, Advanced pane | + +## Connection URL + +Snowflake registers no URL scheme. The CLI config stands in for one: name a section in **CLI Connection Name**, and every field left empty is read at connect from `~/.snowflake/connections.toml`, or a `[connections.]` section of `config.toml`. ## Authentication -**Username & Password**: Standard Snowflake login with your username and password. +### Password + +If the account enforces MFA, put a current code in **MFA Passcode (TOTP)**; it expires in under a minute, so refresh it immediately before connecting. Running `ALTER ACCOUNT SET ALLOW_CLIENT_MFA_CACHING = TRUE;` as ACCOUNTADMIN ends that chore: connect once with a fresh code, and Snowflake returns an MFA token that TablePro keeps in the Keychain. + +### Key pair -**Key Pair (.p8)**: Point to an RSA private key file in PKCS#8 (`.p8`) format and, if the key is encrypted, supply its passphrase. Register the matching public key on your user first: +Point **Private Key File** at an RSA private key in PKCS#8 (`.p8`) format, and add **Private Key Passphrase** for an encrypted key. Register the matching public key on the user first: ```sql ALTER USER jane_doe SET RSA_PUBLIC_KEY='MIIBIjANBgkq…'; ``` -**Single Sign-On (Browser)**: Authenticate through your identity provider (Okta, Entra ID, etc.) in the browser. TablePro opens your IdP's login page and captures the response automatically (`authenticator=externalbrowser`). With `ALLOW_ID_TOKEN = TRUE` on the account, the sign-in token is cached in the Keychain so later connects skip the browser for about four hours. +Every connect signs a one-hour JWT with that key, so the Mac's clock has to be accurate. -**OAuth Token**: Paste a valid OAuth access token issued for your Snowflake account. +### Browser SSO - -Already use the Snowflake CLI? Set **CLI Connection Name** (Advanced) to a connection defined in `~/.snowflake/connections.toml` and TablePro fills in the account, user, auth method, warehouse, database, schema, and role for you. - +Sends you to your identity provider in the default browser and catches the reply on a loopback port, giving up after 2 minutes. With `ALLOW_ID_TOKEN = TRUE` on the account, Snowflake also returns a sign-in token; it goes to the Keychain, and later connects skip the browser until it is rejected. -## Connection settings +### OAuth token -| Field | Required | Notes | -|-------|----------|-------| -| **Account Identifier** | Yes | e.g. `myorg-myaccount` or `xy12345.us-east-1` | -| **Auth Method** | Yes | Password, Key Pair, SSO (Browser), or OAuth Token | -| **Username** | Most | Required for password, key-pair, and SSO | -| **Password** | Password only | Account password | -| **MFA Passcode (TOTP)** | If MFA enforced | Current code from your authenticator app; refresh it before each connect | -| **Private Key File** | Key Pair only | Path to an RSA `.p8` key (PKCS#8) | -| **Private Key Passphrase** | Key Pair only | Only if the key is encrypted | -| **OAuth Token** | OAuth only | A valid Snowflake OAuth access token | -| **Warehouse** | No | Compute warehouse, e.g. `COMPUTE_WH` | -| **Database** | No | Default database | -| **Schema** | No | Default schema (defaults to `PUBLIC`) | -| **Role** | No | Session role (Advanced), e.g. `SYSADMIN` | -| **CLI Connection Name** | No | Name in `~/.snowflake/connections.toml` (Advanced) | +Paste an access token issued for the account. Nothing refreshes it for you. -## Features +## Browsing and sessions -**Database & Schema Browsing**: The sidebar groups objects by database and schema. Switch the active database or schema with `USE DATABASE` / `USE SCHEMA`, or by selecting them in the UI. +The sidebar groups objects by database and schema. Object lists come from `SHOW` commands, which the metadata service answers with no warehouse running; column details come from `INFORMATION_SCHEMA`, which needs one. -**Warehouse & Role Switchers**: Toolbar pickers show the active warehouse and role and switch them with `USE WAREHOUSE` / `USE ROLE`, no reconnect needed. A suspended warehouse resumes automatically on the next query. +Toolbar pickers move the session to another **Warehouse** or **Role** with `USE WAREHOUSE` and `USE ROLE`, no reconnect. A tab bound to a second database stays on that session too: `USE DATABASE` runs ahead of its statements. One TablePro connection is one Snowflake session, held open while idle by a heartbeat. -**Snowflake SQL** ([docs](https://docs.snowflake.com/en/sql-reference-commands)): +## Editing -```sql -SELECT * FROM my_db.public.orders LIMIT 100; +`GET_DDL` supplies the DDL view. Grid saves bind values server-side, VARIANT, OBJECT, and ARRAY cells through `PARSE_JSON`. The structure editor refuses a change Snowflake would reject before anything runs: --- Semi-structured data -SELECT value:id::int AS id, value:name::string AS name -FROM raw, LATERAL FLATTEN(input => raw.payload); +| Change | Result | +|--------|--------| +| Add, rename, or drop a column | Runs | +| Widen a VARCHAR | Runs. Shrinking is refused | +| Change NUMBER precision | Runs at the same scale | +| Any other type change | Refused | +| NOT NULL, comment, primary key | Runs | +| Set a column default to a new value | Refused. Clearing one runs | --- Time travel -SELECT * FROM events AT(OFFSET => -60*5); -``` +## SSL/TLS -**Data Types**: NUMBER, FLOAT, VARCHAR, BINARY, BOOLEAN, DATE, TIME, TIMESTAMP_NTZ/LTZ/TZ, VARIANT, OBJECT, ARRAY, GEOGRAPHY, GEOMETRY. Semi-structured values display as JSON text. +No SSL/TLS pane, and no plaintext option. Every request is HTTPS to the account endpoint on port 443. -**Structure Editing**: Add, rename, and drop columns; widen VARCHAR lengths and adjust NUMBER precision (Snowflake does not allow shrinking or cross-type changes); toggle NOT NULL; edit column comments; change primary keys. Declared primary keys, foreign keys, and clustering keys show in the structure pane (Snowflake treats key constraints as informational). +## Limitations -**DDL**: View object DDL via `GET_DDL`. Create or replace views, truncate and drop tables. +- No SSH tunnel. The form has no SSH pane, and the endpoint is reached over HTTPS only. +- A grid edit keys on the declared primary key, or on every column when there is none, and a duplicate key value updates or deletes every row that matches it. Snowflake never enforces key constraints, so check a table for duplicates before editing it. -**Import**: CSV, TSV, and JSON files import into new or existing tables through parameterized inserts. +## Troubleshooting -**Export**: CSV, JSON, SQL, XLSX formats. +### 394508, MFA is required -## Troubleshooting +The account enforces MFA on password logins. Enter a current code in **MFA Passcode (TOTP)**, or move to key-pair or browser SSO, which never prompt. -**Auth failed**: Verify the account identifier and credentials. For key-pair auth, confirm the public key is registered on the user and the passphrase is correct. +### 394507, the passcode was rejected -**MFA with TOTP is required (394508)**: Your account enforces MFA on password logins. Enter a current code from your authenticator in **MFA Passcode (TOTP)**. It expires quickly, so refresh it right before connecting. Key-pair or SSO auth avoids the prompt entirely. +The code was wrong or already expired. A rejected code is remembered and never resent, so open the connection settings and type a fresh one. -**TOTP Invalid (394507) while browsing after a successful connect**: TablePro opens extra sessions for browsing, and each new login revalidates the saved passcode, which expires within a minute. Enable MFA token caching on the account so one fresh code covers all later sessions: run `ALTER ACCOUNT SET ALLOW_CLIENT_MFA_CACHING = TRUE;` (needs ACCOUNTADMIN), then reconnect with a current code. TablePro caches the returned MFA token in the Keychain and skips the passcode from then on. +### JWT token is invalid -**JWT token is invalid**: The account name in the JWT issuer must match your account locator. Make sure your machine clock is accurate (JWTs are time-sensitive). +Key-pair auth. The JWT issuer carries your account locator and a timestamp, so check the identifier and the Mac's clock, then confirm the public key is registered on the user. -**No databases listed**: Ensure the connecting role has `USAGE` on the databases and a warehouse is set so metadata queries can run. +### No databases in the sidebar -**Browser SSO didn't return**: The login must complete within 2 minutes; the local callback listens on `127.0.0.1`. Disable popup blockers and retry. +The connecting role needs `USAGE` on the databases. Set **Warehouse** too: column metadata runs on `INFORMATION_SCHEMA`, which needs one. -## Limitations +### The browser never came back -- No SSH tunneling (HTTPS only to the Snowflake endpoint) -- Foreign keys display as informational; Snowflake never enforces them +Sign-in has 2 minutes, and the reply arrives on a loopback port. Allow the popup and retry. diff --git a/docs/databases/sqlite.mdx b/docs/databases/sqlite.mdx index b64bb22df..b9200fcfd 100644 --- a/docs/databases/sqlite.mdx +++ b/docs/databases/sqlite.mdx @@ -3,15 +3,13 @@ title: SQLite description: Open and query SQLite database files directly, no server needed --- -SQLite is a file-based database engine. The whole database is one file on your Mac. The driver ships with TablePro, nothing to install. +Point the connection at a file that does not exist yet and it is created on connect, so a scratch database costs one filename and one click. ## Quick setup -Click **Create Connection…**, select **SQLite**, pick the database file with **Browse…**, and click **Save & Connect**. No host, port, or credentials. +Click **Create Connection…**, select **SQLite**, pick the file with **Browse…**, and click **Save & Connect**. There is no host, port, or credential to fill in. The driver ships inside TablePro and reads the file through the SQLite that macOS supplies, so there is no server version to match. -Point the path at a file that does not exist and TablePro creates the database on connect. - -TablePro registers as a handler for `.db`, `.db3`, `.s3db`, `.sl3`, `.sqlite`, `.sqlite3`, and `.sqlitedb` files, so double-clicking one in Finder opens it directly. +`.db`, `.db3`, `.s3db`, `.sl3`, `.sqlite`, `.sqlite3`, and `.sqlitedb` files list TablePro under Finder's **Open With**, as an alternate handler rather than the default one. To make a double-click open them here, select one in Finder, press `Cmd+I`, set **Open with** to TablePro, and click **Change All…**. SQLite connection form with file path field @@ -32,7 +30,7 @@ TablePro registers as a handler for `.db`, `.db3`, `.s3db`, `.sl3`, `.sqlite`, ` The Safari, Photos, and Messages databases sit in folders macOS protects. Grant TablePro Full Disk Access in **System Settings > Privacy & Security > Full Disk Access**, then relaunch the app. Without it, opening those files fails with a permission error. -System databases may also be locked. Quit their parent app before opening. +Those databases are also often locked by the app that owns them. Quit it first. ## Connection URL @@ -43,15 +41,28 @@ sqlite:///path/to/database.db See [Connection URL Reference](/connections/urls) for all parameters. -## Features +## Browsing + +The sidebar lists tables and views and hides the internal `sqlite_*` tables. Each table carries its columns, constraints, indexes, foreign keys, and DDL; triggers can be read and edited. + +Change the file outside TablePro and the object list reloads on its own. Rows already loaded in a tab stay until you refresh that tab. + +One connection is one file, with no database to switch between, and the object list reads the main database only: a file you `ATTACH` in the editor is queryable as `alias.table` but never appears in the sidebar. + +## Limitations -- The sidebar lists tables and views. Internal `sqlite_*` tables are hidden. -- Table info shows columns, constraints, indexes, foreign keys, and DDL. -- Triggers can be viewed and edited. -- A file watcher tracks the database file. When it changes on disk, the sidebar object list reloads on its own. An open data tab keeps its rows until you refresh it. +- Encrypted databases do not open. The driver uses the system SQLite, which takes no key, and the form has nowhere to put one. Decrypt a SQLCipher file with the `sqlcipher` tool first. ## Troubleshooting -**Locked database**: Close other apps using the file, or check for WAL files (`database.sqlite-wal`, `database.sqlite-shm`) left by a crashed process. +### database is locked + +Another process holds a write lock. Quit the app that owns the file, or look for `-wal` and `-shm` files a crashed process left behind. The wait runs until the [query timeout](/customization/general-settings#query-timeout); **Query > Cancel Query** ends it sooner. + +### unable to open database file + +The path is wrong, or the folder is one you cannot read. For anything under `~/Library`, grant Full Disk Access and relaunch. + +### file is not a database -**Can't open**: Verify the path exists and you have read permission on the file. For a protected folder such as `~/Library/Safari`, `~/Library/Messages`, or the Photos library, grant Full Disk Access and relaunch TablePro. +The file is encrypted or is not SQLite at all. `file database.db` names what it actually is. diff --git a/docs/databases/surrealdb.mdx b/docs/databases/surrealdb.mdx index e5751d7c6..c7a861bce 100644 --- a/docs/databases/surrealdb.mdx +++ b/docs/databases/surrealdb.mdx @@ -3,30 +3,39 @@ title: SurrealDB description: Connect to SurrealDB with namespace and database browsing, SurrealQL, and inline record editing --- -TablePro connects to SurrealDB 2.x and 3.x over the HTTP RPC protocol. Browse namespaces and databases in the sidebar, run SurrealQL in the editor, and edit records inline in the data grid. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -The driver talks CBOR, so record links, datetimes, durations, decimals, and UUIDs keep their types instead of arriving as plain strings. +Whichever level your user was defined at, Root, Namespace, Database, or Record Access, **Auth Level** has to name it. SurrealDB cannot infer it from the credentials, and the rest of the form follows from that one field. ## Quick setup -Click **Create Connection…**, select **SurrealDB**, enter the host and port (default `8000`), pick an auth level, and connect. +Click **Create Connection…**, select **SurrealDB**, enter the host and port, pick the auth level that matches your user, and connect. The driver needs SurrealDB 2.0 or later and talks CBOR over the HTTP RPC endpoint, so record links, datetimes, durations, decimals, and UUIDs keep their types. -SurrealDB is a registry plugin. Picking it in the **Choose a Database** sheet prompts to install; connecting to a saved SurrealDB connection installs it without asking. You can also install **SurrealDB Driver** from **Settings > Plugins > Browse**. + -TablePro registers the `surrealdb://` URL scheme, so `surrealdb://user:pass@host:8000` links pre-fill the connection form (see [Connection URL Reference](/connections/urls)). For a remote server, connect through an [SSH tunnel](/connections/ssh-tunneling). +## Connection settings -## Namespaces and databases +| Field | Description | +|-------|-------------| +| **Host** | Server host, such as `localhost` | +| **Port** | HTTP port, default `8000` | +| **Namespace** | The namespace to open | +| **Auth Level** | Which level your credentials belong to | +| **Username / Password** | Every level except Token | +| **Database** | Shown for Database and Record Access. At the other levels, pick one from the sidebar after connecting | +| **Token** | A JWT, shown for Token | +| **Access Method** | The name from `DEFINE ACCESS`, shown for Record Access | -SurrealDB nests tables under a namespace and a database. TablePro maps that straight onto the sidebar: namespaces sit at the top level, databases sit inside them, and tables sit inside a database. Switch either level without reconnecting. +## Connection URL -Set the namespace in the **Namespace** field when you create the connection. Leave **Database** empty to pick one from the sidebar after connecting. +```text +surrealdb://user:pass@host:8000 +``` -Right-click a namespace for **Drop Namespace…** (`REMOVE NAMESPACE`) or a database for **Drop Database…** (`REMOVE DATABASE`). Select several first with Shift-click or Cmd-click and the menu covers all of them, with one confirmation listing every name. See [Work on Several Databases at Once](/features/table-operations#work-on-several-databases-at-once). +The scheme is registered with macOS, so a link like that opens the connection form pre-filled. See [Connection URL Reference](/connections/urls). For a remote server, connect through an [SSH tunnel](/connections/ssh-tunneling). ## Authentication -SurrealDB cannot infer which level your credentials belong to, so pick the one that matches the user you are signing in as. - | Auth Level | Use it for | Needs | |-------|-------------|-------------| | **Root** | A root user, defined with `DEFINE USER … ON ROOT` | Username, password | @@ -35,57 +44,73 @@ SurrealDB cannot infer which level your credentials belong to, so pick the one t | **Record Access** | A record user, defined with `DEFINE ACCESS … TYPE RECORD` | Namespace, database, access method, and the signin fields | | **Token** | A JWT you already hold, including SurrealDB Cloud | The token | -Only a root user can list every namespace. With namespace or database credentials, TablePro browses the scope your user is limited to. +Only a root user lists every namespace; other levels browse the scope the user is limited to. -## Connection settings - -### Required fields - -| Field | Description | -|-------|-------------| -| **Host** | Server host, e.g. `localhost` | -| **Port** | HTTP port, default `8000` | -| **Auth Level** | Root, Namespace, Database, Record Access, or Token | +## Namespaces and databases -### Optional fields +Namespaces sit at the top of the sidebar, databases inside them, tables inside a database. Neither switch reconnects. The namespace and database ride on each request, and a tab bound to a second database keeps the same connection. -| Field | Description | -|-------|-------------| -| **Namespace** | The namespace to open. Required for every level except Root and Token | -| **Database** | The database to open. Required for Database and Record Access; otherwise pick one from the sidebar | -| **SSL Mode** | In the **SSL/TLS** pane. Any mode other than Disabled connects over HTTPS. **Preferred** and **Required (skip verify)** accept a self-signed certificate | -| **Skip TLS Verification** | Advanced section. Trusts any certificate, even with **Verify CA** or **Verify Identity** set | -| **Token** | A JWT, shown when Auth Level is Token | -| **Access Method** | The name from `DEFINE ACCESS`, shown when Auth Level is Record Access | +Right-click a namespace for **Drop Namespace…** (`REMOVE NAMESPACE`) or a database for **Drop Database…** (`REMOVE DATABASE`). Select several with Shift-click or Cmd-click and one confirmation covers all of them. See [Work on Several Databases at Once](/features/table-operations#databases-and-schemas). -## Editing records +## Records in the grid SurrealDB table records with record links and typed values in the data grid SurrealDB table records with record links and typed values in the data grid -The grid writes one field at a time. Changing a cell runs an `UPDATE` that sets only the fields you touched, so computed `FUTURE` fields and fields another writer changed are left alone. +Changing a cell runs an `UPDATE` that sets only the fields you touched, leaving computed `FUTURE` fields and another writer's changes alone. Values go out as typed parameters, so an `int` stays an `int` and a filter value is never parsed as SurrealQL. -- `id` is the primary key and is read-only. Clear it on a new row to let SurrealDB generate one. +- `id` is the primary key and read-only. Leave it empty on a new row for a generated one, or type `table:id` to choose it. - A record link shows as `table:id`. Objects and arrays show as compact JSON and are edited as JSON. - On a `RELATION` table, `in` and `out` sit next to `id`. +- A `SCHEMALESS` table has no declared fields, so its columns come from the rows fetched: a field only some records carry still gets a column, and the rest show an empty cell. + +## SurrealQL + +The editor runs whole SurrealQL scripts, with completions for the common statements: -Values are sent as typed parameters, so an `int` column stays an `int` and a filter value is never treated as SurrealQL. +```sql +SELECT * FROM person LIMIT 100; +RELATE person:alice->wrote->article:one SET time = time::now(); +``` -## Schemaless tables +The Explain button offers **Explain** and **Explain Full**, which rerun the current query with `EXPLAIN` and `EXPLAIN FULL` to show how it resolves, including whether an index is used. -A `SCHEMALESS` table has no declared fields. TablePro reads the columns from the rows it fetched, so a field only some records carry still gets a column, and records missing it show an empty cell. +## SSL/TLS -## Query plans +**Disabled** is the default and connects over HTTP; every other mode connects over HTTPS. **Preferred** and **Required (skip verify)** accept any certificate, **Verify CA** and **Verify Identity** check it against the system trust store. -The Explain button in the editor offers **Explain** and **Explain Full**, which run the current query with SurrealDB's `EXPLAIN` and `EXPLAIN FULL` to show how it resolves, for example whether an index is used. + +**Skip TLS Verification**, a toggle in the Advanced pane, trusts any certificate even when the mode is **Verify CA** or **Verify Identity**. Leave it off outside a development server with a self-signed certificate. + ## Limitations -- Transactions are not available. Each request is independent over HTTP, so `BEGIN` in one request cannot affect another. Run `BEGIN TRANSACTION; … COMMIT TRANSACTION;` as a single query in the editor instead. - The structure editor is read-only. Create and change fields and indexes with `DEFINE FIELD` and `DEFINE INDEX` in the editor. -- Live queries (`LIVE SELECT`) are not supported. +- Editing `in` or `out` on a `RELATION` table does nothing: those fields are dropped from the generated `UPDATE`. Rewire a relation with `RELATE` or an explicit `UPDATE`. +- Multi-request transactions are not available over HTTP. Send `BEGIN TRANSACTION; … COMMIT TRANSACTION;` as one editor query instead. +- Live queries (`LIVE SELECT`) are not supported. Rerun the query to see new records. +- A CA certificate file set in the **SSL/TLS** pane is ignored; verification goes through the system trust store, so install a private CA there. +- Import is not available. Export works, see [Import and Export](/features/import-export). - Range values display but cannot be edited. -- The editor treats SurrealQL as plain text, without syntax coloring. +- The editor treats SurrealQL as plain text, with no syntax coloring. - SurrealDB 1.x is not supported. + +## Troubleshooting + +### … is required for the selected authentication level + +The level needs a scope the form has not been given. Namespace, Database, and Record Access all need **Namespace**; the last two also need **Database**; Record Access also needs **Access Method**. + +### SurrealDB … is not supported. TablePro requires SurrealDB 2.0 or later. + +The version comes from the server's own `/version` reply. Upgrade the server, or point the connection at a 2.x or 3.x instance. + +### SurrealDB over HTTP does not support multi-request transactions. + +A transaction cannot span requests over HTTP RPC. Put the whole transaction in one editor query. + +### The credentials do not match the auth level + +A root user signed in at Database level fails, and so does the reverse. Match the level to how the user was defined. diff --git a/docs/databases/teradata.mdx b/docs/databases/teradata.mdx index fd43b3ba0..501cc0df7 100644 --- a/docs/databases/teradata.mdx +++ b/docs/databases/teradata.mdx @@ -3,53 +3,78 @@ title: Teradata description: Browsing databases and objects on Teradata Vantage, with TLS and transaction mode --- -The driver speaks the Teradata wire protocol from native Swift. Setup is a connection form, not a client install. Sessions use the TD2 logon mechanism with the session data encrypted, and land on gateway port `1025`, or on `443` with TLS on. That TLS port is fixed. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -## Install the plugin +Port `1025` carries a plain session. Any SSL mode moves the session to `443`, where it runs over the gateway's WebSocket endpoint, and that port cannot be changed. A native Swift driver handles both, with no client library to install. -Teradata is a registry driver. Pick Teradata in the database type chooser and TablePro offers to download it, or install it up front from **Settings > Plugins > Browse > Teradata Driver**. The driver loads without restarting the app. + + Teradata connection form + Teradata connection form + + +## Quick setup + +Click **Create Connection…**, select **Teradata**, enter the host, username, and password, and click **Save & Connect**. Any Vantage system that accepts a TD2 logon works; no version is checked. + + ## Connection settings | Field | Description | | --- | --- | -| Host | The Teradata database hostname. | -| Port | The plain gateway port. Default `1025`. TLS connects on `443` instead, and `Preferred` falls back to this port. | -| Database | The default database to use after logon. Optional; leave blank to use your account's default. | -| Username | Your Teradata username. | -| Password | Your Teradata password. | -| Logon Mechanism | Advanced pane. Type `TD2` or `TDNEGO` in uppercase. Default `TD2`. | -| Transaction Mode | Advanced pane. Type `DEFAULT`, `ANSI`, or `TERA` in uppercase. Default `DEFAULT`. | -| SSL Mode | Off by default. `Preferred`, `Required`, `Verify CA`, or `Verify Identity` turn on TLS. | - -## Databases and objects +| **Host** | Teradata database hostname | +| **Port** | Plain gateway port, default `1025` | +| **Database** | Default database after logon. Optional; empty uses your account's default | +| **Username / Password** | Teradata credentials | +| **Logon Mechanism** | Advanced pane. `TD2` or `TDNEGO`, uppercase. Default `TD2` | +| **Transaction Mode** | Advanced pane. `DEFAULT`, `ANSI`, or `TERA`, uppercase | +| **SSL Mode** | **SSL/TLS** pane. **Disabled** by default | -In Teradata a database and a user are both namespaces. TablePro lists them from the data dictionary (`DBC.DatabasesV`) as top-level databases, and their tables, views, and columns come from `DBC.TablesV` and `DBC.ColumnsV`. Macros and stored procedures are not listed. +Leave **Transaction Mode** on `DEFAULT`, which takes whatever the system is configured for, unless a script needs `ANSI` or `TERA` case sensitivity and commit rules on the session. A system reachable only through a bastion works over an [SSH tunnel](/connections/ssh-tunneling). -Identifiers are quoted with double quotes. Teradata has no `LIMIT`, so TablePro pages results with `TOP` on the first page and `QUALIFY ROW_NUMBER()` for later pages. +## Connection URL -## TLS +```text +teradata://user:password@host:1025/database +``` -Teradata carries encrypted sessions over HTTPS on port `443`, separate from the plain gateway port `1025`. TablePro connects with TLS and tunnels the session over the gateway's WebSocket endpoint. The TLS port is fixed at `443` and cannot be changed. Set the SSL Mode to turn it on: +**Import from URL…** accepts the scheme; macOS does not route `teradata://` links. See [Connection URL Reference](/connections/urls). -- `Preferred` tries TLS and falls back to a plain connection on `1025` if the HTTPS port is unreachable. -- `Required` uses TLS but does not check the server certificate. -- `Verify CA` checks the certificate chain, and `Verify Identity` also checks the hostname. Point CA Certificate at your CA file to trust a private authority. +## Databases and objects -To test TLS, use `Required` so a failed handshake surfaces instead of falling back. +A database and a user are both namespaces in Teradata, and both appear as top-level databases in the sidebar with their tables and views inside. A tab bound to a second database keeps the same session: `DATABASE "name"` runs ahead of its statements instead of a reconnect. -## Transaction mode +Identifiers are quoted with double quotes. Teradata has no `LIMIT`, so a table's first page comes back with `TOP` and later pages with `QUALIFY ROW_NUMBER()`. -`ANSI` and `TERA` differ in case sensitivity and commit behavior. `ANSI` compares strings case-sensitively and keeps a transaction open until you commit; `TERA` compares case-insensitively and commits each request unless you wrap it in `BEGIN TRANSACTION`. `DEFAULT` uses whatever the database is configured for. +## SSL/TLS -## Troubleshooting +| Mode | What happens | +| --- | --- | +| **Disabled** | Plain session on the gateway port | +| **Preferred** | TLS on `443`, falling back to a plain session on `1025` when the HTTPS port is unreachable | +| **Required (skip verify)** | TLS, certificate not checked | +| **Verify CA** | Certificate chain checked. Point **CA Certificate** at your CA file for a private authority | +| **Verify Identity** | Chain and hostname checked | -- **Cannot reach the server.** Confirm the host and that port `1025` is open from your network. -- **Logon fails.** Check the username, password, and that the account is not locked. Typing an unsupported mechanism fails before connecting with `unsupported: logon mechanism LDAP; only TD2 and TDNEGO are supported`. A mechanism typed in lowercase is ignored and `TD2` is used instead. +Test TLS on **Required (skip verify)** rather than **Preferred**, or a failed handshake looks like a successful connect. ## Limitations -- Foreign keys are never listed. The Structure tab shows a Foreign Keys view for Teradata and it is always empty. -- LDAP, Kerberos, and JWT logon are not supported. Use `TD2` or `TDNEGO`. +- Foreign keys are never listed. The Structure tab's Foreign Keys view stays empty for Teradata; read the constraints from its DDL view instead. +- Macros and stored procedures are not in the sidebar. Query them from the editor. +- LDAP, Kerberos, and JWT logon fail before the connect is attempted. Use `TD2` or `TDNEGO`. - The TLS port is fixed at `443`. -- Macros and stored procedures are not listed. + +## Troubleshooting + +### unsupported: logon mechanism …; only TD2 and TDNEGO are supported + +**Logon Mechanism** holds something the driver does not implement. Set it to `TD2` or `TDNEGO`. A value the driver does not recognize, a mechanism typed in lowercase included, is read as `TD2` instead. + +### Cannot reach the server + +The connect gives up after 20 seconds. Confirm the host, and that port `1025` is open from your network, or `443` if an SSL mode is set. + +### Logon fails + +Check the username and password, and that the account is not locked. diff --git a/docs/databases/trino.mdx b/docs/databases/trino.mdx index 7d5dd2917..bcfea9a33 100644 --- a/docs/databases/trino.mdx +++ b/docs/databases/trino.mdx @@ -3,56 +3,84 @@ title: Trino description: Catalogs and schemas, type mapping, EXPLAIN variants, and the auth a Trino cluster accepts --- -Trino publishes its client protocol over plain HTTP, and this driver is a REST client for it rather than a bridge to something else. A query is one `POST /v1/statement`; the rows arrive a page at a time until the cluster says it has finished. +import RegistryPlugin from "/snippets/registry-plugin.mdx"; -Presto uses the same protocol under a different header prefix. The current driver targets Trino. +What a write does depends on the catalog's connector, not on TablePro. A statement built correctly here can still come back as `NOT_SUPPORTED` because the connector behind that catalog is read-only or implements only part of SQL. -## Install the plugin + + Trino connection form + Trino connection form + -Trino is a registry driver. Pick Trino in the database type chooser and TablePro offers to download it, or install it up front from **Settings > Plugins > Browse > Trino Driver**. The driver loads without restarting the app. +## Quick setup + +Click **Create Connection…**, select **Trino**, enter the coordinator host and port, set **Username**, and click **Save & Connect**. A cluster with no authentication needs nothing else, and **Catalog** and **Schema** are optional starting points. + + ## Connection settings | Field | Description | | --- | --- | -| Host | The Trino coordinator hostname. | -| Port | The coordinator HTTP port. Default `8080`. Set `8443` when the coordinator serves HTTPS. | -| Username | The user the query runs as. Sent as `X-Trino-User`. | -| Catalog | The default catalog for unqualified table names. Optional; leave blank to browse all catalogs. | -| Schema | The default schema. Optional. | -| Auth Method | Authentication pane. `Username & Password` for LDAP or password-file auth, or `JWT Access Token` for a bearer token. | -| Password | Your password when the auth method is Username & Password. Sent as HTTP Basic auth. | -| Access Token | Authentication pane. Your JWT when the auth method is JWT Access Token. Sent as a bearer token. | -| SSL | Off by default. The SSL pane sets the mode: Preferred and Required both encrypt without checking the certificate, Verify CA checks the certificate chain, and Verify Identity also checks the hostname. This driver has no plaintext fallback, so any mode other than Off forces TLS. Point CA Certificate at your CA file to trust a private authority. | -| Time Zone | Advanced pane. Optional IANA time zone (for example `America/New_York`) for the session. Leave blank to use the server default. | - -Trino requires TLS for any authentication. If you pick Password or JWT, set an SSL mode so the coordinator accepts the credentials. +| **Host** | Trino coordinator hostname | +| **Port** | Default `8080`, or `8443` when the coordinator serves HTTPS | +| **Username** | The user the query runs as | +| **Catalog** | Default catalog for unqualified table names. Optional; empty browses every catalog | +| **Schema** | Default schema. Optional | +| **Auth Method** | Authentication pane. `Username & Password` or `JWT Access Token` | +| **Password / Access Token** | The credential the chosen method needs: the account password, or the JWT in **Access Token** | +| **Time Zone** | Advanced pane. Optional IANA zone (`America/New_York`) for the session | + +## Connection URL + +```text +trino://user@host:8080/catalog +``` + +**Import from URL…** accepts the scheme; macOS does not route `trino://` links. See [Connection URL Reference](/connections/urls). + +## Authentication + +Trino refuses password and JWT credentials over plain HTTP, so both need an SSL mode set. + +| Method | What is sent | +| --- | --- | +| **Username & Password** | HTTP Basic auth, for LDAP or password-file authentication | +| **JWT Access Token** | The token as a bearer credential | +| Client certificate | The **Client Certificate** and **Client Key** from the SSL pane, for mutual TLS. Works with any SSL mode, and nothing is presented with SSL off | ## Catalogs, schemas, and tables -A Trino connection is scoped to a catalog and schema, but every query can name objects in full as `catalog.schema.table`. TablePro shows catalogs from `SHOW CATALOGS`, schemas from `SHOW SCHEMAS`, and tables and columns from each catalog's `information_schema`. Materialized views appear alongside tables and views. Expand a catalog to see its schemas, then a schema to see its tables. Table row counts come from `SHOW STATS`, and table and column comments come from `information_schema` and the `system.metadata` tables. +A connection opens on a catalog and schema, and every query can still name objects in full as `catalog.schema.table`: + +```sql +SELECT * FROM hive.web.clicks JOIN postgres.public.users USING (user_id); +``` + +Expand a catalog in the sidebar for its schemas, then a schema for its tables, with materialized views alongside tables and views. Row counts come from `SHOW STATS`. A tab bound to a second catalog keeps the same session; the catalog rides on each request. Identifiers are quoted with double quotes. Results page with `OFFSET n ROWS FETCH NEXT m ROWS ONLY`, the order Trino's grammar requires. ## Types -Trino's type system maps to TablePro's grid as follows: - -- Numbers keep full precision. `bigint` and `decimal` are read as exact text, not floating point. +- `bigint` and `decimal` are read as exact text, not floating point. - `varbinary` is shown as hex. -- `array`, `map`, and `row` are shown as JSON. -- `json` opens in the JSON viewer. +- `array`, `map`, and `row` are shown as JSON, and `json` opens in the JSON viewer. - `timestamp`, `time`, and their `with time zone` forms keep the value the server returned. ## Editing rows and schema -Edit cells in the data grid and TablePro writes `INSERT`, `UPDATE`, and `DELETE` back. Trino's `information_schema` reports no primary keys, so the WHERE clause matches on every column of the row as it was read. A row that is not unique across all its columns matches more than one row. Values are written with their Trino type, so a `varchar` holding digits stays quoted and a number stays unquoted. Columns whose type cannot be compared with `=`, such as `array`, `map`, and `row`, are left out of the WHERE clause. +Cell edits become `INSERT`, `UPDATE`, and `DELETE`. Values carry their Trino type, so a `varchar` holding digits stays quoted, and columns that cannot be compared with `=` (`array`, `map`, `row`) stay out of the `WHERE` clause. -The Structure tab creates tables and adds, drops, renames, and retypes columns. Statements run in autocommit, matching how most Trino connectors handle DDL. + +Editing one of two identical rows changes both, and deleting one deletes both. With no primary keys reported, a row edit matches on every column of the row as it was read, and there is no transaction to roll it back. Check for duplicates before editing a table with no unique column. + -Whether a write succeeds depends on the connected catalog. Many connectors are read-only or support only part of SQL, so a valid statement can still return `NOT_SUPPORTED` from the connector. Changing a column's type in particular is only supported by a few connectors. +The Structure tab creates tables and adds, drops, renames, and retypes columns, in autocommit. A type change is the operation fewest connectors accept. -Trino connections export but do not import. See [Import and Export](/features/import-export). +## Session and query control + +`SET SESSION` in the editor holds for the rest of the connection: the properties the coordinator reports back are sent with every later request, and `RESET SESSION` clears one. **Query > Cancel Query** (`Cmd+.`) tells the coordinator to kill the running query, which matters where a runaway query burns real compute. ## EXPLAIN variants @@ -66,21 +94,35 @@ Click the Explain dropdown in the query editor to choose: | **Explain (Validate)** | `EXPLAIN (TYPE VALIDATE)`, parses and validates without running the query | | **Explain Analyze** | `EXPLAIN ANALYZE`, runs the query and reports actual timings | -## Authentication - -- **Username & Password** sends HTTP Basic auth. Use it for LDAP or password-file authentication. Requires TLS. -- **JWT Access Token** sends the token as a bearer credential. Requires TLS. -- **Client certificate** works with any TLS mode (Preferred, Required, Verify CA, or Verify Identity). Set the Client Certificate and Client Key files in the SSL pane and TablePro presents them for mutual TLS. With SSL off the connection is plain HTTP and no certificate is sent. +## SSL/TLS -## Troubleshooting +**Disabled** is the default and speaks plain HTTP. There is no plaintext fallback, so every other mode forces TLS. -- **Cannot reach the coordinator.** Confirm the host and that the coordinator port is open. Trino serves HTTP on `8080` and HTTPS on `8443` by default. -- **Authentication failed.** Password and JWT auth need TLS. Turn SSL on and confirm the credentials. -- **A write fails with NOT_SUPPORTED.** The connected catalog's connector does not support that operation. Check the connector's documentation for what it allows. +| Mode | Certificate check | +| --- | --- | +| **Preferred** | None | +| **Required (skip verify)** | None | +| **Verify CA** | Certificate chain. Point **CA Certificate** at your CA file for a private authority | +| **Verify Identity** | Chain and hostname | ## Limitations -- No primary keys, indexes, or foreign keys. Trino's `information_schema` does not report them, so the Structure tab's Indexes view is always empty and row edits key on every column. -- Import is not available. Export works. +- No primary keys, indexes, or foreign keys are reported. The Structure tab's Indexes view is always empty. +- Import is not available. Export works; see [Import and Export](/features/import-export). - Kerberos and OAuth 2.0 authentication are not supported. -- Write support depends on the catalog's connector, not on TablePro. +- The **Query timeout** in [settings](/customization/general-settings#query-timeout) does not reach Trino. A long query runs until you cancel it or the cluster ends it. +- Presto is not supported. It speaks the same protocol under an `X-Presto-` header prefix, and this driver always sends `X-Trino-`. + +## Troubleshooting + +### Cannot reach the coordinator + +Confirm the host and that the coordinator port is open. Trino serves HTTP on `8080` and HTTPS on `8443` by default. + +### Authentication failed + +Password and JWT auth need TLS. Set an SSL mode, then confirm the credentials. + +### NOT_SUPPORTED + +The catalog's connector does not implement that operation, whatever Trino's grammar allows. Check the connector's own documentation for what it supports. diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index f25f8c1b6..f10ced43e 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -3,190 +3,81 @@ title: Architecture description: How the app, plugin system, and core services fit together --- -TablePro is built with: +import DriverCounts from "/snippets/driver-counts.mdx"; -- **SwiftUI** for the UI layer -- **AppKit** for low-level macOS integration (windows, menus, native tabs) -- **Swift Concurrency** (async/await, actors) for all async work -- **Native C libraries** for database connectivity, linked as static `.a` files +Every database driver is a bundle the app loads at runtime. That decision shapes the rest: nothing switches on database type, no driver list is compiled in, and `DatabaseType` is a string-based struct rather than an enum, so a type from a plugin the app has never heard of still round-trips through Codable. -## Design patterns +The UI is SwiftUI with AppKit underneath it for windows, menus, and the data grid. Async work is Swift concurrency throughout. Database connectivity is native C libraries, linked as static `.a` files out of `Libs/`. -### MVVM - -- **Models**: structs (value types, Codable) -- **ViewModels**: `@Observable` classes -- **Views**: SwiftUI, with AppKit bridging where needed - -### Protocol-oriented drivers - -All database connectivity goes through one protocol: - -```swift -protocol DatabaseDriver: AnyObject { - func connect() async throws - func disconnect() - func execute(query: String) async throws -> QueryResult - func fetchTables() async throws -> [TableInfo] - // … -} -``` - -No switch statements on database type. No hardcoded driver list. Plugins register themselves, and the factory resolves them by `DatabaseType.pluginTypeId`. `DatabaseType` is a string-based struct, not an enum, so unknown types from future plugins stay valid. - -### Actor isolation - -Thread-safe shared state uses Swift actors: - -```swift -actor SSHTunnelManager { - private var tunnels: [UUID: SSHTunnel] = [:] - func createTunnel(connectionId: UUID, …) async throws -> Int { … } -} -``` - -## Dependencies - -| Package | Source | Purpose | -|---------|--------|---------| -| CodeEditSourceEditor | Vendored in `LocalPackages/` (with CodeEditTextView, CodeEditLanguages) | Tree-sitter code editor | -| TableProCore | Local package in `Packages/` | Shared SwiftPM modules the macOS and iOS apps both use | -| Sparkle | SPM, 2.9.5+ | Auto-update with EdDSA signing | -| OracleNIO | SPM, TablePro fork pinned by revision | Oracle wire protocol for OracleDriverPlugin | -| rust-dameng | Cargo, TablePro fork pinned by revision | DM8 wire protocol for DamengDriverPlugin | -| swift-certificates | SPM, 1.19.0+ | X.509 types, linked by the app target | -| Yams | SPM, 5.1.0+ | YAML parsing for project folder import | +[`CLAUDE.md`](https://github.com/TableProApp/TablePro/blob/main/CLAUDE.md) in the repository root is the authority on the rules a change has to satisfy: the invariants that have caused real bugs, the ABI policy, and what has to happen before a commit lands. This page is the map of where things live, not the rulebook. ## Plugin system -All database drivers are `.tableplugin` bundles loaded at runtime. This keeps the app binary small and makes adding new databases a matter of dropping in a bundle. +`PluginManager` (`Core/Plugins/`) discovers bundles, version-checks them, and loads them. Four pieces carry the whole system: | Component | Location | Role | |-----------|----------|------| -| TableProPluginKit | `Plugins/TableProPluginKit/` | Shared framework with `DriverPlugin` and `PluginDatabaseDriver` protocols | -| PluginManager | `Core/Plugins/PluginManager.swift` | Discovers, loads, version-checks plugin bundles | -| PluginDriverAdapter | `Core/Plugins/PluginDriverAdapter.swift` | Bridges `PluginDatabaseDriver` to core `DatabaseDriver` | -| DatabaseDriverFactory | `Core/Database/DatabaseDriver.swift` | Resolves `DatabaseType` to loaded plugin | - -`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to the same files, so edit only `Plugins/TableProPluginKit/`. - -### Driver plugins - -Five driver plugins ship inside the app bundle, covering nine database types: - -| Plugin | Database types | Connectivity | -|--------|---------------|--------------| -| MySQLDriverPlugin | MySQL, MariaDB | CMariaDB (libmariadb) | -| PostgreSQLDriverPlugin | PostgreSQL, Redshift, CockroachDB, PGlite | CLibPQ (libpq) | -| SQLiteDriverPlugin | SQLite | System sqlite3 | -| ClickHouseDriverPlugin | ClickHouse | HTTP (URLSession) | -| RedisDriverPlugin | Redis | CRedis | - -The app bundle also carries non-driver plugins: CSVInspectorPlugin, export plugins (CSV, JSON, SQL, XLSX, MQL), and import plugins (CSV, JSON, SQL). - -The remaining 17 driver plugins are downloaded on demand from the [plugin registry](/development/plugin-registry): - -| Plugin | Database types | Connectivity | -|--------|---------------|--------------| -| MongoDBDriverPlugin | MongoDB | CLibMongoc | -| OracleDriverPlugin | Oracle | OracleNIO (SPM fork) | -| DamengDriverPlugin | Dameng DM8 | Rust native wire bridge | -| DuckDBDriverPlugin | DuckDB | CDuckDB | -| MSSQLDriverPlugin | SQL Server | CFreeTDS | -| CassandraDriverPlugin | Cassandra, ScyllaDB | CCassandra | -| EtcdDriverPlugin | Etcd | HTTP (gRPC-gateway JSON) | -| CloudflareD1DriverPlugin | Cloudflare D1 | HTTP (URLSession) | -| DynamoDBDriverPlugin | DynamoDB | HTTP with hand-rolled SigV4 request signing | -| BigQueryDriverPlugin | BigQuery | REST (URLSession) | -| SnowflakeDriverPlugin | Snowflake | REST (URLSession) | -| LibSQLDriverPlugin | LibSQL, Turso | Hrana over HTTP, plus a local SQLite backend | -| ElasticsearchDriverPlugin | Elasticsearch | REST (URLSession) | -| SurrealDBDriverPlugin | SurrealDB | HTTP RPC with CBOR encoding | -| BeancountDriverPlugin | Beancount | Ledger file parsing, optional Python projection | -| TeradataDriverPlugin | Teradata | Native Swift TD2 client (TableProTeradataCore) | -| TrinoDriverPlugin | Trino | Trino client REST protocol over HTTP | - -### PluginKit ABI - -TableProPluginKit builds with Swift Library Evolution (`BUILD_LIBRARY_FOR_DISTRIBUTION = YES`), so its public ABI is resilient. Plugins built against an older PluginKit keep loading under a newer app: the runtime fills unimplemented protocol requirements from their defaults. - -Additive changes (a new requirement with a default implementation, a new field added through a new initializer overload) need no version bump. Breaking changes bump `currentPluginKitVersion` in `PluginManager.swift`, and the loader then rejects mismatched plugins cleanly. Run `scripts/check-pluginkit-abi.sh` before merging any change under `Plugins/TableProPluginKit/`. See [Plugin Development](/development/plugin-development) for the full rules. +| TableProPluginKit | `Plugins/TableProPluginKit/` | Shared framework: the `TableProPlugin`, `DriverPlugin`, and `PluginDatabaseDriver` protocols plus the transfer types | +| PluginManager | `Core/Plugins/PluginManager.swift` | Discovers, validates, and loads plugin bundles | +| PluginDriverAdapter | `Core/Plugins/PluginDriverAdapter.swift` | Bridges `PluginDatabaseDriver` to the app's internal `DatabaseDriver` | +| DatabaseDriverFactory | `Core/Database/DatabaseDriver.swift` | Resolves a `DatabaseType` to a loaded plugin | -### Opt-in plugin protocols +`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to the same files, so edit only the copy under `Plugins/TableProPluginKit/`. -Plugins can adopt protocols beyond `PluginDatabaseDriver` to expose extra capabilities. These are runtime-cast (`as?`), so plugins that do not conform keep working without an ABI bump. + -| Protocol | Purpose | -|----------|---------| -| `PluginDiagnosticProvider` | Provide a user-facing diagnostic for driver errors | -| `PluginProcedureFunctionSupport` | Expose stored procedures and functions in the structure tab | -| `PluginDefaultSortProvider` | Override default-sort behavior per table: `.useAppDefault`, `.suppress`, or `.forceColumns([String])` | -| `PluginPrincipalManagement` | Serve users, roles, and grants to the Users & Roles tab | -| `PluginBrowseFilterProvider` | Describe how the sidebar's object filter behaves for this driver (glob, case sensitivity, type scopes) | +A driver is one of four roles a bundle can take. `ExportFormatPlugin`, `ImportFormatPlugin`, and `DocumentInspectorPlugin` are the others, which is how the CSV, JSON, SQL, XLSX, and MQL formats and the CSV inspector ship. -## Key components +### Opt-in protocols -### DatabaseManager +A driver adopts these on top of `PluginDatabaseDriver` to reach extra surfaces. The app finds them with a runtime cast, so skipping one costs nothing and no ABI bump is involved. -Connection pool and lifecycle management. Primary interface between UI and drivers. Handles connect, disconnect, reconnect, and session tracking. +| Protocol | What adopting it gets you | +|----------|---------------------------| +| `PluginDiagnosticProvider` | A driver-written explanation in front of a raw connection error | +| `PluginProcedureFunctionSupport` | Stored procedures and functions in the structure tab | +| `PluginDefaultSortProvider` | Per-table control of default sorting: `.useAppDefault`, `.suppress`, or `.forceColumns([String])` | +| `PluginPrincipalManagement` | Users, roles, and grants in the Users & Roles tab | +| `PluginBrowseFilterProvider` | Control of the sidebar object filter: glob syntax, case sensitivity, type scopes | +| `SettablePlugin` | A settings pane of your own under **Settings > Plugins** | -### ConnectionHealthMonitor - -Pings active connections every 30 seconds. Auto-reconnects with exponential backoff on failure. - -### Change tracking - -1. A cell edit is recorded by `DataChangeManager` as a pending change -2. Save runs `SQLStatementGenerator`, which turns pending changes into INSERT, UPDATE, and DELETE statements -3. Undo/redo goes through the window's `UndoManager`, registered by `DataChangeManager` -4. `AnyChangeManager` wraps the concrete manager behind the `ChangeManaging` protocol +### PluginKit ABI -### MainContentCoordinator +TableProPluginKit builds with `BUILD_LIBRARY_FOR_DISTRIBUTION = YES`, so its public ABI is resilient: the Swift runtime fills any requirement an already-built plugin never implemented from the protocol's default, and that plugin keeps loading under a newer app. Adding a requirement with a default is free. Removing one, or adding a parameter to an existing public initializer, breaks every shipped binary. -The central coordinator for the main window. It is split across extension files in `Views/Main/Extensions/` (`MainContentCoordinator+Alerts`, `+Filtering`, `+Pagination`, and so on). New coordinator functionality goes in a new extension file, not the main file. +Run `scripts/check-pluginkit-abi.sh` before merging anything under `Plugins/TableProPluginKit/`. [Plugin Development](/development/plugin-development#abi-compatibility) has the additive-versus-breaking rules and what a breaking change obliges you to do. -### Autocomplete engine +## Key services -```mermaid -flowchart LR - CE["CompletionEngine"] --> SCA["SQLContextAnalyzer"] - CE --> SSP["SQLSchemaProvider"] - SSP --> Tables - SSP --> Columns - SCA --> Keywords["SQL Keywords"] -``` +`DatabaseManager` (`Core/Database/DatabaseManager.swift`) is the connection pool and the interface every view and coordinator goes through for connect, disconnect, reconnect, and session tracking. `ConnectionHealthMonitor` pings each live connection every 30 seconds and reconnects with exponential backoff. -- **CompletionEngine**: entry point, produces ranked suggestions -- **SQLContextAnalyzer**: parses cursor position context (table ref, column ref, keyword) -- **SQLSchemaProvider**: actor that caches and serves schema data +`MainContentCoordinator` is the central coordinator, one per connection session rather than per window, built by `SessionStateFactory` and split across extension files in `Views/Main/Extensions/` (`+Alerts`, `+Filtering`, `+Pagination`, and the rest). New coordinator behavior goes in a new extension file rather than the main one. -### MCP server +Autocomplete is three pieces. `CompletionEngine` ranks the suggestions and knows nothing about the editor framework; `QueryCompletionAdapter` bridges it to CodeEdit. `SQLContextAnalyzer` reads what the cursor sits in: a table reference, a column reference, a keyword. `SQLSchemaProvider` is an actor that caches schema and hands concurrent callers the same in-flight `Task` rather than firing a second `fetchTables()`. -The MCP server lives under `Core/MCP/`, and it speaks Model Context Protocol revision `2026-07-28` plus two legacy revisions. +## Change tracking -| Layer | Directory | Role | -|-------|-----------|------| -| Wire | `Wire/` | Codable JSON-RPC 2.0, HTTP, and SSE types | -| Transport | `Transport/` | `NWListener` HTTP server bound to `127.0.0.1`, plus the client transport the `tablepro-mcp` bridge uses | -| Meta | `Meta/` | Protocol versions, per-request `_meta`, client and server capabilities | -| Protocol | `Protocol/` | Dispatcher, method handlers, and the 46 tools | -| Prompts, Completions, Subscriptions | `Prompts/`, `Completions/`, `Subscriptions/` | Prompt catalog rendered from the live schema, argument completion, and `subscriptions/listen` | -| Legacy | `Legacy/` | `initialize`, `Mcp-Session-Id`, and the era adapter for older clients | -| Auth and audit | `Auth/`, `RateLimit/`, `MCPAuditLogger.swift` | Bearer tokens, scopes, connection allowlists, lockouts, hash-chained audit log | + + + `DataChangeManager` records the cell edit as a pending change. Nothing has reached the server. + + + `SQLStatementGenerator` turns the pending set into INSERT, UPDATE, and DELETE statements. + + + Grid edits register on the window's `UndoManager`, which `TabWindowController.windowWillReturnUndoManager` resolves to the selected `ConnectionWorkspace.undoManager`. One window hosts several connections, so the undo stack is per connection, not per window. Structure edits use a private `UndoManager` inside `StructureChangeManager`. + + -`MCPProtocolDispatcher` is an actor. It resolves the era from the request (`_meta` declares the modern one, `Mcp-Session-Id` the legacy one), checks scopes, runs each handler in its own task under a 330-second deadline, and normalizes the result envelope: `resultType`, `serverInfo`, and the cache hints the six cacheable methods carry. +`AnyChangeManager` wraps whichever concrete manager is active behind the `ChangeManaging` protocol, so callers do not care which one they have. -The modern era is stateless, so it has no session store. Legacy sessions still live in `MCPLegacySessionStore`, capped at 16 with least-recently-used eviction, and are dropped when the server stops. +## MCP server -`MCPServerManager` owns the lifecycle on the main actor: it binds a port (falling back to a kernel-assigned one), mints and rotates the bridge credential, writes the handshake file, and tears everything down through `MCPServerComposition`. +`Core/MCP/` holds a Model Context Protocol server bound to loopback, speaking plain HTTP through `NWListener`. Nothing in it is reachable off the machine: no TLS, no remote mode. `MCPProtocolDispatcher` is an actor that resolves the protocol era from the request, checks scopes, runs each handler in its own task under a 330-second deadline, and normalizes the result envelope. `MCPServerManager` owns the lifecycle on the main actor: port binding, the bridge credential, the handshake file. -Nothing in the server is reachable off the machine. It binds loopback and speaks plain HTTP, which the specification permits for a local server; there is no TLS and no remote mode. +The wire contract, the tool catalog, and the version policy are documented for consumers: [MCP Protocol](/external-api/mcp-protocol), [MCP Tools](/external-api/mcp-tools), [Versioning](/external-api/versioning). -See [MCP Protocol](/external-api/mcp-protocol), the [tool catalog](/external-api/mcp-tools), and [Versioning](/external-api/versioning). - -## Data flow - -### Connection +## Connecting ```mermaid flowchart TD @@ -195,29 +86,35 @@ flowchart TD DM --> Factory["DatabaseDriverFactory"] Factory --> PM["PluginManager"] PM --> Adapter["PluginDriverAdapter"] - Adapter --> Connected["Connection Ready"] + Adapter --> Connected["Connection ready"] SSH --> Connected ``` -### Query execution - -```mermaid -flowchart TD - User["Cmd+Enter"] --> Editor["SQL Editor"] - Editor --> DM["DatabaseManager.executeQuery"] - DM --> Driver["DatabaseDriver.execute"] - Driver --> Result["QueryResult"] - Result --> Grid["DataGridView"] -``` +A cancelled connect is the hard case here, because `Task.cancel()` cannot interrupt a driver blocked in a C call. `CLAUDE.md` sets out what a driver owes the app on that path, written down after the same bug shipped four times. -## State management +## State and storage -| Pattern | What | Where | -|---------|------|-------| +| Pattern | What it holds | Where | +|---------|---------------|-------| | `@Observable` | UI state, sessions, active tab | ViewModels | -| UserDefaults via `AppSettingsStorage` | User preferences | `Core/Storage`; a few view-local toggles use `@AppStorage` | -| Keychain | Connection passwords | ConnectionStorage | -| SQLite FTS5 | Query history (full-text search) | QueryHistoryStorage | -| JSON files | Tab state persistence | `TabPersistenceCoordinator`, `TabDiskActor` | +| Keychain | Connection passwords | `ConnectionStorage` | +| UserDefaults | Preferences, filter defaults, favorites, recents | `Core/Storage`, via `AppSettingsStorage` | +| SQLite FTS5 | Query history, full-text searchable | `QueryHistoryStorage` | +| JSON files | Tab state, per-table filters | `TabPersistenceCoordinator`, `TabDiskActor` | + +`CLAUDE.md` carries the full storage table, including which stores sync through iCloud and which stay device-local. + +## Dependencies + +| Package | Source | Purpose | +|---------|--------|---------| +| CodeEditSourceEditor | Vendored in `LocalPackages/`, with CodeEditTextView and CodeEditLanguages | Tree-sitter code editor | +| TableProCore | Local package in `Packages/` | SwiftPM modules the macOS and iOS apps share | +| TableProOracle | Local package in `Packages/`, wraps a pinned TablePro fork of OracleNIO | Oracle wire protocol for OracleDriverPlugin | +| Sparkle | SPM, 2.9.5+ | Auto-update with EdDSA signing | +| swift-certificates | SPM, 1.19.0+ | X.509 types, linked by the app target | +| Yams | SPM, 5.1.0+ | YAML parsing for project folder import | + +`Plugins/DamengDriverPlugin` links a Rust wire-protocol bridge built from `Native/DamengBridge`, not an SPM package. For the repository layout, see [Project Structure](/development/setup#project-structure). diff --git a/docs/development/building.mdx b/docs/development/building.mdx index 2ccc2a4cc..12ee95263 100644 --- a/docs/development/building.mdx +++ b/docs/development/building.mdx @@ -1,132 +1,94 @@ --- title: Building -description: Development builds, release builds, static libraries, DMG packaging, and CI/CD +description: Debug builds, the plugin compile check, clean builds, and what CI runs on a pull request --- -## Development builds +Xcode and `xcodebuild` write the built app to different directories. `Cmd+R` leaves it in +DerivedData; the command line leaves it in `build/Debug/` inside the repo. Both need the +**TablePro** scheme and the **My Mac** destination. -`TablePro.xcodeproj` is generated from `project.yml` by `scripts/generate-project.sh` and is not -in git. Run it after cloning, after editing `project.yml` or `Configs/`, and after adding, moving, -or deleting a source file. See [Development Setup](/development/setup). +`TablePro.xcodeproj` is generated from `project.yml` by `scripts/generate-project.sh` and is not in +git. Run it after cloning, after editing `project.yml` or anything in `Configs/`, and after adding, +moving, or deleting a source file. See [Development Setup](/development/setup). -### Xcode +## Debug builds -- `Cmd+R` to build and run -- `Cmd+B` to build only -- Scheme: **TablePro**, Destination: **My Mac** - -### Command line +In Xcode, `Cmd+B` builds and `Cmd+R` builds and runs. From the command line: ```bash xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation ``` -Build and run: +Build and launch in one line: ```bash xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation && open build/Debug/TablePro.app ``` - -`-skipPackagePluginValidation` is required on every `xcodebuild` invocation. `scripts/build-release.sh`, `scripts/build-plugin.sh`, and `scripts/check-pluginkit-abi.sh` all pass it. - - -Debug builds use automatic signing with an Apple Development identity. Set `TABLEPRO_DEVELOPMENT_TEAM` in `Configs/Secrets.xcconfig` to your own Apple Developer account (a free account works), see [Development Setup](/development/setup). Signing matters even locally: plugin signature verification derives the required team from the running app's signature. - -## Release builds - -```bash -scripts/build-release.sh arm64 # Apple Silicon -scripts/build-release.sh x86_64 # Intel -scripts/build-release.sh both # Both architectures -``` - -Output: `build/Release/TablePro-arm64.app` and `build/Release/TablePro-x86_64.app`. - -The script extracts the right slice from the universal static libraries, strips the main binary, helpers, PluginKit framework, and plugin binaries, then signs each binary individually and the app bundle last with hardened runtime and entitlements. Environment variables control signing: - -| Variable | Default | Purpose | -|----------|---------|---------| -| `SIGN_IDENTITY` | Maintainer's Developer ID | Codesign identity for every binary | -| `NOTARIZE` | `false` | Set `true` to notarize and staple after building | -| `APPLE_ID` | Maintainer's Apple ID | Account for notarization | - -Notarization uses a `notarytool` keychain profile named `TablePro`. Create it once with `xcrun notarytool store-credentials "TablePro" --apple-id … --team-id … --password …`. Do not sign or notarize by hand; the script already does it per binary, which is what Apple's guidance requires. - -## Static libraries - -Static `.a` files (libmariadb, libpq, libsybdb, and others) are hosted on the `libs-v1` GitHub Release, not in git. - -```bash -scripts/download-libs.sh # Download (skips if already present) -scripts/download-libs.sh --force # Re-download and overwrite -scripts/generate-project.sh # Regenerate both .xcodeproj bundles from project.yml -scripts/generate-project.sh ios # Just the iOS one (macos and both are the other options) -``` - -### Publishing libraries (maintainers) - -After rebuilding a library, publish it with `scripts/publish-libs.sh`, naming every lib you rebuilt: +`-skipPackagePluginValidation` belongs on every `xcodebuild` invocation. +`scripts/build-release.sh`, `scripts/build-plugin.sh`, `scripts/check-pluginkit-abi.sh` and every +CI job that shells out to `xcodebuild` pass it. -```bash -scripts/publish-libs.sh libmongoc_arm64.a libmongoc_x86_64.a libmongoc_universal.a libmongoc.a -git add Libs/checksums.sha256 && git commit -m "build: update static library checksums" -``` +Debug builds sign with an Apple Development identity resolved from `TABLEPRO_DEVELOPMENT_TEAM`, so +signing matters even locally: a plugin in the user plugins directory has to carry the same team as +the running app, and the app derives the required team from its own signature. Set the variable in +`Configs/Secrets.xcconfig` as [Development Setup](/development/setup) describes. -The script verifies all libraries you did not name against the checksums committed at HEAD, regenerates `Libs/checksums.sha256`, and uploads the archive with `--clobber`. +## Compiling every plugin - -Never run `shasum -a 256 Libs/*.a > Libs/checksums.sha256` by hand. Regenerating from a stale `Libs/` directory silently reverts other libraries; this shipped a broken libmongoc and rolled back DuckDB once. - - -iOS xcframeworks (`Libs/ios/*.xcframework`) upload to the same release: +An app build compiles only the plugin bundles the app embeds, which leaves every registry-only +driver uncompiled. The `AllPlugins` aggregate target covers all of them in one pass: ```bash -tar czf /tmp/tablepro-libs-ios-v1.tar.gz -C Libs/ios . -gh release upload libs-v1 /tmp/tablepro-libs-ios-v1.tar.gz --clobber --repo TableProApp/TablePro +xcodebuild build -project TablePro.xcodeproj -scheme AllPlugins -destination 'platform=macOS' -skipPackagePluginValidation CODE_SIGNING_ALLOWED=NO ``` -## Creating a DMG +Run it after changing anything under `Plugins/TableProPluginKit/`, which every plugin links. To +build one signed, packaged bundle instead, use `scripts/build-plugin.sh `; the target +names are the keys under `targets:` in `project.yml`, such as `OracleDriver` or +`SurrealDBDriverPlugin`. -`scripts/create-dmg.sh` takes the version, the architecture (default `universal`), and the source app (default `build/Release/TablePro.app`): +## Clean builds -```bash -scripts/build-release.sh arm64 -scripts/create-dmg.sh 0.57.0 arm64 build/Release/TablePro-arm64.app -``` - -Output: `build/Release/TablePro-0.57.0-arm64.dmg`. - -## Clean build - -| Method | Command | -|--------|---------| -| Xcode | `Cmd+Shift+K` | -| CLI | `xcodebuild -project TablePro.xcodeproj -scheme TablePro clean` | -| Nuclear | `rm -rf ~/Library/Developer/Xcode/DerivedData` | +| Reach for | When | +|---|---| +| `Cmd+Shift+K` in Xcode | The usual case | +| `xcodebuild -project TablePro.xcodeproj -scheme TablePro clean` | Same, from a terminal | +| `rm -rf ~/Library/Developer/Xcode/DerivedData` | Package resolution is stuck or a stale module keeps reappearing | -## CI/CD +The last one deletes the caches for every project on the machine, and the next build starts from +nothing. -### App releases +## What a pull request has to pass -`.github/workflows/build.yml`, triggered by `v*` tags (e.g. `v0.57.0`). +`.github/workflows/macos-tests.yml` starts on every pull request, on Xcode 26.4.1. Its first job +decides whether the macOS suites run at all and skips them when nothing relevant changed, so the +workflow still reports a result on a docs-only change. -Jobs: SwiftLint, macOS Tests, Build ARM64, Build x86_64, Registry Readiness. The release job needs all five, so failing tests block the release. It produces DMGs, ZIPs, and Sparkle signatures, with release notes extracted from `CHANGELOG.md`. +| Job | What it does | +|---|---| +| Detect relevant changes | Decides whether the macOS suites run | +| Package Tests | `swift test` for TableProCore and CodeEditTextView | +| Build for testing | Builds `AllPlugins`, `dlopen`s each built bundle, then builds the app and both test bundles | +| Unit tests | `TableProTests` against the prebuilt products | +| UI tests | `TableProUITests`, split across three shards | +| macOS Tests Gate | Fails unless every other job reported success or skipped | -### Plugin releases +A plugin that builds and signs can still fail to load, which is why the load step is separate: +"Bundle failed to load executable" has shipped twice. -`.github/workflows/build-plugin.yml`, triggered by `plugin-*-v*` tags (e.g. `plugin-oracle-v1.0.0`) or by `workflow_dispatch`. The dispatch input takes comma-separated `tag:pluginKitVersion` pairs, e.g. `plugin-mongodb-v1.0.25:13`. Omit `:pluginKitVersion` and the workflow reads `currentPluginKitVersion` from `PluginManager.swift`. +`.github/workflows/repo-hygiene.yml` runs beside it on Ubuntu in under a minute. It runs +`actionlint` over every workflow, which shells out to `shellcheck` for each inline `run:` block; +`shellcheck` at warning severity over every script; the plugin manifest check; and the +shared-contract drift gates in `scripts/audit-refactor-health.sh`. -After a PluginKit ABI bump, re-release every registry plugin in one parallel matrix run: - -```bash -scripts/release-all-plugins.sh -``` +Lint runs in the release workflow rather than this one. Run `swiftlint lint --strict` yourself +before pushing; see [Code Style](/development/code-style). -See [Plugin Registry](/development/plugin-registry) for how published binaries reach users. +## Related - -Pushing more than 3 tags in one push creates no push events at all, so zero workflows fire. Push plugin tags one at a time, or use `workflow_dispatch`. - +Release builds, notarization, DMG packaging and the tag that publishes them are on +[Releasing](/development/releasing). Publishing a driver plugin is on +[Plugin Registry](/development/plugin-registry). diff --git a/docs/development/code-style.mdx b/docs/development/code-style.mdx index 55f4d4810..3b505e30b 100644 --- a/docs/development/code-style.mdx +++ b/docs/development/code-style.mdx @@ -3,68 +3,74 @@ title: Code Style description: Swift conventions, tooling, naming, and file organization rules --- -`.swiftlint.yml` and `.swiftformat` are the source of truth. When in doubt, check those files. +Lint covers `TablePro/` and nothing else. `Plugins/`, `Packages/`, `LocalPackages/` and the test +bundles get no automated style check at all, locally or in CI. On a change outside the app target, +the conventions here are all there is. -## Core rules +`.swiftlint.yml` and `.swiftformat` settle the mechanical half. `CLAUDE.md` carries the rest, the +rules no linter can check: no comments, early returns over nested conditionals, explicit access +control, `String(localized:)` for user-facing strings, OSLog instead of `print()`. -- **No comments.** Code must be self-explanatory through naming and structure. Do not add comments that describe what code does, reference tickets, or explain callers. -- **Early returns.** Use `guard` and early `return` instead of nested `if/else` blocks. Flatten control flow. - -## Tools +## Running the tools ```bash -# Pre-commit gate (CI rejects warnings, so --strict is required) -swiftlint lint --strict - -# Auto-fix lint issues -swiftlint --fix +swiftlint lint --strict # the pre-commit gate, and what the release workflow runs +swiftlint --fix # apply the autocorrectable rules +swiftformat . # format in place +swiftformat --lint . # report without writing +``` -# Format all code -swiftformat . +`--strict` promotes every warning to an error. Several rules are configured as warnings on purpose, +so a plain `swiftlint lint` exits 0 on code the release workflow rejects. There is no SwiftLint +build phase in the generated project, so nothing lints during a normal Xcode build. -# Check formatting without applying -swiftformat --lint . -``` +The scope comes from `included: [TablePro]` in `.swiftlint.yml`, and that key beats any path on the +command line: `swiftlint lint Plugins/` lints `TablePro/` and reports nothing about your plugin. It +exits 0, which reads like a pass. -There is no SwiftLint build phase in the Xcode project. Lint runs manually and in CI. +SwiftFormat is not run in CI at all. It rewrites files, so run it before you stage, not after. ## Formatting -| Rule | Value | -|------|-------| -| Indentation | 4 spaces (never tabs) | -| Line length | 120 chars (SwiftLint warns at 180, errors at 300) | -| Braces | K&R (opening brace on same line) | -| Line endings | LF | -| Semicolons | None | +| Rule | Value | Set by | +|---|---|---| +| Indentation | 4 spaces, never tabs; `case` not indented inside `switch` | `--indent 4`, `--indentcase false` | +| Wrap target | 120 characters | `--maxwidth 120` | +| Hard line limit | Warns at 180, errors at 300 | `line_length` in `.swiftlint.yml` | +| Braces | K&R, `else` and `guard else` on the closing brace line | `--allman false`, `--elseposition same-line`, `--guardelse same-line` | +| Wrapped arguments | One per line, opening paren last, closing paren balanced | `--wraparguments before-first`, `--closingparen balanced` | +| Line endings | LF | `--linebreaks lf` | +| Semicolons | Removed | `--semicolons never` | +| `self.` | Removed where the compiler does not need it | `--self remove` | -Trailing commas are omitted by convention. Both tools have their trailing-comma rules disabled, so nothing enforces it. +`line_length` ignores URLs, function declarations, comments and interpolated strings, so a long +signature or a long log line is not what trips it. + +Trailing commas are omitted throughout the tree, and nothing enforces that: SwiftFormat has +`trailingCommas` disabled and SwiftLint has `trailing_comma` in `disabled_rules`. Match what the +file around you does. ## Naming | Element | Convention | Example | -|---------|------------|---------| -| Classes, Structs, Enums, Protocols | UpperCamelCase | `DatabaseConnection` | +|---|---|---| +| Types and protocols | UpperCamelCase | `DatabaseConnection` | | Enum cases | lowerCamelCase | `.postgresql` | -| Functions, Variables, Constants | lowerCamelCase | `executeQuery()`, `maxRetryAttempts` | -| Booleans | is/has/can prefix | `isConnected`, `hasValidCredentials` | -| Factory methods | make prefix | `makeConnection()` | -| Acronyms | Treat as words | `JsonEncoder` not `JSONEncoder` (except SDK types) | - -## Access control - -Always explicit. Prefer the most restrictive level that works. +| Functions, variables, constants | lowerCamelCase | `executeQuery()`, `maxRetryAttempts` | +| Booleans | `is`, `has`, `can` or `should` prefix | `isConnected`, `hasValidCredentials` | +| Factory methods | `make` prefix | `makeConnection()` | +| Acronyms | All caps, as in Apple's own APIs | `SQLStatementGenerator`, `JSONExportPlugin`, `MCPAuditLogger` | -```swift -// Specify on the extension, not individual members -public extension NSEvent { - var semanticKeyCode: KeyCode? { … } -} -``` +Nothing enforces the acronym row: SwiftFormat's `acronyms` rule is disabled, so no tool rewrites +`Url` to `URL`. One corner of the tree disagrees with it. The MCP wire types under +`TablePro/Core/MCP/Wire/` spell it `HttpRequestParser` while the files beside them spell `MCP` in +full. New types take the all-caps form. ## Imports -One alphabetical block, no blank lines between imports, one blank line after the last import. SwiftFormat enforces this (`--importgrouping alpha`, `blankLinesBetweenImports`, `blankLineAfterImports`). +One alphabetical block, no blank lines inside it, one blank line after it. SwiftFormat enforces +this through `--importgrouping alpha`, `blankLinesBetweenImports` and `blankLineAfterImports`, and +SwiftLint's `sorted_imports` catches an out-of-order import that never went through SwiftFormat. ```swift import AppKit @@ -76,52 +82,53 @@ import TableProPluginKit final class QueryRunner { … } ``` -## Safety +Lowercase module names sort by their own spelling, which is why `os` lands between `Foundation` and +`TableProPluginKit`. -No force unwrapping (`!`) or force casting (`as!`). Use `guard let`, `if let`, `as?`. +## Rules that bite -```swift -// Good -guard let connection = activeConnection else { return } +`.swiftlint.yml` opts into 55 rules beyond the defaults. These are the ones that stop a clean-looking +change most often: -// Bad -let connection = activeConnection! -``` +| Rule | Flags | Severity | +|---|---|---| +| `implicit_return` | `return` in a single-expression function, closure or getter | warning | +| `explicit_init` | `Connection.init()` where `Connection()` compiles | warning | +| `force_unwrapping` | `!` on an optional | warning | +| `force_cast` | `as!` | warning | +| `empty_count` | `items.count == 0` instead of `items.isEmpty` | error | +| `number_separator` | `1000000` instead of `1_000_000` | warning | +| `yoda_condition` | `if 5 == limit` | warning | +| `sorted_imports` | An import out of alphabetical order | warning | +| `extension_access_modifier` | Access control repeated on members instead of set on the extension | warning | -## Logging - -OSLog only. Never `print()`. - -```swift -import os +Every warning in that table fails under `--strict`, which is how the gate runs it. -private static let logger = Logger(subsystem: "com.TablePro", category: "DatabaseManager") -``` +`force_try` sits in `disabled_rules`, so nothing flags `try!`. The rule against forcing still +applies; the linter is not what holds it up. -## Localization +## Custom rules -- `String(localized:)` for user-facing strings in computed properties, AppKit code, alerts, error descriptions -- SwiftUI view literals (`Text("Save")`, `Button("Cancel")`) auto-localize -- Do not localize technical terms: font names, database types, SQL keywords, encoding names -- Never use `String(localized:)` with string interpolation. Use `String(format: String(localized: "Preview %@"), name)` instead. +Two project rules are configured at error severity, so they fire without `--strict`. Violate either +and a UI test writes into your own store instead of a throwaway one: -## SwiftUI patterns +| Rule | Fires on | Exempt | +|---|---|---| +| `storage_environment_directory` | `applicationSupportDirectory` | `AppStorageEnvironment.swift` | +| `storage_environment_defaults` | `UserDefaults.standard` | `AppStorageEnvironment.swift`, `GeneralSettings.swift`, `WorkspaceRailViewController.swift` | -- `@State` for local view state -- `@Observable` for viewmodels (Swift 5.9+) -- Property wrapper order: Environment, State, Binding, regular properties -- Extract large views into subviews +Resolve the directory through `AppStorageEnvironment.shared` and preferences through +`AppStorageEnvironment.shared.defaults`. Only a preference macOS itself owns reads the standard +domain. -## Limits +## Size limits -| Metric | Warning | Error | -|--------|---------|-------| -| File length | 1200 lines | 1800 lines | -| Type body | 1100 lines | 1500 lines | -| Function body | 160 lines | 250 lines | -| Cyclomatic complexity | 40 | 60 | +`.swiftlint.yml` sets thresholds for `file_length`, `type_body_length`, `function_body_length` and +`cyclomatic_complexity`. Read the current numbers there rather than from memory. -When approaching these limits, extract into extension files: +When a type approaches one, split it into `TypeName+Category.swift` files under an `Extensions/` +folder beside it, grouped by domain and not by line count. `MainContentCoordinator` is the worked +example: @@ -129,7 +136,27 @@ When approaching these limits, extract into extension files: + -Group by domain logic, not arbitrary line counts. +A new file is invisible to Xcode until `scripts/generate-project.sh` runs again. + +## Localization + +`CLAUDE.md` holds the localization rule in full. The trap worth repeating is interpolation: + +```swift +String(localized: "Preview \(name)") +``` + +That builds a different key on every call, so it never matches an entry in the strings catalog. The +string ships untranslated and no tool reports it. Take a format argument instead, which is one key +and one catalog entry: + +```swift +String(format: String(localized: "Preview %@"), name) +``` + +Technical terms stay unlocalized: font names, database types, SQL keywords, encoding names. +Exporting and merging a translation is on [Development Overview](/development/overview). diff --git a/docs/development/overview.mdx b/docs/development/overview.mdx index f96e789ed..ab43867ba 100644 --- a/docs/development/overview.mdx +++ b/docs/development/overview.mdx @@ -3,20 +3,31 @@ title: Development Overview description: Build TablePro from source, contribute, or write a database driver plugin --- -TablePro is open source under AGPLv3. These pages cover building the app, the codebase conventions, and writing, testing, and publishing driver plugins. +Two files in the repo root outrank these pages. +[`CLAUDE.md`](https://github.com/TableProApp/TablePro/blob/main/CLAUDE.md) holds the architecture +invariants and the rules a change has to meet before it merges; +[`CONTRIBUTING.md`](https://github.com/TableProApp/TablePro/blob/main/CONTRIBUTING.md) holds branch +names, the commit format, and the pull request checklist. Both are maintained next to the code they +describe, and where either disagrees with a page here, it wins. + +What these pages add is the part neither file carries: the exact commands, the tool versions behind +them, and the plugin SDK. - Clone, install dependencies, open in Xcode. - - - Build configurations, signing, release builds. + Clone, download the libraries, generate the project, build. Module layout, plugin system, editor pipeline. + + Debug builds, the plugin compile check, tests, pull request CI. + + + Release builds, notarization, DMG, static libraries, the release tag. + - Swift conventions, lint config, no-comment rule. + Lint and format config, naming, the rules CI enforces. Write a driver plugin against TableProPluginKit. @@ -28,3 +39,23 @@ TablePro is open source under AGPLv3. These pages cover building the app, the co Publish and distribute database driver plugins. + +## Translating the app + +Interface strings live in one `.xcstrings` catalog that interleaves every language inside every +key, so a hand edit means working next to languages you do not speak. `scripts/localization.py` +gives one flat JSON file per language instead: + +```bash +scripts/localization.py status # coverage, per language +scripts/localization.py export vi # writes Localization/mac.vi.json +scripts/localization.py import vi # merges the edits back into the catalog +``` + +Edit the `translation` values in the exported file, then import. Add `--target ios` for the iPhone +and iPad app, which has a catalog of its own. Commit the catalog and not the export: `Localization/` +is gitignored, and a merge rewrites only the strings that changed, so the diff shows the +translation and nothing else. + +Run `scripts/localization.py verify` after changing the script. It re-reads and rewrites each +catalog and checks the result is byte for byte identical. diff --git a/docs/development/plugin-development.mdx b/docs/development/plugin-development.mdx index 505ffbd4d..563002112 100644 --- a/docs/development/plugin-development.mdx +++ b/docs/development/plugin-development.mdx @@ -3,26 +3,26 @@ title: Plugin Development description: Write a database driver plugin, implement the PluginKit protocols, and package it as a .tableplugin bundle --- -`DriverPlugin` describes the database (name, port, capabilities, SQL dialect) and creates driver instances. `PluginDatabaseDriver` is the connection itself: connect, query, fetch schema. Implement both in a `.tableplugin` bundle linked against **TableProPluginKit** (`Plugins/TableProPluginKit/`). `PluginDriverAdapter` then bridges your driver to the app's internal `DatabaseDriver` protocol and resolves it by database type ID, so a working plugin gets the full UI: connection form, sidebar, data grid, editor, import and export. +Start from a plugin that already works. `Plugins/SurrealDBDriverPlugin/` is the compact reference: no C bridge, an HTTP transport, the schema-aware query hooks filled in, and an editor language of its own. -The best starting point is an existing plugin. `Plugins/SurrealDBDriverPlugin/` is a compact reference with no C bridge: it talks HTTP, implements the schema-aware query-building hooks, and ships a custom editor language. +Two protocols do the work. `DriverPlugin` describes the database (name, port, capabilities, SQL dialect) and creates driver instances; `PluginDatabaseDriver` is the connection itself. Implement both in a `.tableplugin` bundle linked against **TableProPluginKit**, and `PluginDriverAdapter` bridges your driver to the app's internal `DatabaseDriver`. A driver that connects and lists tables gets the whole UI without asking: connection form, sidebar, data grid, editor, import, export. ## Bundle layout -A plugin is a macOS loadable bundle target with `WRAPPER_EXTENSION = tableplugin`, linking TableProPluginKit. The principal class (set via `INFOPLIST_KEY_NSPrincipalClass`) must be your `DriverPlugin` class. `Info.plist` keys: +A plugin is a macOS loadable bundle target with `WRAPPER_EXTENSION = tableplugin` that links TableProPluginKit. Set `INFOPLIST_KEY_NSPrincipalClass` to your `DriverPlugin` class. | Key | Type | Required | Purpose | |-----|------|----------|---------| -| `TableProPluginKitVersion` | integer | Yes | PluginKit ABI version the plugin was built against (19) | -| `TableProProvidesDatabaseTypeIds` | array of strings | Recommended | Database type IDs the plugin serves. Enables lazy loading. | -| `TableProMinAppVersion` | string | No | Loader rejects the plugin on older app versions | -| `CFBundleShortVersionString` | string | Yes | Plugin version, used for registry update checks | +| `TableProPluginKitVersion` | integer | Yes | The PluginKit ABI the plugin was built against. Current value: 19 | +| `TableProProvidesDatabaseTypeIds` | array of strings | Recommended | Database type IDs the plugin serves, which is what makes lazy loading possible | +| `CFBundleShortVersionString` | string | Yes | Plugin version, read by registry update checks | +| `TableProMinAppVersion` | string | No | The loader rejects the plugin on an older app | -Without `TableProProvidesDatabaseTypeIds`, the plugin loads eagerly at app startup and `PluginManager` logs a warning. With it, the app registers the metadata from `Info.plist` and loads the binary on first use. +Leave `TableProProvidesDatabaseTypeIds` out and the plugin loads eagerly at startup, blocking launch, and `PluginManager` logs a warning naming the key. With it, the app registers your metadata from `Info.plist` and loads the binary on first use. ## Implementing DriverPlugin -Your principal class conforms to `TableProPlugin` and `DriverPlugin`. Only a handful of members have no default: +Your principal class conforms to `TableProPlugin` and `DriverPlugin`. Nine members have no default, and this is all of them: ```swift final class SurrealDBPlugin: NSObject, TableProPlugin, DriverPlugin { @@ -42,13 +42,13 @@ final class SurrealDBPlugin: NSObject, TableProPlugin, DriverPlugin { } ``` -`DriverConnectionConfig` carries host, port, username, password, database, SSL configuration, and an `additionalFields` dictionary populated from your `additionalConnectionFields` declarations. +`DriverConnectionConfig` carries host, port, username, password, database, SSL settings, and an `additionalFields` dictionary filled from whatever `additionalConnectionFields` you declared. -Everything else on `DriverPlugin` is an optional static with a default: connection mode, URL schemes, brand color, editor language, `sqlDialect` (keywords, functions, completions), capability flags (`supportsSSH`, `supportsSchemaEditing`, `supportsTriggers`, and about twenty more), navigation model, and system database names. Override what differs from the defaults; see `Plugins/TableProPluginKit/DriverPlugin.swift` for the full list. +The remaining fifty-odd statics all have defaults: connection mode, URL schemes, brand color, editor language, `sqlDialect` (keywords, functions, completions), navigation model, system database names, and two dozen `supports*` capability flags. Override the ones that differ. `Plugins/TableProPluginKit/DriverPlugin.swift` is the full list. ## Implementing PluginDatabaseDriver -The driver protocol has around 80 requirements, but most have default implementations. You must implement: +Twelve requirements have no default. Everything else on the protocol does. | Group | Methods | |-------|---------| @@ -57,65 +57,57 @@ The driver protocol has around 80 requirements, but most have default implementa | Schema | `fetchTables(schema:)`, `fetchColumns(table:schema:)`, `fetchIndexes(table:schema:)`, `fetchForeignKeys(table:schema:)`, `fetchTableDDL(table:schema:)`, `fetchViewDefinition(view:schema:)`, `fetchTableMetadata(table:schema:)` | | Databases | `fetchDatabases()`, `fetchDatabaseMetadata(_:)` | -Notable defaults you may want to override: +Four defaults are worth a second look before you accept them: -- `ping()` runs `SELECT 1`; transactions run `BEGIN` / `COMMIT` / `ROLLBACK` via `execute(query:)`. -- `fetchAllColumns(schema:)` and `fetchAllForeignKeys(schema:)` loop per table (N+1 round-trips). SQL drivers should replace them with one bulk query. -- `quoteIdentifier`, `escapeStringLiteral`, `executeParameterized`, and `streamRows` have generic SQL defaults. -- Non-SQL databases implement the query-building hooks (`buildBrowseQuery`, `buildFilteredQuery`, `generateStatements`) so browsing and editing work without SQL. Implement the `schema:`-aware overloads if your database has schemas; the schema-less defaults drop the schema. +- `ping()` runs `SELECT 1`, and the transaction methods run `BEGIN` / `COMMIT` / `ROLLBACK` through `execute(query:)`. An engine without those keywords overrides all four. +- `fetchAllColumns(schema:)` and `fetchAllForeignKeys(schema:)` loop one round-trip per table. Any SQL driver should replace them with a single catalog query. +- `quoteIdentifier`, `escapeStringLiteral`, `executeParameterized`, and `streamRows` assume generic SQL. +- A non-SQL database implements `buildBrowseQuery`, `buildFilteredQuery`, and `generateStatements` instead, which is what makes browsing and editing work without SQL. Implement the `schema:`-aware overloads if your database has schemas; the schema-less defaults throw the schema away. -## How loading works - -`PluginManager` loads plugins from two places: the app's `PlugIns/` directory (built-in, no signature check) and `~/Library/Application Support/TablePro/Plugins` (user-installed, must be signed by TablePro's signing team). - -Before loading any bundle, `validateBundleVersions` checks `TableProPluginKitVersion` against the app's compatible range, `[minimumCompatiblePluginKitVersion, currentPluginKitVersion]` in `PluginManager.swift`. A missing key, a version above the range, or a version below it all reject the plugin with a clear error instead of a crash. `TableProMinAppVersion` is checked next, then the bundle's principal class must conform to `TableProPlugin`. +[Testing a Custom Plugin](/development/testing-plugins) covers getting the built bundle into a running app and reading the failure if it does not load. ## ABI compatibility -TableProPluginKit builds with Swift Library Evolution, so its public ABI is resilient: a plugin built against an older PluginKit keeps loading under a newer app, and the runtime fills protocol requirements the plugin does not implement from their defaults. - -If you change PluginKit itself, the rules are: +TableProPluginKit builds with Swift Library Evolution, so a plugin built against an older PluginKit keeps loading under a newer app: the runtime fills requirements it never implemented from their defaults. Adding a requirement that has a default costs nothing. -- **Additive, no version bump**: a new protocol requirement with a default implementation, a new field on a non-frozen struct added through a new initializer overload, reordering requirements. -- **Breaking, bump required**: changing an existing requirement's signature, removing a requirement, adding a requirement without a default, adding a case to a `@frozen` enum, changing a `@frozen` type's layout. Bump `currentPluginKitVersion` and every plugin's `TableProPluginKitVersion`, then re-release all registry plugins. -- **Never remove a published requirement, even one that defaulted to `nil`.** Library Evolution fills in requirements added after a plugin was built, but it cannot rescue one removed out from under it. Removing a requirement deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table, so it fails to load with "Bundle failed to load executable". That shipped on 0.58 and broke MongoDB, Oracle, Cassandra, and Elasticsearch (#1917). If the app stops using a requirement, leave it in place with its default. -- **Never add a parameter to an existing public initializer or function, even with a default value.** It replaces the mangled symbol and every already-built plugin fails to load. Add a new overload and mark the old one `@_disfavoredOverload`. +Two changes look additive and are not. Both have shipped, and both surface the same way: every registry plugin fails to load with "Bundle failed to load executable". -Run `scripts/check-pluginkit-abi.sh` before merging any change under `Plugins/TableProPluginKit/`. It builds the public interface at your tree and at the base ref and diffs them. Commit or stash first; the script exits if the working tree is dirty. The base ref is an optional argument and defaults to `origin/main`, so pass the merge base when you review a branch. +- **Removing a published requirement, even one that defaulted to `nil`.** Library Evolution rescues a requirement added after a plugin was built, never one removed out from under it. Removing one deletes its method descriptor and its default-implementation symbol, and every shipped plugin hard-references both in its witness table. If the app stops using a requirement, leave it in place with its default. (0.58, #1917: MongoDB, Oracle, Cassandra, Elasticsearch.) +- **Adding a parameter to an existing public initializer or function, even with a default value.** It replaces the mangled symbol. Add a new overload for the new field and mark the old one `@_disfavoredOverload`, so new code gets the full initializer and old binaries keep theirs. -## Test locally - -The app derives the required signing team from its own signature, so a plugin built by the same Xcode as your debug app passes verification. Build the plugin target in Xcode, then install it into the user plugins directory and relaunch TablePro: +`CLAUDE.md` carries the full additive-versus-breaking list and the checklist a breaking bump obliges you to run, including re-releasing every registry plugin before the app ships. ```bash -scripts/install-plugin-dev.sh MyDriverPlugin +scripts/check-pluginkit-abi.sh [base-ref] ``` -The script copies the bundle from DerivedData, so build from Xcode, not from a CLI `xcodebuild` (that writes to `build/Debug` instead). Do not patch the copied `Info.plist`: any edit invalidates the signature and dyld refuses to load the executable. See [Testing a Custom Plugin](/development/testing-plugins) for the **Copy Plug-Ins** build phase and the unsigned-plugin escape hatch. +Run it before merging any change under `Plugins/TableProPluginKit/`. It builds the framework at your tree and at the base ref with one toolchain and diffs the two public interfaces, so a Swift version difference between machines can never fake a diff. The base ref defaults to `origin/main`; pass the merge base when reviewing a branch. Commit or stash first, since it refuses a dirty working tree. ## Building outside this repository -A plugin has to link TableProPluginKit. Until recently the only way to get it was to check out this repository and build the whole app project, which is why every driver lives under `Plugins/` here. That was a packaging limit, not a rule of the plugin system. - -`scripts/build-pluginkit-xcframework.sh` produces a distributable `TableProPluginKit.xcframework` covering arm64 and x86_64. Run it here, or download a published one from the `pluginkit-v` release, and link it from your own Xcode project. Your plugin target then needs nothing from this repository. +A plugin needs TableProPluginKit, not this repository. Every driver happens to live under `Plugins/` here. A new one does not have to. ```bash +scripts/generate-project.sh scripts/build-pluginkit-xcframework.sh -# build/pluginkit/TableProPluginKit.xcframework +# build/pluginkit/TableProPluginKit.xcframework, plus a zip and its SHA-256 ``` -Two things the script enforces, because getting either wrong produces a framework that links today and breaks every consumer later: - -- It builds with `BUILD_LIBRARY_FOR_DISTRIBUTION=YES`, so the framework is ABI-resilient and a plugin built against an older PluginKit keeps loading under a newer app. -- It fails if the result carries no `.swiftinterface`, which is what a build without Library Evolution looks like. +Run that, or download a published XCFramework from the `pluginkit-v` release, and link it from your own Xcode project. Your plugin target then needs nothing else from this repository. -Link the XCFramework as **Do Not Embed**. The app supplies TableProPluginKit at runtime; a copy inside your bundle would be a second, conflicting one. +Link it as **Do Not Embed**. The app supplies TableProPluginKit at runtime, so a copy inside your bundle is a second, conflicting one. The script refuses to emit a framework with no `.swiftinterface`, which is what a build without Library Evolution looks like: it links today and breaks every consumer on the next release. -## Distribute +## Publishing -Publishing to TablePro's registry takes two steps in this repo: +Two steps put a plugin in TablePro's own registry, and both happen in this repository: -1. Add an entry for your plugin to `.github/plugin-registry.json`, keyed by the slug its tag will use: target, bundle name, bundle ID, whether it is also bundled in the app, display name, summary, database type IDs, icon, category, homepage. Without one the release workflow stops with `Unknown plugin`. `icon` and `databaseTypeIds` must match what your `DriverPlugin` class declares; `scripts/ci/check-plugin-manifest.py` fails the release if they drift. -2. Push the tag `plugin--v`. CI builds both architectures, signs, notarizes, and updates `plugins.json`. + + + Add your plugin to `.github/plugin-registry.json`, keyed by the slug its tag will use. Without an entry the release workflow stops with `Unknown plugin`. `icon` and `databaseTypeIds` there have to match what your `DriverPlugin` class declares, and `scripts/ci/check-plugin-manifest.py` fails PR CI and the release workflow when they drift. + + + `plugin--v`. CI builds both architectures, signs, notarizes, and updates `plugins.json`. + + -CI signs with TablePro's own certificate, so a third-party plugin ships through a pull request, not a tag of your own. See [Plugin Registry](/development/plugin-registry) for the manifest format, the self-describing `metadata` block, and the publishing flow. +CI signs with TablePro's certificate, so a plugin published there arrives through a pull request, not a tag of your own. The other route stays outside this repository: sign the bundle with your own Developer ID and serve it from a registry URL you host, and the app installs it once the user agrees to trust your team by name. [Plugin Registry](/development/plugin-registry) has the manifest format, the `metadata` block that renders your connection form before the download finishes, and how to point the app at a private registry. diff --git a/docs/development/plugin-registry.mdx b/docs/development/plugin-registry.mdx index b2d817b4b..68a12d209 100644 --- a/docs/development/plugin-registry.mdx +++ b/docs/development/plugin-registry.mdx @@ -3,7 +3,9 @@ title: Plugin Registry description: Registry manifest format, binary selection, publishing, and PluginKit compatibility --- -The plugin registry is a JSON manifest (`plugins.json`) hosted at [github.com/TableProApp/plugins](https://github.com/TableProApp/plugins). TablePro fetches it to populate **Settings > Plugins > Browse** and to auto-install a driver when a user selects a database type with no loaded plugin. This page covers the manifest and publishing pipeline; for install and update behavior in the app, see [Plugins & Themes](/features/plugins). To build a plugin, see [Plugin Development](/development/plugin-development). +The registry lives in TablePro's repository, so publishing there means a pull request. Signing a plugin with your own Developer ID and serving it from a manifest you host is the other route, and the format on this page is the same either way. For what install and update look like to a user, see [Plugins & Themes](/features/plugins); to build the plugin in the first place, [Plugin Development](/development/plugin-development). + +TablePro's manifest is `plugins.json` at [github.com/TableProApp/plugins](https://github.com/TableProApp/plugins). The app fetches it to fill **Settings > Plugins > Browse** and to auto-install a driver when someone picks a database type with no plugin loaded. ## Manifest format @@ -14,44 +16,42 @@ The plugin registry is a JSON manifest (`plugins.json`) hosted at [github.com/Ta } ``` -The current schema version is 2. The app rejects a manifest with a higher `schemaVersion` than it supports and falls back to its cached copy. - -## Entry fields +Schema version 2 is current. A manifest declaring a higher version is rejected and the app falls back to its cached copy. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `id` | string | Yes | Bundle identifier (e.g., `com.TablePro.OracleDriver`) | +| `id` | string | Yes | Bundle identifier, such as `com.TablePro.OracleDriver` | | `name` | string | Yes | Display name | | `version` | string | Yes | Semantic version | | `summary` | string | Yes | One-line description | | `author` | object | Yes | `{ "name": "…", "url": "…" }`, `url` optional | | `homepage` | string | No | Project URL | | `category` | string | Yes | `database-driver`, `export-format`, `import-format`, `theme`, `other` | -| `databaseTypeIds` | [string] | No | `DatabaseType.pluginTypeId` values. Drives auto-install. | -| `binaries` | [object] | Yes | Per-architecture binaries, see below | -| `minAppVersion` | string | No | Minimum TablePro version | +| `databaseTypeIds` | [string] | No | `DatabaseType.pluginTypeId` values, which is what drives auto-install | +| `binaries` | [object] | Yes | Per-architecture binaries | +| `minAppVersion` | string | No | Below this the install fails before any download | | `iconName` | string | No | SF Symbol or bundled icon name | | `isVerified` | bool | No | Defaults to `false` | -| `metadata` | object | No | Self-describing plugin metadata, see below | +| `metadata` | object | No | Self-describing plugin metadata | Each entry in `binaries`: | Field | Type | Required | Description | |-------|------|----------|-------------| | `architecture` | string | Yes | `arm64` or `x86_64` | -| `pluginKitVersion` | int | Yes for drivers | PluginKit ABI version the binary was built against | +| `pluginKitVersion` | int | Yes for drivers | The PluginKit ABI the binary was built against | | `downloadURL` | string | Yes | Direct URL to the `.zip` | | `sha256` | string | Yes | SHA-256 hex of the ZIP | -v1 manifests carried top-level `downloadURL`, `sha256`, and `minPluginKitVersion` instead of `binaries`. The app still decodes them as a fallback: when `binaries` is absent it synthesizes one entry per architecture, using `minPluginKitVersion` as the binary's `pluginKitVersion`. New entries must use `binaries`. +v1 manifests carried top-level `downloadURL`, `sha256`, and `minPluginKitVersion` in place of `binaries`. The app still decodes them, synthesizing one entry per architecture. Write new entries with `binaries`. ## Binary selection -For a database driver, the app filters `binaries` to the current architecture, keeps those whose `pluginKitVersion` falls within `[minimumCompatiblePluginKitVersion, currentPluginKitVersion]` (both declared in `PluginManager.swift`, both 19), and installs the highest. A driver binary without `pluginKitVersion` never resolves, and install fails with `noCompatibleBinary`. +For a driver, the app filters `binaries` to the running architecture, keeps those whose `pluginKitVersion` falls inside `[minimumCompatiblePluginKitVersion, currentPluginKitVersion]`, and installs the highest. Both bounds are declared in `PluginManager.swift`. A driver binary with no `pluginKitVersion` never resolves, and the install fails with `noCompatibleBinary`. -Themes carry no native code, so they match on architecture only. +Themes carry no native code, so they match on architecture alone. ## Example entry @@ -87,48 +87,20 @@ Themes carry no native code, so they match on architecture only. ## Plugin metadata -The optional `metadata` object makes a registry plugin self-describing: the app uses it to render the connection form, sidebar, and editor for a database type before the plugin is installed. Registry driver entries should carry it, or users see a bare form until the download completes. `update-registry.py` preserves an existing `metadata` block across releases; edit it by hand in the registry repo. +The optional `metadata` object makes an entry self-describing, so the app renders the connection form, sidebar, and editor for a database type before the plugin is installed. Carry it on every driver entry: without it, someone picking your database type stares at a bare form until the download finishes. | Group | Fields | |-------|--------| | Identity and form | `displayName`, `iconName`, `defaultPort`, `brandColorHex`, `connectionMode`, `requiresAuthentication`, `additionalConnectionFields`, `postConnectActions`, `urlSchemes`, `fileExtensions` | -| Capabilities | `supportsSSH`, `supportsSSL`, `supportsForeignKeys`, `supportsSchemaEditing`, `supportsDatabaseSwitching`, `supportsImport`, `supportsExport`, `supportsReadOnlyMode`, `supportsHealthMonitor`, and similar flags | +| Capabilities | `supportsSSH`, `supportsSSL`, `supportsForeignKeys`, `supportsSchemaEditing`, `supportsDatabaseSwitching`, `supportsImport`, `supportsExport`, `supportsReadOnlyMode`, `supportsHealthMonitor`, and the rest of the flags | | Naming and navigation | `systemDatabaseNames`, `systemSchemaNames`, `defaultSchemaName`, `tableEntityName`, `containerEntityName`, `navigationModel`, `databaseGroupingStrategy` | | Editor | `editorLanguage`, `queryLanguageName`, `sqlDialect` (keywords, functions, data types, pagination style), `statementCompletions`, `explainVariants`, `columnTypesByCategory` | -The full field list is `RegistryPluginMetadata` in `TablePro/Core/Plugins/Registry/RegistryModels.swift`. - -## Registry plugins and databaseTypeIds - -`databaseTypeIds` tells the app which plugin to install when a user picks a database type with no loaded driver. The slug in a tag must be a key in `.github/plugin-registry.json`. Note the non-obvious tags for Cloudflare D1 and etcd. - -| Tag prefix | databaseTypeIds | -|-----------|-----------------| -| `plugin-mongodb` | `"MongoDB"` | -| `plugin-oracle` | `"Oracle"` | -| `plugin-dameng` | `"Dameng"` | -| `plugin-duckdb` | `"DuckDB"` | -| `plugin-beancount` | `"Beancount"` | -| `plugin-mssql` | `"SQL Server"` | -| `plugin-cassandra` | `"Cassandra"`, `"ScyllaDB"` | -| `plugin-etcd` | `"etcd"` | -| `plugin-cloudflare-d1` | `"Cloudflare D1"` | -| `plugin-dynamodb` | `"DynamoDB"` | -| `plugin-bigquery` | `"BigQuery"` | -| `plugin-snowflake` | `"Snowflake"` | -| `plugin-libsql` | `"libSQL"`, `"Turso"` | -| `plugin-elasticsearch` | `"Elasticsearch"` | -| `plugin-surrealdb` | `"SurrealDB"` | -| `plugin-teradata` | `"Teradata"` | -| `plugin-trino` | `"Trino"` | - -Bundled plugins (MySQL, PostgreSQL, SQLite, ClickHouse, Redis, and the import/export formats) ship inside the app and are never published to the registry. +`RegistryPluginMetadata` in `TablePro/Core/Plugins/Registry/RegistryModels.swift` is the full field list. `update-registry.py` copies an existing `metadata` block forward on every release, so it is edited by hand in the registry repository and never regenerated. ## Publishing a plugin -A new plugin needs an entry in `.github/plugin-registry.json` before its first tag. Without one the workflow exits with `Unknown plugin`. - -Tag the commit and push that one tag: +Every plugin CI can publish has an entry in `.github/plugin-registry.json`, keyed by the slug that appears in its tag. That file maps the slug to a build target, so the mapping has no derivation rule: `mssql` builds `MSSQLDriver` and `cloudflare-d1` builds `CloudflareD1DriverPlugin`. Add the entry before the first tag, or the workflow exits with `Unknown plugin`. ```bash git tag -a plugin-oracle-v1.0.26 -m "plugin-oracle-v1.0.26" @@ -136,40 +108,41 @@ git push origin plugin-oracle-v1.0.26 ``` -Push plugin tags one at a time. A push with more than 3 tags creates no push events on GitHub, so no workflow fires and nothing gets published. +Push plugin tags one at a time. A push carrying more than three tags creates no push events on GitHub, so no workflow fires and nothing is published. -Alternatively, dispatch the workflow directly. The `tags` input takes comma-separated `tag:pluginKitVersion` pairs; when `:pluginKitVersion` is omitted, the workflow reads `currentPluginKitVersion` from `PluginManager.swift`: +Dispatching the workflow works too. Its `tags` input takes comma-separated `tag:pluginKitVersion` pairs, and dropping the `:` part makes the workflow read `currentPluginKitVersion` from `PluginManager.swift`: ```bash gh workflow run build-plugin.yml --field "tags=plugin-oracle-v1.0.26" ``` -CI then builds both architectures, signs and notarizes the bundles, verifies each bundle's `TableProPluginKitVersion` matches the release label, creates a GitHub release, re-verifies the published assets, and updates `plugins.json` via `.github/scripts/update-registry.py` (atomic write with a rebase-and-retry loop against concurrent matrix jobs). +Either way CI builds both architectures, signs and notarizes the bundles, checks each bundle's declared PluginKit version against the release label, creates the GitHub release, re-verifies the published assets, and updates `plugins.json` through `.github/scripts/update-registry.py`, which writes atomically and rebases on a retry when the matrix jobs collide. + +Most bundled plugins never appear in the registry at all, because their binaries ride with the app release. Six of them keep a registry arm anyway (SQLite, ClickHouse, Redis, XLSX export, MQL export, SQL import), so a fix can reach users who are already on a shipped app without waiting for the next one. A bulk ABI re-release skips those six. ## PluginKit compatibility -TableProPluginKit builds with Swift Library Evolution, so a plugin built against any PluginKit version in the app's `[minimum, current]` range loads, and the runtime fills newer requirements from their defaults. What that means for releases: +A plugin built against any PluginKit version inside the app's `[minimum, current]` range loads, and the runtime fills newer requirements from their defaults. What that means for releases: -- **Additive change** (new requirement with a default, new field on a non-frozen type): no version bump, no re-publish. Existing binaries keep serving. -- **Breaking change** (removed or changed requirement, frozen-layout change, requirement without a default): raise `currentPluginKitVersion` and `minimumCompatiblePluginKitVersion` together, then run `scripts/release-all-plugins.sh `. It bumps each registry plugin's patch version and fires one `workflow_dispatch` so all plugins build as a parallel matrix. -- The app release workflow runs `scripts/check-registry-readiness.py --floor --current ` and fails the release until every database driver has a compatible binary, so the app never ships ahead of its plugins. -- **Retention**: `update-registry.py` keeps binaries for the two newest PluginKit versions per plugin (`--keep-kit-versions 2`). Older binaries are pruned, so users two or more PluginKit versions behind hit `noCompatibleBinary` and need to update the app. +- **Additive change** (a new requirement with a default, a new field on a non-frozen type): no bump, no re-publish. The binaries already out there keep serving. +- **Breaking change** (a removed or changed requirement, a frozen-layout change, a requirement without a default): raise `currentPluginKitVersion` and `minimumCompatiblePluginKitVersion` together, then run `scripts/release-all-plugins.sh `. It reads the registry-only plugins out of `.github/plugin-registry.json`, bumps each one's patch version, and fires a single `workflow_dispatch` so they all build as one matrix. +- **Retention**: `update-registry.py` keeps binaries for the two newest PluginKit versions per plugin. Older ones are pruned, so a user two or more versions behind hits `noCompatibleBinary` and has to update the app. -When an installed driver predates a breaking bump, the app updates it in the background on next connect. See [After an app update](/features/plugins#after-an-app-update). +The app's own release workflow runs `scripts/check-registry-readiness.py --floor --current ` and fails until every registry driver has a compatible binary, so the app cannot ship ahead of its plugins. When an installed driver predates a breaking bump, the app repairs it in the background on the next connect. See [After an app update](/features/plugins#after-an-app-update). ## Caching -The app fetches the manifest from `raw.githubusercontent.com/TableProApp/plugins/main/plugins.json`, which caches at the edge for about five minutes. Every fetch revalidates with a conditional request, the list refreshes at launch and when the plugin browser opens (throttled to one check per five minutes), and an install prompt forces a fresh fetch before reporting a plugin as missing. CI also purges the jsDelivr cache after each registry push for older app versions that still fetch from there. A newly published plugin is visible in the app within minutes. +The app fetches the manifest from `raw.githubusercontent.com/TableProApp/plugins/main/plugins.json`, which caches at the edge for about five minutes. Every fetch revalidates conditionally, the list refreshes at launch and when the plugin browser opens (throttled to one check per five minutes), and an install prompt forces a fresh fetch before it reports a plugin missing. CI also purges the jsDelivr cache after each registry push, for older app versions that still fetch from there. A newly published plugin shows up in the app within minutes. ## Theme distribution -Themes use the same manifest format with `category: "theme"`. Differences from driver plugins: +Themes use the same manifest with `category: "theme"`. Four things differ from a driver: -- Pure JSON data, no executable code, no code signing, no `.tableplugin` bundle -- ZIP contains `.json` files (valid `ThemeDefinition`); theme packs with multiple themes are supported -- Installed to `~/Library/Application Support/TablePro/Themes/Registry/` -- No `pluginKitVersion` needed; binaries match on architecture only, and the flat v1 fields still work +- Pure JSON data. No executable code, no code signing, no `.tableplugin` bundle +- The ZIP holds `.json` files, each a valid `ThemeDefinition`. Packs with several themes work +- They install to `~/Library/Application Support/TablePro/Themes/Registry/` +- No `pluginKitVersion` is needed, and the flat v1 fields still decode ```json { @@ -187,7 +160,7 @@ Themes use the same manifest format with `category: "theme"`. Differences from d ## Custom registry URL -For enterprise or private registries: +Point the app at a private or enterprise manifest, which is also how a plugin signed with your own Developer ID reaches your users: ```bash defaults write com.TablePro com.TablePro.customRegistryURL "https://your-registry.example.com/plugins.json" @@ -195,4 +168,4 @@ defaults write com.TablePro com.TablePro.customRegistryURL "https://your-registr defaults delete com.TablePro com.TablePro.customRegistryURL ``` -HTTP caching keys on the full URL, so changing the registry URL takes effect on the next fetch. These commands are also listed in [Settings](/customization/settings). +HTTP caching keys on the full URL, so a changed registry URL takes effect on the next fetch. A plugin served this way installs once the user agrees to trust its signing team by name. Both commands are also listed in [Settings](/customization/settings). diff --git a/docs/development/releasing.mdx b/docs/development/releasing.mdx new file mode 100644 index 000000000..27d28e311 --- /dev/null +++ b/docs/development/releasing.mdx @@ -0,0 +1,109 @@ +--- +title: Releasing +description: Release builds, notarization, DMG packaging, static libraries, and the release tag +--- + +`scripts/build-release.sh` is the whole release build. It signs every binary in the bundle itself, +each plugin, helper and framework first and the app bundle last, so signing again by hand +afterwards undoes its work. + +Everything on this page needs credentials the maintainer holds. A contributor building from source +wants [Building](/development/building) instead. + +## Release builds + +```bash +scripts/build-release.sh arm64 # Apple silicon +scripts/build-release.sh x86_64 # Intel +scripts/build-release.sh both # both, the default with no argument +``` + +Output lands in `build/Release/TablePro-arm64.app` and `build/Release/TablePro-x86_64.app`. + +The script extracts the matching slice from the universal static libraries, copies every +non-system dynamic library the binaries reference into `Contents/Frameworks` and rewrites their +install names, strips the main binary and the helpers, then signs with hardened runtime and +`TablePro/TablePro.entitlements`. + +| Variable | Default | Purpose | +|---|---|---| +| `SIGN_IDENTITY` | The maintainer's Developer ID Application identity | Codesign identity for every binary | +| `NOTARIZE` | `false` | Set `true` to notarize and staple after building | +| `NOTARY_PROFILE` | `TablePro` | `notarytool` keychain profile holding the credentials | + +Create the notarization profile once: + +```bash +xcrun notarytool store-credentials "TablePro" --apple-id … --team-id … --password … +``` + +Notarization zips each app, submits it, waits, and staples the ticket. `scripts/lib/notarize.sh` +reads both the exit status and the reported status: `notarytool` has exited 0 on a rejected +submission. + +## Packaging a DMG + +`scripts/create-dmg.sh` takes a version, an architecture (default `universal`) and a source app +(default `build/Release/TablePro.app`). With no version it reads `MARKETING_VERSION` from +`Configs/Version.xcconfig`, which is the only place the app version is declared. + +```bash +scripts/build-release.sh arm64 +scripts/create-dmg.sh 0.67.0 arm64 build/Release/TablePro-arm64.app +``` + +That writes `build/Release/TablePro-0.67.0-arm64.dmg`. The script prefers `create-dmg` +(`brew install create-dmg`) and falls back to `hdiutil` when it is missing. + +## Static libraries + +The `.a` files under `Libs/` and the xcframeworks under `Libs/ios/` live on the `libs-v1` GitHub +Release, not in git. Each has a checksum baseline committed beside it, and +`scripts/download-libs.sh` verifies the working copy against the baseline at `HEAD` on every run, +including runs that download nothing. + +That means an archive and its baseline have to move together. Publish through the scripts, which do +both: + +```bash +scripts/publish-libs.sh libmongoc_arm64.a libmongoc_x86_64.a libmongoc_universal.a libmongoc.a +git add Libs/checksums.sha256 && git commit -m "build: update static library checksums" +``` + +Name every library you rebuilt. `publish-libs.sh` verifies each library you did *not* name against +the checksums at `HEAD`, regenerates `Libs/checksums.sha256`, and uploads the archive with +`--clobber`. + +The iOS xcframeworks go through their own script, which takes `--dry-run` and refuses a publish +that changes nothing: + +```bash +scripts/publish-ios-libs.sh +git add Libs/ios/checksums.sha256 && git commit -m "build: update iOS xcframework checksums" +``` + +Never write `shasum -a 256 Libs/*.a > Libs/checksums.sha256` by hand. Regenerating from a stale +`Libs/` silently reverts every library you did not rebuild, which shipped a broken libmongoc and +rolled DuckDB back a version. Uploading the xcframeworks with a bare `tar` and `gh release upload` +fails from the other side: 299 MB published with no baseline behind it, and the next +`download-libs.sh` run on every machine and every CI job stops. + +## Shipping a version + +`.github/workflows/build.yml` fires on a `v*` tag push, and otherwise only on a manual dispatch. Bump +`Configs/Version.xcconfig` first: the release job compares the tag against `MARKETING_VERSION` and +fails with `tag v… does not match MARKETING_VERSION …` when they disagree. + +Four jobs gate the release: SwiftLint, macOS Tests, Build (a matrix over `arm64` and `x86_64`) and +Registry Readiness. A red suite or a registry with no plugin binary compatible with the shipping +app blocks the tag. + +The release job then verifies the artifacts, signs both update archives with the Sparkle key, +extracts the release notes from `CHANGELOG.md`, publishes the GitHub release, and only afterwards +commits `appcast.xml`. A tag containing `-alpha`, `-beta` or `-rc` publishes as a prerelease. + +## Plugins + +Driver plugins release on their own `plugin-*-v*` tags through a separate workflow, and the registry +decides which binary a user gets. [Plugin Registry](/development/plugin-registry) covers the +manifest, the tag names, and the bulk re-release after a PluginKit ABI change. diff --git a/docs/development/setup.mdx b/docs/development/setup.mdx index c42fc3de9..fa2bf7f4c 100644 --- a/docs/development/setup.mdx +++ b/docs/development/setup.mdx @@ -3,23 +3,26 @@ title: Development Setup description: Clone, configure signing, and build TablePro in Xcode --- +The Xcode project is generated and the static libraries are downloaded. A fresh clone carries +neither, so two scripts run before anything opens in Xcode. Xcode itself has to be 26.0 or newer: +the app calls SwiftUI's `glassEffect(_:in:)` behind `if #available(macOS 26.0, *)`, and that symbol +ships in the macOS 26 SDK. The deployment target stays at macOS 14.0, which is what the built app +runs on, not what you build on. + ## Prerequisites | Software | Version | Notes | |----------|---------|-------| -| Xcode | 26.0+ | Ships the macOS 26 SDK the app builds against. Every CI job pins 26.4.1. | -| Swift | 6 language mode | Comes with Xcode, set by `SWIFT_VERSION` in `Configs/Base.xcconfig`. The two local packages under `Packages/` declare it through `swift-tools-version: 6.0` instead. | -| XcodeGen | 2.46.0 | Generates `TablePro.xcodeproj` from `project.yml`. `brew install xcodegen` | - -The app references macOS 26 SDK symbols (`NSGlassEffectView` in the quick switcher), so an older Xcode cannot compile it. The deployment target stays at macOS 14.0 Sonoma, which is what the built app runs on, not what you build on. - -Optional but recommended: +| Xcode | 26.0+ | Every CI job pins 26.4.1 through `.github/actions/setup-xcode` | +| XcodeGen | 2.46.0 exactly | `brew install xcodegen`. Generated `pbxproj` output differs between XcodeGen versions, so `scripts/generate-project.sh` warns on any other version | +| SwiftLint | any | `brew install swiftlint`. The release workflow runs `swiftlint lint --strict` | +| SwiftFormat | any | `brew install swiftformat`. Not run in CI | +| GitHub CLI | any | `brew install gh`. Optional: `scripts/download-libs.sh` falls back to `curl` | -| Tool | Install | Purpose | -|------|---------|---------| -| SwiftLint | `brew install swiftlint` | Linting | -| SwiftFormat | `brew install swiftformat` | Code formatting | -| GitHub CLI | `brew install gh` | Used by `download-libs.sh` | +The Swift language mode comes from `SWIFT_VERSION = 6.0` in `Configs/Base.xcconfig`, and each +package under `Packages/` sets its own through `swift-tools-version: 6.0`. Never pass +`SWIFT_VERSION=` on an `xcodebuild` command line to test a language-mode change: the override +reaches the package targets too and reports their errors as yours. ## Quick start @@ -30,99 +33,98 @@ Optional but recommended: ``` - - ```bash - scripts/download-libs.sh - ``` - - This pulls two archives from the `libs-v1` GitHub Release into `Libs/`: the macOS `.a` files (libmariadb, libpq, libduckdb, libmongoc, and others) and the iOS xcframeworks. About 257 MB downloaded, 620 MB on disk once expanded. Expect a minute or two. - - - Skipping this step causes linker errors. The static libraries are not checked into git. - - - - + ```bash brew install xcodegen swiftlint swiftformat ``` - + ```bash - scripts/generate-project.sh + scripts/download-libs.sh ``` - `TablePro.xcodeproj` and `TableProMobile/TableProMobile.xcodeproj` are generated from - `project.yml` and are not in git. Run this again after changing `project.yml` or - `Configs/`, and after adding, moving, or deleting a source file: XcodeGen collects - sources at generation time, so a new file is not in the project until you regenerate. - + Two archives come off the `libs-v1` GitHub Release: the macOS `.a` files (libmariadb, libpq, + libduckdb, libmongoc, and the rest) and the iOS xcframeworks. 257 MB downloaded, 621 MB on + disk. Every run, including one that downloads nothing, verifies `Libs/` against the checksums + committed at `HEAD`. - - ```bash - open TablePro.xcodeproj - ``` + None of it is in git, so a build that skips this step fails at link time on missing symbols. - + ```bash cp Configs/Secrets.xcconfig.example Configs/Secrets.xcconfig ``` - Set your own team and a unique bundle identifier: + Then edit the copy. The template ships the literal `YOUR_TEAM_ID`, which fails to sign exactly + the way leaving the file out does: ``` TABLEPRO_DEVELOPMENT_TEAM = YOUR_TEAM_ID TABLEPRO_APP_BUNDLE_IDENTIFIER = com.yourhandle.TablePro ``` - A free personal team works. The Debug configuration signs with - `TablePro.Debug.entitlements`, which drops the iCloud capability, so no paid Apple - Developer Program membership is needed. Plugin bundles and the MCP server sign ad-hoc, - so they need no team at all. + A free personal team works. Debug builds sign against `TablePro/TablePro.Debug.entitlements`, + which drops the iCloud container and CloudKit, so no paid Apple Developer Program membership is + needed and sync turns itself off at runtime. Plugin bundles and the MCP helper sign ad-hoc and + need no team at all. - `Configs/Secrets.xcconfig` is gitignored, so these settings cannot reach a commit, and - they survive regenerating the project. Do not change signing in the Xcode UI: the - project is generated, and the next `scripts/generate-project.sh` discards the edit. + `Configs/Secrets.xcconfig` is gitignored, so these settings cannot reach a commit and they + survive regenerating the project. Changing signing in the Xcode UI has no effect that lasts: + the project is generated, and the next `scripts/generate-project.sh` discards the edit. + + + + ```bash + scripts/generate-project.sh + ``` + + This writes `TablePro.xcodeproj` and `TableProMobile/TableProMobile.xcodeproj` from their + `project.yml` specs. Pass `macos` or `ios` to generate just one of them. Run it again after + editing a `project.yml` or anything in `Configs/`, and after adding, moving, or deleting a + source file, because XcodeGen collects sources at generation time and a new file is not in + the project until you regenerate. - Select the **TablePro** scheme, set destination to **My Mac**, press `Cmd+R`. + ```bash + open TablePro.xcodeproj + ``` - Or from the command line: + Select the **TablePro** scheme, set the destination to **My Mac**, press `Cmd+R`. Or stay on + the command line: ```bash xcodebuild -project TablePro.xcodeproj -scheme TablePro -configuration Debug build -skipPackagePluginValidation ``` - `Cmd+R` writes the app to DerivedData - (`~/Library/Developer/Xcode/DerivedData/TablePro-*/Build/Products/Debug`). The - `xcodebuild` command above writes to `build/Debug/TablePro.app` inside the repo instead. + The two write to different places. See [Building](/development/building).
-Writing a driver plugin? See [Plugin Development](/development/plugin-development) to build one and [Testing a Custom Plugin](/development/testing-plugins) to run it in your local build. - - + Xcode setup Xcode setup +Writing a driver plugin? [Plugin Development](/development/plugin-development) covers building one +and [Testing a Custom Plugin](/development/testing-plugins) covers running it in your local build. + ## Project structure - + @@ -131,34 +133,40 @@ Writing a driver plugin? See [Plugin Development](/development/plugin-developmen - + + + - + - + + + + - + - + - + -`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to `Plugins/TableProPluginKit`. Edit the files under `Plugins/` only. +`Packages/TableProCore/Sources/TableProPluginKit` is a symlink to `Plugins/TableProPluginKit`. Edit +the files under `Plugins/` only. ## Running tests @@ -166,29 +174,54 @@ Writing a driver plugin? See [Plugin Development](/development/plugin-developmen xcodebuild -project TablePro.xcodeproj -scheme TablePro test -skipPackagePluginValidation ``` -Or in Xcode: `Cmd+U`. To run a single class or method, pass `-only-testing:TableProTests/TestClassName` or `-only-testing:TableProTests/TestClassName/testMethodName`. +Or press `Cmd+U` in Xcode. To narrow the run, pass `-only-testing:TableProTests/TestClassName` or +`-only-testing:TableProTests/TestClassName/testMethodName`. `-only-testing:TableProUITests` runs the +UI automation on its own. ## Linting and formatting -Before committing: - ```bash swiftlint lint --strict swiftformat . ``` -CI rejects warnings, so plain `swiftlint lint` is not enough. Full tooling reference: [Code Style](/development/code-style). +`--strict` turns warnings into errors, which is how the release workflow runs it, so a plain +`swiftlint lint` can pass on a change that later blocks a tag. Full tooling reference: +[Code Style](/development/code-style). ## Troubleshooting -| Problem | Fix | -|---------|-----| -| Linker errors about missing symbols | Run `scripts/download-libs.sh` | -| SPM resolution fails | Clean build folder (`Cmd+Shift+K`), reopen Xcode | -| Build fails after pulling | Delete derived data: `rm -rf ~/Library/Developer/Xcode/DerivedData` | -| `TablePro.xcodeproj` does not exist | Run `scripts/generate-project.sh` | -| Code signing errors | Change Team in Signing & Capabilities | - - -Pass `-skipPackagePluginValidation` on every `xcodebuild` invocation so the build does not stop on package plugin validation. `scripts/build-release.sh`, `scripts/build-plugin.sh`, `scripts/check-pluginkit-abi.sh`, and CI all pass it. - +### `xcodegen: command not found` + +`scripts/generate-project.sh` needs XcodeGen on `PATH`. Run `brew install xcodegen`, then check +`xcodegen --version` reports 2.46.0. + +### `TablePro.xcodeproj` does not exist + +It is generated, not committed. Run `scripts/generate-project.sh`. + +### `Undefined symbols for architecture arm64` + +`Libs/` is empty or stale. Run `scripts/download-libs.sh`, or `scripts/download-libs.sh --force` to +overwrite what is already there. + +### `Libs does not match the checksums committed in git.` + +`download-libs.sh` compares the extracted libraries against the baseline at `HEAD` and refuses to +continue when they differ. Re-run with `--force`. If you rebuilt a library on purpose, publish it +so the baseline moves with it: see [Releasing](/development/releasing). + +### Signing errors after a fresh clone + +`Configs/Base.xcconfig` defaults `TABLEPRO_DEVELOPMENT_TEAM` to the maintainer's team, and it +includes `Secrets.xcconfig` with `#include?`, so a build without that file still runs and still +fails to sign. Create the file and set your own team. + +### A new source file does not compile + +XcodeGen globs sources when the project is generated. Run `scripts/generate-project.sh`. + +### Package resolution fails + +Clean the build folder with `Cmd+Shift+K` and reopen Xcode. If that does not clear it, +`rm -rf ~/Library/Developer/Xcode/DerivedData`. diff --git a/docs/development/testing-plugins.mdx b/docs/development/testing-plugins.mdx index e1919c890..598e62867 100644 --- a/docs/development/testing-plugins.mdx +++ b/docs/development/testing-plugins.mdx @@ -3,18 +3,22 @@ title: Testing a Custom Plugin description: Load a locally built driver plugin in a debug build of TablePro --- -A plugin in the user plugins directory has to carry the same Apple team as the running app. Your own debug build therefore takes what your Xcode just signed, and a release build of TablePro rejects the same bundle. See [Plugin Development](/development/plugin-development) for writing the driver, and the [Plugin Registry](/development/plugin-registry) for publishing it once it works. +A plugin's signature is checked only outside the app's own `PlugIns` directory. That one rule explains all three ways to run a plugin you built locally, and which of them a release build of TablePro would also accept. The dev script is the normal route; the other two are for when the signature check itself is in your way. See [Plugin Development](/development/plugin-development) for writing the driver and [Plugin Registry](/development/plugin-registry) for publishing it. -## How plugin loading works +## Where plugins load from -TablePro loads plugins from two locations and treats them differently: +`PluginManager` reads two directories on launch and treats them differently. -- **Inside the app bundle** (`PlugIns/`): built-in plugins. No signature check runs. -- **User plugins directory** (`~/Library/Application Support/TablePro/Plugins`): user-installed plugins. Each bundle must be signed by the same Apple team as the running app, or the app rejects it with "Plugin code signature verification failed" (shown under a "Plugin Installation Failed" alert when installing). +| Location | Source | Checked before loading | +|----------|--------|------------------------| +| `PlugIns/` inside the app bundle | Built-in | `Info.plist` versions only | +| `~/Library/Application Support/TablePro/Plugins` | User-installed | `Info.plist` versions, then the code signature | -The required team comes from the app's own signature. A plugin built by the same Xcode as your local app build carries your team, so your local build accepts it. A release build of TablePro rejects it, because your team is not TablePro's; release users get plugins through the registry, where CI signs every binary. +The version keys go first either way: a plugin whose `TableProPluginKitVersion` falls outside the app's compatible range, or whose `TableProMinAppVersion` is newer than the running app, is rejected before anything looks at its signature. -Do not reuse a built-in plugin's bundle ID. Installing such a plugin fails with a conflict error, and during discovery the copy with the higher version wins; on a version tie the built-in wins and the user copy is deleted. +The signature check has two passes. A bundle signed by the same Apple team as the running app loads with no further question, which is why your own debug build accepts what your own Xcode signed. A bundle carrying any other Developer ID loads only if the user has trusted that team by name, and that consent is collected during an install through a registry, nowhere else ([Plugins & Themes](/features/plugins#plugins-from-other-developers)). Hand-copy such a bundle into the user plugins directory and it fails to load, because nothing on that path asks. + +Do not reuse a built-in plugin's bundle ID. Installing over one fails outright, and at discovery the higher version wins, with the built-in taking a tie and the losing user copy deleted from disk. ## Install with the dev script @@ -22,20 +26,48 @@ Do not reuse a built-in plugin's bundle ID. Installing such a plugin fails with scripts/install-plugin-dev.sh MyDriverPlugin ``` -The script finds `MyDriverPlugin.tableplugin` in DerivedData, copies it into the user plugins directory, and writes the registry metadata sidecar. Relaunch TablePro to load it. +It finds the newest `MyDriverPlugin.tableplugin` under DerivedData, copies it into the user plugins directory, and writes the registry metadata sidecar. Relaunch TablePro to load it. Build from Xcode first: the script reads DerivedData, and a command-line `xcodebuild` writes to `build/Debug` instead, where the script never looks. + + +Never edit the copied bundle, `Info.plist` included. Any change invalidates the code signature and dyld then refuses to load the executable. If the plugin's `TableProMinAppVersion` is ahead of your built app, raise the app's `MARKETING_VERSION` in `Configs/Version.xcconfig` and rebuild the app. + + +## Bundle it into the app instead + +A plugin embedded in `PlugIns/` skips the signature check entirely, which is the shortest path when the team check is the thing in your way. Add the target to the app's dependency list in `project.yml`: + +```yaml +targets: + TablePro: + dependencies: + - target: MyDriverPlugin + embed: true + copy: + destination: plugins +``` + +Then run `scripts/generate-project.sh` and build. XcodeGen puts the bundle in an **Embed Dependencies** copy phase with `CodeSignOnCopy`, so it is re-signed with your debug identity on the way in. -Do not modify the copied bundle, for example by patching its `Info.plist`. Any edit invalidates the code signature and dyld refuses to load the executable. If the plugin's `TableProMinAppVersion` is newer than your built app, bump the app's `MARKETING_VERSION` and rebuild the app instead. +`TablePro.xcodeproj` is generated and gitignored, so adding the plugin through the Xcode UI survives exactly until the next `scripts/generate-project.sh`. Edit `project.yml`. Take the entry back out before you open a pull request, or the plugin rides into a release build. -## Alternative: bundle it into the app +## Skip the signature check + +A Debug build skips signature verification altogether when `TABLEPRO_ALLOW_UNSIGNED_PLUGINS` is set to `1`. Set it under **Product > Scheme > Edit Scheme > Run > Arguments**, in the environment variables list. -Add your `.tableplugin` to the **Copy Plug-Ins** build phase of the **TablePro** target (Build Phases tab), then run with `Cmd+R`. The plugin lands in the app's `PlugIns/` directory and loads as a built-in plugin, so no signature check runs; the phase also re-signs it with your debug identity as it copies (`CodeSignOnCopy`). +This is the route for a bundle built on another machine, or for testing your own Developer ID signing before there is any registry entry to install from, since it also bypasses the developer-trust prompt. The bundle still has to be validly signed for dyld to load it at all. The escape hatch is compiled out of Release builds, so it cannot mask a signing problem that would reach a user. -Remove it from the phase after testing so it does not ride into a release build. +## When it does not load -## Alternative: skip the signature check +The reason lands in a **Plugin Installation Failed** alert when installing, and in the plugin's row under **Settings > Plugins > Installed** otherwise. -A debug build skips the app's signature check when the `TABLEPRO_ALLOW_UNSIGNED_PLUGINS` environment variable is set to `1`. Set it under **Product > Scheme > Edit Scheme > Run > Arguments**. Use this when the team check fails, for example with a bundle built on another machine. The bundle must still be validly signed for dyld to load it. Release builds always verify. +| Message | What it means | What to do | +|---------|---------------|------------| +| **Plugin code signature verification failed: …** | Unsigned, ad-hoc signed, modified after signing, or signed by a certificate that is neither your app's team nor a Developer ID | Rebuild rather than patch. Any edit to a signed bundle breaks the seal | +| **This plugin is signed by …, a developer you have not trusted yet.** | A valid Developer ID that this Mac has not consented to | Install it through a registry so the trust prompt runs, or set `TABLEPRO_ALLOW_UNSIGNED_PLUGINS=1` in a Debug build | +| **Plugin was built for PluginKit version …; this release of TablePro needs version …** | The bundle predates a breaking ABI bump | Rebuild the plugin against the current TableProPluginKit | +| **Plugin requires app version … or later, but current version is …** | `TableProMinAppVersion` is ahead of the app | Raise `MARKETING_VERSION` in `Configs/Version.xcconfig` and rebuild | +| **A built-in plugin "…" already provides this bundle ID** | The bundle ID collides with a bundled plugin | Give your plugin its own bundle ID | -When the plugin works, publish it through the [Plugin Registry](/development/plugin-registry) so users receive a signed binary. +Once it loads and works, publish it through the [Plugin Registry](/development/plugin-registry) so users get a signed binary. diff --git a/docs/docs.json b/docs/docs.json index f4ed9eb40..779015e95 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -123,6 +123,7 @@ "icon": "plug", "pages": [ "connections/index", + "connections/connection-form", "connections/urls", "connections/ssh-tunneling", "connections/ssh-profiles", @@ -139,6 +140,7 @@ "group": "Databases", "icon": "database", "pages": [ + "databases/index", "features/plugins", "databases/dynamodb", "databases/redshift", @@ -173,6 +175,8 @@ "icon": "code", "pages": [ "features/sql-editor", + "features/code-folding", + "features/sql-files", "features/autocomplete", "features/query-parameters", "features/vim-mode", @@ -185,6 +189,7 @@ "icon": "table", "pages": [ "features/data-grid", + "features/query-results", "features/filtering", "features/change-tracking", "features/json-viewer", @@ -245,9 +250,11 @@ "group": "Customization", "pages": [ "customization/settings", + "customization/general-settings", "customization/appearance", "customization/editor-settings", - "customization/data-settings" + "customization/data-settings", + "customization/notifications" ] }, { @@ -283,6 +290,7 @@ "group": "Drive the app", "pages": [ "external-api/url-scheme", + "external-api/connection-import", "external-api/terminal", "external-api/raycast", "external-api/ios-shortcuts" @@ -318,6 +326,7 @@ "development/setup", "development/architecture", "development/building", + "development/releasing", "development/code-style", "development/plugin-development", "development/plugin-registry", diff --git a/docs/external-api/connection-import.mdx b/docs/external-api/connection-import.mdx new file mode 100644 index 000000000..88ccd6a83 --- /dev/null +++ b/docs/external-api/connection-import.mdx @@ -0,0 +1,87 @@ +--- +title: Connection Import +description: Every query parameter a tablepro://import link accepts, and which ones the app refuses +--- + +Passwords are never accepted in an import link, and neither is anything else that decides where a +credential comes from. What is left is the rest of the connection form, one query parameter per +field, and a review sheet the user reads before anything is saved. + +```text +tablepro://import?name=Staging&host=db.example.com&port=5432&type=postgresql&username=admin +``` + +`name`, `host` and `type` are required. `type` takes any registered database type name, +case-insensitive: `MySQL`, `PostgreSQL`, `MongoDB`, `Redis`, `ClickHouse`, `Oracle`, `DuckDB`, +`Cassandra`, and every other type a loaded plugin registers. + +Enum-valued parameters must match a listed value exactly, since these are stored raw values rather +than display names, and an unrecognized one falls back rather than failing the link. Percent-encode +values that contain spaces: `sslMode=Verify%20CA`. + +## Core parameters + +| Parameter | Description | +|-----------|-------------| +| `port` | Server port. Defaults to the type's standard port. | +| `username` | Database username. | +| `database` | Default database name. | +| `color` | Sidebar color: `Red`, `Orange`, `Yellow`, `Green`, `Blue`, `Purple`, `Pink`, `Gray` or `None`. Falls back to `None`. | +| `tagName` | Tag to assign. Repeat the parameter for more than one tag. | +| `groupName` | Group to place the connection in. | +| `safeModeLevel` | `silent`, `alert`, `alertFull`, `safeMode`, `safeModeFull` or `readOnly`. Falls back to `silent`. | +| `aiPolicy` | `alwaysAllow`, `askEachTime` or `never`. Falls back to the app default. | +| `redisDatabase` | Redis database index to select on connect. | +| `startupCommands` | SQL to run after connecting. The review sheet shows it in full. | +| `localOnly` | `1` keeps the connection off iCloud sync. | + +## SSH parameters + +Set `ssh=1`, or none of these are read. + +| Parameter | Description | +|-----------|-------------| +| `sshHost` | SSH server hostname. | +| `sshPort` | SSH port. Default `22`. | +| `sshUsername` | SSH username. | +| `sshAuthMethod` | `Password`, `Private Key`, `SSH Agent`, `Keyboard Interactive` or `None`. Falls back to `Password`. | +| `sshPrivateKeyPath` | Path to the private key file. | +| `sshAgentSocketPath` | Custom SSH agent socket path. | +| `sshJumpHosts` | JSON array of `{host, port, username, authMethod, privateKeyPath}`. Jump `authMethod` is `Private Key` or `SSH Agent`, falling back to `SSH Agent`. | +| `sshTotpMode` | `none`, `auto_generate` or `prompt_at_connect`. Falls back to `none`. | +| `sshTotpAlgorithm` | `SHA1`, `SHA256` or `SHA512`. Default `SHA1`. | +| `sshTotpDigits` | Code length. Default `6`. | +| `sshTotpPeriod` | Period in seconds. Default `30`. | + +## SSL parameters + +| Parameter | Description | +|-----------|-------------| +| `sslMode` | `Disabled`, `Preferred`, `Required`, `Verify CA` or `Verify Identity`. Falls back to `Disabled`. | +| `sslCaCertPath` | CA certificate file path. | +| `sslClientCertPath` | Client certificate file path. | +| `sslClientKeyPath` | Client key file path. | + +## Driver fields + +An `af_` prefix passes a driver-specific field straight to the plugin: `af_replicaSet=myrs` sets +`replicaSet` on a [MongoDB](/databases/mongodb) connection. Field names come from the plugin, so the +engine's own page is where they are listed. + +Six field names are dropped on import, along with any name starting with `aws`, because each decides +where a credential comes from: `af_preConnectScript`, `af_preTunnelHost`, `af_preTunnelPort`, +`af_promptForPassword`, `af_sslClientKeyPassphrase` and `af_usePgpass`. The same rule covers +`.tablepro` files, described in +[Connection Sharing](/features/connection-sharing#import). Add any of these by hand in the connection +editor afterwards. + +## What the user sees + +The review sheet shows the type, host and port, database, username, SSH auth method, SSL mode, color, +tag, group, driver fields, and startup SQL. Only the name is editable. **Add Connection** saves it; +when a connection with the same name, host, port and type is already there, the sheet warns +*"A connection with this name, host, and type already exists."* and the button becomes +**Add as Copy**. + +The saved connection has no password, so open it in the connection editor and add one before +connecting. diff --git a/docs/external-api/index.mdx b/docs/external-api/index.mdx index 5a76d584e..2b565ab63 100644 --- a/docs/external-api/index.mdx +++ b/docs/external-api/index.mdx @@ -3,74 +3,51 @@ title: External API description: URL scheme, MCP server, pairing flow, terminal, and iOS Shortcuts for driving TablePro from other apps --- -The TablePro External API is the public contract that lets other apps drive TablePro. Raycast, Cursor, Claude Desktop, shell scripts, and DDEV all use the same surface. This page is the entry point. Pick the subpage that matches what you want to do. - -## Layers - -The URL scheme drives the GUI, MCP exchanges data, and pairing bootstraps trust. The three are independent, and most clients use a mix. +Deep links drive the GUI. MCP moves data. That one split decides which page you need, and pairing is +the step in front of MCP that gets a client its token. `tablepro://` deep links open connections, tables, and queries in the GUI. - 46 JSON-RPC tools, resources and prompts for AI clients, over stdio or local HTTP. + JSON-RPC tools, resources and prompts for AI clients, over stdio or local HTTP. - One-click flow to issue a scoped token to an extension. + One-click flow that issues a scoped token to an extension. -Two more surfaces build on these. TablePro also opens plain database URLs (`mysql://…`) handed to it by macOS, which is how the [Terminal and DDEV](/external-api/terminal) integration works. On iPhone and iPad, TablePro exposes App Intents for [iOS Shortcuts](/external-api/ios-shortcuts). - -## When to use which - -| Goal | Use | -|------|-----| -| Open a connection from a script or other app | URL scheme | -| Open a database from the shell or a DDEV project | [Terminal and DDEV](/external-api/terminal) | -| Run a query and read rows back | MCP `execute_query` | -| Browse schema for an AI model | MCP `list_tables`, `describe_table` | -| Issue a token to a Raycast or Cursor extension | Pairing | -| Read a table's rows with filters and paging | MCP `browse_table` | -| Get a task written against the live schema | MCP prompts, such as `explain_table` or `review_query` | -| Navigate to a tab the user already has open | MCP `list_recent_tabs` + `focus_query_tab` | -| Insert rows from iPhone or iPad | [iOS Shortcuts](/external-api/ios-shortcuts) | - -## Security model - -Every request clears the same gates: the connection's external access level (`blocked`, `readOnly` by default, or `readWrite`), the token's scope and connection allowlist, and the connection's AI policy plus Safe Mode. The effective permission is the lower of the token's scope and the connection's access. See [Tokens](/external-api/tokens) for the full model. - -The MCP server binds `127.0.0.1` only. An unauthenticated local caller is read-only and cannot be granted more. - -Every request is recorded in the activity log, with the token behind it. Statements are stored as a digest, not as text. Open **Settings > Integrations** and click **View Activity** to inspect. +Two surfaces build on those. macOS hands TablePro plain database URLs (`mysql://…`) and `.sql` files, +which is what the [Terminal and DDEV](/external-api/terminal) integration rides on. On iPhone and +iPad, App Intents put three actions in [Shortcuts](/external-api/ios-shortcuts). ## Quick start -- Install the [Raycast extension](/external-api/raycast) and run `Pair with TablePro`. -- Or wire stdio MCP into your [MCP client](/external-api/mcp-clients) without an extension. -- Or open a deep link from your shell: +Opening something in the GUI needs no setup at all: ```bash open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1" ``` -## Versioning +Reading data back needs a token. Install the [Raycast extension](/external-api/raycast) and run +**Pair with TablePro**, or wire stdio MCP into your own [MCP client](/external-api/mcp-clients) and +skip the extension. -The External API follows TablePro's semver. Paths and tools are additive within a major version. See [Versioning](/external-api/versioning) for the deprecation policy. +## Security model -## Subpages +The MCP server binds `127.0.0.1`, and on stock settings every request carries a token. A call is +allowed only where the token's scope, the token's connection allowlist, and the connection's own +**External Clients** level all permit it; the effective permission is the lowest of them. +[Tokens](/external-api/tokens) has the full model. On top of that, an AI policy of **Never** refuses +the connection outright, and [Safe Mode](/features/safe-mode) still holds destructive statements +behind a confirmation. + +Each request lands in the activity log with the token behind it, and a statement is stored as a +SHA-256 digest rather than as text. Open **Settings > Integrations** and click **View Activity** to +read it. + +## Versioning -- [URL Scheme](/external-api/url-scheme): every `tablepro://` action and parameter. -- [Terminal and DDEV](/external-api/terminal): open databases and files from the shell, `ddev tablepro`, trusted links. -- [iOS Shortcuts](/external-api/ios-shortcuts): App Intents for adding rows and opening connections. -- [MCP Protocol](/external-api/mcp-protocol): protocol versions, required metadata, headers, and error codes. -- [MCP Tools](/external-api/mcp-tools): the 46 tools with their arguments and result shapes. -- [MCP Resources](/external-api/mcp-resources): resources you can read, and their URI templates. -- [MCP Prompts](/external-api/mcp-prompts): the prompt catalog and `completion/complete`. -- [MCP Subscriptions](/external-api/mcp-subscriptions): `subscriptions/listen` and the notification filter. -- [Pairing](/external-api/pairing): sequence diagram and PKCE flow. -- [Tokens](/external-api/tokens): scope model, allowlists, revocation. -- [Raycast](/external-api/raycast): extension install and command list. -- [MCP Clients](/external-api/mcp-clients): stdio MCP setup for Claude Desktop, Claude Code, Cursor, Cline, Continue, Zed, Windsurf, Goose, and custom clients. -- [Versioning](/external-api/versioning): stability policy. +Paths, tools and result fields are additive within a major version, and the app is still on 0.x. See +[Versioning](/external-api/versioning) for what that means for a client you have to keep working. diff --git a/docs/external-api/ios-shortcuts.mdx b/docs/external-api/ios-shortcuts.mdx index e74f541fa..52c090be4 100644 --- a/docs/external-api/ios-shortcuts.mdx +++ b/docs/external-api/ios-shortcuts.mdx @@ -3,62 +3,67 @@ title: iOS Shortcuts description: Add rows to a table or open a connection from the Shortcuts app on iPhone and iPad --- -The two insert actions never open the app. They reach a saved connection in the background, write the rows, and return the count. App Intents put all three actions in the Shortcuts app, the Share Sheet, and Siri. - -This is iOS only. On macOS, use the [URL scheme](/external-api/url-scheme) and [MCP](/external-api/mcp-tools). - -## Availability - -The iOS app updates separately from the Mac app, so a Mac release note about these actions does not mean your iPhone or iPad already has them. - -| Action | Needs | -| --- | --- | -| **Open Connection** | Any recent build. | -| **Add Row to Table** | iOS app build 17 or later. | -| **Add Rows to Table** | iOS app build 17 or later. | - -Check your build under **Settings > About** in the iOS app. If you are on an older build, update through TestFlight; the two insert actions will not appear in Shortcuts until you do. - -To see every action the app offers, open Shortcuts, tap the action list, and go to **Apps > TablePro**. Searching the app name works too, but the app list is the complete view. - -## Actions - -| Action | Use it for | -| --- | --- | -| **Add Row to Table** | Insert a single row. | -| **Add Rows to Table** | Insert many rows from a JSON array, CSV text, or a file. | -| **Open Connection** | Open a saved connection in the TablePro app. | - -Open Connection launches the app on the chosen connection. The two insert actions run in the background and share the same pickers: - -- **Connection**: the picker lists every saved connection. Inserts work on MySQL, MariaDB, PostgreSQL, Redshift, SQL Server, SQLite, DuckDB, and Oracle. Pick any other type, Redis for example, and the action fails with an unsupported database type error. -- **Database or Schema**: optional. Leave it empty to use the connection's configured database. On schema databases like PostgreSQL it lists schemas; on others it lists databases. -- **Table**: the table to insert into. The list is read from the chosen connection. - -## Siri - -The actions register these phrases, with the connection name spoken in place of the brackets: - -- "Open [connection] in TablePro" or "Connect to [connection] in TablePro" -- "Add a row in TablePro" -- "Add rows in TablePro" +You point an action at a table, hand it a JSON object, and the row lands in the database with the app +still closed. App Intents put three actions in Shortcuts, the Share Sheet and Siri, and two of +them run entirely in the background. + +This is iOS only. On macOS, use the [URL scheme](/external-api/url-scheme) and +[MCP](/external-api/mcp-tools). + +| Action | Does | Needs | +| --- | --- | --- | +| **Add Row to Table** | Inserts one row from a JSON object or a CSV row. | iOS app build 17 | +| **Add Rows to Table** | Inserts many rows from a JSON array, CSV text, or a file. | iOS app build 17 | +| **Open Connection** | Launches the app on a saved connection. | Any recent build | + +Your build is under **Settings > About** in the iOS app, and the two insert actions do not appear in +Shortcuts on anything older. The iOS app ships on its own schedule; see +[iPhone and iPad](/ios). + +To browse everything the app offers, open Shortcuts, tap the action list, and go to +**Apps > TablePro**. + +## Build one + + + + In a new shortcut, add **Add Row to Table**. + + + Pick a **Connection**, then a **Table** read live from it. **Database or Schema** is optional and + lists schemas on PostgreSQL and the like, databases elsewhere; empty means the connection's own. + + + Feed **Row (JSON or CSV)** from a Shortcuts **Dictionary** action keyed by column name, which + builds the row without typing JSON. **Add Rows to Table** has **Rows (JSON or CSV)** and a + **File** field instead. + + + It returns the number of rows inserted, ready for a later step, and speaks *"Added one row to + bookmarks."* + + + +Inserts work on MySQL, MariaDB, PostgreSQL, Redshift, SQL Server, SQLite, DuckDB and Oracle. Any +other type, Redis for example, fails with *"Redis connections do not support adding rows from +Shortcuts."* + +Siri takes "Open [connection] in TablePro", "Connect to [connection] in TablePro", "Add a row in +TablePro" and "Add rows in TablePro", with a connection name in place of the brackets. ## Data formats -The row data is matched to the table's columns by name. A column you don't include is left out of the insert, so the database applies its default (an auto-increment primary key, for example). A key that matches no column in the table fails the whole action with an error naming the unknown keys; nothing is inserted. - -An empty or `null` value for a primary key column is dropped from the insert, so auto-increment still applies even when the key appears in the data. +Keys match columns by name. A column you leave out stays out of the `INSERT` and takes its default, +and an empty or `null` primary key is dropped for the same reason, so auto-increment still fires. ### JSON -A single object is one row: +An object is one row; an array of objects is many, for **Add Rows to Table**. ```json { "title": "TablePro docs", "url": "https://docs.tablepro.app", "tags": "reference" } ``` -An array of objects is many rows (Add Rows to Table): - ```json [ { "title": "First", "url": "https://example.com/1" }, @@ -66,15 +71,13 @@ An array of objects is many rows (Add Rows to Table): ] ``` -`null` inserts a SQL `NULL`. Numbers and booleans are inserted as text. - -Wrap very large or high-precision numbers in quotes so they stay exact. A bare JSON number past 64-bit integer range (a long unsigned ID, for example) is read as a floating-point value and loses digits; `"18446744073709551615"` as a string keeps every digit. - -A Shortcuts **Dictionary** action passes straight into the Data field, so you can build the row visually with key/value pairs instead of typing JSON. +`null` inserts a SQL `NULL`, and numbers and booleans are inserted as text. Quote anything that has +to stay exact: a bare JSON number past 64-bit integer range is read as floating point and loses +digits, while `"18446744073709551615"` keeps every one. ### CSV -The first row is the header and names the columns: +The first row is the header and names the columns. Quoting follows the usual rules. ```text title,url @@ -82,16 +85,23 @@ First,https://example.com/1 Second,https://example.com/2 ``` -Quoted fields with commas and embedded quotes follow the usual CSV rules. Pass CSV as text in the Data field, or as a file in the File field. +## Limits -## Result +Up to 10,000 rows per run. **Add Rows to Table** runs the batch in one transaction wherever the +engine supports it, so a row that fails rolls back the whole batch instead of leaving half of it in. -Each action returns the number of rows inserted, so you can show it or use it in a later step, and speaks a short confirmation ("Added 2 rows to bookmarks."). +## When it refuses -## Notes +Nothing is inserted in any of these cases. -- The action fails while the device is locked, since it reads the connection password from the Keychain. -- A read-only connection refuses the insert with an error. A connection set to Confirm Writes asks you to confirm before the rows are added. -- Add Rows to Table runs the batch in one transaction on databases that support it, so a row that fails rolls the whole batch back instead of leaving a partial insert. -- Data that maps to no columns (an empty object, for example) returns an error rather than reporting that it added nothing. -- Up to 10,000 rows per run. +| Message | Cause | +| --- | --- | +| *"…is read-only, so rows cannot be added."* | The connection's safe mode blocks writes. A connection set to Confirm Writes asks first instead. | +| *"…has no column named …"* | A key in the data matches no column. | +| *"The data has no values to insert into …"* | Nothing in the data maps to a column. | +| *"No data was provided to add."* | Both the text field and the file were empty. | +| *"Too many rows. Add up to 10000 rows at a time."* | Over the cap. | +| *"Add Row to Table expects one row. Use Add Rows to Table for multiple rows."* | An array reached the single-row action. | + +The action also fails while the device is locked, because it reads the connection's password from the +Keychain. diff --git a/docs/external-api/mcp-clients.mdx b/docs/external-api/mcp-clients.mdx index 425a9af0e..1502a0e7c 100644 --- a/docs/external-api/mcp-clients.mdx +++ b/docs/external-api/mcp-clients.mdx @@ -3,7 +3,7 @@ title: MCP Clients description: Connect Claude Desktop, Claude Code, Cursor, VS Code, Cline, Continue, Zed, Windsurf, Antigravity, Goose, and custom clients to TablePro --- -Any MCP client that can run a command on disk can connect to TablePro. The pattern is the same everywhere: +Paste one absolute path into your client's MCP config and stop there. `tablepro-mcp` ships inside the app bundle; it starts TablePro when nothing is running, reads the port, and carries the token, so none of those end up in the file. ```json { @@ -15,26 +15,11 @@ Any MCP client that can run a command on disk can connect to TablePro. The patte } ``` -`tablepro-mcp` ships inside the app bundle. It bridges stdio JSON-RPC to the app's local HTTP server, so your client does not have to speak the new protocol, know the port, or hold a token. +For an install outside `/Applications`, use your own bundle's path. Setapp puts it at `~/Applications/Setapp/TablePro.app/Contents/MacOS/tablepro-mcp`. -If TablePro is installed somewhere other than `/Applications` (Setapp, a custom path), use the absolute path to your bundle's `tablepro-mcp`. +## Clients that take the JSON above -## What the bridge does - -1. Reads `~/Library/Application Support/TablePro/mcp-handshake.json` and checks it can be trusted: the file is a regular file, owned by you, mode `0600`, in a directory no one else can write, not expired, and its recorded PID is a live process inside the same app bundle. -2. If any of that fails, runs `open -g tablepro://integrations/start-mcp` to launch TablePro, then polls every 200ms for up to 10 seconds for a good handshake. You do not need to start TablePro first. A TablePro started this way runs in the background: no Dock icon, no app switcher entry, no menu bar. It takes all three for as long as it has a window to show you, and drops them again when you close it. Opening TablePro yourself makes it an ordinary app for the rest of that run. -3. Calls `server/discover` and refuses to start if the server does not speak `2026-07-28`. -4. Answers `initialize` and `ping` itself, so a client that expects a handshake gets one, and forwards everything else upstream with the modern metadata and headers attached. - -You never put a token in the client config. Every server start mints a bridge token named `__stdio_bridge__` and writes it into the handshake file. It carries **read and write** scopes, not admin, so an agent going through the bridge can read and write but cannot call `confirm_destructive_operation`. The token lives an hour and is rotated 15 minutes before it expires; a 401 makes the bridge re-read the handshake and retry once on its own. - - -To give an agent a narrower scope or a connection allowlist, use the [HTTP transport](#http-transport) with a token you mint yourself. - - -## Clients using the standard shape - -These take the exact JSON above; only the config file differs. +Only the config file differs. | Client | Config file | How to open it | |--------|-------------|----------------| @@ -54,7 +39,7 @@ The double dash separates Claude Code's flags from the command it runs. stdio is ## VS code -VS Code has native MCP support (1.99+). Run **MCP: Open User Configuration** from the Command Palette, or create `.vscode/mcp.json` in the workspace. The top-level key is `servers`, not `mcpServers`: +Native MCP support arrived in 1.99. Run **MCP: Open User Configuration** from the Command Palette, or create `.vscode/mcp.json` in the workspace. The top-level key is `servers`, not `mcpServers`: ```json { @@ -117,9 +102,23 @@ Run `goose session` and ask for the tool list to confirm. Click **Connect a Client…** in **Settings > Integrations**. The sheet covers Claude Desktop, Claude Code, Cursor, and Zed, with numbered steps and copyable snippets that already carry the right binary path for your install. +## What the config leaves out + +TablePro does not have to be open first. Finding no trusted handshake, the bridge runs `open -g tablepro://integrations/start-mcp` and polls for one every 200ms for up to 10 seconds. A TablePro started that way stays out of the Dock, the app switcher and the menu bar until it has a window to show; open the app yourself and it behaves normally for the rest of that run. + +The token is not yours to manage either. Each server start mints one named `__stdio_bridge__`, writes it into the handshake file, and rotates it 15 minutes before its one-hour expiry. It is **Read & Write**, so an agent on the bridge can read and write but cannot call `confirm_destructive_operation`. + +The port is absent for the same reason: TablePro writes the port it actually bound into the handshake file, so a conflict resolves itself and your config never changes. + +Two installs share that one handshake file, so the bridge reaches whichever TablePro is running rather than the bundle whose path you configured. Quit the one you do not want. + + +For a narrower scope than `readWrite`, or a connection allowlist, use the [HTTP transport](#http-transport) with a token you mint yourself. + + ## HTTP transport -Use HTTP when the client cannot spawn a local process, or when you want a narrower scope than the bridge token. Mint a token in **Settings > Integrations > Authentication** and point the client at the server: +Use HTTP when the client cannot spawn a local process. Mint a token in **Settings > Integrations > Authentication** and point the client at the server: ```json { @@ -136,42 +135,48 @@ Use HTTP when the client cannot spawn a local process, or when you want a narrow Replace `23508` with the port shown in **Settings > Integrations**. Other clients use the same `url` plus `headers` shape, sometimes under `type: streamable-http`. Check the client's docs. -Three things to know before you go this way: - -- **The server is loopback only, over plain HTTP.** There is no remote mode and no TLS. A client on another machine cannot reach it. Forward a port over SSH if you need that, and understand what you are exposing. -- **`GET /mcp` answers 405.** A client that opens a `GET` stream for notifications will fail. Notifications arrive on the response stream of the request they belong to. -- **A client that speaks `2025-11-25` or `2025-06-18` still works** through the old `initialize` handshake and `Mcp-Session-Id`. A client on `2025-03-26` does not: that version was dropped. +Three things change on this route. The server binds `127.0.0.1` over plain HTTP with no remote mode and no TLS, so a client on another machine needs an SSH port forward. `GET /mcp` answers `405`, so a client that opens a `GET` stream for notifications fails; notifications ride the response stream of the request they belong to. And `2025-11-25` and `2025-06-18` still work through the old `initialize` handshake and `Mcp-Session-Id`, while `2025-03-26` is refused. Writing a client against `2026-07-28` directly means sending the required `_meta` and headers on every request. See [MCP Protocol](/external-api/mcp-protocol). -## What the AI sees +## Check it worked + +Ask the client to list TablePro tools, or call `list_connections`. + + +The tool list contains `list_connections`, `list_tables`, `describe_table` and `execute_query`, and `list_connections` returns your saved connections. + + +Clients see the full [tool catalog](/external-api/mcp-tools), the [resources](/external-api/mcp-resources) and the [prompts](/external-api/mcp-prompts), and the server's instructions tell the model to start from `list_connections` and call `describe_table` before writing SQL. A connection's **External Clients** level and the token's scope still apply on top of that. + +## When it does not work + +The bridge writes its failure to stderr and as a JSON-RPC error, so the message in the client names the layer that refused. + +### TablePro is not running -Clients see the full [tool catalog](/external-api/mcp-tools), the [resources](/external-api/mcp-resources), and the [prompts](/external-api/mcp-prompts). The server's instructions tell the model to start from `list_connections` and to call `describe_table` before writing SQL. +The bridge could not launch the app, or got no trusted handshake within 10 seconds. Open TablePro and check that **Settings > Integrations** shows the server running; if it does, toggle **Enable MCP Server** off and on, then relaunch the client. -The connection's external access and the token's scope still apply. A read-only connection rejects writes whatever the model tries, and a refusal is the user's policy, not a transient error worth retrying. +### The local MCP endpoint did not prove it belongs to TablePro -## Verify the connection +The handshake file points at a process the bridge will not trust, usually one left behind by a TablePro that is gone. Quit TablePro, delete `~/Library/Application Support/TablePro/mcp-handshake.json`, and reopen the app, which rewrites it. -Ask the client to list TablePro tools, or call `list_connections`. Success looks like a tool list containing `list_connections`, `list_tables`, `describe_table` and `execute_query`, and a `list_connections` call returning your saved connections. +### This TablePro build does not speak 2026-07-28 -If it fails, the response says which layer rejected it: +The bridge is newer than the app it found. Update TablePro. -- **The stdio process exits immediately.** The bridge could not launch TablePro, or got no trusted handshake within 10 seconds. It writes the reason to stderr and as a JSON-RPC error. Open TablePro and check that **Settings > Integrations** shows the server running. -- **"The local MCP endpoint did not prove it belongs to TablePro."** The handshake file points somewhere the bridge does not trust. Quit TablePro, delete `~/Library/Application Support/TablePro/mcp-handshake.json`, and reopen the app. -- **`401 Unauthorized`** (`-33009`). The bridge token is stale. The bridge re-reads the handshake and retries once, so you only see this when TablePro is not running. -- **`403 Forbidden`** (`-33007`). The connection's external access is `blocked` or `readOnly`, or the token's allowlist excludes it. Change **External Access** in the connection editor. -- **`429 Too Many Requests`** (`-33010`). Five failed auth attempts inside 60 seconds triggered a 5-minute lockout. Wait it out, or restart TablePro. +### This connection is read only for external clients -## Troubleshooting +**External Clients** on that connection is **Read Only** and the statement writes. Change it in the connection editor's **Advanced** pane, or run the query in the app's editor. -**Handshake timeout.** TablePro launched but did not answer `tablepro://integrations/start-mcp` within 10 seconds. Open **Settings > Integrations**, toggle **Enable MCP Server** off and on, then relaunch the client. +### Forbidden (`-33007`) -**Stale handshake file.** The bridge detects a handshake from a dead process and relaunches TablePro on its own. If it still fails, delete the file and reopen the app; TablePro rewrites it. +**External Clients** is **Blocked**, the connection's AI policy is **Never**, or the token's allowlist excludes it. Any of the three also hides the connection from `list_connections`. -**Setapp or a non-default install path.** Replace `/Applications/TablePro.app` with your install path. For Setapp that is `~/Applications/Setapp/TablePro.app`. +### Unauthenticated (`-33009`) -**Port conflict.** If the configured port is taken, TablePro asks the kernel for a free one and writes the real port into the handshake file. Nothing in your client config changes, because the port was never in it. +The bridge token is stale. The bridge re-reads the handshake and retries once on its own, so this reaches you only when TablePro is not running. -**The server restarted.** Restarting from Settings rotates the bridge token and rewrites the handshake. The bridge picks the new one up on its next failed call. There is nothing to restart on your side. +### Rate limited (`-33010`) -**Tool calls return "Connection is read only for external clients".** External access on that connection is `readOnly` and the statement writes. Change it in TablePro, or run the query in the app's editor. +Five failed auth attempts inside 60 seconds triggered a 5-minute lockout. Wait it out, or restart TablePro. The other [rate limits](/external-api/tokens#rate-limits) return the same code. diff --git a/docs/external-api/mcp-prompts.mdx b/docs/external-api/mcp-prompts.mdx index 1c0f60541..e01b0c986 100644 --- a/docs/external-api/mcp-prompts.mdx +++ b/docs/external-api/mcp-prompts.mdx @@ -3,9 +3,9 @@ title: MCP Prompts description: The eight prompts TablePro renders from a live schema, and how completion/complete fills their arguments --- -A prompt is a task the user picks from their client's prompt menu. TablePro fills it in from the live database before handing it over, so the model gets the real columns, the real indexes and the real row counts instead of a template asking it to guess. +import MCPGates from "/snippets/mcp-gates.mdx"; -Prompts and completions used to be advertised and return nothing. They are real now. +Picking `explain_table` from your client's prompt menu makes TablePro read the table first. So the model gets its real columns, indexes and DDL rather than a template asking it to guess. Rendering happens on the server, inside `prompts/get`. ## Listing and getting @@ -24,7 +24,7 @@ Prompts and completions used to be advertised and return nothing. They are real The result is a `description` and one user message. Argument values must be scalars: a number or a boolean is read as its text, an array or object is rejected. An argument the prompt does not declare is rejected with `-32602`, as is a missing required one. -Rendering reads the database, so `prompts/get` needs `resources:read` and the same connection access every tool call does. The read is recorded in the activity log. + The read is recorded in the activity log. ## Shared arguments @@ -39,71 +39,56 @@ Most prompts take these: ## The catalog -### `explain_schema` +Required arguments come first; optional ones follow in parentheses. -Tour the tables of a live database and how they relate. +| Prompt | Arguments | What it renders and asks for | +|--------|-----------|------------------------------| +| `explain_schema` | `connection` (`database`, `schema`, `audience`) | An inventory of up to 200 tables with row counts, then full structure for the 8 largest. Asks for a guided tour of the schema. | +| `explain_table` | `connection`, `table` (`database`, `schema`, `audience`) | One table's columns, keys, indexes and DDL, explained to someone who has never used it, ending in three queries worth running. | +| `data_quality_audit` | `connection`, `table` (`database`, `schema`) | One table's structure, turned into a checklist of runnable data quality queries ordered cheapest first. | +| `question_to_sql` | `connection`, `question` (`tables`, `database`, `schema`) | An inventory of up to 150 tables plus full structure for up to 6, and rules that forbid inventing columns and require a `LIMIT`. | +| `review_query` | `connection`, `query` (`explain_plan`, `database`, `schema`) | Your query, your plan if you pass one, and the structure of up to 6 tables it names. Reviews correctness, then cost, then risk. | +| `propose_indexes` | `connection`, `query` (`explain_plan`, `database`, `schema`) | The same tables with the indexes they already have. Asks for ranked `CREATE INDEX` statements and any index the query makes redundant. | +| `write_migration` | `connection`, `change` (`table`, `database`, `schema`) | Current structure with DDL when a table is named, plus the connection's Safe Mode level. Asks for the migration, the rollback and a verifying query. | +| `summarize_query_history` | `connection` (`period`, `limit`) | The recorded history for that connection, summarized by intent, with every write, every failure and the slowest statements called out. | -**Arguments**: `connection`, `database`, `schema`, `audience` (`newcomer` (default), `analyst`, `engineer`). +`audience` is `newcomer` (default), `analyst` or `engineer`. `period` is `today`, `this_week` (default), `this_month` or `all`, and `limit` runs 1 to 500, default 50. `tables` on `question_to_sql` is comma-separated; the largest few are used when it is omitted, and only the first 6 either way. -Renders an inventory of up to 200 tables with row counts, then full structure for the 8 largest, and asks for the tour in a fixed order: what the database is for, the core entities and their join keys, which tables are lookups or logs, what looks wrong (missing primary keys, foreign keys the names imply but the schema does not declare), and where to start reading. +Statement text is truncated at 8,000 characters where `review_query` and `propose_indexes` echo your `query` and `explain_plan`, and at 600 characters per row in the `summarize_query_history` table. -### `explain_table` +## What comes back -One table's columns, keys, indexes and DDL, explained to someone who has never used it. +One user message, as Markdown. `explain_table` renders this shape: -**Arguments**: `connection`, `table` (required), `database`, `schema`, `audience`. +````markdown +Explain the table `orders` on a live PostgreSQL database. -Asks what one row represents, what each non-obvious column holds and what a NULL means there, how the table connects to the rest of the schema through its foreign keys, which access patterns the indexes serve and which obvious filter has none, and three queries someone would actually run. +## Connection +- Connection: Production (PostgreSQL) +- Scope: app.public +- Server version: 16.4 +- Safe mode: silent -### `data_quality_audit` +## Structure +### orders +Approximate rows: 1,204,331 -Turn one table's structure into a runnable checklist of data quality queries. +| Column | Type | Nullable | Key | Default | Comment | +| --- | --- | --- | --- | --- | --- | +| id | uuid | no | PK | | | -**Arguments**: `connection`, `table` (required), `database`, `schema`. +Indexes: +… -Covers nullability, uniqueness, referential integrity (including columns named like a foreign key that declare no constraint), domain checks, format checks, and duplicates on the business key. Every check has to come with a runnable query for this engine and a description of a clean result, ordered cheapest first. - -### `question_to_sql` - -Write a query that answers a question, using the real columns of the named tables. - -**Arguments**: `connection`, `question` (required), `tables` (comma-separated; the largest few when omitted), `database`, `schema`. - -Renders up to 150 tables as an inventory and full structure for up to 6. The instructions forbid inventing columns, require an explicit `JOIN ... ON` with the join key named, require a `LIMIT` unless the answer is an aggregate, and ask for the engine's own identifier quoting. - -### `review_query` - -Check a query for correctness, cost and risk against the live schema before running it. - -**Arguments**: `connection`, `query` (required), `explain_plan`, `database`, `schema`. - -TablePro matches identifiers in the query against the table inventory and renders the structure of up to 6 tables it recognizes. Without `explain_plan` the prompt asks the model to name the `EXPLAIN` command to run and what to look for in it. The review is ordered: correctness, cost against the row counts shown, risk (locks, transaction safety, cancellability), and a rewrite only if one is needed. - -### `propose_indexes` - -Suggest indexes for a slow query from its plan and the indexes the tables already have. - -**Arguments**: `connection`, `query` (required), `explain_plan`, `database`, `schema`. - -Every candidate has to come with the exact `CREATE INDEX` for this engine, the predicate it serves quoted from the query, why the existing indexes do not already serve it, and what it costs. The model is asked to rank the candidates and to name any index the query makes redundant. - -### `write_migration` - -Turn a described schema change into migration statements plus the rollback, for this engine. - -**Arguments**: `connection`, `change` (required), `table`, `database`, `schema`. - -Renders the current structure with DDL when a table is named, and says so plainly when none is. Asks for the forward migration, the rollback to exactly the structure shown, what breaks while it runs, a batched backfill if one is needed, and a query that proves the change landed. It also passes the connection's Safe Mode level through so the model can flag statements a restricted mode would reject. - -### `summarize_query_history` - -Summarize what was run against a connection over a period. - -**Arguments**: `connection`, `period` (`today`, `this_week` (default), `this_month`, `all`), `limit` (1-500, default 50). - -Reads the recorded history for that connection and asks for a summary grouped by intent, every write called out, failures with whatever looks like the fix, the slowest statements, and loose ends such as a migration applied to one table but not its sibling. +DDL: +```sql +CREATE TABLE orders (…) +``` -Query text is limited to 8,000 characters per statement in the rendered prompt. +Cover, in this order: +1. What one row of this table represents, in one sentence. +… +```` ## Completions @@ -151,10 +136,6 @@ For a `ref/resource` completion TablePro fills the context from the URI itself, ### Matching and limits -Values whose name starts with what you typed come first, then values that merely contain it, each group sorted naturally. Matching ignores case. Fixed value lists such as `audience` keep their declared order instead. - -At most 100 values come back. `total` is the number of matches before that cap and `hasMore` says whether it applied. - -Schema reads are cached for 15 seconds per token and per scope, so typing a table name does not hit the database on every keystroke. +Values whose name starts with what you typed come first, then values that merely contain it, each group sorted naturally, case ignored. Fixed value lists such as `audience` keep their declared order instead. Completing `connection` with a value that matches no name falls back to matching UUIDs, so pasting an id into a field that normally takes a name still works. -Completing `connection` with a value that matches no name falls back to matching UUIDs, so pasting an id into a field that normally takes a name still works. +At most 100 values come back; `total` counts the matches before that cap and `hasMore` says whether it applied. Schema reads are cached for 15 seconds per token and per scope, so typing a table name does not hit the database on every keystroke. diff --git a/docs/external-api/mcp-protocol.mdx b/docs/external-api/mcp-protocol.mdx index 7cbccc50c..8fc9a54f5 100644 --- a/docs/external-api/mcp-protocol.mdx +++ b/docs/external-api/mcp-protocol.mdx @@ -3,9 +3,49 @@ title: MCP Protocol description: "Wire contract for TablePro MCP server: protocol versions, required metadata, headers, result envelope, cache hints, and error codes" --- -TablePro's MCP server speaks [Model Context Protocol](https://modelcontextprotocol.io) revision **2026-07-28**, and still answers two older revisions for clients that have not moved yet. This page is the wire contract. For the tools themselves see [MCP Tools](/external-api/mcp-tools). +`2026-07-28` is the revision to write against, and two older ones still answer for clients that have not moved. This page is the wire contract; for the tools themselves see [MCP Tools](/external-api/mcp-tools). + +Most people never read it. The bundled `tablepro-mcp` stdio bridge speaks this for you, and every client in [MCP Clients](/external-api/mcp-clients) goes through the bridge. Read on if you are writing a client against the HTTP endpoint directly. + +## One round trip + +Everything below is a detail of this request. + +```bash +curl -sS http://127.0.0.1:23508/mcp \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer tp_your_token_here' \ + -H 'MCP-Protocol-Version: 2026-07-28' \ + -H 'Mcp-Method: tools/call' \ + -H 'Mcp-Name: list_connections' \ + -d '{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "list_connections", + "arguments": {}, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": {} + } + } + }' +``` -Most people never read this page. The bundled `tablepro-mcp` stdio bridge speaks it for you, and every client in [MCP Clients](/external-api/mcp-clients) goes through the bridge. Read on if you are writing a client that talks to the HTTP endpoint directly. +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [{ "type": "text", "text": "{\"connections\":[…]}" }], + "structuredContent": { "connections": [] }, + "isError": false, + "resultType": "complete", + "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "tablepro", "version": "0.67.0" } } + } +} +``` ## Endpoint @@ -15,13 +55,13 @@ One path, one method: POST http://127.0.0.1:/mcp ``` -The server binds `127.0.0.1` and speaks plain HTTP. The specification permits that for a server on the same machine, and there is no remote mode. `GET` and `DELETE` on `/mcp` answer `405 Method Not Allowed` with `Allow: POST, OPTIONS`. `OPTIONS` answers `204` for CORS preflight. Any other path answers `404`. +The server binds `127.0.0.1` and speaks plain HTTP, which the specification permits for a server on the same machine, and there is no remote mode. `GET` and `DELETE` on `/mcp` answer `405 Method Not Allowed` with `Allow: POST, OPTIONS`. `OPTIONS` answers `204` for CORS preflight. Any other path answers `404`. The `Host` header must name a loopback host (`localhost`, `127.0.0.1`, `::1`) and, if it carries a port, the port the server bound. Anything else answers `403 forbidden_host`. `Origin`, when present, must be `https://claude.ai` or `https://app.cursor.com`. Any other origin answers `403 forbidden_origin`. A request with no `Origin` is fine, which is the normal case for a local client. -The port comes from the handshake file at `~/Library/Application Support/TablePro/mcp-handshake.json`. Do not hardcode it. +Read the port from the handshake file at `~/Library/Application Support/TablePro/mcp-handshake.json`. Do not hardcode it. ## Protocol versions @@ -33,28 +73,11 @@ The port comes from the handshake file at `~/Library/Application Support/TablePr `2025-03-26` is no longer accepted. It requires JSON-RPC batching, which TablePro never implemented. -The list lives in `TablePro/Core/MCP/Meta/MCPProtocolVersion.swift` and is served by `server/discover`. Read it from there rather than copying it into your client. - -An unsupported version comes back as error `-32022` with the supported list in `data.supported`. +`server/discover` serves the live list in `supportedVersions`. Read it from there rather than copying it into your client. An unsupported version comes back as error `-32022` with the supported list in `data.supported`. ## Every request carries its version -There is no handshake in the modern era. No `initialize`, no `Mcp-Session-Id`, no `ping`, no `GET` stream, no SSE resumability. Each POST stands on its own, so it has to say who is calling and what it speaks. Two `_meta` fields are required on every request: - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "tools/list", - "params": { - "_meta": { - "io.modelcontextprotocol/protocolVersion": "2026-07-28", - "io.modelcontextprotocol/clientCapabilities": {}, - "io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" } - } - } -} -``` +There is no handshake in the modern era: no `initialize`, no `Mcp-Session-Id`, no `ping`, no `GET` stream, no SSE resumability. Each POST stands on its own, so it has to say who is calling and what it speaks. Two `_meta` fields are required on every request: - `io.modelcontextprotocol/protocolVersion` (**required**): a supported version string. Missing or empty is `-32602`; unsupported is `-32022`. - `io.modelcontextprotocol/clientCapabilities` (**required**): a JSON object. `{}` is valid and means you support nothing extra. A non-object is `-32602`. @@ -78,7 +101,7 @@ The modern era mirrors part of the body into headers so a proxy can route and au | `Mcp-Param-` | optional | the matching value in the body | | `Authorization` | when authentication is on | `Bearer ` | -`Mcp-Param-` is opt-in. Send it and it is validated; omit it and nothing happens. The name after the prefix is matched case-insensitively against the arguments object for `tools/call`, and against `params` for everything else, descending into nested objects. Values compare as text against strings, numerically against numbers, and as `true`/`false` against booleans. An array or object value never matches. +`Mcp-Param-` is opt-in: send it and it is validated, omit it and nothing happens. The name after the prefix is matched case-insensitively against the arguments object for `tools/call` and against `params` for everything else, descending into nested objects. Values compare as text against strings, numerically against numbers, and as `true`/`false` against booleans. An array or object value never matches. ### Non-ASCII header values @@ -88,9 +111,7 @@ HTTP headers are ASCII. A table name with an accent, or a value with a leading s Mcp-Name: =?base64?Y2Fmw6k=?= ``` -The payload between `=?base64?` and `?=` is standard base64 of the UTF-8 bytes. TablePro decodes the wrapper before comparing to the body. Wrap only when you have to: a value that is already printable ASCII with no leading or trailing space should be sent as is. The rule is implemented in `MCPBase64Sentinel.encodeIfNeeded`. - -A malformed wrapper, or a header carrying a control character, is `-32020`. +The payload between `=?base64?` and `?=` is standard base64 of the UTF-8 bytes, and TablePro decodes the wrapper before comparing to the body. Wrap only when you have to: a value that is already printable ASCII with no leading or trailing space goes as is. A malformed wrapper, or a header carrying a control character, is `-32020`. ## Methods @@ -136,7 +157,7 @@ Replaces `initialize` for stateless clients. It takes no parameters beyond `_met "io.modelcontextprotocol/serverInfo": { "name": "tablepro", "title": "TablePro", - "version": "0.68.0", + "version": "0.67.0", "websiteUrl": "https://tablepro.app" } } @@ -169,13 +190,9 @@ Six methods return `ttlMs` and `cacheScope` so a client can cache without guessi | `resources/templates/list` | 3600000 | `public` | | `tools/list` | 300000 | `private` | | `resources/list` | 30000 | `private` | -| `resources/read` | varies, see below | `private` | - -`resources/read` sets the TTL from the URI: 5s for history, 15s for the connections list, 60s for schema, table and database listings, 300s for a DDL read. - -`public` means the answer does not depend on the token. `private` means it does: `tools/list` is filtered by the token's scopes, and a resource read is filtered by its connection allowlist. A `ttlMs` of `0` means do not cache. +| `resources/read` | 5000 to 300000 by URI | `private` | -No other method carries a cache hint, and a result that carries one from a method that may not is stripped before it goes out. +`public` means the answer does not depend on the token. `private` means it does: `tools/list` is filtered by the token's scopes, and a resource read by its connection allowlist. A `ttlMs` of `0` means do not cache. No other method carries a cache hint, and a hint that reaches a result from a method that may not carry one is stripped before it goes out. ## Pagination @@ -185,14 +202,12 @@ A cursor is opaque and bound to the method that issued it. Handing a `tools/list ## Progress -Send `_meta.progressToken` on a request and the server streams `notifications/progress` back **on that request's own response stream**. The response switches to `text/event-stream`, the progress notifications arrive as SSE frames, and the final JSON-RPC response is the last frame. There is no separate `GET` stream to open, and no notification ever arrives out of band. +Send `_meta.progressToken` on a request and the server streams `notifications/progress` back **on that request's own response stream**. The response switches to `text/event-stream`, progress notifications arrive as SSE frames with no event name, and the final JSON-RPC response is the last frame. There is no separate `GET` stream to open, and no notification ever arrives out of band. ``` -event: message data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"t1","progress":0.3,"total":1.0,"message":"Executing"}} -event: message -data: {"jsonrpc":"2.0","id":1,"result":{...}} +data: {"jsonrpc":"2.0","id":1,"result":{}} ``` `progress` must advance; an update that does not move forward is dropped. `execute_query` reports at 0.0, 0.3 and 1.0. @@ -209,15 +224,13 @@ The notification is answered with `202` whether or not anything matched. The can Cancellation is per principal: a token can only cancel its own requests. Revoking a token cancels everything it has in flight. -The server also cancels for you. Every handler has a 330-second deadline (`subscriptions/listen` is exempt), and the server polls for a dropped connection once a second, so a client that walks away does not leave a query running. +The server also cancels for you. Every handler has a 330-second deadline, and the server polls for a dropped connection once a second, so a client that walks away does not leave a query running. [`subscriptions/listen`](/external-api/mcp-subscriptions#closing) is the one method that deadline does not reach. ## Approvals and input required A statement that writes needs the user's consent. Where that consent is collected depends on what your client declared in `clientCapabilities`. -**Without `elicitation`**: TablePro shows its own confirmation dialog on the user's Mac and the call blocks until they answer. - -**With `elicitation`**: the call returns `resultType: "input_required"` instead: +Without `elicitation`, TablePro shows its own confirmation dialog on the user's Mac and the call blocks until they answer. With it, the call returns `resultType: "input_required"` instead: ```json { @@ -302,9 +315,9 @@ A tool that fails because the database said no returns a **successful** JSON-RPC The text starts with a machine-readable code: `invalid_argument`, `not_connected`, `not_found`, `denied`, `timeout`, `unsupported`, `query_failed`, `user_cancelled`, or `internal_failure`. Error text is redacted before it leaves the process, so a driver message that echoed a password does not reach the client. -Protocol errors are reserved for the envelope: a malformed request, a missing scope, an expired token. If your client only handles JSON-RPC errors it will read every failed query as a success. +Protocol errors are reserved for the envelope: a malformed request, a missing scope, an expired token. A client that only handles JSON-RPC errors reads every failed query as a success. -## Limits +## Transport limits | Limit | Value | |-------|-------| @@ -313,14 +326,8 @@ Protocol errors are reserved for the envelope: a malformed request, a missing sc | Concurrent connections | 64 | | Idle connection timeout | 30 seconds | | Handler deadline | 330 seconds | -| Requests | 240 per 60 seconds per caller | -| Concurrent requests | 8 per caller | -| Failed auth attempts | 5 per 60 seconds, then a 5-minute lockout | -| Pairing exchange attempts | 5 per 5 minutes, then a 15-minute lockout | - -The request limits are keyed by token id, or by client address for an unauthenticated caller. Going over either returns `-33010` with a `Retry-After` header. `subscriptions/listen` is exempt, since it holds its slot for as long as the client is listening. -Auth lockouts are keyed by both client address and token id, so one misbehaving client cannot lock out another on the same loopback address. +Per-caller request rates, auth lockouts and pairing lockouts are counted separately and are listed under [rate limits](/external-api/tokens#rate-limits). ## Legacy era @@ -329,8 +336,6 @@ A request with no `io.modelcontextprotocol/protocolVersion` in `_meta` is treate 1. `POST /mcp` with `initialize` and a `protocolVersion` of `2025-11-25` or `2025-06-18`. Any other value is `-32022`. 2. The response carries `protocolVersion`, `capabilities`, `serverInfo` and `instructions`. 3. Every later request carries `Mcp-Session-Id`. A session belongs to the token that created it; presenting it under another token is `-33001`. -4. A session lives until the server stops. The store holds at most 16 at a time and evicts the least recently used past that. A session the server does not recognize is `-33001`, and the client has to `initialize` again. - -Legacy clients see `resources.subscribe: false` and cannot call `server/discover` or `subscriptions/listen`. Their results carry no `resultType`, no cache hints, and no `serverInfo` in `_meta`. +4. A session lives until the server stops. The store holds at most 16 and evicts the least recently used past that. A session the server does not recognize is `-33001`, and the client has to `initialize` again. -Legacy is there so an old client keeps working, not as a target to build against. Anything new should speak `2026-07-28`. +Legacy clients see `resources.subscribe: false` and cannot call `server/discover` or `subscriptions/listen`. Their results carry no `resultType`, no cache hints, and no `serverInfo` in `_meta`. Legacy is there so an old client keeps working, not as a target to build against. diff --git a/docs/external-api/mcp-resources.mdx b/docs/external-api/mcp-resources.mdx index 1736982ef..92388c23f 100644 --- a/docs/external-api/mcp-resources.mdx +++ b/docs/external-api/mcp-resources.mdx @@ -3,36 +3,38 @@ title: MCP Resources description: Read-only resources for connections, schema, tables, and query history, with their URI templates --- -Resources are read-only views of TablePro state. A client reads them to find its way around before calling a tool. Every read is gated the same way a tool call is: the token's `resources:read` scope, its connection allowlist, and the connection's own external access and AI policy. +import MCPGates from "/snippets/mcp-gates.mdx"; -URIs use the `tablepro://` scheme inside the MCP transport. They are not the same thing as the shell-level [URL scheme deep links](/external-api/url-scheme), which happen to share the prefix. +Eight URIs, all read-only, all JSON. A client reads them to find its way around before it calls a tool. Reads count against the caller's [request rate](/external-api/tokens#rate-limits) like anything else. ## Discovery `resources/list` returns `tablepro://connections`, plus four entries for each connection that is currently connected and visible to this token: its schema, its tables, its databases, and its history. The list is sorted by connection name. -`resources/templates/list` returns the URI templates, so a client can build a URI for a connection that is not open yet. +`resources/templates/list` returns the seven templated URIs, so a client can build one for a connection that is not open yet. Both methods page at 50 entries with `nextCursor`. -Both paginate at 50 entries with `nextCursor`. `resources/list` carries a 30-second private cache hint; the template list carries an hour, public. +## The URIs -## Templates +| URI | Name | Payload | +|-----|------|---------| +| `tablepro://connections` | `connections` | Every saved connection this token may use, with live session state | +| `tablepro://connections/{connection_id}/schema` | `connection_schema` | Tables and their columns for the browsed database | +| `tablepro://connections/{connection_id}/databases` | `connection_databases` | `{ "databases": ["app", "analytics"] }` | +| `tablepro://connections/{connection_id}/schemas{?database}` | `connection_schemas` | `{ "schemas": ["public", "reporting"], "database": "app" }` | +| `tablepro://connections/{connection_id}/tables{?database,schema,row_counts}` | `connection_tables` | Table and view names without columns, as the `list_tables` tool returns them | +| `tablepro://connections/{connection_id}/tables/{table}{?database,schema}` | `table_description` | Columns, indexes, foreign keys and an approximate row count, as `describe_table` returns them | +| `tablepro://connections/{connection_id}/tables/{table}/ddl{?database,schema}` | `table_ddl` | The `CREATE` statement as the engine reports it, as `get_table_ddl` returns it | +| `tablepro://connections/{connection_id}/history{?limit,search,date_filter}` | `connection_history` | Queries recently run against this connection, newest first | -| Template | Name | -|----------|------| -| `tablepro://connections` | `connections` | -| `tablepro://connections/{connection_id}/schema` | `connection_schema` | -| `tablepro://connections/{connection_id}/databases` | `connection_databases` | -| `tablepro://connections/{connection_id}/schemas{?database}` | `connection_schemas` | -| `tablepro://connections/{connection_id}/tables{?database,schema,row_counts}` | `connection_tables` | -| `tablepro://connections/{connection_id}/tables/{table}{?database,schema}` | `table_description` | -| `tablepro://connections/{connection_id}/tables/{table}/ddl{?database,schema}` | `table_ddl` | -| `tablepro://connections/{connection_id}/history{?limit,search,date_filter}` | `connection_history` | +`{connection_id}` must be a UUID; a name is `-32602`. On the `tables` URI, `row_counts=true` asks for approximate counts and `1` and `yes` also work. `completion/complete` fills every variable in these templates against the live schema, including `table`, `database` and `schema`. See [Completions](/external-api/mcp-prompts#completions). -`{connection_id}` must be a UUID; a name is `-32602`. `completion/complete` fills every variable in these templates against the live schema, including `table`, `database` and `schema`. See [Completions](/external-api/mcp-prompts#completions). + +These URIs live inside the MCP transport and are not the shell-level [URL scheme deep links](/external-api/url-scheme), which happen to share the `tablepro://` prefix. + ## Reading -`resources/read` needs `params.uri` and, in the modern era, an `Mcp-Name` header carrying the same URI. +`resources/read` needs `params.uri` and, in the modern era, an `Mcp-Name` header carrying the same URI. Every URI except `tablepro://connections` reads a live session, and TablePro opens the connection first if it is not open yet. A connection that will not open is `-32602`. ```json { @@ -46,14 +48,10 @@ Both paginate at 50 entries with `nextCursor`. `resources/list` carries a 30-sec } ``` -The payload is a JSON string inside `text`. The shapes below are what you get after parsing it. - -Cache hints vary by URI: 5 seconds for history, 15 for the connections list, 60 for schema, table and database reads, 300 for a DDL read. All are `private`, because what you can see depends on your token. +The payload is a JSON string inside `text`. The shapes on the rest of this page are what you get after parsing it. Each read carries a `private` cache hint whose TTL depends on the URI; the numbers are in [Cache hints](/external-api/mcp-protocol#cache-hints). ## `tablepro://connections` -Every saved connection this token may use, with live session state. - ```json { "connections": [ @@ -73,12 +71,10 @@ Every saved connection this token may use, with live session state. } ``` -`database` is the session's browsed database when connected, the saved default otherwise. `type` uses display casing (`MySQL`, `PostgreSQL`, `SQLite`). Connections with `external_access: blocked` or `ai_policy: never` are omitted, as are connections outside the token's allowlist. Same payload as the `list_connections` tool. +`database` is the session's browsed database when connected, the saved default otherwise. `type` uses display casing (`MySQL`, `PostgreSQL`, `SQLite`). Connections with `external_access: blocked` or `ai_policy: never` are omitted, as are connections outside the token's allowlist. ## `tablepro://connections/{id}/schema` -Tables and their columns for the database the connection is browsing. - ```json { "database": "app", @@ -100,32 +96,8 @@ Capped at 100 tables. Beyond that the payload also carries `truncated: true` and This is the only subscribable resource. See [Subscriptions](/external-api/mcp-subscriptions). -## `tablepro://connections/{id}/tables{?database,schema,row_counts}` - -Table and view names without columns. Pass `database` and `schema` to look outside the browsed one, and `row_counts=true` for approximate counts. `row_counts` also accepts `1` and `yes`. - -Same payload as the `list_tables` tool. - -## `tablepro://connections/{id}/tables/{table}{?database,schema}` - -Columns, indexes, foreign keys and an approximate row count for one table. Same payload as `describe_table`. - -## `tablepro://connections/{id}/tables/{table}/ddl{?database,schema}` - -The `CREATE` statement, as the engine reports it. Same payload as `get_table_ddl`. - -## `tablepro://connections/{id}/databases` - -`{ "databases": ["app", "analytics"] }`. - -## `tablepro://connections/{id}/schemas{?database}` - -`{ "schemas": ["public", "reporting"], "database": "app" }`. - ## `tablepro://connections/{id}/history{?limit,search,date_filter}` -Queries recently run against this connection, newest first. - | Parameter | Meaning | |-----------|---------| | `limit` | 1 to 500, default 50. Out-of-range values are clamped, not rejected. | @@ -158,8 +130,8 @@ Queries recently run against this connection, newest first. | Code | Meaning | |------|---------| -| `-32602` | Missing `uri`, wrong scheme, unknown URI, connection id that is not a UUID, empty table name, or a connection that is not open | +| `-32602` | Missing `uri`, wrong scheme, unknown URI, connection id that is not a UUID, empty table name, or a connection that would not open | | `-32020` | The `Mcp-Name` header does not match `params.uri` | | `-33007` | The token's allowlist rejects the connection, or its external access is `blocked` | -Resource-not-found is `-32602`, not a code of its own. The full table is in [MCP Protocol](/external-api/mcp-protocol#errors). +Resource-not-found is `-32602` rather than a code of its own; [MCP Protocol](/external-api/mcp-protocol#errors) lists the rest. diff --git a/docs/external-api/mcp-subscriptions.mdx b/docs/external-api/mcp-subscriptions.mdx index 9dc78d446..9ac5ff948 100644 --- a/docs/external-api/mcp-subscriptions.mdx +++ b/docs/external-api/mcp-subscriptions.mdx @@ -3,9 +3,7 @@ title: MCP Subscriptions description: subscriptions/listen, the notification filter TablePro honors, and what it deliberately does not send --- -`subscriptions/listen` replaces `resources/subscribe` and the old `GET` stream. One long-lived POST carries every notification the client asked for, and the request stays open until the client hangs up. - -This is a modern-era method. Legacy clients get `-32601`. +Hold one POST open and TablePro streams every notification you asked for down it until you hang up. `subscriptions/listen` replaces `resources/subscribe` and the old `GET` stream, and it is modern-era only: a legacy client gets `-32601`. ## Opening a subscription @@ -46,9 +44,7 @@ Before anything else, the server answers with what it will actually send: } ``` -Read it. It is not an echo. Anything TablePro will not deliver is missing from it, so a client that compares the two knows immediately what it is not going to hear about, rather than waiting forever for an event that is never coming. - -The response switches to `text/event-stream` at this point, and stays open. A keepalive comment goes out every 15 seconds so an intermediary does not time the connection out. +Compare it against what you asked for. Anything missing will never arrive. ## What TablePro honors @@ -56,20 +52,24 @@ The response switches to `text/event-stream` at this point, and stays open. A ke |--------------|:--------:|-------| | `resourcesListChanged` | yes | Fires when the set of connected connections this token can see changes. | | `resourceSubscriptions` | yes | Only `tablepro://connections/{id}/schema` URIs. | -| `toolsListChanged` | no | Dropped from the acknowledgment. | -| `promptsListChanged` | no | Dropped from the acknowledgment. | - -`toolsListChanged` and `promptsListChanged` are dropped because both lists are fixed. `server/discover` reports `tools.listChanged: false` and `prompts.listChanged: false` for the same reason. Ask for them and they will not appear in the acknowledgment. +| `toolsListChanged` | no | Both lists are fixed, and `server/discover` reports `listChanged: false` for each. | +| `promptsListChanged` | no | Same. | Every URI in `resourceSubscriptions` is checked twice. It has to parse as a connection schema URI, and its connection has to be inside the token's allowlist. Anything else is dropped silently, so the acknowledgment is the only place you find out. A token without `resources:read` gets an empty filter back, whatever it asked for. -The URI you get back is canonical, so a URI that differs only in case or percent-encoding comes back normalized. +The URI you get back is canonical, so one that differs only in case or percent-encoding comes back normalized. + +## Reading the stream + +The response switches to `text/event-stream` at the acknowledgment and stays open. Frames carry no event name, so each one is `data: ` plus a JSON line, terminated by a blank line. A line starting with `:` is the keepalive TablePro sends every 15 seconds to stop an intermediary timing the connection out; skip it. + +Tell the two kinds of frame apart by the envelope. A frame with an `id` equal to your request id is the final JSON-RPC response and the stream is over. A frame with a `method` and no `id` is a notification. ## The notifications ### `notifications/resources/list_changed` -Sent when the set of connections visible to this token changes: one connects, one disconnects, one is blocked for external clients, or its AI policy changes to `never`. +Sent when the set of connections visible to this token changes: one connects, one disconnects, one is blocked for external clients, or its AI policy changes to **Never**. TablePro compares the visible set against the last one it sent you, so a change that does not move your set produces no notification. A connection you cannot see connecting is not your event. @@ -96,13 +96,13 @@ Sent when a subscribed connection's schema changes, carrying the URI: } ``` -It fires on a completed schema load, not on the start of one. A refresh in flight, or a connection whose schema has never loaded, produces nothing. Re-read `tablepro://connections/{id}/schema` when it arrives. +It fires on a completed schema load, not on the start of one, so a refresh in flight and a schema that has never loaded both produce nothing. Re-read `tablepro://connections/{id}/schema` when it arrives. ## Closing The subscription ends when the client closes the connection, cancels the request with `notifications/cancelled`, or the server stops. The final JSON-RPC response is an empty result carrying the subscription id in `_meta`. -There is no deadline on `subscriptions/listen`. It is the one method exempt from the 330-second handler timeout. The server checks every open subscription for a dropped connection every 15 seconds and closes the ones that went away, so a client that dies without closing does not leak. +There is no deadline on `subscriptions/listen`. It is the one method exempt from the 330-second handler timeout and from the per-caller concurrency limit. The server checks every open subscription for a dropped connection every 15 seconds and closes the ones that went away, so a client that dies without closing does not leak. ## Progress is not a subscription diff --git a/docs/external-api/mcp-tools.mdx b/docs/external-api/mcp-tools.mdx index 6ca9f5ba9..a13ada732 100644 --- a/docs/external-api/mcp-tools.mdx +++ b/docs/external-api/mcp-tools.mdx @@ -3,520 +3,195 @@ title: MCP Tools description: The 46 tools TablePro's MCP server exposes, with arguments, defaults, result shapes, and scope requirements --- -TablePro exposes 46 tools over MCP. They cover connections, schema, data, server administration, and the app's own windows and tabs. Every tool declares a JSON Schema for its input and its output, so a client can validate both sides without reading this page. - -For the wire format, headers and error codes see [MCP Protocol](/external-api/mcp-protocol). For prompts see [MCP Prompts](/external-api/mcp-prompts). +If a dedicated tool covers the job, it beats hand-written SQL. It quotes identifiers for the engine in front of it, applies the user's row limits, and runs on connections where raw execution is refused. All 46 publish a JSON Schema for input and output, so a client can validate both sides without reading this page. For the wire format, headers and error codes see [MCP Protocol](/external-api/mcp-protocol). ## How to read this page -Each entry lists arguments and the result. Unless stated otherwise: +In the tables below, required arguments come first and optional ones follow in parentheses. Unless a row says otherwise: -- `connection_id` is a connection UUID from `list_connections` and is required. -- `database` and `schema` are optional. Omitted, they use the database and schema the connection is currently browsing. Passing them targets somewhere else **without moving the app's selection**. +- `connection_id` is a connection UUID from `list_connections`. +- `database` and `schema` default to whatever the connection is currently browsing. Passing them targets somewhere else **without moving the app's selection**. - An unknown argument is rejected with `-32602`. There is no silent ignoring. - The result comes back twice: as JSON text in `content[0]`, and as a typed object in `structuredContent`. Read `structuredContent`. +- Timestamps are ISO 8601, error text is redacted, and "a result set" means `columns[]`, `rows[][]`, `row_count`, `rows_affected`, `execution_time_ms`, `is_truncated`, plus `status_message`, `database` and `schema` when they apply. -Row limits default to the server's **Default row limit** setting (500) and are capped by **Maximum row limit** (10,000). Timeouts default to **Query timeout** (30 seconds) and are capped at 300. Both are in **Settings > Integrations**. - -## Scopes and gates - -Each entry lists the scope it needs. A call has to clear four gates: - -1. **Token scope.** `tools:read` for a read, `tools:write` for anything that changes state, `admin` for `confirm_destructive_operation`. An unauthenticated loopback caller has `tools:read` and `resources:read` and can never be granted more. -2. **Connection allowlist.** A token limited to named connections gets `-33007` for anything else. -3. **Connection policy.** `externalAccess: blocked` hides the connection: `list_connections` omits it and any tool that names it is refused. AI policy `never` does the same. AI policy `askEachTime` shows an approval dialog on the first call per session that touches the connection, and the answer is remembered for the rest of the session. -4. **Safe Mode.** A write goes through the connection's Safe Mode, which may ask the user to confirm it or to authenticate. See [Approvals](/external-api/mcp-protocol#approvals-and-input-required). - -`externalAccess: readOnly` rejects write SQL sent through `execute_query`, and rejects `confirm_destructive_operation` outright, even with a `readWrite` token. - -Statements that read or write files, or that run server-side code, are refused on every tool. So is more than one statement in a single call. - -## Connections - -### `list_connections` - -Every saved connection this client may use, with live status. Connections blocked for external clients, or with AI policy `never`, are omitted, as are connections outside the token's allowlist. - -**Arguments**: none. - -**Returns** `connections[]` with `id`, `name`, `type`, `host`, `port`, `database`, `is_connected`, `ai_policy`, `external_access`, `safe_mode`. `database` is the session's browsed database when connected, the saved default otherwise. - -**Scope**: `tools:read`. - -### `connect` - -Open a session for a saved connection. Returns only once the driver is connected. - -**Arguments**: `connection_id`. - -**Returns** `status` (`"connected"`), `connection_id`, `current_database`, and `current_schema` and `server_version` when known. - -**Scope**: `tools:write`. - -### `disconnect` - -Close the open session. - -**Arguments**: `connection_id`. - -**Returns** `status` (`"disconnected"`), `connection_id`. - -**Scope**: `tools:write`. - -### `get_connection_status` +Row limits default to the server's **Default row limit** setting (500) and are capped by **Maximum row limit** (10,000). Timeouts default to **Query timeout** (30 seconds) and are capped at 300. Both live in **Settings > Integrations**. -**Arguments**: `connection_id`. +## Tool index -**Returns** `status` (`connected`, `connecting`, `disconnected`, `error`), `connection_id`, `current_database`, plus `current_schema`, `server_version`, `connected_at`, `last_active_at` and `error` when they apply. Timestamps are ISO 8601. `error` text is redacted. +| Group | Tools | +|-------|-------| +| [Connections](#connections) | `list_connections` `connect` `disconnect` `get_connection_status` `switch_database` `switch_schema` | +| [Schema discovery](#schema-discovery) | `list_databases` `list_schemas` `list_tables` `describe_table` `get_table_ddl` `search_schema` `list_indexes` `list_foreign_keys` `list_triggers` `get_view_definition` `list_routines` `list_partitions` `get_table_statistics` `get_database_statistics` | +| [Reading data](#reading-data) | `browse_table` `count_rows` `execute_query` `explain_query` `export_data` `quote_identifiers` | +| [Writing data](#writing-data) | `insert_rows` `confirm_destructive_operation` `transaction_control` | +| [Databases and objects](#databases-and-objects) | `describe_create_database_options` `create_database` `drop_database_object` `list_maintenance_operations` `run_maintenance` | +| [Server administration](#server-administration) | `get_server_dashboard` `stop_server_session` `list_principals` `list_grants` `list_session_contexts` | +| [The user's workspace](#the-users-workspace) | `list_favorite_tables` `list_recent_tables` `list_recent_tabs` `focus_query_tab` `open_connection_window` `open_table_tab` `search_query_history` | -**Scope**: `tools:read`. - -### `switch_database` +## Scopes and gates -Move the connection's browse cursor to another database. This changes what the user sees in TablePro. To run one statement elsewhere, pass `database` to that tool instead. +Fourteen tools need `tools:write`: `connect`, `disconnect`, `switch_database`, `switch_schema`, `insert_rows`, `confirm_destructive_operation`, `transaction_control`, `create_database`, `drop_database_object`, `run_maintenance`, `stop_server_session`, `focus_query_tab`, `open_connection_window`, `open_table_tab`. The other 32 need `tools:read`, `execute_query` among them until the statement writes. -**Arguments**: `connection_id`, `database`. +Past the scope, a call clears three more gates: -**Returns** `status` (`"switched"`), `connection_id`, `current_database`. +1. **Connection allowlist.** A token limited to named connections gets `-33007` for anything else. +2. **Connection policy.** **External Clients** set to **Blocked** hides the connection: `list_connections` omits it and any tool that names it is refused. An AI policy of **Never** does the same. **Ask Each Time** shows an approval dialog on the first call that touches the connection, and the answer is remembered for that token until it is revoked. +3. **Safe Mode.** A write goes through the connection's Safe Mode, which may ask the user to confirm it or to authenticate. See [Approvals](/external-api/mcp-protocol#approvals-and-input-required). -**Scope**: `tools:write`. +`confirm_destructive_operation` needs `admin` as well, so it takes a Full Access token, and **Read Only** refuses it outright whatever the token carries. No token skips the user's approval. -### `switch_schema` +Statements that read or write files, or that run server-side code, are refused on every tool. So is more than one statement in a single call. -Same, for the schema. +## Connections -**Arguments**: `connection_id`, `schema`. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `list_connections` | none | `connections[]` with `id`, `name`, `type`, `host`, `port`, `database`, `is_connected`, `ai_policy`, `external_access`, `safe_mode` | +| `connect` | `connection_id` | `status` (`connected`), `connection_id`, `current_database`, plus `current_schema` and `server_version` when known. Returns once the driver is up | +| `disconnect` | `connection_id` | `status` (`disconnected`), `connection_id` | +| `get_connection_status` | `connection_id` | `status` (`connected`, `connecting`, `disconnected`, `error`), `connection_id`, `current_database`, plus `current_schema`, `server_version`, `connected_at`, `last_active_at` and `error` when they apply | +| `switch_database` | `connection_id`, `database` | `status` (`switched`), `connection_id`, `current_database` | +| `switch_schema` | `connection_id`, `schema` | `status` (`switched`), `connection_id`, `current_schema` | -**Returns** `status`, `connection_id`, `current_schema`. +`list_connections` omits connections blocked for external clients, connections whose AI policy is **Never**, and connections outside the token's allowlist. Its `database` is the session's browsed database when connected, the saved default otherwise. -**Scope**: `tools:write`. +The two `switch_` tools move what the user sees in TablePro. To run one statement elsewhere, pass `database` or `schema` to that tool instead. ## Schema discovery -### `list_databases` - -**Arguments**: `connection_id`. - -**Returns** `databases[]`, sorted. - -**Scope**: `tools:read`. - -### `list_schemas` - -**Arguments**: `connection_id`, `database`. - -**Returns** `schemas[]` and `database`. - -**Scope**: `tools:read`. - -### `list_tables` - -Tables and views, sorted by schema then name. - -**Arguments**: `connection_id`, `database`, `schema`, `include_row_counts` (default `false`). - -**Returns** `tables[]` (`name`, `type`, `schema`, `comment`, `row_count`), `database`, `schema`, `row_counts_included`, `row_counts_are_approximate`. - -Row counts are fetched one table at a time, so they are skipped when the schema holds more than 200 objects. They come from engine statistics, not `COUNT(*)`. - -**Scope**: `tools:read`. - -### `describe_table` - -The one call to make before writing SQL against an unfamiliar table. Columns, indexes, foreign keys, DDL and an approximate row count, all read against the same database and schema. - -**Arguments**: `connection_id`, `table`, `database`, `schema`. - -**Returns** `table`, `database`, `schema`, `columns[]`, `indexes[]`, `foreign_keys[]`, `ddl`, `approximate_row_count`. - -A column carries `name`, `data_type`, `is_nullable`, `is_primary_key`, and where the engine reports them `is_generated`, `default_value`, `extra`, `comment`, `allowed_values`. An index carries `name`, `columns`, `is_unique`, `is_primary`, `type`, `where_clause`. A foreign key carries `name`, `column`, `referenced_table`, `referenced_column`, `referenced_schema`, `on_delete`, `on_update`. - -**Scope**: `tools:read`. - -### `get_table_ddl` - -**Arguments**: `connection_id`, `table`, `database`, `schema`. - -**Returns** `table`, `schema`, `ddl`. - -**Scope**: `tools:read`. - -### `search_schema` - -Find tables and columns whose name contains a substring, so a column can be located without describing every table. - -**Arguments**: `connection_id`, `term`, `limit` (1-500, default 50), `database`, `schema`. - -**Returns** `term`, `matches[]` (`kind` is `table` or `column`, plus `name`, `table`, `schema`, `object_type`, `data_type`), `is_truncated`. Table matches come first. - -**Scope**: `tools:read`. - -### `list_indexes` - -One table, or the whole schema when `table` is omitted. - -**Arguments**: `connection_id`, `table`, `database`, `schema`. - -**Returns** `database`, `schema`, `tables[]` where each entry is `{ table, indexes[] }`. Tables with no index are left out. - -**Scope**: `tools:read`. - -### `list_foreign_keys` - -**Arguments**: `connection_id`, `table`, `database`, `schema`. - -**Returns** `database`, `schema`, `tables[]` where each entry is `{ table, foreign_keys[] }`. - -**Scope**: `tools:read`. - -### `list_triggers` - -**Arguments**: `connection_id`, `table` (required), `database`, `schema`. - -**Returns** `table`, `triggers[]` (`name`, `timing`, `event`, `statement`, `is_enabled`), sorted by name. - -**Scope**: `tools:read`. - -### `get_view_definition` - -**Arguments**: `connection_id`, `view` (required), `database`, `schema`. - -**Returns** `view`, `schema`, `definition`. - -**Scope**: `tools:read`. - -### `list_routines` - -Stored procedures and user-defined functions. - -**Arguments**: `connection_id`, `kind` (`procedure` or `function`, omit for both), `database`, `schema`. - -**Returns** `routines[]` (`name`, `kind`, `schema`, `qualified_name`, `signature`). - -**Scope**: `tools:read`. - -### `list_partitions` - -Direct partitions of a partitioned table. Empty on engines without partitioning. - -**Arguments**: `connection_id`, `table` (required), `database`, `schema`. - -**Returns** `table`, `partitions[]` in the same shape as `list_tables` entries. - -**Scope**: `tools:read`. - -### `get_table_statistics` - -**Arguments**: `connection_id`, `table` (required), `database`, `schema`. - -**Returns** `table` plus whatever the engine records: `data_size_bytes`, `index_size_bytes`, `total_size_bytes`, `average_row_length`, `row_count`, `comment`, `engine`, `collation`, `created_at`, `updated_at`. - -**Scope**: `tools:read`. - -### `get_database_statistics` - -**Arguments**: `connection_id`, `database` (omit for every database). - -**Returns** `databases[]` (`name`, `table_count`, `size_bytes`, `is_system_database`), sorted by name. - -**Scope**: `tools:read`. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `list_databases` | `connection_id` | `databases[]`, sorted | +| `list_schemas` | `connection_id` (`database`) | `schemas[]`, `database` | +| `list_tables` | `connection_id` (`database`, `schema`, `include_row_counts`) | `tables[]` (`name`, `type`, `schema`, `comment`, `row_count`), `database`, `schema`, `row_counts_included`, `row_counts_are_approximate` | +| `describe_table` | `connection_id`, `table` (`database`, `schema`) | `table`, `database`, `schema`, `columns[]`, `indexes[]`, `foreign_keys[]`, `ddl`, `approximate_row_count` | +| `get_table_ddl` | `connection_id`, `table` (`database`, `schema`) | `table`, `schema`, `ddl` | +| `search_schema` | `connection_id`, `term` (`limit`, `database`, `schema`) | `term`, `matches[]` (`kind` is `table` or `column`, plus `name`, `table`, `schema`, `object_type`, `data_type`), `is_truncated`. Table matches first | +| `list_indexes` | `connection_id` (`table`, `database`, `schema`) | `database`, `schema`, `tables[]` of `{ table, indexes[] }`. Tables with no index are left out | +| `list_foreign_keys` | `connection_id` (`table`, `database`, `schema`) | `database`, `schema`, `tables[]` of `{ table, foreign_keys[] }` | +| `list_triggers` | `connection_id`, `table` (`database`, `schema`) | `table`, `triggers[]` (`name`, `timing`, `event`, `statement`, `is_enabled`), sorted by name | +| `get_view_definition` | `connection_id`, `view` (`database`, `schema`) | `view`, `schema`, `definition` | +| `list_routines` | `connection_id` (`kind`, `database`, `schema`) | `routines[]` (`name`, `kind`, `schema`, `qualified_name`, `signature`) | +| `list_partitions` | `connection_id`, `table` (`database`, `schema`) | `table`, `partitions[]`, shaped like `list_tables` entries | +| `get_table_statistics` | `connection_id`, `table` (`database`, `schema`) | `table` plus whatever the engine records: `data_size_bytes`, `index_size_bytes`, `total_size_bytes`, `average_row_length`, `row_count`, `comment`, `engine`, `collation`, `created_at`, `updated_at` | +| `get_database_statistics` | `connection_id` (`database`) | `databases[]` (`name`, `table_count`, `size_bytes`, `is_system_database`), sorted by name | + +`describe_table` is the one call to make before writing SQL against an unfamiliar table. A column always carries `name`, `data_type`, `is_nullable` and `is_primary_key`, and picks up `is_generated`, `default_value`, `extra`, `comment` and `allowed_values` where the engine reports them; indexes and foreign keys work the same way. + +`include_row_counts` defaults to `false`. Counts come from engine statistics rather than `COUNT(*)`, and are fetched one table at a time, so `list_tables` skips them when the schema holds more than 200 objects. + +`search_schema` locates a column without describing every table; its `limit` runs 1 to 500, default 50. `list_routines` takes `kind` as `procedure` or `function`, omitted for both. ## Reading data -### `browse_table` - -Read rows with filters, sorting and paging, built by TablePro's own query builder for this engine. Prefer it over hand-writing `SELECT`: it quotes identifiers, escapes values, and pages the way the app does. - -**Arguments**: `connection_id`, `table` (required), `columns[]` (omit for all), `filters[]`, `logic` (`and` or `or`, default `and`), `sort[]`, `limit` (default the server's row limit), `offset` (default 0), `timeout_seconds`, `database`, `schema`. - -A filter is `{ column, operator, value, second_value, case_sensitive }`. `column` and `operator` are required, `second_value` is the upper bound for `BETWEEN`, and `case_sensitive` defaults to whatever the operator normally does. The operator list is TablePro's own filter vocabulary, published in the tool's input schema as an `enum`; read it from `tools/list` rather than hardcoding it. Raw SQL filters are not available over MCP. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `browse_table` | `connection_id`, `table` (`columns`, `filters`, `logic`, `sort`, `limit`, `offset`, `timeout_seconds`, `database`, `schema`) | A result set | +| `count_rows` | `connection_id`, `table` (`exact`, `filters`, `logic`, `database`, `schema`) | `table`, `row_count`, `is_approximate`, `filter_count` | +| `execute_query` | `connection_id`, `query` (`max_rows`, `timeout_seconds`, `database`, `schema`) | A result set | +| `explain_query` | `connection_id`, `query` (`analyze`, `variant`, `timeout_seconds`, `database`, `schema`) | `statement` as actually sent, `execution_time_ms`, `columns[]`, `rows[][]`, and where available `plan_text`, `plan` and `available_variants[]` | +| `export_data` | `connection_id`, `format` (`query` **or** `tables`, `sql_table`, `output_path`, `max_rows`, `database`, `schema`) | `format`, `rows_exported`, `is_truncated`, `path` when a file was written, and `exports[]` (`label`, `row_count`, `is_truncated`, `data`) | +| `quote_identifiers` | `connection_id` and at least one of `identifiers`, `literals` (`database`, `schema`) | `identifiers[]` as `{ input, quoted }` and `literals[]` as `{ input, escaped }`, in the order supplied | -A sort entry is `{ column, direction }` with `direction` one of `ascending` (default) or `descending`. Entries apply in the order given. +### `browse_table` -**Returns** a result set: `columns[]`, `rows[][]`, `row_count`, `rows_affected`, `execution_time_ms`, `is_truncated`, and `status_message`, `database`, `schema` when they apply. +A filter is `{ column, operator, value, second_value, case_sensitive }`. `column` and `operator` are required, `second_value` is the upper bound for `BETWEEN`, and `case_sensitive` defaults to whatever the operator normally does. `logic` is `and` (default) or `or`. The operator list is TablePro's own filter vocabulary, published in the tool's input schema as an `enum`; read it from `tools/list` rather than hardcoding it. Raw SQL filters are not available over MCP. -**Scope**: `tools:read`. +A sort entry is `{ column, direction }`, `direction` being `ascending` (default) or `descending`, and entries apply in the order given. `offset` defaults to 0. ### `count_rows` -**Arguments**: `connection_id`, `table` (required), `exact` (default `false`), `filters[]`, `logic`, `database`, `schema`. - Without filters this returns the engine's fast estimate unless `exact` is set. With filters it always counts for real, because an estimate cannot answer a filtered question. -**Returns** `table`, `row_count`, `is_approximate`, `filter_count`. - -**Scope**: `tools:read`. - ### `execute_query` -Run one statement. - -**Arguments**: `connection_id`, `query` (required, 100 KB max), `max_rows`, `timeout_seconds`, `database`, `schema`. - -Reads need `tools:read`. Anything that writes needs `tools:write` and goes through the connection's Safe Mode. `DROP` and `TRUNCATE` are refused here; use `confirm_destructive_operation`. Statements that reach the filesystem or run server-side code are refused. One statement per call. - -**Returns** the result set shape shown under `browse_table`. - -Emits progress at 0.0 (resolving), 0.3 (executing) and 1.0 (done) when the request carries a `progressToken`. - -**Scope**: `tools:read`, plus `tools:write` for a write statement. +One statement, 100 KB at most. `DROP` and `TRUNCATE` are refused here; use `confirm_destructive_operation`. When the request carries a `progressToken`, the tool emits progress at 0.0 (resolving), 0.3 (executing) and 1.0 (done). ### `explain_query` -Ask the engine for the plan, and get both the raw text and, where TablePro can parse it, a node tree with costs and row estimates. - -**Arguments**: `connection_id`, `query` (required, no `EXPLAIN` prefix), `analyze` (default `false`), `variant`, `timeout_seconds`, `database`, `schema`. - -**Returns** `statement` (what was actually sent, with its prefix), `execution_time_ms`, `columns[]`, `rows[][]`, and where available `plan_text`, `plan` (`root`, `planning_time_ms`, `execution_time_ms`) and `available_variants[]` (`id`, `label`, `sql_prefix`). - -Call it with no `variant` to see what this engine offers. `analyze: true` runs the statement for real, so an analyzed write needs `tools:write` and Safe Mode approval. - -**Scope**: `tools:read`. +Pass the query with no `EXPLAIN` prefix, and with no `variant` to see what this engine offers in `available_variants[]`. `analyze: true` runs the statement for real, so an analyzed write needs `tools:write` and Safe Mode approval. ### `export_data` -Export a query result or whole tables as CSV, JSON, or SQL `INSERT` statements. - -**Arguments**: `connection_id`, `format` (`csv`, `json`, `sql`), `query` **or** `tables[]` (exactly one), `sql_table`, `output_path`, `max_rows`, `database`, `schema`. - -With `output_path` the file is written inside the user's Downloads folder and never overwrites: a name that is taken gets `-1`, `-2` and so on. The path must stay inside Downloads, must end in the format's extension, and must not name a hidden file. Without `output_path` the text comes back inline in each export entry. - -SQL output uses the connection's own quoting and literal rules, so exporting a query needs `sql_table` to name the target table. - -**Returns** `format`, `rows_exported`, `is_truncated`, `path` when a file was written, and `exports[]` (`label`, `row_count`, `is_truncated`, `data`). - -**Scope**: `tools:read`. - -### `quote_identifiers` - -Quote identifiers and escape string literals using the connection's own driver rules. Use it before putting any name or value into hand-written SQL. - -**Arguments**: `connection_id`, `identifiers[]`, `literals[]` (at least one of the two), `database`, `schema`. - -**Returns** `identifiers[]` as `{ input, quoted }` and `literals[]` as `{ input, escaped }`, in the order supplied. - -**Scope**: `tools:read`. +Formats are `csv`, `json` and `sql`; pass exactly one of `query` or `tables[]`. Without `output_path` the text comes back inline in each export entry. With it, the file lands in the user's Downloads folder and never overwrites, so a name that is taken gets `-1`, `-2` and so on; the path must stay inside Downloads, end in the format's extension, and not name a hidden file. SQL output uses the connection's own quoting and literal rules, so exporting a query needs `sql_table` to name the target table. ## Writing data -### `insert_rows` - -Insert rows using bound parameters, so values are never spliced into SQL text. - -**Arguments**: `connection_id`, `table`, `columns[]`, `rows[][]` (all required), `database`, `schema`. - -Each row is an array with one value per column. At most 1,000 rows per call. - -**Returns** `table`, `rows_submitted`, `rows_affected`. - -**Scope**: `tools:write`. - -### `confirm_destructive_operation` - -Run one destructive statement: `DROP`, `TRUNCATE`, `ALTER … DROP`. The user approves it before it runs, through your elicitation prompt if your client supports elicitation, otherwise through TablePro's own dialog on the user's Mac. - -**Arguments**: `connection_id`, `query` (required), `timeout_seconds`, `database`, `schema`. - -A statement that is not destructive is rejected. Use `execute_query` for those. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `insert_rows` | `connection_id`, `table`, `columns`, `rows` (`database`, `schema`) | `table`, `rows_submitted`, `rows_affected` | +| `confirm_destructive_operation` | `connection_id`, `query` (`timeout_seconds`, `database`, `schema`) | A result set | +| `transaction_control` | `connection_id`, `action` (`database`, `schema`) | `status`, the action that completed, and `connection_id` | -**Returns** the result set shape. +`insert_rows` binds parameters, so values are never spliced into SQL text. Each row is an array with one value per column, and a call carries at most 1,000 rows. -**Scope**: `tools:write` **and** `admin`, on a connection whose external access is `readWrite`. No token permission skips the approval. +`confirm_destructive_operation` runs one `DROP`, `TRUNCATE` or `ALTER … DROP`. Anything that is not destructive is rejected; use `execute_query` for those. The user approves it first, through your elicitation prompt if your client supports elicitation, otherwise through TablePro's own dialog on their Mac. -### `transaction_control` - -Begin, commit, or roll back a transaction on the connection's shared session. - -**Arguments**: `connection_id`, `action` (`begin`, `commit`, `rollback`), `database`, `schema`. - -The transaction stays open across calls until it is committed or rolled back, and it runs on the same session as the user's own tabs. Leave nothing open. - -**Returns** `status` (the action that completed), `connection_id`. - -**Scope**: `tools:write`. +`transaction_control` takes `action` as `begin`, `commit` or `rollback`. The transaction stays open across calls until it is committed or rolled back, and it runs on the same session as the user's own tabs. Leave nothing open. ## Databases and objects -### `describe_create_database_options` - -What `create_database` accepts on this engine. - -**Arguments**: `connection_id`. - -**Returns** `is_supported`, `fields[]` (`key`, `label`, `default_value`, `options[]`). - -**Scope**: `tools:read`. - -### `create_database` - -**Arguments**: `connection_id`, `name`, `options` (a flat object of string values from `describe_create_database_options`). - -**Returns** `status` (`"created"`), `database`. - -**Scope**: `tools:write`, plus the user's approval. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `describe_create_database_options` | `connection_id` | `is_supported`, `fields[]` (`key`, `label`, `default_value`, `options[]`) | +| `create_database` | `connection_id`, `name` (`options`) | `status` (`created`), `database` | +| `drop_database_object` | `connection_id`, `kind`, `name` | `status` (`dropped`) and `database` or `schema` | +| `list_maintenance_operations` | `connection_id` | `operations[]` and `is_supported` | +| `run_maintenance` | `connection_id`, `operation` (`table`, `options`, `database`, `schema`) | `operation`, `statements[]`, `results[]`, one result set per statement | -### `drop_database_object` +`create_database` takes `options` as a flat object of string values drawn from `describe_create_database_options`, and asks the user before it runs. `drop_database_object` takes `kind` as `database` or `schema`, deletes everything inside, cannot be undone, and is approved first. -Drop a whole database or a whole schema. This deletes everything inside it and cannot be undone, so the user approves it first. +`run_maintenance` takes an `operation` name from `list_maintenance_operations` (`VACUUM`, `ANALYZE`, `OPTIMIZE` and whatever else the engine supports) and omits `table` for the whole database. TablePro generates the statements and runs them in order, asking for approval before each one. -**Arguments**: `connection_id`, `kind` (`database` or `schema`), `name`. - -**Returns** `status` (`"dropped"`) and `database` or `schema`. - -**Scope**: `tools:write`. - -### `list_maintenance_operations` - -**Arguments**: `connection_id`. - -**Returns** `operations[]` (`VACUUM`, `ANALYZE`, `OPTIMIZE` and the like, whatever this engine supports) and `is_supported`. - -**Scope**: `tools:read`. - -### `run_maintenance` - -**Arguments**: `connection_id`, `operation` (a name from `list_maintenance_operations`), `table` (omit for the whole database), `options` (string values), `database`, `schema`. - -TablePro generates the statements and runs them in order, asking for approval before each one. - -**Returns** `operation`, `statements[]`, `results[]` (one result set per statement). - -**Scope**: `tools:write`. + +`describe_create_database_options`, `list_maintenance_operations` and `list_session_contexts` answer `is_supported: false` on an engine that has no such concept rather than failing, and `list_partitions` answers with an empty list. Everything else that an engine cannot do comes back as an `unsupported` tool error. + ## Server administration -### `get_server_dashboard` - -The live panels TablePro shows: active sessions, server metrics, and slow queries. Available on PostgreSQL, MySQL, SQL Server, ClickHouse, DuckDB, and SQLite. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `get_server_dashboard` | `connection_id` (`panels`) | The requested panels: `sessions`, `metrics`, `slow_queries` | +| `stop_server_session` | `connection_id`, `process_id` (`mode`) | The result set the engine produced | +| `list_principals` | `connection_id` | `principals[]` (`name`, `host`, `is_role`, `can_login`, `member_of[]`, `connection_limit`, `comment`, and `attributes[]` of `{ key, label, is_enabled }`), sorted by name | +| `list_grants` | `connection_id`, `principal` (`host`) | `principal`, `grants[]` (`privilege`, `scope`, `is_grantable`). `scope` is a dotted path, `*` for a server-wide grant | +| `list_session_contexts` | `connection_id` | `is_supported`, `contexts[]` (`id`, `label`, `value`, `options[]`) | -**Arguments**: `connection_id`, `panels[]` (`sessions`, `metrics`, `slow_queries`; omit for all). +`get_server_dashboard` reads the live panels TablePro shows, and runs on PostgreSQL, MySQL, SQL Server, ClickHouse, DuckDB and SQLite. Name the panels in `panels[]` or omit it for all three. A session carries `id` (the process id `stop_server_session` takes), `user`, `database`, `state`, `duration_seconds`, `query`, `can_kill` and `can_cancel`; a metric carries `id`, `label`, `value`, `unit`; a slow query carries `duration`, `query`, `user`, `database`. -**Returns** the requested panels. A session carries `id` (the process id `stop_server_session` takes), `user`, `database`, `state`, `duration_seconds`, `query`, `can_kill`, `can_cancel`. A metric carries `id`, `label`, `value`, `unit`. A slow query carries `duration`, `query`, `user`, `database`. +`stop_server_session` takes `mode` as `cancel` (default) to stop the running query or `kill` to end the session. The user always approves this one, whatever Safe Mode says. -**Scope**: `tools:read`. - -### `stop_server_session` - -Cancel the running query on a server session, or kill the session outright. - -**Arguments**: `connection_id`, `process_id` (from `get_server_dashboard`), `mode` (`cancel` or `kill`, default `cancel`). - -The user always approves this one, whatever Safe Mode says. - -**Returns** the result set the engine produced. - -**Scope**: `tools:write`. - -### `list_principals` - -Users and roles defined on the server. - -**Arguments**: `connection_id`. - -**Returns** `principals[]` (`name`, `host`, `is_role`, `can_login`, `member_of[]`, `connection_limit`, `comment`, `attributes[]` of `{ key, label, is_enabled }`), sorted by name. - -**Scope**: `tools:read`. - -### `list_grants` - -**Arguments**: `connection_id`, `principal` (required), `host`. - -**Returns** `principal`, `grants[]` (`privilege`, `scope`, `is_grantable`). `scope` is a dotted path, `*` for a server-wide grant. - -**Scope**: `tools:read`. - -### `list_session_contexts` - -Session-level contexts the engine exposes, such as a Snowflake warehouse or role, with the value the session currently holds. - -**Arguments**: `connection_id`. - -**Returns** `is_supported`, `contexts[]` (`id`, `label`, `value`, `options[]`). - -**Scope**: `tools:read`. +`list_session_contexts` reports session-level contexts the engine exposes, such as a Snowflake warehouse or role, with the value the session currently holds. ## The user's workspace -These read and drive the TablePro app itself. They never bypass the connection allowlist or an `externalAccess: blocked` connection. - -### `list_favorite_tables` - -Tables the user starred. A good hint at what matters in this database. - -**Arguments**: `connection_id`. - -**Returns** `favorites[]` (`name`, `database`, `schema`), sorted. - -**Scope**: `tools:read`. - -### `list_recent_tables` +These read and drive the TablePro app itself, and none of them bypasses the connection allowlist or a blocked connection. -**Arguments**: `connection_id`. +| Tool | Arguments | Returns | +|------|-----------|---------| +| `list_favorite_tables` | `connection_id` | `favorites[]` (`name`, `database`, `schema`), sorted | +| `list_recent_tables` | `connection_id` | `recent_tables[]` (`name`, `database`, `schema`, `is_view`, `opened_at` as Unix epoch seconds), newest first | +| `list_recent_tabs` | (`connection_id`, `limit`) | `tabs[]` (`connection_id`, `connection_name`, `tab_id`, `tab_type`, `display_title`, `is_active`, plus `table_name`, `database_name`, `schema_name` and `window_id` when known) | +| `focus_query_tab` | `tab_id` | `status` (`focused`), `tab_id`, `connection_id`, `window_id` | +| `open_connection_window` | `connection_id` | `status` (`opened`), `connection_id`, `tab_id`, `window_id`, `is_connected` | +| `open_table_tab` | `connection_id`, `table_name` (`database_name`, `schema_name`) | `status` (`opened`), `connection_id`, `table_name`, `tab_id`, `window_id` | +| `search_query_history` | `query` (`connection_id`, `limit`, `since`, `until`) | `entries[]` carrying `id`, `query`, the connection and database it ran on, `source`, `statement_type`, `executed_at`, `execution_time_ms`, `row_count`, `was_successful` and `error_message`, newest first | -**Returns** `recent_tables[]` (`name`, `database`, `schema`, `is_view`, `opened_at` as Unix epoch seconds), newest first. +Starred tables are a good hint at what matters in a database. `list_recent_tabs` covers every readable connection when `connection_id` is omitted, its `limit` runs 1 to 500, default 20, and `tab_type` is one of `query`, `table`, `createTable`, `erDiagram`, `serverDashboard`, `insights`, `usersRoles`. -**Scope**: `tools:read`. +`open_connection_window` and `open_table_tab` connect first if they have to, and return once the window has a tab or fail after 8 seconds. -### `list_recent_tabs` - -Tabs open in TablePro for connections this client may reach. - -**Arguments**: `connection_id` (omit for every readable connection), `limit` (1-500, default 20). - -**Returns** `tabs[]` (`connection_id`, `connection_name`, `tab_id`, `tab_type`, `display_title`, `is_active`, plus `table_name`, `database_name`, `schema_name`, `window_id` when known). `tab_type` is one of `query`, `table`, `createTable`, `erDiagram`, `serverDashboard`, `insights`, `usersRoles`. - -**Scope**: `tools:read`. - -### `focus_query_tab` - -Bring an open tab to the front. - -**Arguments**: `tab_id` (from `list_recent_tabs`). - -**Returns** `status` (`"focused"`), `tab_id`, `connection_id`, `window_id`. - -**Scope**: `tools:write`. - -### `open_connection_window` - -Open or focus a window for a saved connection, connecting it if needed. Returns once the window has a tab, or fails after 8 seconds. - -**Arguments**: `connection_id`. - -**Returns** `status` (`"opened"`), `connection_id`, `tab_id`, `window_id`, `is_connected`. - -**Scope**: `tools:write`. - -### `open_table_tab` - -Open a table in TablePro and bring it forward. Connects the connection if it is not open yet. - -**Arguments**: `connection_id`, `table_name` (required), `database_name`, `schema_name`. - -**Returns** `status` (`"opened"`), `connection_id`, `table_name`, `tab_id`, `window_id`. - -**Scope**: `tools:write`. - -### `search_query_history` - -Search the query history TablePro keeps on this Mac. Results cover only connections this client may reach, with or without `connection_id`. - -**Arguments**: `query` (required; pass `""` to match everything and filter by date alone), `connection_id`, `limit` (1-500, default 50), `since` and `until` as Unix epoch seconds. - -**Returns** `entries[]` (`id`, `query`, `connection_id`, `database_name`, `schema_name`, `database_type`, `source`, `statement_type`, `executed_at`, `execution_time_ms`, `row_count`, `was_successful`, `error_message`), newest first. `error_message` is redacted. - -**Scope**: `tools:read`. +`search_query_history` searches the history TablePro keeps on this Mac. Pass `""` to match everything and filter by date alone; `since` and `until` are Unix epoch seconds, and `limit` runs 1 to 500, default 50. Results cover only connections this client may reach, with or without `connection_id`. ## Annotations Every tool ships MCP annotations so a client can decide what to auto-run: -- `readOnlyHint: true` on everything that only reads. -- `destructiveHint: true` on `confirm_destructive_operation`, `drop_database_object`, and `stop_server_session`. +- `readOnlyHint: true` on the 29 tools that only read. `execute_query` is not one of them. +- `destructiveHint: true` on `confirm_destructive_operation`, `drop_database_object` and `stop_server_session`. - `idempotentHint: true` where repeating the call is safe. -- `openWorldHint: true` where the call reaches a live server rather than TablePro's own state. - -`execute_query` is deliberately not marked read-only: whether it writes depends on the statement. +- `openWorldHint: true` on the 14 that can reach the database server: every write except `switch_database`, `switch_schema` and `focus_query_tab`, plus `execute_query`, `explain_query` and `export_data`. ## Errors diff --git a/docs/external-api/pairing.mdx b/docs/external-api/pairing.mdx index a48c76596..96c877cbe 100644 --- a/docs/external-api/pairing.mdx +++ b/docs/external-api/pairing.mdx @@ -3,9 +3,12 @@ title: Pairing description: One-click flow that issues a scoped MCP token to an extension through a PKCE-style code exchange --- -Pairing is how an extension gets a TablePro token without the user copying one by hand. The extension opens a `tablepro://integrations/pair` deep link, the user approves scopes and connections in TablePro, and the extension exchanges a one-time code for the token over localhost HTTP. +Four steps: generate a verifier, open a deep link, catch the one-time code, trade it for a token over +localhost. TablePro releases the token only to the caller that can produce the verifier, so an app +that intercepts the redirect holds a code it cannot spend. -The flow is PKCE-style: the extension generates a verifier, hashes it into a challenge, and TablePro releases the token only to the caller that presents the verifier. An app that intercepts the redirect cannot exchange the code. +Nothing about this is Raycast-specific. Any client that can receive a callback, through its own URL +scheme or a loopback HTTP listener, can pair. ## Sequence @@ -35,96 +38,96 @@ sequenceDiagram TablePro sheet asking to approve a pairing request with scope, connection, and expiry pickers -## Extension steps - -### 1. Generate a verifier and challenge +## The whole client, in one file ```ts import { randomBytes, createHash } from "node:crypto"; - -function base64url(buffer: Buffer): string { - return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); -} - -const verifier = base64url(randomBytes(32)); -const challenge = base64url(createHash("sha256").update(verifier).digest()); -``` - -Keep the verifier in memory until the exchange step. Do not log it. - -### 2. Open the pair deep link - -Open `tablepro://integrations/pair?client=…&challenge=…&redirect=…&scopes=…`. Parameters are documented in the [URL scheme reference](/external-api/url-scheme#start-pairing). - -### 3. Receive the code and exchange it - -TablePro opens your `redirect` URL with a one-time code. For `raycast://` redirects the code arrives as `?context={"code":""}` (Raycast's launch-context convention); for any other scheme it is a flat `?code=` query parameter. An `error` parameter instead of a code means the user denied the request; stop and show the description. - -Read the MCP port from `~/Library/Application Support/TablePro/mcp-handshake.json`, then: - -```ts -const port = await readHandshakePort(); -const res = await fetch(`http://127.0.0.1:${port}/v1/integrations/exchange`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code, code_verifier: verifier }), +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; + +const b64url = (b: Buffer) => + b.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + +// Step 1. Keep the verifier in memory until the exchange. Never log it. +const verifier = b64url(randomBytes(32)); +const challenge = b64url(createHash("sha256").update(verifier).digest()); + +// Step 2. Open the deep link. The parameters are on the URL scheme page. +const params = new URLSearchParams({ + client: "My Editor on macbook-pro", + challenge, + redirect: "http://127.0.0.1:7391/callback", + scopes: "readWrite", }); -const { token } = await res.json(); +await openUrl(`tablepro://integrations/pair?${params}`); // however your host opens a URL + +// Step 3. Your callback receives ?code=, or ?error=denied when the user says no. +export async function exchange(code: string): Promise { + const handshakePath = `${homedir()}/Library/Application Support/TablePro/mcp-handshake.json`; + const { port } = JSON.parse(await readFile(handshakePath, "utf8")); + + const res = await fetch(`http://127.0.0.1:${port}/v1/integrations/exchange`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code, code_verifier: verifier }), + }); + if (!res.ok) throw new Error(`pairing failed: ${res.status}`); + + const { token } = await res.json(); + return token; // Step 4. Straight into the Keychain, never a plain file. +} ``` -The exchange endpoint requires no bearer auth. The single-use code plus the verifier is the auth. It is the one path other than `/mcp` the server serves, and like `/mcp` it accepts `POST` only. - -A failed verification burns the code. That is deliberate: leaving a code alive after a mismatch would turn the five-minute window into an unlimited guessing game against the verifier. - -### 4. Store the token - -Store the token in the macOS Keychain. In Raycast, write it to a password preference, which is Keychain-backed. Do not keep it in `LocalStorage` or plain files. +Three constraints decide whether TablePro accepts the request at all: -## What TablePro does on approval +- The **verifier** is 43 to 128 characters from `A-Z a-z 0-9 - . _ ~`. 32 random bytes in base64url + is 43. +- The **challenge** is its base64url SHA-256, so exactly 43 base64url characters. +- The **redirect** is a loopback `http` or `https` URL (`127.0.0.1`, `localhost`, `::1`), or a + private-use scheme an installed app has registered. Anything else, or one carrying credentials, is + refused with *"The redirect address is not a local callback, so pairing was refused."* -The approval sheet shows the client name, a scope picker (starts at the requested scope; the user can pick any level), a connections multi-select (defaults to all unless `connection-ids` was provided), and an expiry picker (defaults to never). The query parameters are a request, not a grant. +One delivery detail: a `raycast://` redirect gets the code wrapped as `?context={"code":""}`, +Raycast's launch-context convention, and every other scheme gets a flat `?code=`. -On approve, TablePro: +The exchange endpoint takes no bearer token: the single-use code plus the verifier is the credential. +It and `/mcp` are the only paths the server serves, and both accept `POST` only. -1. Mints the token and holds the plaintext in memory as a pending exchange keyed by a UUID code, valid for 5 minutes. Only the salted SHA-256 hash reaches storage. -2. Opens the `redirect` URL with the code. +## What the user approves -Pairing again mints a second token rather than replacing the first. An extension that re-pairs should stop using its old token, and the user can revoke it in **Settings > Integrations > Authentication**. +The sheet names the client and counts down the five minutes the request is good for, dimming +**Approve** when it runs out. Three controls sit under that: **Permission Level** (starting at what +the link asked for, movable in either direction), **Allowed Connections** (all, or a checked subset), +and **Expiration** (never, 1, 7, 30 or 90 days). The link's parameters are a request, not a grant. -At most 50 exchange codes can be pending at once. Beyond that, new pair requests fail until older codes expire. If the code cannot be stored, or the redirect cannot be built, the token TablePro just minted is deleted rather than left dangling. +Approving mints the token, holds the plaintext against a one-time code for 5 minutes, and opens the +redirect. Pairing again mints a second token rather than replacing the first, so an extension that +re-pairs should stop using its old one. Revoke either under +**Settings > Integrations > Authentication**. ## Security properties | Property | How | |----------|-----| -| Token is never in a URL | The token travels over localhost HTTP, not in a deep link. | -| Redirect interception is harmless | The intercepted `code` cannot be exchanged without the verifier. | -| Code is single-use | Successful exchange or 5-minute expiry deletes the pending exchange. | -| Plaintext token is not persisted by TablePro | Only the salted SHA-256 hash is saved, in the login keychain. | -| User sees and approves scopes | The sheet shows what was requested, what is granted, and which connections. | -| User can revoke any time | **Settings > Integrations > Authentication > Revoke**. | +| The token is never in a URL | It travels over localhost HTTP; the deep link carries only a code. | +| Intercepting the redirect is useless | The code cannot be exchanged without the verifier. | +| A code is single-use | Success, a failed verification, or 5 minutes deletes it. | +| The plaintext token is not persisted | Only a salted SHA-256 hash is saved, in the login keychain. | ## Errors +A failed verification burns the code, so retrying with a guessed verifier is not an option: start a +new pair request. + | Code | Meaning | |------|---------| +| `400` | Malformed body, a missing `code` or `code_verifier`, a field over 1,024 bytes, or a verifier that is not 43 to 128 unreserved characters. | | `403 Challenge mismatch` | The verifier does not hash to the stored challenge. | -| `404 Pairing code not found` | The code does not exist or was already exchanged. | +| `404 Pairing code not found` | The code never existed, or was already exchanged. | | `410 Pairing code expired` | The pending exchange is older than 5 minutes. | -| `429 Too Many Requests` | Five failed exchanges from this address inside 5 minutes. The address is locked out for 15 minutes and the response carries `Retry-After`. | - -A failed exchange is recorded in the activity log under the `auth` category with outcome `denied`. - -If the user clicks **Deny**, TablePro opens the `redirect` URL with `error=denied` and `error_description=user_denied`, wrapped in the `context` JSON for `raycast://` redirects and appended as flat query parameters otherwise. - -## Implementing pairing in another extension - -The flow is not Raycast-specific. Any client that can receive a callback can use it: +| `429 Too Many Requests` | Five failed exchanges from this address within 5 minutes. Locked out for 15, with `Retry-After` on the response. | -1. Generate a verifier and challenge. -2. Open `tablepro://integrations/pair?…` with a callback URL the OS can route back to you. -3. Read the MCP port from the handshake file. -4. POST `{ code, code_verifier }` to `/v1/integrations/exchange`. -5. Store the returned token in the Keychain. +Every failed exchange lands in the activity log under the `auth` category with outcome `denied`. -If the client cannot register a custom URL scheme, run a localhost HTTP server on a chosen port and pass `http://127.0.0.1:/callback` as the `redirect`. +Clicking **Deny** opens the redirect with `error=denied` and `error_description=user_denied`, wrapped +in the `context` JSON for `raycast://` and appended as flat parameters otherwise. diff --git a/docs/external-api/raycast.mdx b/docs/external-api/raycast.mdx index a710a8b28..0f58991e6 100644 --- a/docs/external-api/raycast.mdx +++ b/docs/external-api/raycast.mdx @@ -3,24 +3,37 @@ title: Raycast Extension description: Install and pair the TablePro Raycast extension, and use its commands and AI tools --- -Searching connections, browsing schemas, and running SQL all happen inside the Raycast window, without switching to the app. Nine commands and twelve AI tools reach TablePro through the `tablepro://` URL scheme and the [MCP server](/external-api/mcp-tools), after one pairing step. Requires TablePro 0.37.0 or later. +Searching connections, browsing schemas and running SQL happen inside the Raycast window, with no +switch to the app. Nine commands and twelve AI tools reach TablePro over the `tablepro://` scheme and +the [MCP server](/external-api/mcp-tools), after one pairing step. Needs TablePro 0.37.0 or later. ## Install -Install it from the [Raycast Store](https://www.raycast.com/ngoquocdat/tablepro), or search **TablePro** in Raycast's Store command. The source is MIT-licensed in the [Raycast extensions monorepo](https://github.com/raycast/extensions/tree/main/extensions/tablepro). +Install from the [Raycast Store](https://www.raycast.com/ngoquocdat/tablepro), or search **TablePro** +in Raycast's Store command. The source is MIT-licensed in the +[Raycast extensions monorepo](https://github.com/raycast/extensions/tree/main/extensions/tablepro). +It talks only to TablePro on localhost; passwords stay in the macOS Keychain and the extension never +reads them. + +One preference, **TablePro App**, points at the app bundle so the extension can find and launch it. +It defaults to `/Applications/TablePro.app`, so change it only for a build that lives elsewhere. ## Pair -Run **Pair with TablePro**. TablePro opens an approval sheet where you pick the scope (read-only by default), the connections the token can reach, and an optional expiry. On approve, a one-time code returns to Raycast over a deep-link callback, and the extension exchanges it for the token over localhost. Re-run the command any time to issue a fresh token; TablePro revokes the old one. +Run **Pair with TablePro**. An approval sheet opens in TablePro where you pick the scope (read-only +to start), the connections the token may reach, and an optional expiry. Approving sends a one-time +code back to Raycast, which exchanges it for a token over localhost. [Pairing](/external-api/pairing) +documents the protocol. -The protocol behind this is documented in [Pairing](/external-api/pairing). +Re-run the command whenever you want a fresh token. The previous one keeps working until you revoke +it in **Settings > Integrations**. ## Commands | Command | Mode | Description | |---------|------|-------------| | Search Connections | view | Search saved connections, open one in TablePro, copy a deep link. | -| Open Connection | no-view | Open a connection by name from the root search, e.g. `Open Connection prod`. | +| Open Connection | no-view | Open a connection by name from the root search, such as `Open Connection prod`. | | TablePro Menu Bar | menu-bar | Recent connections and quick actions in the menu bar. Refreshes every 10 minutes. | | Search Schema | view | Browse databases and schemas across connections. | | Search Tables | view | Pick a connection, list its tables, copy DDL, open a table tab. | @@ -36,7 +49,10 @@ The protocol behind this is documented in [Pairing](/external-api/pairing). ## AI tools -The extension is an AI Extension: with Raycast AI, mention `@tablepro` in a chat and Raycast picks tools from this catalog. The tools call the TablePro MCP server; see [MCP Tools](/external-api/mcp-tools) for the underlying schemas. +This is also an AI Extension: with Raycast AI, mention `@tablepro` in a chat and Raycast picks tools +from this catalog. Each one calls the MCP server, so a call cannot exceed the +[token's scope](/external-api/tokens). AI tool calls themselves go through Raycast's AI provider, +under Raycast's [privacy policy](https://www.raycast.com/privacy). | Tool | Description | |------|-------------| @@ -53,18 +69,11 @@ The extension is an AI Extension: with Raycast AI, mention `@tablepro` in a chat | `get-connection-status` | Live status of a connection. | | `list-recent-tabs` | Tabs currently open in TablePro. | -Write access follows the [security model](/external-api/index#security-model): a tool call cannot exceed the token's scope or the connection's external access level. - -## Preferences - -| Preference | Type | Description | -|------------|------|-------------| -| TablePro App | App picker | Path to the TablePro app. Used to detect the install and open the app. Default `/Applications/TablePro.app`. | - ## Troubleshooting -If TablePro is running but the MCP server is not, the extension opens `tablepro://integrations/start-mcp` and retries. A `401` means the token was revoked: re-run **Pair with TablePro**. For `403` access errors and other MCP failures, see [MCP client troubleshooting](/external-api/mcp-clients#troubleshooting). - -## Privacy - -The extension talks only to TablePro on localhost and makes no third-party network requests. Connection passwords stay in the macOS Keychain; the extension never reads them. AI tool calls go through Raycast's AI provider under Raycast's [privacy policy](https://www.raycast.com/privacy). +| Symptom | What to do | +|---|---| +| Paired, but the connection list is empty | TablePro omits every connection the token cannot reach. Either the allowlist you approved left it out, or its **External Clients** level is **Blocked**. Re-pair with the connection selected, or raise the level in the [connection form's Advanced tab](/connections/connection-form#advanced). | +| Commands hang on a running TablePro | The MCP server is stopped. The extension opens `tablepro://integrations/start-mcp` and retries on its own; give it a moment. | +| `401` | The token was revoked or has expired. Run **Pair with TablePro** again. | +| `403` and other MCP errors | See [MCP client troubleshooting](/external-api/mcp-clients#when-it-does-not-work). | diff --git a/docs/external-api/terminal.mdx b/docs/external-api/terminal.mdx index 463c57ac8..79dd1b100 100644 --- a/docs/external-api/terminal.mdx +++ b/docs/external-api/terminal.mdx @@ -3,44 +3,35 @@ title: Terminal and DDEV description: Opening databases and files from a shell script, and wiring up a ddev tablepro command --- -Run `open -b com.TablePro "mysql://root@127.0.0.1:3306/shop"` from any shell and that database opens in a window. A saved connection is matched if there is one; if not, the session is temporary and nothing lands on disk. `.sql` files and database files take the same route, and DDEV projects get a `ddev tablepro` command of their own. - -## Open a database from the shell - -Pass a [connection URL](/connections/urls) to `open`: - -```bash -open "mysql://root@127.0.0.1:3306/shop" -open "postgresql://user:pass@127.0.0.1:5432/app?table=orders" -``` - -TablePro uses a saved connection if one matches, otherwise it opens a temporary session that is never written to disk. - -### Force TablePro over another client - -`open "mysql://…"` goes to whichever app owns the `mysql://` scheme. If TablePlus or another client is installed, it may win. Target TablePro by its bundle identifier: +`open "mysql://…"` goes to whichever app claims the `mysql://` scheme, and with TablePlus or another +client installed that may not be TablePro. Name TablePro instead, either by bundle identifier or +through the `tablepro` command: ```bash open -b com.TablePro "mysql://root@127.0.0.1:3306/shop" +tablepro "postgresql://user:pass@127.0.0.1:5432/app?table=orders" ``` -### Install the tablepro command +Any [connection URL](/connections/urls) works, including its query parameters. TablePro uses a saved +connection when one matches on type, host, port, database and user; otherwise the session is +temporary and never written to disk. Running `tablepro` with no argument opens the app and nothing else. -Open **Settings > General > Command Line** and click **Install**. This writes a small `tablepro` script to `/usr/local/bin` that always opens TablePro: +## Install the tablepro command -```bash -tablepro "mysql://root@127.0.0.1:3306/shop" -tablepro -``` +Open **Settings > General**, find **Command Line**, and click **Install**. It writes a small script at +`/usr/local/bin/tablepro` whose whole body is `exec open -b com.TablePro "$@"`. Command Line install section in TablePro settings Command Line install section in TablePro settings -`/usr/local/bin` is owned by root on a stock macOS install, and may not exist at all. When TablePro cannot write there itself, macOS asks for your administrator password, and TablePro then creates the folder and the command. Cancel the password prompt and nothing is written. +That directory is root-owned on a stock macOS install, so expect an administrator password prompt. +Cancel it and nothing is written. If the write fails anyway, Settings prints the exact `sudo` command +to paste into Terminal; run it, switch back, and the status updates itself. -If that fails too, Settings falls back to showing the exact `sudo` command to run in Terminal. Run it and switch back to TablePro, and Settings picks up the change on its own. +**Uninstall** removes the script again. When something else already occupies that path, **Install** +is dimmed and Settings says *"A different file already exists at …"*: remove that file first. ## Open a file @@ -51,27 +42,35 @@ tablepro schema.sql tablepro app.db ``` -- `.sql`, `.psql`, and `.pgsql` files open in a query tab on the current connection. If a window already has the file open, that window comes to front. With no connection open, the file waits and opens once you connect. -- Database files open a connection to the file itself, nothing is saved: `.db`, `.db3`, `.s3db`, `.sl3`, `.sqlite`, `.sqlite3`, and `.sqlitedb` open with SQLite; `.duckdb` and `.ddb` open with the DuckDB plugin. A window already connected to the same file comes to front instead of opening twice. +- `.sql`, `.psql` and `.pgsql` open in a query tab on the current connection. A window already + holding that file comes to front. With no connection open, the file waits until you connect. +- Database files open a connection to the file itself and save nothing: `.db`, `.db3`, `.s3db`, + `.sl3`, `.sqlite`, `.sqlite3` and `.sqlitedb` open with SQLite, `.duckdb` and `.ddb` with the + DuckDB plugin. A window already connected to that file comes to front instead of a second one + opening. ## DDEV -[DDEV](https://ddev.com) ships a `tablepro` command. From any project: +[DDEV](https://ddev.com) ships a `tablepro` command of its own. From any project: ```bash cd my-project ddev tablepro ``` -If `ddev tablepro` is not found, update DDEV. - -This is the right way to open a DDEV database, because DDEV gives the project a new host port on every `ddev start`. A saved connection goes stale; the command reads the current port each time. It works with both MySQL/MariaDB and PostgreSQL projects. DDEV's database, user, and password are all `db`. +If `ddev tablepro` is not found, update DDEV. It covers MySQL, MariaDB and PostgreSQL projects, and +DDEV's database, user and password are all `db`. Prefer it over a saved connection: `ddev start` +hands the project a new host port each time, and the command reads the current one. ## Trusting a link -The first time a link connects, TablePro asks you to confirm it. This stops a web page from silently opening a connection. +The first time a link connects, TablePro asks: **Open External Database Connection?**, showing the +host, user and database. This is what stops a web page from opening a connection behind your back. -For a database on your own machine (`localhost`, `127.0.0.1`, or `::1`), the alert also offers **Always Allow**. Choose it and TablePro stops asking for that database, so `ddev tablepro` connects straight away. Trust is keyed on the database type, host, database name, user, and the `name` parameter, but not the port, because DDEV changes the port on every start. +For a database on your own machine (`localhost`, `127.0.0.1`, `::1`, or any `127.x.x.x` address) the +alert adds **Always Allow**. Choose it and that database stops asking, which is what makes +`ddev tablepro` a one-step command. Trust is keyed on database type, host, database name, user and +the URL's `name` parameter, deliberately not the port, since DDEV changes the port on every start. Alert asking to confirm an external database link, with Always Allow for local databases @@ -79,7 +78,10 @@ For a database on your own machine (`localhost`, `127.0.0.1`, or `::1`), the ale -Only databases on your own machine can be trusted. A link to a remote host always asks, every time. +A link to a remote host asks every time. Only a database on this machine can be trusted. -Review and remove trusted links in **Settings > General > Trusted Links**. A link that carries a filter condition (`?query=` or `?condition=`) or a pre-connect script still asks for confirmation, even when the connection is trusted. +**Settings > General > Trusted Links** lists what you have trusted, with **Forget** per entry and +**Forget All**. Two things still prompt even on a trusted connection: a link carrying a filter +(`?query=`, `?condition=` or `?raw=`) confirms the filter, and a connection with a pre-connect script +shows the script. diff --git a/docs/external-api/tokens.mdx b/docs/external-api/tokens.mdx index 24e15d26f..59592c4cf 100644 --- a/docs/external-api/tokens.mdx +++ b/docs/external-api/tokens.mdx @@ -3,57 +3,26 @@ title: Tokens description: Token model, scopes, connection allowlists, expiry, revocation, and the activity log --- -Bearer tokens authenticate external requests to the MCP server. Each token carries a permission level, an optional connection allowlist, and an optional expiry. TablePro stores only a salted SHA-256 hash, in the login keychain. The plaintext is shown once at creation and never again. +**Read Only** is where the **Generate Token** sheet starts, and it is the right level for a client that only reads. Move to **Read & Write** when an agent has to change data, and to **Full Access** only for `DROP` and `TRUNCATE`, which nothing below it can reach. The plaintext is shown once at creation and never again; what persists is a salted SHA-256 hash in the login keychain. -## Require authentication - -**Settings > Integrations > Authentication > Require authentication** is on by default. Turning it on for the first time with no tokens yet mints a full-access "Default token" and reveals the plaintext once. - -With the toggle off, a request from your own machine with no `Authorization` header is accepted as an anonymous loopback caller. That caller is **read-only and can never be anything else**: it gets `tools:read` and `resources:read`, and the scope check refuses to grant `tools:write` or `admin` to a caller with no issued token, no matter what it asks for. It can read schema and run `SELECT`; it cannot write, cannot drop, and cannot administer. - -Presenting an `Authorization` header still goes through normal token validation, so an invalid token is rejected even with the toggle off. - -The server is loopback only. There is no remote mode, so there is no "a remote client still needs a token" case to reason about. - -## Stored fields - -| Field | Purpose | -|-------|---------| -| `name` | Label in the token list. Pairing uses the client name. | -| `prefix` | First 8 characters of the plaintext, so a token can be identified without revealing it. | -| `tokenHash`, `salt` | SHA-256 of salt plus plaintext. The plaintext is never persisted. | -| `permissions` | `readOnly`, `readWrite`, or `fullAccess`. | -| `connectionAccess` | All connections, or a named set of connection UUIDs. | -| `expiresAt` | Optional. Absent means never. | -| `isActive` | `false` after revocation. | -| `createdAt`, `lastUsedAt` | Shown in the token list. | +## Permission levels -The plaintext is `tp_` followed by 32 base64url-encoded random bytes. +| Level | Scopes | What it can do | +|-------|--------|----------------| +| Anonymous loopback | `tools:read`, `resources:read` | Read schema and run `SELECT`. | +| **Read Only** | `tools:read`, `resources:read` | The same, under an issued token. | +| **Read & Write** | plus `tools:write` | `INSERT`, `UPDATE`, `DELETE`, connect and disconnect, switch database or schema, open and focus tabs. | +| **Full Access** | plus `admin` | Also `confirm_destructive_operation`, the only tool that runs `DROP` or `TRUNCATE`. | -Tokens live in the login keychain under the `com.TablePro` service. A pre-existing `mcp-tokens.json` is migrated into the keychain on first load and the file is deleted. +Anonymous loopback is the floor and cannot be raised: the scope check refuses `tools:write` and `admin` to a caller with no issued token, whatever it asks for. -## Scopes +`confirm_destructive_operation` also needs the connection's external access to be **Read & Write**, and no permission level skips the user's approval. [Scopes and gates](/external-api/mcp-tools#scopes-and-gates) names which tools sit on which scope. -A token's permission level maps to the scopes the server enforces: - -| Permission | Scopes granted | -|------------|----------------| -| `readOnly` | `tools:read`, `resources:read` | -| `readWrite` | `tools:read`, `tools:write`, `resources:read` | -| `fullAccess` | `tools:read`, `tools:write`, `resources:read`, `admin` | - -What each level can do: - -| Permission | Read schema | SELECT | INSERT / UPDATE / DELETE | DROP / TRUNCATE | connect, switch database | open and focus tabs | -|------------|:-----------:|:------:|:------------------------:|:---------------:|:------------------------:|:-------------------:| -| Anonymous loopback | yes | yes | no | no | no | no | -| `readOnly` | yes | yes | no | no | no | no | -| `readWrite` | yes | yes | yes | no | yes | yes | -| `fullAccess` | yes | yes | yes | yes, with approval | yes | yes | +## Require authentication -`confirm_destructive_operation` is the only tool that needs `admin`, so `DROP` and `TRUNCATE` need a `fullAccess` token. It also needs the connection's external access to be `readWrite`, and the user still approves the statement before it runs. No permission level skips that approval. +**Settings > Integrations > Authentication > Require authentication** is on by default. Turning it on for the first time with no tokens yet mints a **Read & Write** token named "Default token" and reveals the plaintext once. -Window and tab tools (`open_connection_window`, `open_table_tab`, `focus_query_tab`) need `tools:write` because they change what the user sees. `list_recent_tabs` needs only `tools:read`. None of them bypasses the connection allowlist or a blocked connection. +With the toggle off, a request from your own machine with no `Authorization` header is accepted as the anonymous loopback caller in the table above. Presenting an `Authorization` header still goes through normal token validation, so an invalid token is rejected either way. The server is loopback only, with no remote mode. ## Connection allowlist @@ -61,42 +30,60 @@ A token grants access to all connections or to a named set. A call that targets ## Combining with external access -The effective permission is the lower of the token's scope and the connection's external access. +The effective permission is the lower of the token's scope and the connection's **External Clients** level. | Token | Connection access | Effective | |-------|------------------|-----------| -| `readOnly` | `readWrite` | read only | -| `readWrite` | `readOnly` | read only | -| `fullAccess` | `readOnly` | read only, and `confirm_destructive_operation` is refused | -| `fullAccess` | `readWrite` | read and write | -| any | `blocked` | denied, and the connection is not even listed | - -## The stdio bridge token - -Every server start mints a token named `__stdio_bridge__` and writes it into the handshake file for the bundled bridge. It is `readWrite`, not full access, so an agent going through the bridge can read and write but cannot call `confirm_destructive_operation`. - -It expires after an hour and is rotated 15 minutes before that, deleted when the server stops, and any stale copy is removed the next time the store loads. It is hidden from the Settings token list, though its name can appear in the activity log. For a narrower scope than `readWrite`, skip the bridge and connect over [HTTP with a token you mint](/external-api/mcp-clients#http-transport). +| **Read Only** | **Read & Write** | read only | +| **Read & Write** | **Read Only** | read only | +| **Full Access** | **Read Only** | read only, and `confirm_destructive_operation` is refused | +| **Full Access** | **Read & Write** | read and write | +| any | **Blocked** | denied, and the connection is not even listed | ## Creating a token -1. **Pairing** (most common). See [Pairing](/external-api/pairing). Pairing the same client twice mints a second token; the first stays valid until it is revoked. -2. **Settings UI**: **Settings > Integrations > Authentication**, then generate one. Pick a name, permission level, connection allowlist, and expiry. The plaintext is shown once. +Pairing is the usual route: see [Pairing](/external-api/pairing). To mint one by hand, open **Settings > Integrations > Authentication**, click **Generate Token**, and pick a name, permission level, connection allowlist and expiry. The plaintext is shown once. ## Expiry -Optional, and worth setting for anything with write access. An expired token stops authenticating: requests get `-33008` with HTTP `401` and `WWW-Authenticate: Bearer error="invalid_token", error_description="token expired"`. +Optional, and worth setting for anything with write access. The sheet offers 30, 60 and 90 days, a custom date, or **Never**. An expired token stops authenticating: requests get `-33008` with HTTP `401` and `WWW-Authenticate: Bearer error="invalid_token", error_description="token expired"`. + +## Rotating a token + +Mint the replacement before you retire the old one; two tokens for one client are fine, and pairing the same client twice does exactly this. Point the client at the new token, confirm it works, then revoke the old one. Revoking first cuts the client off mid-call, because revocation takes effect immediately. ## Revocation The token list in **Settings > Integrations > Authentication** shows prefix, name, permission level, allowlist, last use, and expiry. Each row offers: - **Revoke**: marks the token inactive. It stays in the list and cannot be reactivated. -- **Delete**: removes the row entirely. +- **Delete…**: removes the row entirely. Revoking cancels every request that token has in flight and clears the connection approvals the user had granted it, so a re-issued token has to ask again. The next call gets `-33009` with HTTP `401`. **Disconnect** in the Connected Clients list revokes that client's token. It is not a soft disconnect: the client has to pair again or be given a new token. +## The stdio bridge token + +Every server start mints a token named `__stdio_bridge__` and writes it into the handshake file for the bundled bridge. It is **Read & Write**, so an agent going through the bridge can read and write but cannot call `confirm_destructive_operation`. + +It expires after an hour and is rotated 15 minutes before that, deleted when the server stops, and any stale copy is removed the next time the store loads. It is hidden from the Settings token list, though its name can appear in the activity log. For a narrower scope, skip the bridge and connect over [HTTP with a token you mint](/external-api/mcp-clients#http-transport). + +## Stored fields + +| Field | Purpose | +|-------|---------| +| `name` | Label in the token list. Pairing uses the client name. | +| `prefix` | First 8 characters of the plaintext, so a token can be identified without revealing it. | +| `tokenHash`, `salt` | SHA-256 of salt plus plaintext. The plaintext is never persisted. | +| `permissions` | `readOnly`, `readWrite`, or `fullAccess`. | +| `connectionAccess` | All connections, or a named set of connection UUIDs. | +| `expiresAt` | Optional. Absent means never. | +| `isActive` | `false` after revocation. | +| `createdAt`, `lastUsedAt` | Shown in the token list. | + +The plaintext is `tp_` followed by 32 base64url-encoded random bytes. Tokens live in the login keychain under the `com.TablePro` service; a pre-existing `mcp-tokens.json` is migrated in on first load and deleted. + ## Activity log Every authentication, tool call, resource read, and query is recorded in `~/Library/Application Support/TablePro/mcp-audit.db` with the token id, never the plaintext. Open it with **Settings > Integrations > View Activity…**. @@ -110,7 +97,7 @@ Every authentication, tool call, resource read, and query is recorded in `~/Libr | Connection | the connection the call targeted, or `-` | | Outcome | `success`, `denied`, `error`, `rateLimited` | -**Statement text is not stored.** A `query.executed` row carries `sqlDigest`, the SHA-256 of the statement, alongside its duration and row count. You can prove two rows ran the same statement, and you can match a row against a statement you already have, but the log itself never holds the SQL. Error text that is stored is redacted and truncated first. +Statement text is not stored. A `query.executed` row carries `sqlDigest`, the SHA-256 of the statement, alongside its duration and row count, which is enough to match a row against a statement you already hold. Error text that is stored is redacted and truncated first. Entries are chained: each row's hash covers the previous one, so a row cannot be removed or edited without breaking the chain. Entries older than 90 days are pruned when the store opens. @@ -125,7 +112,7 @@ Entries are chained: each row's hash covers the previous one, so a row cannot be | Failed pairing exchanges | 5 per 5 minutes | | Pairing lockout | 15 minutes | -Request limits are keyed by token id, or by client address for an anonymous caller. Auth failures are counted against both the client address and the token, so one misbehaving client cannot lock out another on the same loopback address. A successful auth clears the bucket. Anything over the limit returns `-33010` with HTTP `429` and a `Retry-After` header. +Request limits are keyed by token id, or by client address for an anonymous caller, and `subscriptions/listen` is exempt. Auth failures count against both the client address and the token, so one misbehaving client cannot lock out another on the same loopback address, and a successful auth clears the bucket. Anything over a limit returns `-33010` with HTTP `429` and a `Retry-After` header. Transport limits such as the body cap are on [MCP Protocol](/external-api/mcp-protocol#transport-limits). ## What a token cannot do @@ -140,6 +127,4 @@ Request limits are keyed by token id, or by client address for an anonymous call | Create, change, or read other tokens | no | | Create or edit connection records | no | -Statements that reach the filesystem or run server-side code are refused before they reach the driver, on every tool. - -The reachable surface is the [tool catalog](/external-api/mcp-tools), the [resources](/external-api/mcp-resources), and the [prompts](/external-api/mcp-prompts). Nothing outside those lists is exposed. +Statements that reach the filesystem or run server-side code are refused before they reach the driver, on every tool. The reachable surface is the [tool catalog](/external-api/mcp-tools), the [resources](/external-api/mcp-resources), and the [prompts](/external-api/mcp-prompts). Nothing outside those lists is exposed. diff --git a/docs/external-api/url-scheme.mdx b/docs/external-api/url-scheme.mdx index 2444fe7eb..0c832e859 100644 --- a/docs/external-api/url-scheme.mdx +++ b/docs/external-api/url-scheme.mdx @@ -3,28 +3,35 @@ title: URL Scheme description: Every tablepro:// deep link action with parameters and examples --- -The `tablepro://` URL scheme drives the TablePro GUI from outside the app. Use it from the shell with `open`, from another app with `NSWorkspace.shared.open(url:)`, or from a Raycast extension with `open()` from `@raycast/api`. - -The scheme covers two kinds of actions: - -- **Navigate**: open a connection, table, or query tab. -- **Pair**: bootstrap an MCP token for an extension. - -Data exchange is not part of the URL scheme. For that, use [MCP](/external-api/mcp-tools). +macOS hands the URL to TablePro, which parses it, confirms anything that could do damage, and opens +something on screen. Nothing comes back over the scheme: to read rows, use +[MCP](/external-api/mcp-tools). + +Call it from a shell with `open`, from another app with `NSWorkspace.shared.open(url:)`, or from a +Raycast extension with `open()` from `@raycast/api`. + +| Path after `tablepro://` | Opens | +|---|---| +| [`connect/`](#open-a-connection) | The saved connection, or fronts its window | +| [`connect//table/…`](#open-a-table) | A table tab, optionally after switching database or schema | +| [`connect//query?sql=…`](#run-a-query) | A query tab with the SQL prefilled, after a confirmation | +| [`import?…`](#import-a-connection) | A review sheet that saves a new connection | +| [`integrations/pair?…`](#start-pairing) | The pairing approval sheet | +| [`integrations/start-mcp`](#lazy-start-the-mcp-server) | Nothing. Starts the MCP server and returns | ## Connection IDs are UUIDs -Connection paths use the connection's UUID, not its display name. +Connection paths take the connection's UUID, not its display name. ```text tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1 ``` -There is no UI action that copies a connect link. To find a connection's UUID, call the MCP [`list_connections`](/external-api/mcp-tools) tool or read the [`tablepro://connections`](/external-api/mcp-resources) resource; both return the UUID for each saved connection. The welcome window's **Copy TablePro Link** context menu item copies a `tablepro://import?…` link for sharing the connection, not a `connect` link. - - -Pre-0.38 builds accepted `tablepro://connect//…` paths. Name-based paths were removed in 0.38.0. Bookmarks built against older versions must be regenerated with UUID-keyed paths. - +No UI action copies a connect link. To find a UUID, call the MCP +[`list_connections`](/external-api/mcp-tools) tool or read the +[`tablepro://connections`](/external-api/mcp-resources) resource. The welcome window's +**Copy TablePro Link** context menu item copies a `tablepro://import?…` link for sharing a connection +definition, which is a different thing. ## Open a connection @@ -32,9 +39,11 @@ Pre-0.38 builds accepted `tablepro://connect//…` paths. Name-based paths tablepro://connect/ ``` -Opens the saved connection. If the connection is already open in a window, that window comes to front. If the UUID does not match a saved connection, an error sheet appears. +Opens the saved connection, or brings its window to front when it is already open. A UUID that +matches no saved connection shows an error sheet. -If the connection has a pre-connect script, TablePro shows the script text in a confirmation dialog before connecting. Cancelling the dialog cancels the whole deep link. This applies to every `connect/…` link: connection, table, and query. +A connection with a pre-connect script shows the script text in a confirmation dialog first, and +cancelling that dialog cancels the whole link. This applies to every `connect/…` form. ```bash open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1" @@ -48,20 +57,23 @@ tablepro://connect//database//table/ tablepro://connect//database//schema//table/ ``` -The first form opens the table in the connection's current database and schema. The second selects a database first. The third (Postgres-style) selects both, database before schema. +The first form uses the connection's current database and schema. The second switches database first. +The third switches both, database before schema, which is the Postgres shape. -On a connection that has schemas but no databases to switch between, such as Oracle, a lone `database` segment names the schema. A segment naming a dimension the connection does not have is ignored rather than applied to the other one. +On an engine with schemas and no database to switch between, such as Oracle, a lone `database` +segment names the schema. A segment naming a dimension the engine does not have is dropped rather +than applied to the other one. -Table and schema names with spaces or special characters must be percent-encoded. +Percent-encode table and schema names that contain spaces or reserved characters. ```bash -# Open a table in the current database +# The current database open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/table/users" -# Open a table in a specific database +# A named database open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/database/analytics/table/events" -# Postgres: select database and schema +# Database and schema open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/database/app/schema/reporting/table/daily_events" ``` @@ -71,17 +83,18 @@ open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/database/app/schem tablepro://connect//query?sql= ``` -Opens a query tab with the SQL pre-filled. TablePro always shows a confirmation dialog first, previewing the first 300 characters of the SQL. A tab that already holds exactly this SQL comes to front instead of a second tab opening. The query does not auto-execute; the user runs it from the editor. The SQL has a 51,200-character cap. +A dialog titled **Open Query from Link** appears first, previewing the first 300 characters of the +SQL; **Open Query** opens the tab. Nothing executes, the user runs it from the editor. A tab already +holding exactly this SQL comes to front instead of a second one opening. -To run SQL from a script and read rows back, use the MCP [`execute_query`](/external-api/mcp-tools) tool instead. The URL scheme is for handing SQL into the GUI, not for headless execution. +`sql` is required and caps at 51,200 UTF-16 units. Longer than that and the link is dropped. ```bash open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/query?sql=SELECT%20*%20FROM%20users%20LIMIT%2010" ``` -| Parameter | Required | Description | -|-----------|----------|-------------| -| `sql` | yes | Percent-encoded SQL. Max 51,200 characters. | +To run SQL from a script and read rows back, use the MCP +[`execute_query`](/external-api/mcp-tools) tool. This link hands SQL to the GUI and stops there. ## Start pairing @@ -89,19 +102,19 @@ open "tablepro://connect/9f1f0c3e-2e3d-4b14-9c3a-1d2f4ad1f6f1/query?sql=SELECT%2 tablepro://integrations/pair?client=&challenge=&redirect=&scopes=&connection-ids= ``` -Starts a pairing flow. TablePro presents an approval sheet, the user picks scopes and connections, and TablePro returns a one-time code via the `redirect` URL. +Opens the approval sheet. The user picks scope, connections and expiry, and TablePro returns a +one-time code to `redirect`. | Parameter | Required | Description | |-----------|----------|-------------| -| `client` | yes | Display name shown in the approval sheet, e.g. `Raycast on macbook-pro`. | -| `challenge` | yes | Base64url-encoded SHA-256 hash of the verifier (PKCE). | -| `redirect` | yes | URL to receive the `code` query parameter on success. | -| `scopes` | no | Requested scope, a single value: `readWrite` or `fullAccess`. Matching is case-insensitive and accepts `read_write`, `read-write`, `full_access`, `full-access`, and `full`. Any other value, or omitting the parameter, requests `readOnly`. | +| `client` | yes | Display name shown in the approval sheet, such as `Raycast on macbook-pro`. | +| `challenge` | yes | Base64url SHA-256 of the verifier: exactly 43 characters (PKCE). | +| `redirect` | yes | Where the code is delivered. A loopback `http(s)` URL or a private-use scheme registered by an installed app; anything else is refused. | +| `scopes` | no | One value: `readWrite` or `fullAccess`. Matching is case-insensitive and takes `read_write`, `read-write`, `full_access`, `full-access` and `full`. Any other value, or none, requests `readOnly`. | | `connection-ids` | no | Comma-separated UUIDs to preselect in the allowlist. Defaults to all. | -The user can change scopes and connections in the approval sheet. The query parameters are a request, not a grant. - -Example invocation from a Raycast extension: +These parameters are a request, not a grant: the sheet lets the user raise or lower every one of +them. ```ts import { open } from "@raycast/api"; @@ -115,7 +128,7 @@ const params = new URLSearchParams({ await open(`tablepro://integrations/pair?${params}`); ``` -See [Pairing](/external-api/pairing) for the full sequence and the exchange step. +[Pairing](/external-api/pairing) has the exchange step and the error codes. ## Lazy-start the MCP server @@ -123,11 +136,15 @@ See [Pairing](/external-api/pairing) for the full sequence and the exchange step tablepro://integrations/start-mcp ``` -Starts the MCP server if it is not already running, then returns. Used by the bundled `tablepro-mcp` CLI to bootstrap on cold launch. +Starts the MCP server if it is not running, then returns. MCP does not have to be enabled in Settings +first. The bundled `tablepro-mcp` CLI uses this to bootstrap on a cold launch. -The user does not need to enable MCP in Settings beforehand. The server binds to the configured port (default `23508`); if that port is taken, it falls back to a free port in the `51000-52000` range. Either way it writes a handshake file at `~/Library/Application Support/TablePro/mcp-handshake.json` with the actual port. +The server takes the configured port (`23508` by default) and falls back to a kernel-assigned free +port when that one is busy. Either way it writes the port it actually bound to a handshake file at +`~/Library/Application Support/TablePro/mcp-handshake.json`. -A launch whose only reason is this URL leaves TablePro in the background, with no Dock icon and no app switcher entry, until it has a window to show. The bundled `tablepro-mcp` CLI sets `TABLEPRO_BACKGROUND_LAUNCH=1` on the `open` it runs so the app knows before it finishes launching; another caller reaches the same state a moment later, after the icon has briefly appeared. +A launch whose only reason is this URL stays in the background: no Dock icon, no app switcher entry, +until it has a window to show. ```bash open "tablepro://integrations/start-mcp" @@ -136,72 +153,22 @@ open "tablepro://integrations/start-mcp" ## Import a connection ```text -tablepro://import?name=&host=&port=

&type=&username=&database= +tablepro://import?name=&host=&type=&… ``` -Adds a saved connection from query parameters. TablePro shows a review sheet with the parsed details before saving anything; the name is editable, everything else is read-only. **Add Connection** saves it, or **Add as Copy** when a connection with the same name, host, and type already exists. Passwords are never accepted in the URL; the user adds one in the connection editor before connecting. - -Required parameters: `name`, `host`, `type`. - -`type` accepts any registered database type name (case-insensitive). Examples: `MySQL`, `PostgreSQL`, `MongoDB`, `Redis`, `ClickHouse`, `Oracle`, `DuckDB`, `Cassandra`. +Shows a review sheet with the parsed connection, then saves it on **Add Connection**. `name`, `host` +and `type` are required; the rest of the connection form maps to query parameters listed in +[Connection Import](/external-api/connection-import). Passwords are never accepted in a link. ```bash open "tablepro://import?name=Staging&host=db.example.com&port=5432&type=postgresql&username=admin&database=mydb" ``` -Enum-valued parameters below must match the listed values exactly (they are raw values, not display names). Percent-encode values with spaces: `sslMode=Verify%20CA`. - -### Core parameters - -| Parameter | Description | -|-----------|-------------| -| `port` | Server port. Defaults to the database type's standard port. | -| `username` | Database username. | -| `database` | Default database name. | -| `color` | Sidebar color: `Red`, `Orange`, `Yellow`, `Green`, `Blue`, `Purple`, `Pink`, or `Gray`. | -| `tagName` | Tag to assign. Repeat the parameter to assign more than one tag. | -| `groupName` | Group to place the connection in. | -| `safeModeLevel` | Safe Mode level: `silent`, `alert`, `alertFull`, `safeMode`, `safeModeFull`, or `readOnly`. | -| `aiPolicy` | AI access policy: `alwaysAllow`, `askEachTime`, or `never`. Any other value falls back to the app default. | -| `redisDatabase` | Redis database index to select on connect. | -| `startupCommands` | Statements to run after connecting. | -| `localOnly` | Set to `1` to exclude the connection from sync. | - -### SSH parameters - -Set `ssh=1` to enable SSH tunneling. - -| Parameter | Description | -|-----------|-------------| -| `sshHost` | SSH server hostname. | -| `sshPort` | SSH port. Default `22`. | -| `sshUsername` | SSH username. | -| `sshAuthMethod` | `Password`, `Private Key`, `SSH Agent`, `Keyboard Interactive`, or `None`. Unrecognized values fall back to `Password`. | -| `sshPrivateKeyPath` | Path to private key file. | -| `sshAgentSocketPath` | Custom SSH agent socket path. | -| `sshJumpHosts` | JSON array of jump hosts. | -| `sshTotpMode` | TOTP mode for two-factor SSH auth: `none`, `auto_generate`, or `prompt_at_connect`. | -| `sshTotpAlgorithm` | TOTP hash algorithm: `SHA1`, `SHA256`, or `SHA512`. Default `SHA1`. | -| `sshTotpDigits` | TOTP code length. Default `6`. | -| `sshTotpPeriod` | TOTP period in seconds. Default `30`. | - -### SSL parameters - -| Parameter | Description | -|-----------|-------------| -| `sslMode` | `Disabled`, `Preferred`, `Required`, `Verify CA`, or `Verify Identity`. Unrecognized values fall back to `Disabled`. | -| `sslCaCertPath` | CA certificate file path. | -| `sslClientCertPath` | Client certificate file path. | -| `sslClientKeyPath` | Client key file path. | - -### Plugin-specific fields - -Use the `af_` prefix to pass driver-specific fields. For example, `af_replicaSet=myrs` passes `replicaSet` to the MongoDB plugin. - -`af_preConnectScript` is stripped on import. Imported connections cannot carry a pre-connect shell script; the user has to add one by hand in the connection editor. - ## Errors -Malformed URLs are dropped without any UI: an invalid UUID, unknown path, missing required parameter, or over-limit SQL is logged to the system log (subsystem `com.TablePro`) and nothing opens. Check Console.app when a link does nothing. +A URL that fails to parse is dropped with no UI at all: an invalid UUID, an unknown path, a missing +required parameter, over-limit SQL. The reason goes to the system log under subsystem `com.TablePro`, +so check Console.app when a link does nothing. -Failures after parsing show an error sheet. Example: a valid UUID with no matching connection shows `No saved connection with ID "9F1F0C3E-2E3D-4B14-9C3A-1D2F4AD1F6F1".` under the title **Connection Failed**. +A URL that parses and then fails shows an error sheet. A valid UUID with no matching connection gives +`No saved connection with ID "…".` under the title **Connection Failed**. diff --git a/docs/external-api/versioning.mdx b/docs/external-api/versioning.mdx index b8b6478b4..dd2efc910 100644 --- a/docs/external-api/versioning.mdx +++ b/docs/external-api/versioning.mdx @@ -3,78 +3,73 @@ title: Versioning description: Which MCP protocol versions TablePro accepts, and the stability policy for the URL scheme, tools, and resources --- -Two different versions matter here, and they move independently. +Ask the server which protocol versions it takes, never this page: `server/discover` returns them in +`supportedVersions`, and an unsupported one is rejected with `-32022` whose `data.supported` carries +the same list. A client that hardcodes the list breaks on the release that adds one. -**TablePro's version** follows semver and governs the External API contract: the URL scheme, the MCP tool catalog, the resource list, the prompt catalog, and the pairing flow. +The other number, TablePro's own, governs everything else on this tab: the URL scheme, the MCP tool +catalog, the resource list, the prompt catalog, and the pairing flow. It follows semver. -**The MCP protocol version** is set by the [specification](https://modelcontextprotocol.io), not by TablePro. TablePro accepts several at once and says which on request. +## Which era gets new work -## Which protocol versions are accepted +The newest protocol version is the only one that gets new features. It is stateless, it carries two +methods no older version can reach, and it is what a new client should be built against. -Ask the server. `server/discover` returns the list: +Older versions keep working through `initialize` and `Mcp-Session-Id` so a shipped client does not +break on an app update, and they see a reduced feature set. One stays accepted for as long as the +[specification](https://modelcontextprotocol.io) lists it as current or recent. Dropping one is a +`Removed` entry in the CHANGELOG, never a silent change. [MCP Protocol](/external-api/mcp-protocol) +has the version list and the per-era differences. -```json -{ "supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18"] } -``` - -The single source of truth is `MCPProtocolVersion` in `TablePro/Core/MCP/Meta/MCPProtocolVersion.swift`. Read the list from `server/discover` rather than hardcoding it, and it stays right when TablePro adds or drops one. - -An unsupported version is rejected with `-32022`, and the error's `data.supported` carries the same list. - -## The dual-era policy - -TablePro serves two eras at once, and they are not equal. - -**The modern era is `2026-07-28`.** It is stateless: no `initialize`, no session id, no `GET` stream. Every request declares its own version and capabilities in `_meta`, mirrors part of itself into headers, and gets a result carrying `resultType` and, where it applies, a cache hint. Two methods exist only here: `server/discover` and `subscriptions/listen`. This is what new clients get built against, and it is the only era that gets new features. - -**The legacy era is `2025-11-25` and `2025-06-18`.** They keep working through `initialize` and `Mcp-Session-Id` so an existing client does not break on an app update. Legacy clients see `resources.subscribe: false`, get no `resultType` and no cache hints, and cannot reach the two modern methods. Nothing new will be added there. - -`2025-03-26` is no longer accepted. It requires JSON-RPC batching, which TablePro never implemented, so a client that asked for it was already relying on behavior that did not exist. - -A legacy version stays supported for as long as the specification lists it as current or recent, and it goes away when it stops being either. Its removal is a `Removed` entry in the CHANGELOG, not a silent change. - -The bundled `tablepro-mcp` bridge makes all of this a non-issue for stdio clients: it answers the client's `initialize` locally and speaks `2026-07-28` upstream, so an old client reaches a modern server without knowing it. See [MCP Clients](/external-api/mcp-clients). +For stdio clients this is somebody else's problem: the bundled `tablepro-mcp` bridge answers the +client's `initialize` locally and speaks the newest version upstream. See +[MCP Clients](/external-api/mcp-clients). ## Stability rules -Within a major TablePro version, the External API is **additive only**: - -- New URL scheme actions can be added. -- New MCP tools, prompts, and resources can be added. -- New tool input fields can be added when they are optional with a sensible default. -- New tool output fields can be added. -- New error codes can be added. +Within a major TablePro version the External API is additive only: -Within a major version, none of the following will happen: +- New URL scheme actions, MCP tools, prompts, resources and error codes can appear. +- A new tool input field can appear when it is optional and has a default. +- A new tool output field can appear. -- A URL scheme path will not be removed or change meaning. -- A tool or prompt will not be removed or renamed. -- A required input field will not be added to an existing tool. -- An output field will not be removed or change type. -- A resource will not be removed. +And within a major version, none of these happen: -Error codes are the one place this already moved. TablePro's own codes left the `-32000` block, which the specification froze, for `-33000` and above. A client that matched on the old numbers has to be updated once. The current table is in [MCP Protocol](/external-api/mcp-protocol#errors). +- A URL scheme path is removed or changes meaning. +- A tool or prompt is removed or renamed. +- A required input field is added to an existing tool. +- An output field is removed or changes type. +- A resource is removed. -## Breaking changes +Error codes are the one place this already moved. TablePro's own codes left the `-32000` block, which +the specification froze, for `-33000` and above; a client that matched on the old numbers needs +updating once. The current table is in [MCP Protocol](/external-api/mcp-protocol#errors). -Breaking changes ship only at major TablePro version bumps (1.x to 2.x). The 0.x series is pre-1.0; we treat minor versions as the release boundary and only break with explicit notice. +## Breaking changes before 1.0 -## Deprecation lifecycle +The app is on 0.x, so the minor version is the release boundary. A break can land between 0.67 and +0.68, but never silently. Once 1.0 ships, breaks move to major bumps. -1. **Announce.** The docs mark the surface deprecated and name the replacement. The CHANGELOG mentions it. -2. **Grace.** The deprecated surface keeps working for at least one minor version after the announcement. -3. **Remove.** A later release removes it, marked `BREAKING` in the CHANGELOG. +Nothing in the External API is deprecated right now. When something is, it is deprecated in the docs +first, with the replacement named and a CHANGELOG entry to match. It then keeps working for at least +one more minor version. A later release removes it, marked `BREAKING` in the CHANGELOG. ## What is not under the contract -These can change at any time without notice: +These can change in any release, without notice: -- Transport framing, internal message routing, and any HTTP path other than `/mcp` and `/v1/integrations/exchange`. -- The handshake file at `~/Library/Application Support/TablePro/mcp-handshake.json`. Use the bundled `tablepro-mcp` bridge rather than parsing it. -- Token storage. Tokens are managed in Settings. -- The audit log format at `~/Library/Application Support/TablePro/mcp-audit.db`. Read it through the activity window. -- The connections file at `~/Library/Application Support/TablePro/connections.json`. Its shape can shift between minor versions; fall back to `list_connections`. +- Transport framing, internal message routing, and any HTTP path other than `/mcp` and + `/v1/integrations/exchange`. +- The handshake file at `~/Library/Application Support/TablePro/mcp-handshake.json`. Use the bundled + `tablepro-mcp` bridge instead of parsing it. +- Token storage. Tokens are managed in **Settings > Integrations**. +- The audit log at `~/Library/Application Support/TablePro/mcp-audit.db`. Read it through the + activity window. +- The connections file at `~/Library/Application Support/TablePro/connections.json`. Its shape can + shift between minor versions; use `list_connections` instead. -## Reporting issues +## Reporting a contradiction -If something contradicts these rules, file an issue at [github.com/TableProApp/TablePro](https://github.com/TableProApp/TablePro/issues). Include the TablePro version, the call, expected and actual behavior, and the matching activity log row if there is one. +File an issue at [github.com/TableProApp/TablePro](https://github.com/TableProApp/TablePro/issues) +with the TablePro version, the call, expected and actual behavior, and the matching activity log row +if there is one. diff --git a/docs/faq.mdx b/docs/faq.mdx index d16e4e272..5c698ab8a 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -3,14 +3,16 @@ title: FAQ description: Short answers on licensing, privacy, offline use, database coverage, syncing, and moving from another client --- -Each answer is the short version, with a link to the page that goes deeper. Anything not covered is worth an issue on [GitHub](https://github.com/TableProApp/TablePro/issues), where driver requests and support questions are tracked in the open. +import DriverCounts from "/snippets/driver-counts.mdx"; + +Short answers only, each linking to the page that carries the detail. Anything not covered is worth an issue on [GitHub](https://github.com/TableProApp/TablePro/issues), where driver requests and support questions are tracked in the open. ## Cost and licensing -Yes. It runs without a license and has no trial countdown, so every driver, the SQL editor, import and export, and the AI assistant work unlicensed. A license adds the features in the tier table and nothing else changes. See [Licensing](/features/licensing). +Yes, and there is no trial countdown. A license adds the features in the tier table and changes nothing else. See [Licensing](/features/licensing). @@ -18,11 +20,11 @@ Two tiers. Starter covers iCloud Sync, encrypted connection export, environment -Yes. The app is free to run and nothing counts seats. To share connections and saved queries with teammates, the Team tier hands out seats from your account on tablepro.app, and each teammate activates with an invite code instead of a key. See [Team Plan](/features/team). +Yes. Nothing counts seats. To share connections and saved queries with teammates, the Team tier hands out seats from your account on tablepro.app, and each teammate activates with an invite code instead of a key. See [Team Plan](/features/team). -The GNU Affero General Public License v3.0. The full source, plugins included, is on [GitHub](https://github.com/TableProApp/TablePro). That is a separate thing from a TablePro license key, which activates the paid features rather than the app itself. +The GNU Affero General Public License v3.0, plugins included. A TablePro license key is a separate thing: it activates the paid features, not the app. @@ -32,11 +34,19 @@ The GNU Affero General Public License v3.0. The full source, plugins included, i -Your queries, results, and schemas stay on your Mac. The one thing TablePro sends on its own is an anonymous heartbeat every 24 hours: a hashed machine ID, platform, app and OS version, architecture, language, which database types are open, how many connections are open, and whether a license is present. Turn it off with **Share anonymous usage data** in **Settings > General**. AI is the exception, since the context you allow reaches the provider you configured. See [Settings](/customization/settings) and [AI Assistant](/features/ai-assistant). +Your queries, results, and schemas stay on your Mac. TablePro sends one anonymous heartbeat every 24 hours, carrying no query text, hostnames, database names, or connection names; [Privacy](/security/privacy) lists every field in it. Turn it off with **Share anonymous usage data** in **Settings > General**. AI is the exception, since the context you allow reaches the provider you configured. -Passwords go into the macOS Keychain, not the connections file, and an ordinary export leaves them out. Publishing to the Team Library sends the connection definition and query text only, and MCP clients query through your saved connections without seeing a password. Two exceptions: **Copy Connection String** puts the password on the clipboard in plain text, and an encrypted export (Starter) carries credentials behind a passphrase. See [Connection Sharing](/features/connection-sharing). +Passwords go into the macOS Keychain, not the connections file. An ordinary export leaves them out, the Team Library carries the connection definition and query text only, and MCP clients query through your saved connections without seeing one. Two exceptions: **Copy Connection String** puts the password on the clipboard in plain text, and an encrypted export (Starter) carries credentials behind a passphrase. See [Connection Sharing](/features/connection-sharing). + + + +Under `~/Library/Application Support/TablePro`: connection definitions without their passwords, query history, saved queries, open tabs, per-table filters, and any plugin you installed. Preferences are in `~/Library/Preferences/com.TablePro.plist`. [Privacy](/security/privacy) names every file and what it holds. + + + +Drag the app to the Trash, then delete the Application Support folder and the preferences file. Keychain items and anything already synced to iCloud have to go separately. [Installation](/installation) has the commands. @@ -46,19 +56,19 @@ Passwords go into the macOS Keychain, not the connections file, and an ordinary -27, from MySQL, PostgreSQL, and SQLite through MongoDB, Redis, ClickHouse, BigQuery, and DuckDB. The table on the [home page](/) lists each one, and [Managing Connections](/connections) adds the transport each supports and whether it can go through an SSH tunnel, SSL, Cloudflare Tunnel, Cloud SQL Proxy, or a SOCKS proxy. +The table on the [home page](/) lists all 27 with their default ports. [Managing Connections](/connections) adds the transport each supports and whether it can go through an SSH tunnel, SSL, Cloudflare Tunnel, Cloud SQL Proxy, or a SOCKS proxy. -Its driver is probably not installed yet. The database type chooser badges registry drivers **Not Installed**; pick one anyway and the download runs before the connection opens, with no restart. **Settings > Plugins > Browse** holds the full catalog. If the database is genuinely missing, open a request on [GitHub](https://github.com/TableProApp/TablePro/issues). See [Plugins & Themes](/features/plugins). +Its driver is probably not installed yet. The database type chooser badges registry drivers **Not Installed**; pick one anyway and the download runs before the connection opens, with no restart. **Settings > Plugins > Browse** holds the full catalog. See [Plugins & Themes](/features/plugins). -Five drivers ship inside the app and cover 9 databases. Another 17 drivers come from the registry and install on the first connection that needs them, which keeps the app download at about 20 MB. Each one must match the SHA-256 checksum in the registry manifest and carry a valid code signature before it loads. + Each one must match the SHA-256 checksum in the registry manifest and carry a valid code signature before it loads. -An app update can raise the plugin interface version, and a plugin built against the old one is rejected at load instead of crashing. TablePro repairs it on its own: at launch it fetches the registry and reinstalls a compatible build, retrying when the registry is unreachable. Until then the plugin sits in a banner in **Settings > Plugins > Installed** with the reason and an action. +An app update can raise the plugin interface version, and a plugin built against the old one is rejected at load rather than crashing. TablePro repairs it at the next launch by reinstalling a compatible build from the registry. Until then the plugin sits in a banner in **Settings > Plugins > Installed** with the reason and an action. @@ -68,7 +78,7 @@ An app update can raise the plugin interface version, and a plugin built against -Connecting, browsing, and querying need nothing but the database. The license is re-verified offline on every launch from the signed copy on disk, and when the license server cannot be reached it keeps working for 30 days after the last successful validation. What does need the network: installing or updating a plugin, and any AI provider that is not local. See [Licensing](/features/licensing). +Connecting, browsing, and querying need nothing but the database. What does need the network: installing or updating a plugin, and any AI provider that is not local. A license keeps working for 30 days past its last successful validation when the license server cannot be reached. See [Licensing](/features/licensing). @@ -76,11 +86,11 @@ Through iCloud, with a Starter or Team license. Sync is off by default and each -Yes, for iPhone and iPad on iOS 18 or later. It reads the same CloudKit data as the Mac app but carries connections, groups, and tags only, and it updates on its own schedule rather than with a Mac release. See [iCloud Sync](/features/icloud-sync) and [iOS Shortcuts](/external-api/ios-shortcuts). +Yes, for iPhone and iPad on iOS 18 or later. It reads the same CloudKit data as the Mac app but carries connections, groups, and tags only. See [iPhone and iPad](/ios). -No. macOS 14 or later, plus iPhone and iPad on iOS 18 or later. A Linux port is a prototype with nothing to install yet, and there is no Windows build at all. Progress is tracked on [GitHub](https://github.com/TableProApp/TablePro). +No, macOS and iOS only. A Linux port is a prototype with nothing to install yet, and there is no Windows build at all. Progress is tracked on [GitHub](https://github.com/TableProApp/TablePro). @@ -90,7 +100,7 @@ No. macOS 14 or later, plus iPhone and iPad on iOS 18 or later. A Linux port is -**File > Import from Other App…** reads the connections TablePlus already has, groups included, and TablePlus does not need to be running. TablePlus keeps each password as its own Keychain item, so macOS asks per item; choose **Always Allow** and the run finishes without asking again. Saved queries, query history, and column widths do not come across. The same dialog handles Sequel Ace, DBeaver, DataGrip, Beekeeper Studio, and Navicat. See [Switching to TablePro](/switching). +**File > Import from Other App…** reads the connections TablePlus already has, groups included, and TablePlus does not need to be running. Each password is its own Keychain item, so macOS asks per item; choose **Always Allow** and it stops asking. Saved queries, query history, and column widths do not come across. The same dialog handles Sequel Ace, DBeaver, DataGrip, Beekeeper Studio, and Navicat. See [Switching to TablePro](/switching). diff --git a/docs/features/acknowledgements.mdx b/docs/features/acknowledgements.mdx index 0eda7e736..729302e14 100644 --- a/docs/features/acknowledgements.mdx +++ b/docs/features/acknowledgements.mdx @@ -3,40 +3,13 @@ title: "Acknowledgements" description: "The open source libraries TablePro ships, and the licenses that cover them" --- -TablePro is built on open source work by other people. **Help › Acknowledgements** lists every library the app redistributes, with its version, its license, and the full license text. +**Help > Acknowledgements** opens a window listing every library that ends up inside the app or one of its plugin bundles. That covers the C libraries linked into the drivers, MariaDB Connector/C, libpq, FreeTDS, libssh2, hiredis, the MongoDB C driver, the DataStax C/C++ driver, DuckDB and OpenSSL among them; the Swift packages the app links, such as Sparkle, Yams and swift-certificates; and the editor packages vendored into the repository. -## What the list covers +Pick one and the detail pane gives its exact shipped version, its SPDX identifier, its copyright lines, and the full license text. All of it is selectable, so a compliance list is a matter of copy and paste. **Homepage** and **License** link out to the project itself. -Every library that ends up inside the shipped app or inside one of its plugin bundles: +Two labels carry meaning: -- The C libraries linked into the database drivers, such as MariaDB Connector/C, libpq, FreeTDS, libssh2, hiredis, the MongoDB C driver, the DataStax C/C++ driver, DuckDB and OpenSSL. -- The Swift packages the app links, such as Sparkle, Yams and swift-certificates. -- The editor packages vendored into the repository. +- **Modified** marks a library TablePro patches before building. The patches live in `scripts/patches/` in the public repository, next to the build script that pins the version. FreeTDS is patched for federated authentication and libssh2 for backported security fixes; nothing else is. +- **License Not Yet Confirmed** is a separate section for libraries whose own project publishes no license file. TablePro cannot state one for them, and lists them anyway so the gap stays visible. -Each entry names the exact version that shipped, so the license shown is the one that applied to that release and not to some later one. - -## Why the full text is there - -Most of these licenses require it. MIT asks for "the above copyright notice and this permission notice" in all copies. BSD asks for the conditions and the disclaimer. The PostgreSQL license names the two paragraphs that have to travel with it. MariaDB Connector/C is LGPL 2.1 and FreeTDS is LGPL 2.0, and section 6 of each asks the distributed work to say that the library is used, that its use is covered by that license, and to include a copy of it. - -So the app ships the license bodies rather than a credits list. A name and a version alone would not meet the terms. - -## Modified libraries - -An entry marked **Modified** is one TablePro patches before building. The patches live in `scripts/patches/` in the public repository, next to the build script that pins the version, so the modified source is available alongside the unmodified upstream release it is based on. - -Two libraries are patched today: FreeTDS, for federated authentication support, and libssh2, for backported security fixes. - -## Libraries whose license is not yet confirmed - -A small number of entries appear under **License Not Yet Confirmed**. These are libraries whose own project does not publish a license file, so TablePro cannot state one for them. They are listed rather than left out, because leaving them out would make the list look complete when it is not. - -## Keeping the list honest - -The version in each entry is checked against the version the build actually pins. `ThirdPartyLicenseInventoryTests` reads the pins out of `scripts/build-*.sh` and out of `Package.resolved`, and fails the test suite when an entry disagrees with them, or when a library is built with no entry at all. - -The check does not correct the version by itself. A version bump can change a license, as OpenSSL's did at 3.0 and Redis's did at 7.4, so the failure is there to make someone re-read the upstream license before the entry moves. - -## Reporting a problem - -If a library is missing, attributed to the wrong license, or credited incorrectly, [open an issue](https://github.com/TableProApp/TablePro/issues). TablePro's own source is available under the AGPL v3 at [github.com/TableProApp/TablePro](https://github.com/TableProApp/TablePro). +TablePro's own source is available under the AGPL v3 at [github.com/TableProApp/TablePro](https://github.com/TableProApp/TablePro). If a library is missing, credited wrongly, or attributed to the wrong license, [open an issue](https://github.com/TableProApp/TablePro/issues). diff --git a/docs/features/ai-assistant.mdx b/docs/features/ai-assistant.mdx index 1dcf031ef..2c26ccad2 100644 --- a/docs/features/ai-assistant.mdx +++ b/docs/features/ai-assistant.mdx @@ -3,147 +3,129 @@ title: AI Assistant description: "Built-in AI for SQL: chat with tool calling, inline suggestions, explain, optimize, and fix-error across 14 providers" --- -One provider is active at a time, and it answers every AI request in the app, from the chat panel to the ghost text in the editor. Switch provider and all of them switch with it. Provider keys stay in the macOS Keychain. +One provider is active at a time and answers every AI request in the app, from the chat panel to the ghost text in the editor. Switch it and all of them switch with it. Keys are yours and live in the macOS Keychain, so every request bills to your own account with that provider. ## Configure a provider -Open **Settings > AI** (`Cmd+,`). The **Enable AI Features** toggle at the top gates the whole tab; turning it off also hides the per-connection AI policy in the connection form. +Open **Settings > AI** (`Cmd+,`). **Enable AI Features** at the top gates the whole tab; turning it off also hides the per-connection AI policy in the connection form. AI settings AI settings -1. Click **Add Provider…** and pick a type: GitHub Copilot, ChatGPT, Cursor, Claude, Claude Agent, OpenAI, OpenRouter, Gemini, xAI, Ollama, llama.cpp, MLX, or OpenCode Zen. **Add Custom Provider…** at the bottom of the menu takes any OpenAI-compatible endpoint. -2. Enter the API key, or sign in for Copilot, ChatGPT, Cursor, and xAI. -3. Enter a model name, or pick one from the fetched list. -4. Click **Test Connection**. + + + Click **Add Provider…** and pick a type. **Add Custom Provider…** at the bottom takes any OpenAI-compatible endpoint. + + + Paste an API key, or sign in for GitHub Copilot, ChatGPT, Cursor, and xAI. + + + Type a model name or pick one from the fetched list, then click **Test Connection**. + + -The **Active Provider** picker at the top selects which provider handles AI requests. For reasoning models (Claude, OpenAI, xAI, ChatGPT), the provider detail sheet adds a per-model reasoning effort picker. Reasoning replies render their thinking in a collapsible **Reasoning** block above the answer. +The **Active Provider** picker decides which one handles requests. Where a model supports reasoning effort, its detail sheet adds a picker for it and replies render their thinking in a collapsible **Reasoning** block. -Provider notes: +With no preference, start with Claude or OpenAI on an API key: both take a key, tools, and images with no further setup. The others each come with something to know. -- **GitHub Copilot**: signs in with GitHub's device flow. Tool calls go through Copilot's tool bridge and the same approval flow as other providers. -- **ChatGPT**: sign in with your ChatGPT account to use the Codex quota from Plus, Pro, Business, and Enterprise plans, no API key. **Import from Codex CLI** reuses an existing Codex login. Unofficial interface; may change. -- **Claude Agent**: runs the `claude` command line tool that Claude Code installs, so AI chat bills against your Claude subscription instead of an API key. Install Claude Code and run `claude /login` in Terminal first; there is no API key field. Pick Opus, Sonnet, or Haiku from the model list. It answers from your schema when the MCP server is on in **Settings > Integrations**, and falls back to plain chat when it is off. The provider sheet lists the tradeoffs, and they are worth reading before you commit to this path: - - TablePro runs `claude` in headless mode. Claude Code is built for you to use directly, not as a backend for other apps, so this path can break with any Claude Code release. - - Your subscription is governed by Anthropic's [Consumer Terms](https://www.anthropic.com/legal/consumer-terms) and [Usage Policy](https://www.anthropic.com/legal/aup). Read them and decide whether this use fits. TablePro cannot grant rights under them. - - Replies count against your Claude subscription limits, shared with claude.ai and your own Claude Code sessions, so running out here stops your work there too. - - Prompts, schema, and query text reach Anthropic under your consumer plan and its privacy settings, not the API's commercial terms. - - `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` are removed from the tool's environment, so replies always draw on the subscription. - - For a metered path on Anthropic's commercial terms, use the **Claude** provider with an API key. -- **Cursor**: paste an API key from the Cursor dashboard, or leave it blank and click **Sign in with Cursor** (needs the Cursor CLI installed). Cursor runs as an agent, not a chat-completions endpoint. It cannot call TablePro's database tools, so Edit and Agent modes do not run queries through it. -- **xAI**: paste an API key, or click **Sign in with xAI** to use a SuperGrok or X Premium+ subscription. Sign-in opens a Grok Build consent screen; this path is unofficial and may change. Preset models: Grok 4.5 and Grok 4.3. -- **Ollama, llama.cpp, MLX**: local providers, no API key. Each preset fills in its default endpoint (`http://localhost:11434` for Ollama, `http://localhost:8080` for `llama-server` and `mlx_lm.server`); edit it if you moved the host or port. Start the server first, then **Test Connection** and pick a model from the fetched list. llama.cpp and MLX speak the OpenAI-compatible API; for tool calling, start `llama-server` with `--jinja`. For any other OpenAI-compatible server, use the custom endpoint. +| Provider | Sign-in | Worth knowing | +|---|---|---| +| GitHub Copilot | GitHub device flow | Runs its own tool loop, so the tool call limit never applies | +| ChatGPT | Your ChatGPT account, on the Codex quota from Plus, Pro, Business, and Enterprise plans. **Import from Codex CLI** reuses an existing login | Unofficial interface, may change | +| Cursor | API key, or **Sign in with Cursor** with the Cursor CLI installed | Runs as an agent, not a chat-completions endpoint, so it cannot call TablePro's tools: Edit and Agent modes run no queries through it | +| xAI | API key, or **Sign in with xAI** on a SuperGrok or X Premium+ subscription | Sign-in opens a Grok Build consent screen and is unofficial. Presets: Grok 4.5, Grok 4.3 | +| Ollama, llama.cpp, MLX | None. Start the server first | Presets fill in `http://localhost:11434` for Ollama and `http://localhost:8080` for `llama-server` and `mlx_lm.server`. Start `llama-server` with `--jinja` for tool calling | +| Claude Agent | Claude Code's `claude` command, no key | Bills against a Claude subscription, with tradeoffs | + +### Claude Agent + +Install Claude Code, run `claude /login` in Terminal, then pick Opus, Sonnet, or Haiku. Replies bill against your Claude subscription instead of an API key. The provider answers from your schema when the MCP server is on in **Settings > Integrations**, and falls back to plain chat when it is off. + +The `claude` tool runs headless here. Claude Code is built for you to use directly rather than as a backend for other apps, so this path can break with any Claude Code release. Your subscription is governed by Anthropic's [Consumer Terms](https://www.anthropic.com/legal/consumer-terms) and [Usage Policy](https://www.anthropic.com/legal/aup), which TablePro cannot grant rights under. Replies also count against the limits you share with claude.ai and your own Claude Code sessions, so running out here stops your work there. For a metered path on commercial terms, add the **Claude** provider with an API key. ## Chat -Open the inspector (`Cmd+Option+I` or the toolbar inspector button) and switch the segmented control at the top from **Details** to **AI Chat**. There is no dedicated chat shortcut or menu item. +Open the inspector (`Cmd+Option+I`) and switch the segmented control at its top from **Details** to **AI Chat**. There is no dedicated chat shortcut or menu item. AI chat panel AI chat panel -Type a question and press Return. Code blocks have **Copy** and **Insert** buttons; Insert fills the current query tab if it is empty, otherwise opens a new tab. Token counts show below each response. Failed responses show **Retry**, successful ones **Regenerate**, a reply paused at the tool call limit shows **Continue**, and **Stop** cancels a streaming reply. +Type a question and press Return. Code blocks carry **Copy** and **Insert**, and Insert fills the current query tab when it is empty and opens a new one otherwise. A token count sits under each response, a failed one offers **Retry**, a finished one **Regenerate**, and **Stop Generating** cancels a reply mid-stream. -Conversations auto-save and auto-title from your first message. The clock icon in the inspector header opens recent conversations; the pencil-and-square icon starts a new one. Editing a previously sent message puts its text and context attachments back in the composer and removes that turn and everything after it; send to re-run. +Conversations save themselves and take their title from your first message. The clock icon in the inspector header opens recent ones; the pencil-and-square icon starts a new one. Editing a message you already sent puts its text and attachments back in the composer and drops that turn and everything after it. -If the provider supports images (Claude, OpenAI, xAI), paste or drag images into the composer. They appear as thumbnail chips and are sent with your message. +Paste or drag images into the composer on any provider that takes them, which is all of them except GitHub Copilot, Cursor, ChatGPT, and Claude Agent. -### Chat modes (ask / edit / agent) +### Chat modes -The mode picker in the composer footer controls which tools the AI can call. The mode is an app-level setting: it persists across chats and restarts, and fresh installs start in **Ask**. +The mode picker in the composer footer controls which tools the AI can call. It is an app-level setting that survives restarts, and a fresh install starts in **Ask**. | Mode | Tools available | When to use | |------|----------------|-------------| | **Ask** | Read-only schema lookups: `list_connections`, `get_connection_status`, `list_databases`, `list_schemas`, `list_tables`, `describe_table`, `get_table_ddl`. | Questions, exploration, drafting queries you run yourself. | | **Edit** | All Ask tools, plus `execute_query` for `SELECT`, `INSERT`, `UPDATE`, `DELETE`. Destructive DDL (`DROP`, `TRUNCATE`, `ALTER…DROP`) stays blocked. | Letting the AI run the queries it proposes. | -| **Agent** | All tools, plus `confirm_destructive_operation` for destructive DDL. Runs tools in a loop, up to 25 tool calls per turn by default. | Multi-step migrations and schema changes. | +| **Agent** | All tools, plus `confirm_destructive_operation` for destructive DDL. Runs tools in a loop. | Multi-step migrations and schema changes. | -Mode and [safe mode](/features/safe-mode) are independent gates. Agent mode does not bypass safe mode. - -These tools never change what's open in the app. `list_tables`, `list_schemas`, `describe_table`, `get_table_ddl`, and `execute_query` take an optional `database` and query the database or schema they're pointed at, without moving the sidebar's selected database or touching an open tab. +Mode and [Safe Mode](/features/safe-mode) are independent gates. Agent mode does not bypass Safe Mode. ### Tool calling -In Edit and Agent modes, each tool call appears as a card in the reply. Read-only tools run immediately. Write tools show three buttons: - -- **Run**: approve this single call. -- **Always for this connection**: approve and whitelist the tool for this connection. Whitelists are per-tool and per-connection, and sync with the connection via iCloud. -- **Cancel**: reject the call. The model gets an error result and continues. +In Edit and Agent modes each tool call appears as a card in the reply. Read-only tools run immediately. Write tools wait for **Run**, for **Always for this connection** (which whitelists that one tool on that one connection and syncs with it over iCloud), or for **Cancel**, which hands the model an error result and lets it carry on. Per-card tool approval Per-card tool approval -If the connection's safe-mode level is **Silent**, write tools auto-approve. If it is **Read-Only**, they auto-deny. Destructive operations are the exception: `confirm_destructive_operation` always requires a per-call click. Silent mode does not auto-approve it, and **Always for this connection** is refused for it. The tool also requires the model to pass the verbatim phrase `I understand this is irreversible`. +Safe Mode **Silent** auto-approves write tools and **Read-Only** auto-denies them. Destructive operations are the exception: `confirm_destructive_operation` always needs a click, Silent does not auto-approve it, **Always for this connection** is refused for it, and the model must pass the verbatim phrase `I understand this is irreversible`. -Provider support for tool calling: every provider except Cursor. Claude Agent reaches the same tools through the MCP server, so it needs that server turned on. Ollama, llama.cpp, MLX, and custom endpoints depend on the model. +Every provider except Cursor can call tools. Claude Agent reaches them through the MCP server, so that has to be on, and local models depend on the model. -### Tool call limit - -A reply may make 25 tool calls before it pauses. The pause keeps every tool call and result already in the reply, and shows **Continue** to carry on with a fresh budget, plus **Adjust Limit** to open the setting. Change the number, or turn the limit off, under **Agent** in [Settings > AI](/customization/settings#agent). A higher limit costs more tokens per reply and can dilute the answer as the conversation grows. - -GitHub Copilot runs its own tool loop, so the limit and the pause do not apply to it. +A reply pauses after 25 tool calls, keeps everything it has done, and offers **Continue** for a fresh budget or **Adjust Limit** to change the number, 5 to 200, under **Agent** in **Settings > AI**. Every call is another request with the schema attached, so a higher limit costs tokens. ### Attach context with `@` -Type `@` in the composer to open a picker at the caret: **Schema**, a specific **Table**, **Current Query**, **Query Results**, or a **Saved Query** ([favorites](/features/favorites)). Up/Down navigates, Return or Tab inserts, Escape dismisses. The `@` button in the composer footer offers the same items minus saved queries. - -Attached items show as chips. Saved query chips resolve at send time, so the AI sees the query's current SQL. +Type `@` in the composer to open a picker at the caret: **Schema**, a specific **Table**, **Current Query**, **Query Results**, or a **Saved Query** ([favorites](/features/favorites)). Return or Tab inserts, Escape dismisses. The `@` button in the footer offers the same list minus saved queries. A saved query chip resolves at send time, so the AI sees its current SQL. ### Slash commands -Type `/` to run a command: **`/explain`**, **`/optimize`**, and **`/fix`** act on the active query; **`/help`** lists commands. Add your own under **Settings > AI > Custom Slash Commands**. Templates substitute `{{query}}`, `{{schema}}`, `{{database}}`, and `{{body}}` (text typed after the command) at send time. +Type `/` for **`/explain`**, **`/optimize`**, and **`/fix`**, which act on the active query, or **`/help`** for the list. Add your own under **Settings > AI > Custom Slash Commands**: a template substitutes `{{query}}`, `{{schema}}`, `{{database}}`, and `{{body}}`, the text typed after the command, at send time. ### Model picker -The cpu icon next to the mode picker lists every configured provider and its models. Your pick overrides the active provider for this chat panel and stays until you pick something else; it does not reset between turns. - -## Per-connection AI rules - -Pin plain-text context to a connection so the AI sees it on every turn: open the connection's edit form and pick **AI Rules** in the sidebar. Use it for facts the schema cannot show, like `Always filter orders by deleted_at IS NULL` or `Never select users.ssn`. The text is appended to the system prompt and syncs with the connection via iCloud. - -## Inline suggestions - -Toggle **Enable inline suggestions while typing** in the AI tab (off by default). After you pause typing, the active provider suggests a completion rendered as ghost text at the cursor. Press `Tab` to accept, `Escape` to dismiss; typing anything else also dismisses it. +The cpu icon beside the mode picker lists every configured provider and its models. A pick there overrides the active provider for this chat panel until you change it, across turns. -The pause length is the **Debounce** setting next to the toggle: 500 ms by default, adjustable from 100 to 3000 ms. With Copilot active, suggestions come from Copilot's inline-completion model; any other provider serves them through chat completions. +## Explain, optimize, and fix -## Editor actions +**Query > Explain with AI** (`Cmd+L`) and **Query > Optimize with AI** (`Cmd+Option+L`) act on the whole query tab; the editor's right-click menu has both and acts on the selection instead, appearing only when text is selected. When a query fails, **Fix with AI** on the inline error banner sends the query and the error. All three open a new conversation and send at once. -**Query > Explain with AI** (`Cmd+L`) and **Query > Optimize with AI** (`Cmd+Option+L`) act on the whole query tab. The editor's right-click menu has the same two items and acts on the selected text; they only appear when text is selected. Either way, a new conversation starts and the request is sent right away. Rebind the shortcuts under **Settings > Keyboard** in the **Editor & Query** category. +The reply comes back as a walkthrough. Optimize and Fix diff your query against the rewritten one, switchable between **Unified** and **Split**; Explain anchors its steps to the query as it stands. Each numbered step carries a one-line reason and a tag (**Critical**, **Change**, or **Context**), expands to the lines it refers to, and offers **Jump to lines** and **Ask about this change**, which keeps a follow-up anchored to that step. **Apply to Editor** replaces the editor content after you confirm; nothing is applied on its own. A model that answers in prose instead renders as a normal message. -## Fix with AI - -When a query fails, the inline error banner above the results shows a **Fix with AI** button. It opens the chat panel, starts a new conversation, and sends the failed query and the error straight to the AI, which answers with a walkthrough. - -## SQL walkthroughs +## Per-connection AI rules -Explain, Optimize, and Fix answer with a walkthrough in the chat panel instead of plain text, so you can review the change before trusting it: +Pin plain-text context to a connection so the AI sees it every turn: open the connection's edit form and pick **AI Rules** in the sidebar. Use it for what the schema cannot show, like `Always filter orders by deleted_at IS NULL` or `Never select users.ssn`. The text joins the system prompt and syncs with the connection over iCloud. -- **Before/after diff.** Optimize and Fix show a diff between your query and the AI's version. Switch between **Unified** and **Split** with the toggle in the walkthrough header. Explain shows the query with the steps anchored to it. -- **Numbered steps.** Each step has a one-line reason and an importance tag (**Critical**, **Change**, or **Context**). Expand a step to see the exact lines it refers to. **Jump to lines** scrolls to them and highlights them for a moment. -- **Ask about a step.** Use **Ask about this change** on any step to send a follow-up question that stays anchored to that step. -- **Apply to Editor.** When the AI rewrote your query, **Apply to Editor** replaces the editor content after you confirm. Nothing is applied automatically. +## Inline suggestions -The AI writes the steps and the rewritten query; TablePro computes the diff itself, so the diff always reflects the real change. If a model returns plain prose instead, the response renders as normal text. +Turn on **Enable inline suggestions while typing** in the AI tab; it is off by default. Pause typing and the active provider suggests a completion as ghost text at the cursor. `Tab` accepts, `Escape` dismisses, and typing anything else dismisses it too. -## Context +The pause is the **Debounce** setting beside the toggle: 500 ms by default, adjustable from 100 to 3,000 ms in 50 ms steps. With Copilot active the suggestions come from Copilot's inline-completion model; every other provider serves them through chat completions. -Under **Settings > AI > Context**: +## What leaves your Mac -| Setting | Default | -|---------|---------| -| Include database schema | On | -| Include current query | On | -| Include query results | Off | -| Max schema tables | 20 | +Every message carries a system prompt built from **Settings > AI > Context**, so these settings decide what the provider sees on every turn, not only the first. -## Privacy +| Setting | Default | What it sends | +|---------|---------|---------------| +| Include database schema | On | Columns and foreign keys for up to **Max schema tables** tables | +| Include current query | On | The editor's text, truncated at 2,000 characters | +| Include query results | Off | The rows in the results grid | +| Max schema tables | 20 | | -Set a per-connection AI policy in the connection form's **Advanced** tab: **Use Default**, **Always Allow**, **Ask Each Time**, or **Never**. The app-wide default is **Ask Each Time**. **Never** also blocks external AI tool calls against the connection. +Ollama, llama.cpp, and MLX run on your own machine, so nothing reaches a third party. Every other provider is a network call to that vendor under the terms of your account with them. -External clients (Raycast, Claude Desktop, and other MCP clients) reach the same tools through the [External API](/external-api), capped by the connection's **External Clients** level and the token's scope; see [Tokens](/external-api/tokens). +Per connection, the **Advanced** pane of the connection form sets an AI policy: **Use Default**, **Always Allow**, **Ask Each Time**, or **Never**. The app-wide default is **Ask Each Time**, and **Never** also blocks external AI tool calls against that connection. External clients such as Raycast and Claude Desktop reach the same tools through the [External API](/external-api), bounded by the connection's **External Clients** level and the token's scope. diff --git a/docs/features/autocomplete.mdx b/docs/features/autocomplete.mdx index c014892ec..8dbbb5aa1 100644 --- a/docs/features/autocomplete.mdx +++ b/docs/features/autocomplete.mdx @@ -3,7 +3,9 @@ title: Autocomplete description: Schema-aware SQL autocomplete for keywords, tables, columns, and functions --- -What the popup offers depends on the clause under the cursor. Tables come after FROM and JOIN, a table's columns after its alias and a dot, and inside WHERE you get the operators your database declares. MongoDB connections complete MQL instead, following the shape of the query rather than the clause. +An alias and a dot give you that table's columns, and that holds for a subquery or a `WITH` table that exists only in the statement you are writing. Everything the popup offers is decided by where the cursor sits. + +It opens itself after FROM, JOIN, ON and the other clauses with a short answer, and stays shut where a full list would be noise, such as after a comma in a SELECT list. `Ctrl+Space` overrides both. -Suggestions appear as you type, from the first character, and after `.` or a space, with a 50ms debounce. There is no minimum prefix. With nothing typed yet, the popup stays hidden in clauses where listing everything is noise (for example after a comma in a SELECT list); it still opens automatically after FROM, JOIN, ON, and other clauses where browsing helps. - | Key | Action | |-----|--------| | `Ctrl+Space` | Open the popup anywhere, even with an empty prefix | @@ -27,19 +27,18 @@ Suggestions appear as you type, from the first character, and after `.` or a spa | `Return` / `Tab` | Accept the selected suggestion | | `Escape` | Dismiss and keep typing | -## Completion types +There is no setting for autocomplete and nothing to turn off. What the popup offers depends entirely on where the cursor sits. -### SQL keywords +## SQL keywords ```sql SEL| -- SELECT FROM users WH| -- WHERE -SELECT * FROM users WHERE name LIKE '%test%' ORD| -- ORDER BY ``` -### Table names +## Table names -Tables appear after FROM, JOIN, INSERT INTO, and similar keywords: +Tables appear after FROM, JOIN, INSERT INTO and similar keywords, and lead the list ahead of keywords there. The clause is read at the cursor, so a second or third JOIN still offers tables. ```sql SELECT * FROM | -- All tables @@ -47,13 +46,9 @@ SELECT * FROM us| -- Tables starting with "us": users, user_roles SELECT * FROM a JOIN b ON a.id = b.id JOIN | -- All tables, even after an ON condition ``` -The clause is detected at the cursor, so a second or third JOIN suggests tables even when an ON condition comes before it. In a table position, tables lead the list ahead of keywords. - -### Column names +## Column names -Columns are suggested in SELECT, WHERE, ORDER BY, GROUP BY, and other column contexts. - -If a FROM clause exists anywhere in the statement (even after the cursor), columns come from those tables. If no FROM clause exists yet, columns from all cached tables appear as fallback; ambiguous names are qualified: `users.id`, `orders.id`. +A FROM clause anywhere in the statement, even after the cursor, decides which tables columns come from. With no FROM clause yet every cached table contributes, and ambiguous names are qualified: `users.id`, `orders.id`. ```sql SELECT na| -- Columns matching "na" from all cached tables @@ -62,20 +57,9 @@ SELECT u.| FROM users u -- Columns from users via alias SELECT * FROM users WHERE | -- Columns from users ``` -### Alias resolution - -Type an alias followed by `.` to see that table's columns: - -```sql -SELECT - u.| -- id, name, email, created_at (from users) -FROM users u -JOIN orders o ON u.id = o.| -- id, user_id, total (from orders) -``` - -### Derived tables and CTEs +## Aliases, subqueries and CTEs -An alias for a subquery (derived table) or a `WITH` table completes the columns that subquery's SELECT list produces, including `AS` renames: +An alias followed by `.` gives that table's columns. A derived table or a `WITH` table gives the columns its own SELECT list produces: ```sql SELECT ahs.| -- country, avg_score @@ -92,25 +76,18 @@ WITH totals AS ( SELECT t.| FROM totals t -- region, total ``` -Explicit (`AS name`), bare (`country`), and qualified (`t.col`) columns resolve. A `SELECT *` subquery and unaliased expressions like `AVG(score)` have no name to suggest, so they are skipped. +`AS name`, bare names and qualified `t.col` all resolve. A `SELECT *` subquery and an unaliased `AVG(score)` have no name to offer, so they are skipped. -### Functions +## Functions and operators -SQL functions appear in SELECT, WHERE, and expression contexts: +Functions appear in SELECT, WHERE and expression contexts, the connection's own dialect alongside the common SQL ones: `CONVERT_TIMEZONE` on Snowflake, `SAFE_CAST` on BigQuery. ```sql -SELECT | -- COUNT, SUM, AVG, MAX, MIN, etc. SELECT COUNT(| -- Columns and * -WHERE date_column > | -- NOW(), CURRENT_DATE, etc. +WHERE date_column > | -- NOW(), CURRENT_DATE, … ``` -Functions from the connection's SQL dialect (e.g. `CONVERT_TIMEZONE` on Snowflake, `SAFE_CAST` on BigQuery) appear alongside the common SQL functions. - -### Operators - -Operators the connection's dialect declares appear in WHERE, ON, HAVING, and AND contexts, each with what it does and the types it works on. - -On PostgreSQL that covers the JSON operators, array and range containment, regex matching, full-text search, and the network operators: +Operators the dialect declares appear in WHERE, ON, HAVING and AND, each with what it does and the types it takes. On PostgreSQL that covers JSON, array and range containment, regex, full-text search and the network operators: ```sql WHERE data -| -- -> (field as json), ->> (field as text) @@ -119,20 +96,39 @@ WHERE tags &| -- && arrays overlap WHERE email ~| -- ~ POSIX regex, ~* case insensitive ``` -The list distinguishes operators that only work on `jsonb` from the ones `json` accepts too, so `@>` is not offered as if it worked on a `json` column. +An operator restricted to `jsonb` is held back on a `json` column rather than offered and rejected by the server. -### Casts +## Casts and enum values -Typing `::` offers the dialect's type names, in the spelling you write rather than the internal catalog name: +Typing `::` offers the dialect's type names in the spelling you write, not the internal catalog name. Comparing against a column whose type declares a fixed set of values, a PostgreSQL enum for instance, offers those values quoted: ```sql SELECT id::| -- integer, bigint, text, timestamptz, jsonb, uuid, … SELECT payload::js| -- json, jsonb, jsonpath +WHERE status = | -- 'pending', 'active', 'archived' +WHERE mood <> 'ha| -- 'happy' ``` -### MongoDB +## Schema names + +On multi-schema engines, schemas complete in FROM and `public.users` resolves in FROM, JOIN, UPDATE, INSERT INTO and CREATE INDEX. + +Where the hierarchy is database, schema, table (Snowflake, BigQuery), every segment completes and a schema you have not opened in the sidebar is fetched on demand: + +```sql +SELECT * FROM ANALYTICS_| -- databases +SELECT * FROM ANALYTICS_PROD.| -- schemas +SELECT * FROM ANALYTICS_PROD.DBT_MARTS.| -- tables in that schema +SELECT * FROM ANALYTICS_PROD.DBT_MARTS.ORDERS o WHERE o.| -- columns +``` + +## Favorite keywords + +A favorite with a keyword, stored in the database or set by `@keyword` frontmatter in a linked file, appears as a top-priority match. Type the keyword, accept it, and the favorite's full SQL replaces it inline. A `;;` in that SQL says where the cursor lands afterwards. See [Favorites](/features/favorites#cursor-placement). + +## MongoDB -MongoDB connections use MQL instead of SQL, and the popup follows the shape of the query rather than SQL clauses. What you get depends on exactly where the cursor is: +MongoDB connections complete MQL, so the popup follows the shape of the query rather than SQL clauses: ```js db.| // collections, plus getCollectionNames(), createCollection(), … @@ -145,56 +141,16 @@ db.orders.aggregate([{ | // $match, $group, $lookup, $unwind, $face db.orders.aggregate([{ $group: { | // $sum, $avg, $first, $push, and the expression operators ``` -The operator sets are kept apart on purpose. Offering `$match` inside a filter document, or `$gte` where a pipeline stage belongs, is worse than offering nothing. `$set` and `$unset` mean different things as an update operator and as a pipeline stage, and the popup describes whichever one applies at the cursor. +`$set` and `$unset` mean different things as an update operator and as a pipeline stage; the description shown is whichever applies at the cursor. -Field names come from a sample of the collection's documents and include nested paths, so a document with `address: { city, zip }` suggests `address`, `address.city` and `address.zip`. Objects inside an array contribute paths too. Shallower fields sort first. Nothing in the query is executed to build the list. - -The sample is cached per collection and cleared when you switch database or refresh the connection. - -Completion is suppressed inside comments, and braces or brackets inside a string literal do not count as opening a document. - -### Enum values - -Comparing against a column whose type declares a fixed set of values suggests those values, quoted and ready to accept. On PostgreSQL that covers enum types: - -```sql -WHERE status = | -- 'pending', 'active', 'archived' -WHERE status IN (| -- same list -WHERE mood <> 'ha| -- 'happy' -``` - -The values come from the column cache the editor already holds, so nothing extra is queried while you type. A column with no declared value set suggests nothing. - -### Favorite keywords - -Favorites you've assigned a keyword to (DB-stored or linked-file `@keyword` frontmatter) appear in the popup as a top-priority match. Type the keyword, accept the suggestion, and the favorite's full SQL replaces the keyword inline. A `;;` in the favorite's SQL sets where the cursor lands after expansion. See [Favorites](/features/favorites#cursor-placement) for how to assign keywords and place the marker. - -### Schema names - -For databases with multiple schemas (PostgreSQL): - -```sql -SELECT * FROM | -- public, schema1, schema2 -SELECT * FROM public.| -- Tables in public schema -``` - -Schema-qualified names like `public.users` resolve in FROM, JOIN, UPDATE, INSERT INTO, and CREATE INDEX. - -For databases organized as database, schema, table (Snowflake, BigQuery), every segment completes. Tables of schemas you haven't opened in the sidebar are fetched on demand: - -```sql -SELECT * FROM ANALYTICS_| -- databases -SELECT * FROM ANALYTICS_PROD.| -- schemas -SELECT * FROM ANALYTICS_PROD.DBT_MARTS.| -- tables in that schema -SELECT * FROM ANALYTICS_PROD.DBT_MARTS.ORDERS o WHERE o.| -- columns -``` +Field names come from a sample of the collection's documents, nested paths included, so `address: { city, zip }` offers `address`, `address.city` and `address.zip`. Objects inside an array contribute paths too, and shallower fields sort first. The sample is cached per collection and cleared when you switch database or refresh the connection. -## Schema cache +Comments suppress completion, and a brace or bracket inside a string literal does not open a document. -On connection, TablePro fetches table names and loads columns in the background. The column cache holds up to 50 tables with LRU eviction. There is no time-based expiry: cached columns stay until you switch databases or refresh the connection. A failed schema load waits 30 seconds before retrying, to avoid hammering the server. +## My new table does not complete -After external schema changes (migrations, CLI work), press `Cmd+R` (**Database > Refresh**) to reload it. Right-clicking the sidebar's Tables header and choosing **Refresh** does the same. +Table and column names are cached at connect, so a migration or a `CREATE TABLE` run from a terminal stays invisible until the cache reloads. Press `Cmd+R` (**Database > Refresh**), or right-click the sidebar's Tables header and choose **Refresh**. Switching databases reloads it too. -## Performance +## Large files -Works on files of any size, including multi-megabyte dumps. For files over 500 KB, only a ~10 KB window around the cursor is analyzed. +Completion works on files of any size, multi-megabyte dumps included. Past 500 KB only a 10 KB window around the cursor is read, so suggestions follow the statements near you rather than the whole file. diff --git a/docs/features/backup-restore.mdx b/docs/features/backup-restore.mdx index a21e902e5..6df883453 100644 --- a/docs/features/backup-restore.mdx +++ b/docs/features/backup-restore.mdx @@ -3,60 +3,64 @@ title: Backup & Restore description: Dump and restore PostgreSQL or Redshift databases with pg_dump and pg_restore, with progress, cancel, and SSH tunnel reuse --- -Dump a PostgreSQL or Redshift database to a `.dump` file with `pg_dump`, and restore one back with `pg_restore`. Both live in the **File** menu on any connected PostgreSQL or Redshift session. - - -PostgreSQL and Redshift only. Restore is disabled on read-only [Safe Mode](/features/safe-mode). Backup stays available on read-only connections because it does not write to the database. - - -## Requirements - -TablePro shells out to your local `pg_dump` and `pg_restore`. Install them with Homebrew: +`pg_dump` and `pg_restore` have to be on your Mac first. TablePro shells out to them and ships no copy of its own: ```bash brew install libpq brew link --force libpq ``` -TablePro finds `pg_dump` and `pg_restore` on `PATH` and the standard Homebrew and Postgres.app locations. +The lookup takes the first match from `/usr/bin/which`, then `/opt/homebrew/bin`, `/usr/local/bin`, and Postgres.app's latest version. That path decides which client version does the work. -## Backup dump + +PostgreSQL and Redshift only. For any other engine, use a SQL export from [Import & Export](/features/import-export). Restore is disabled on a read-only [Safe Mode](/features/safe-mode) connection; backup is not, because it writes nothing to the database. + -1. Choose **File > Backup Dump…**. -2. Pick the database to dump. -3. Choose where to save the `.dump` file. -4. Watch progress. Click **Cancel** to stop and remove the partial file. + + Backup Dump sheet + Backup Dump sheet + -The default filename is `-.dump`. Dumps use the custom archive format (`pg_dump -Fc`) so they round-trip through `pg_restore`. +## Backup dump -When the dump finishes, the result sheet shows the file size and a **Show in Finder** button. + + + Choose **File > Backup Dump…** on a connected session and pick the database. + + + The default filename is `-.dump`. Dumps use the custom archive format (`pg_dump -Fc`), which is what `pg_restore` reads back. + + + **Cancel** confirms, sends `SIGTERM`, and removes the partial file. On success the result sheet gives the size and **Show in Finder**. + + ## Restore dump -1. Choose **File > Restore Dump…**. -2. Pick the `.dump` file. -3. Pick the target database. -4. Watch progress. Click **Cancel** to stop. The target database is not rolled back; review and clean up as needed. - -Restore progress is indeterminate: the live byte counter runs only during backups, so long restores show activity without a percentage. + + + Choose **File > Restore Dump…** and select a file `pg_dump` wrote in custom archive format. + + + `pg_restore` runs with `--no-owner --no-acl`, so the connection user ends up owning the restored objects. + + + Restore shows no percentage. **Cancel** confirms and sends `SIGTERM`, and the target database is left as it stands: drop it and restore into a fresh one, or clean up the partial objects yourself. + + -`pg_restore` runs with `--no-owner --no-acl`, so the connection user owns the restored objects. TablePro does not pass `--clean`; restoring on top of an existing schema with conflicting objects produces stderr errors that show in the result sheet. +No `--clean` is passed, so restoring on top of a schema that already holds conflicting objects produces errors instead of replacing them. ## SSH tunnels and SSL -Both flows reuse the connection's active SSH tunnel. No second port forward is opened. SSL connections pass their mode to the tools via `PGSSLMODE`, including `verify-ca` and `verify-full`. - -## Cancelling - -Cancel asks for confirmation, then sends `SIGTERM`. - -- **Backup**: TablePro removes the partial `.dump` file. -- **Restore**: the target database may be left in a partial state. Drop it and restore again into a fresh database, or clean up the partial objects manually. +Both flows reuse the connection's active SSH tunnel, with no second port forward. SSL connections pass their mode to the tools through `PGSSLMODE`, `verify-ca` and `verify-full` included. ## Failures -A non-zero exit shows the last 64 KB of `pg_dump` or `pg_restore` stderr in a scrollable monospaced view. Common causes: +A non-zero exit shows the last 64 KB of `pg_dump` or `pg_restore` stderr in a scrollable monospaced view. Three causes account for most of them. -- **Binary not found**: install `libpq` and link it so `pg_dump` and `pg_restore` are on your `PATH`. -- **Authentication failed**: TablePro passes the connection password via `PGPASSWORD` and runs the tools with `--no-password` to avoid a TTY prompt. If the role lacks `LOGIN` or the database is wrong, the failure surfaces here. -- **Restore conflict**: the target database has objects that conflict with the dump. Restore into a fresh database or drop the conflicting objects first. +| What you see | What to do | +|---|---| +| *"pg_dump was not found on this system. Install it with `brew install libpq` and link it."* | Install and link `libpq` so the binaries are on `PATH` | +| An authentication failure | The password goes through `PGPASSWORD` and the tools run with `--no-password`, so this is the role or the database rather than a missing prompt. Check that the role has `LOGIN` | +| Objects that conflict with the dump | Restore into a fresh database, or drop the conflicting objects first | diff --git a/docs/features/change-tracking.mdx b/docs/features/change-tracking.mdx index 3ee1d068a..cc04110f9 100644 --- a/docs/features/change-tracking.mdx +++ b/docs/features/change-tracking.mdx @@ -1,34 +1,67 @@ --- title: Change Tracking -description: Queue cell edits, row inserts, and deletions locally, review the SQL, then save +description: Queue cell edits, row inserts and deletions, review the SQL, then save --- -Changes are queued in memory, not applied immediately. Edit cells, insert rows, delete rows, then review everything before saving. Nothing touches the database until you click **Save Changes**. The queue is per-tab; switching tabs preserves your edits. +import StagedUntilSave from "/snippets/staged-until-save.mdx"; + + The toolbar counts what is pending, modified cells stay highlighted, `Cmd+Shift+P` shows the statements the queue will produce, and `Cmd+S` runs them. The queue belongs to the tab, so switching tabs leaves it alone. Pending edits, inserts, and deletions highlighted in the data grid Pending edits, inserts, and deletions highlighted in the data grid -## Data changes +## Editing a cell + +Double-click a cell, or press `Enter` on it. `Enter` commits the edit to the queue, `Escape` cancels. Setting a value back to what it was drops it from the queue again. Some types open an editor of their own: + +| Column type | Editor | +|---|---| +| Date, time, datetime, timestamp | Calendar and time picker, or edit as text | +| Foreign key | Searchable list of referenced values | +| Boolean, `BIT`, `TINYINT(1)` | Checkbox, with a third state for nullable columns | +| `ENUM` | Searchable value list | +| `SET` (MySQL/MariaDB) | Multi-select checkboxes | +| Array of a simple type (PostgreSQL) | Ordered list, one row per element | +| `JSON`, `JSONB`, `BLOB`, binary | [Cell viewers](/features/json-viewer) | + + + Cell editor + Cell editor + + +Right-click an editable cell and open **Set Value** for the common ones without typing: **Empty**, **NULL** on a nullable column, **Default** on a column that has one, and date functions such as `NOW()` or `CURRENT_DATE` on date columns. + +## Adding and deleting rows + +- **Add Row** (`Cmd+Shift+N`), the **+** button, or **Edit > Add Row**. The row appears at the bottom, columns with a default pre-filled as `DEFAULT`. Later edits to it fold into the INSERT rather than queueing as updates. +- **Duplicate Row** (`Cmd+Shift+D`) on the right-click menu copies a row and resets its primary key to `DEFAULT`, so the database assigns a new one. +- **Delete**: select rows by their row numbers (`Shift`-click for a range, `Cmd`-click for separate rows) and press `Delete`. They stay visible with a strikethrough, and saving asks "Delete 5 rows?" before they go. +- **Paste** on the right-click menu inserts copied rows as new rows. +- **Fill Column** on the header right-click menu writes one value into every loaded row, skipping primary key columns. + +## When the grid will not edit -**Edit cells**: double-click a cell to edit (see [Data Grid](/features/data-grid)). Changes queue immediately. Changing a value back to its original removes it from the queue. Columns the driver marks immutable, such as MongoDB's `_id`, cannot be edited. +A tab opened from the sidebar edits its table directly. A query tab edits only when the app can prove the rows came from exactly one table: - -Generated columns are read-only: MySQL and MariaDB `STORED` and `VIRTUAL` columns, PostgreSQL `GENERATED ALWAYS AS … STORED` columns, and ClickHouse `MATERIALIZED` and `ALIAS` columns. The server computes them and rejects any value you send, so TablePro leaves them out of every INSERT and UPDATE it writes, including SQL export. - +| Query | Editable | +|---|---| +| `SELECT * FROM orders o WHERE o.id = 1` | Yes, alias or not | +| Comments, `DISTINCT`, `ORDER BY`, `LIMIT`, `FOR UPDATE` | Yes | +| `SELECT * FROM public.orders` on the schema you are browsing | Yes | +| The same query naming any other schema | No | +| Joins, comma joins, subqueries in `FROM`, CTEs, `UNION`, `EXCEPT`, `INTERSECT`, `FROM ONLY` | No | -**Add rows**: click **+** in the toolbar, use **Edit > Add Row**, or press `Cmd+Shift+N`. A new row appears at the bottom of the grid; columns with default values are pre-filled with `DEFAULT`. Edits to cells in a new row fold into the INSERT, not tracked as separate updates. +To change rows a join returned, open one of its tables from the sidebar and edit there. To edit through another schema, switch the session to that schema first. -**Delete rows**: select rows and press `Delete`. Deleted rows show a strikethrough and stay visible until you save. Saving asks you to confirm before the rows are permanently deleted. +Two more shapes look editable and refuse. A column renamed with `AS` cannot be written back, though the rest of the row still can. A query that renames or omits the primary key blocks the save outright, because nothing is left to identify the row by. - -Deleting many rows saves as one batched DELETE with OR-combined conditions, not one query per row. Very large selections are split into a few statements to stay within the engine's bind-parameter limit. - +Individual columns stay read-only where the server owns them: generated columns, which are computed on write and left out of every INSERT and UPDATE, SQL export included, and columns the driver marks immutable, such as MongoDB's `_id`. A connection set to the Read-Only [safe mode](/features/safe-mode) level edits nothing at all. ## Saving -Click **Save Changes** (the toolbar checkmark) or press `Cmd+S`. TablePro generates parameterized SQL, executes it, clears the queue, and refreshes the grid. +Press `Cmd+S`, or click the toolbar checkmark. Values go out as bound parameters; `DEFAULT` and SQL functions such as `NOW()` are written into the statement as they stand. | Change | Generated SQL | |--------|--------------| @@ -36,23 +69,20 @@ Click **Save Changes** (the toolbar checkmark) or press `Cmd+S`. TablePro genera | Row insertion | `INSERT INTO … (columns) VALUES (?, …)` | | Row deletion | `DELETE FROM … WHERE pk = ? OR pk = ?` | -Tables without a primary key are updated and deleted by matching every original column value; NULL values match with `IS NULL`. Saving fails with an error only when a row cannot be identified. +A table without a primary key is matched on every original column value instead, with `IS NULL` for the nulls. -There is no discard button. To drop pending changes, undo them, or accept the **Discard Unsaved Changes?** prompt that appears when you refresh, sort, filter, change pages, or close the tab with edits pending. +On an engine with transactions, the statements run inside one, so a failure rolls the whole save back and the table is left as it was. Without them, the statements that ran before the failure stand. Either way a failed save reports **Save Failed** with the server's message and keeps the queue intact, so correct the value and save again. A save that succeeds clears the queue, clears undo, and reloads the grid. -## Previewing SQL +There is no discard button. Undo the edits, or take the **Discard Unsaved Changes?** prompt that appears when you refresh, sort, filter, change page, or close the tab with edits pending. -Click the eye icon in the toolbar or press `Cmd+Shift+P` to see the exact SQL before saving. The preview inlines parameter values so you can verify what will run, and **Copy All** copies every statement. +## Previewing the SQL -## Undo & redo +`Cmd+Shift+P`, or the eye button in the toolbar, lists the statements with their parameter values inlined, so the preview reads as what will run. **Copy All** copies every statement. -| Action | Shortcut | -|--------|----------| -| Undo | `Cmd+Z` | -| Redo | `Cmd+Shift+Z` | +## Undo and redo -`Cmd+Z` is context-aware: it undoes text edits when the SQL editor is focused and data changes when the grid is focused. Deleting multiple rows is a single undo action; undoing restores them all at once. Stacks are per-tab, and saving clears them. +`Cmd+Z` and `Cmd+Shift+Z`. Focus decides what they act on: text in the SQL editor, queued changes in the grid. Deleting several rows undoes as one step. The stacks are per tab, and a save clears them. -## Schema changes +## Structure changes -Structure edits (columns, indexes, foreign keys, primary key) use the same queue: changes are tracked locally with visual indicators, previewable as DDL, and applied with **Save Changes**. See [Table Structure](/features/table-structure). +Column, index, foreign key, and primary key edits queue in the same place and go out with the same Save, previewed as DDL instead of DML. See [Table Structure](/features/table-structure). diff --git a/docs/features/code-folding.mdx b/docs/features/code-folding.mdx new file mode 100644 index 000000000..1cdf671cd --- /dev/null +++ b/docs/features/code-folding.mdx @@ -0,0 +1,40 @@ +--- +title: Code Folding +description: Collapse statements, CTEs, subqueries and BEGIN blocks in the SQL editor and every other code view +--- + +Move the pointer into the gutter and chevrons appear. Every region that spans more than one line gets one, which covers whole statements, table bodies, CTEs, subqueries, `BEGIN` blocks and block comments. Click to collapse, click again to expand. + +The chevron points down while the region is showing and right while it is folded, the way a disclosure control behaves everywhere else in macOS. Resting on one marks how far its region reaches. Where a statement and the block inside it open on the same line, the chevron folds the larger of the two. + +A folded region keeps its chevron whether or not the pointer is in the gutter, so nothing is hidden without a visible way back. Move the pointer away from a gutter with nothing folded and it is line numbers again. + +## Collapsed chips + +A collapsed region is replaced inline by a chip showing the start of what it hides and how many lines that is, so a folded `CREATE TABLE` still says what is inside it. Click the chip to bring the region back. Pointing at a chip also marks its chevron in the gutter, since the two are ways to reach the same fold. + +In the query editor, resting on a chip peeks at the block without expanding it. The peek shows the whole block, opening line included, with the editor's own highlighting and theme, and says how many lines it left out when the block is longer than it can show. Moving away, scrolling, typing or clicking closes the peek, and the caret never leaves the editor. + +## Commands + +| Command | Shortcut | +|---|---| +| **Toggle Fold** | `Cmd+Option+Left` | +| **Fold All** | `Cmd+Option+Shift+Left` | +| **Unfold All** | `Cmd+Option+Shift+Right` | + +All three are on the Query menu and rebindable in **Settings > Keyboard**. Fold All collapses top-level statements only; fold the regions inside them one at a time. The editor's right-click menu carries **Fold** over an expanded region and **Unfold** over a collapsed one. + +Chevrons are reachable with VoiceOver. Each reports as a disclosure control that says whether its block is folded, and pressing it folds or unfolds the block. + +## What folds and what does not + +Strings, comments and PostgreSQL dollar-quoted bodies are never read as structure, so a `(` inside a string opens no fold. Documents over 2 million characters get no folding at all, the same limit that turns off syntax highlighting. + +Collapsed regions survive closing and reopening a tab. If the file changed while the tab was closed, the folds that no longer fit are dropped. + +## Where it works + +Turn folding off in **Settings > Editor**. Beyond the SQL editor it covers the DDL and trigger views, the SQL import preview, the AI review sheet, AI chat code blocks, and the JSON cell viewer, all of which number their lines while folding is on. + +Turning line numbers off in **Settings > Editor** removes the gutter and the chevrons with it. Toggle Fold, Fold All, Unfold All and the collapsed chips all still work. diff --git a/docs/features/connection-sharing.mdx b/docs/features/connection-sharing.mdx index bd3e7e23b..65e13f297 100644 --- a/docs/features/connection-sharing.mdx +++ b/docs/features/connection-sharing.mdx @@ -3,118 +3,93 @@ title: Connection Sharing description: Export connections to a file, share links, import from other apps, and resolve passwords from secret managers --- -Export connections to a `.tablepro` file and share with your team. Passwords are left out of the file and stay in your Keychain unless you make an encrypted export. Sections tagged Starter or Team need a [license](/features/licensing) of that tier. +Passwords stay in your Keychain, so every route here hands over a connection definition and leaves the other person to supply their own credential. An encrypted export is the one exception. -## Export +| Route | Use it when | Passwords | Needs | +|---|---|---|---| +| `.tablepro` file | One-off, any number of connections | Only with **Include Credentials** | Nothing | +| `tablepro://` link | One connection, pasted into a chat | Never | Nothing | +| [Linked Folder](#linked-folders) | A shared set has to stay current | Never | Starter | +| [Team Catalog](#team-catalog) | Your team already shares a repo or a drive | Never | Team | +| [Team Library](/features/team#share-connections-with-your-team) | Your team shares nothing on disk | Never | Team | +| [iCloud Sync](/features/icloud-sync) | Your own second Mac, not a colleague | Optional | Starter | -Right-click a connection > **Share > Export to File…**. Select multiple first to export together. **File > Export Connections…** exports all. +## Export -Exported: name, host, port, database, username, type, SSH/SSL config, color, tags, group, Safe Mode level, Startup Commands, Pre-Connect Script, AI Policy, the Local only flag, the Redis database index, and any driver field the plugin does not mark secret (region, project ID, and so on). Not exported: passwords, key passphrases, TOTP secrets, or any driver field marked secret. +Right-click a connection > **Share > Export to File…**, selecting several first to export them together, or **File > Export > Export Connections…** for all of them. -The Share menu also has three copy items. **Copy Connection String** writes a full database URL, and that URL carries the password in plain text, plus the SSH password when the connection tunnels. Treat what it puts on the clipboard as a credential. **Copy TablePro Link** and **Copy as JSON** carry no secrets. +The file carries what the connection form holds, from host and port through SSH and SSL configuration, color, tags, group, Safe Mode level, startup commands, AI policy, the Local only flag, and any driver field the plugin does not mark secret. Passwords, key passphrases, TOTP secrets, and secret driver fields stay behind. Export connections Export connections -## Import - -- **File > Import Connections…** -- **File > Import from URL…** (paste a database URL, see [Connection URLs](/connections/urls)) -- Right-click empty area > **Import Connections…** -- Double-click a `.tablepro` file -- Drag onto TablePro + +**Copy Connection String** writes a database URL that carries the password in plain text, plus the SSH password when the connection tunnels. TablePro marks the clipboard item concealed, which keeps it out of the history of clipboard managers that follow that convention and out of nothing else. **Copy TablePro Link** and **Copy as JSON** carry no secrets. + -A preview shows each connection before importing: +### Encrypted export -| Badge | Meaning | -|-------|---------| -| Green checkmark | Ready | -| Yellow triangle | SSH key or cert not found | -| "duplicate" tag | Already exists | +Needs a [Starter license](/features/licensing). -Duplicates match by host, port, database, and username, and start unchecked. Check to import, then pick **As Copy**, **Replace**, or **Skip**. +Turn on **Include Credentials** in the export sheet and enter a passphrase of 8 characters or more. Credentials go out under AES-256-GCM, keyed from the passphrase with PBKDF2 at 600,000 iterations. Importing asks for the passphrase. - - Import preview - Import preview + + Encrypted export + Encrypted export -## On iPhone - -TablePro for iPhone reads and writes the same `.tablepro` file. Import: tap the **more** menu (•••) above the connection list > **Import Connections**, or open a `.tablepro` file from Files or AirDrop. Export: **more** menu > **Export Connections** shares through the system share sheet. Passwords are left out unless you turn on **Include passwords** and set a passphrase. +## Import -## Import from other apps +Open a `.tablepro` file through **File > Import > Import Connections…**, by right-clicking the empty area of the connection list, by double-clicking the file, or by dragging it onto TablePro. **File > Import > Import from URL…** takes a database URL instead, described in [Connection URLs](/connections/urls). -1. **File > Import from Other App…** -2. Pick the source app. Navicat asks for the `.ncx` file you exported from Navicat. -3. Review the list, uncheck what you don't want, resolve duplicates, click **Import**. +A preview badges each connection before anything is saved: a green checkmark for ready, a yellow triangle when an SSH key or certificate is missing, a "duplicate" tag for one that already exists. Duplicates match by host, port, database, and username and start unchecked. Check one, then pick **As Copy**, **Replace**, or **Skip**. - - Import from other app - source picker - Import from other app - source picker + + Import preview + Import preview -| App | Databases | Passwords | -|-----|-----------|-----------| -| TablePlus | MySQL, PostgreSQL, MongoDB, SQLite, Redis, and more | From Keychain | -| Sequel Ace | MySQL | From Keychain | -| DBeaver | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from config file | -| DataGrip | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | From Keychain or `c.kdbx` | -| Beekeeper Studio | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from its `app.db` store | -| Navicat | MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, MongoDB | Decrypted from `.ncx` file | - -Groups and folders carry over. The source app doesn't need to be running. DBeaver is read from its data folder, so every edition works. DataGrip imports from its recent projects with SSH tunnel and SSL settings; master-password-protected connections can't be read. Beekeeper Studio imports include encrypted passwords and SSH bastion hosts. Navicat needs an export first: **File > Export Connections** in Navicat, with **Export Password** on to bring passwords across. +Anything that decides where a credential comes from is stripped on the way in, from a file and from a link alike: AWS options including IAM authentication, the region, the profile and the RDS endpoint; **Use Password File**; **Prompt for password**; the SSL client key passphrase; the pre-tunnel host and port; and the **Pre-Connect Script**. Startup SQL does arrive, and the confirmation sheet shows it in full. Read it before you save, because it runs on every connect with your credentials. ## Share via link -Right-click a connection > **Share > Copy TablePro Link**. Produces a `tablepro://import?…` URL with the name, host, port, type, username, database, and any SSH or SSL settings. No passwords, key passphrases, or TOTP secrets. The recipient opens the link, reviews the prefilled form, adds their own password, and saves. +**Share > Copy TablePro Link** produces a `tablepro://import?…` URL with the name, host, port, type, username, database, and any SSH or SSL settings. The recipient opens it, reviews the prefilled form, adds a password, and saves. ```text tablepro://import?name=Staging&host=db.example.com&port=5432&type=PostgreSQL&username=admin ``` -TablePro also opens `tablepro://connect/` URLs that launch a saved connection. There is no menu item for this form; build the URL from the connection's UUID. See [URL Scheme](/external-api/url-scheme#open-a-connection). - -### What a link cannot set - -A link comes from outside, so TablePro drops the settings that decide where a connection finds or sends a credential. These are ignored on import, whether they arrive from a link or a `.tablepro` file: - -- Any `aws*` option, including IAM authentication, the region, the profile name, and the RDS endpoint used to sign a token -- `usePgpass` -- `preTunnelHost` and `preTunnelPort` -- `preConnectScript` -- `promptForPassword` -- `sslClientKeyPassphrase` +`tablepro://connect/` opens a saved connection rather than importing one. No menu item builds it; see [URL Scheme](/external-api/url-scheme#open-a-connection). -Startup SQL and the remaining driver options do carry across, and the confirmation sheet lists them in full before you add the connection. Read the startup SQL: it runs on every connect, with your credentials. - -## Encrypted Export +## Import from other apps -Needs a [Starter license](/features/licensing). +Choose **File > Import > Import from Other App…**, pick the source, then review the list and resolve duplicates before clicking **Import**. Groups and folders carry over, and the source app does not have to be running. -Include passwords in the export, protected by a passphrase. + + Import from other app - source picker + Import from other app - source picker + -1. Right-click > **Share > Export to File…** -2. Turn on **Include Credentials** -3. Enter a passphrase (8+ characters), confirm -4. **Export…** +| App | Databases | Passwords | Notes | +|-----|-----------|-----------|-------| +| TablePlus | MySQL, PostgreSQL, MongoDB, SQLite, Redis, and more | From Keychain | | +| Sequel Ace | MySQL | From Keychain | | +| DBeaver | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from config file | Read from the data folder, so every edition works | +| DataGrip | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | From Keychain or `c.kdbx` | Reads recent projects with SSH and SSL settings. A master password blocks it | +| Beekeeper Studio | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from its `app.db` store | Brings SSH bastion hosts across | +| Navicat | MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, MongoDB | Decrypted from `.ncx` file | Export from Navicat first, with **Export Password** on, then pick the `.ncx` | -Credentials are encrypted with AES-256-GCM; the key derives from your passphrase via PBKDF2 (600,000 iterations). Importing prompts for the passphrase. +## On iPhone - - Encrypted export - Encrypted export - +TablePro for iPhone reads and writes the same file. Tap the **more** menu (•••) above the connection list and choose **Import Connections**, or open a `.tablepro` file from Files or AirDrop. **Export Connections** shares through the system share sheet, leaving passwords out unless you turn on **Include passwords** and set a passphrase. ## Linked Folders Needs a [Starter license](/features/licensing). -Watch a shared directory for `.tablepro` files. Connections appear read-only in the sidebar. Each person enters their own password. Works with Git repos, Dropbox, network drives. - -Press `Cmd+,`, then go to **Settings > Account > Linked Folders > Add Folder…** +Press `Cmd+,`, then **Settings > Account > Linked Folders > Add Folder…** and point it at a directory of `.tablepro` files: a Git repo, a Dropbox folder, a network drive. Those connections appear read-only in the sidebar, and each person enters their own password. Linked Folders @@ -125,60 +100,47 @@ Press `Cmd+,`, then go to **Settings > Account > Linked Folders > Add Folder…* Needs a [Team license](/features/licensing). -Right-click a connection > **Share > Publish to Team Catalog…** writes each selected connection as its own `.tablepro` file into a shared folder. The first time, TablePro asks for the folder: a Git repo, Dropbox folder, or network drive your team shares. Republishing overwrites the file. Passwords, passphrases, and TOTP secrets are never written. Teammates add the same folder as a [Linked Folder](#linked-folders) and get the connections read-only. +**Share > Publish to Team Catalog…** writes each selected connection as its own `.tablepro` file into a shared folder, asking for the folder the first time. Republishing overwrites the file, and passwords, passphrases, and TOTP secrets are never written. Teammates add that folder as a Linked Folder. -## Environment Variables +## Environment variables Needs a [Starter license](/features/licensing). -Use `$VAR` and `${VAR}` in `.tablepro` files. Resolved from TablePro's process environment at connect time, so set variables with `launchctl setenv NAME value` or launch TablePro from a terminal. +Write `$VAR` or `${VAR}` in a `.tablepro` file and it resolves from TablePro's process environment at connect time. An app launched from the Dock inherits no shell exports, so set the variable with `launchctl setenv NAME value` or launch TablePro from a terminal. ```json -{ - "host": "${DB_HOST}", - "username": "$DB_USER" -} +{ "host": "${DB_HOST}", "username": "$DB_USER" } ``` ## Password sources -Connections in `~/Library/Application Support/TablePro/connections.json` can declare where their password comes from instead of storing it in the Keychain. Useful when a script provisions connections, for example one Docker database per git worktree. The password resolves at connect time and is not synced to iCloud, since the path, variable, or command is specific to one Mac. - -TablePro keeps a signature for `connections.json`, so it can tell whether the file it is reading is the one it last wrote. If the file changed outside the app, password sources do not run and the connection reports why. Open the connection and save it from TablePro to accept the change. This is what stops a program that can write to your home folder from adding a `command` source and getting it executed the next time TablePro reopens your session. - -Add a `passwordSource` object to the connection: +A connection in `~/Library/Application Support/TablePro/connections.json` can say where its password comes from instead of keeping one in the Keychain, which suits a script that provisions connections. The source resolves at connect time, replaces the Keychain lookup rather than supplementing it, and never syncs to iCloud. A source that fails to resolve fails the connection. ```json { "passwordSource": { "kind": "file", "path": "~/.config/tablepro/secrets/feature-x.pw" } } { "passwordSource": { "kind": "env", "variable": "STAGING_DB_PASSWORD" } } { "passwordSource": { "kind": "command", "shell": "op read op://vault/feature-x/password" } } -``` - -- `file`: reads the password from the file. A trailing newline is trimmed. Use `chmod 600` to keep it private. -- `env`: reads the named environment variable. Apps launched from the Dock don't inherit shell exports; use `launchctl setenv` or launch from a terminal. -- `command`: runs the command through `/bin/bash` and reads stdout. A trailing newline is trimmed, a non-zero exit fails the connection, and the command has a 30 second timeout. - -When `passwordSource` is set it replaces the Keychain lookup. If resolution fails, the connection reports the error instead of falling back to the Keychain. - -### Secret managers - -1Password, HashiCorp Vault, and AWS Secrets Manager have dedicated kinds. They build the CLI call for you and quote every argument, so a reference can't break out into the shell. - -```json { "passwordSource": { "kind": "onePassword", "reference": "op://vault/feature-x/password" } } { "passwordSource": { "kind": "vault", "path": "secret/data/staging/db", "field": "password" } } { "passwordSource": { "kind": "awsSecretsManager", "secretId": "prod/db", "jsonKey": "password" } } ``` -- `onePassword`: runs `op read` on the reference. Sign in to the 1Password CLI first (`op signin`). -- `vault`: runs `vault kv get` for one field at a path. Set `VAULT_ADDR` and `VAULT_TOKEN` in TablePro's environment. -- `awsSecretsManager`: runs `aws secretsmanager get-secret-value`. Set `jsonKey` to pull one field out of a JSON secret; leave it off to use the whole value. +| `kind` | Reads from | Set up first | +|---|---|---| +| `file` | The file at `path`, minus a trailing newline | `chmod 600` the file | +| `env` | The named environment variable | Same Dock caveat as environment variables | +| `command` | stdout of the command, run through `/bin/bash`. A non-zero exit fails the connect | | +| `onePassword` | `op read` on the reference | `op signin` | +| `vault` | `vault kv get` for one field at a path | `VAULT_ADDR` and `VAULT_TOKEN` in TablePro's environment | +| `awsSecretsManager` | `aws secretsmanager get-secret-value`. `jsonKey` pulls one field out of a JSON secret; omit it for the whole value | | + +Every command gets 30 seconds. The three CLI kinds quote each argument, so a reference cannot break out into the shell, and they need the tool on `PATH`, in `/usr/local/bin`, or in `/opt/homebrew/bin`. -The CLI tools must be on `PATH`; TablePro also looks in `/usr/local/bin` and `/opt/homebrew/bin`. Same 30 second timeout as `command`. +Editing `connections.json` by hand costs one extra step. TablePro stamps the file when it writes it, and a file that changed underneath it fails the connect with *"Your connections file was changed outside TablePro, so this connection's password source was not run. Open the connection and save it again to confirm the change."* Save it once from the app and sources run again. ## File format -JSON. The envelope needs `formatVersion`, `exportedAt`, and `appVersion`; each connection needs `name`, `host`, `port`, `database`, `username`, and `type` (empty strings are fine where a value doesn't apply). +JSON. The envelope needs `formatVersion`, `exportedAt`, and `appVersion`; each connection needs `name`, `host`, `port`, `database`, `username`, and `type`, and an empty string is fine where a value does not apply. Groups and tags match by name and are created when missing. Groups export flat, name and color only. Paths use `~/` so they travel. ```json { @@ -200,14 +162,3 @@ JSON. The envelope needs `formatVersion`, `exportedAt`, and `appVersion`; each c "tags": [{ "name": "production", "color": "Red" }] } ``` - -Groups and tags match by name; missing ones are created. Groups export flat: name and color only, no hierarchy. Paths use `~/` for portability. - -## Sharing vs iCloud Sync - -| | Sharing | iCloud Sync | -|---|---|---| -| **For** | Team | Your Macs | -| **How** | Files, links | CloudKit | -| **Passwords** | Per-user (encrypted export with a license) | Optional sync | -| **Requires** | Nothing (Starter for extras) | Starter license + iCloud | diff --git a/docs/features/csv-inspector.mdx b/docs/features/csv-inspector.mdx index ebaef9958..a1d5a0e66 100644 --- a/docs/features/csv-inspector.mdx +++ b/docs/features/csv-inspector.mdx @@ -3,67 +3,57 @@ title: CSV Inspector description: Open, view, and edit CSV and TSV files without importing them into a database --- -TablePro opens `.csv` and `.tsv` files directly as documents. No scratch database, no import step. The file is the source of truth; Save writes back to it. +Opening a `.csv` or `.tsv` maps the file instead of reading it into memory. Row offsets are indexed up front, cells are parsed as you scroll to them, and only the rows you edit are held. Rows arrive a page at a time, at the same **Default page size** the data grid uses, and the status bar carries the row and column counts with the page controls. CSV Inspector CSV Inspector -## Opening a file +Double-click a file in Finder, right-click it and choose Open With > TablePro, or drag it onto the TablePro Dock icon. There is no scratch database and no import step; to load the same file into a table instead, use **File > Import > Import Data…** and see [Import & Export](/features/import-export). -- Double-click a `.csv` or `.tsv` in Finder, or right-click and choose Open With > TablePro. -- Drag a file onto the TablePro Dock icon. +## Detection -File > Open File… opens SQL files only, so it is not a path into the inspector. +Four things are guessed when the file opens. -## Auto-detection +| Guess | How | +|---|---| +| Delimiter | Comma, tab, semicolon, or pipe, whichever appears most often outside quoted regions in the first 64 KB. A `.tsv` always uses tab | +| Encoding | A BOM selects UTF-8, UTF-16 BE, or UTF-16 LE. Without one, the first 256 KB decide: valid UTF-8 reads as UTF-8, anything else as Windows-1252 | +| Line ending | CRLF, LF, or CR, whichever comes first in the first 64 KB | +| Header row | Row one becomes the header when at least half its cells are non-empty and not numbers. Otherwise the columns are named `Column 1`, `Column 2`, and every row is data | -When a file opens, TablePro detects: +**Edit > CSV/JSON Editing > Set CSV Properties…** sets the delimiter, quote character, escape character, encoding, and line ending by hand; the escape is either a doubled quote, the RFC 4180 default, or a backslash. **Reload** then re-reads the file, and asks first if you have unsaved edits. -- **Delimiter**: comma, tab, semicolon, or pipe, whichever appears most often outside quoted regions in the first 64 KB. `.tsv` files always use tab. -- **Encoding**: a BOM selects UTF-8, UTF-16 BE, or UTF-16 LE. Without a BOM, TablePro probes up to the first 256 KB: valid UTF-8 reads as UTF-8, anything else reads the whole file as Windows-1252. -- **Line ending**: CRLF, LF, or CR, whichever appears first in the first 64 KB. -- **Header row**: row one is treated as headers when at least half of its cells are non-empty and not numbers. Otherwise TablePro generates `Column 1`, `Column 2`, … and treats every row as data. +When only the header guess is wrong, **Edit > CSV/JSON Editing > Switch First Row Between Header and Data** (`Cmd+Shift+H`) flips it without a reload. Turning the header off pushes that row down into the data and renames the columns; turning it on promotes the first data row. Both are undoable, and Save writes the file to match. -When a guess is wrong, choose **Edit > Set CSV Properties…** to pick the delimiter, quote character, escape character, encoding, and line ending by hand, then **Reload** to re-read the file with those settings. The escape character is either a doubled quote (the RFC 4180 default) or a backslash. Reload discards unsaved edits and asks first if you have any. +## Editing rows -## Pagination +Double-click a cell, or press `Return` on it, to edit. `Return` commits and closes the editor; `Tab` commits and moves to the next cell, wrapping to the next row. -Rows load in pages. The page size comes from Default page size in [Settings > Data > Pagination](/customization/settings), 1,000 rows by default. The status bar shows row and column counts, and previous/next page controls when the file spans more than one page. +The toolbar **Add Row** button appends a row, scrolls to it, and selects it. **Insert Row Above** and **Insert Row Below** on the right-click menu place one next to the row you clicked. Select rows and press `Delete`, or use the toolbar **Delete** button, to remove them; the next row takes the selection, so the arrow keys keep working from where you were. Deleting rows that hold data asks first, and deleting blank rows does not. -## Editing +`Cmd+Z` and `Cmd+Shift+Z` cover every change. An insert, a bulk delete, and a paste are each one step. -- Double-click a cell (or press Return on it) to edit. Return commits and closes the editor. Tab commits and moves to the next cell, wrapping to the next row. -- The toolbar `Add Row` button appends a new row, scrolls to it, and selects it. -- Right-click a row and choose **Insert Row Above** or **Insert Row Below** to add a blank row next to it. Both also appear in the Edit menu, where they insert next to the selected row, or at the top or bottom when nothing is selected. -- Select rows and press `Delete` (the toolbar `Delete` button, or right-click a row and choose Delete) to remove them. The next row takes selection so arrow keys keep working from where you were. -- Deleting rows that hold data asks you to confirm first. Deleting blank rows skips the prompt. -- Cmd+Z and Cmd+Shift+Z undo and redo every change. Each insert, a bulk delete, or a paste is a single undo step. - -## Column operations +## Columns CSV Inspector Columns Menu CSV Inspector Columns Menu -Right-click a column header, or open the toolbar `Columns` menu, to reach the same per-column actions: +Right-click a column header, or open the toolbar **Columns** menu, for the same actions. Most of them are on the Edit menu too, under **CSV/JSON Editing**, where they act on the selected columns. -- **Rename Column…** prompts for a new name; the column stays in place. -- **Insert Column Left / Right** inserts a new column next to the one you clicked, with a name you supply. -- **Split Column…** splits each value into new columns at every match of a delimiter or a regular expression. The original column is replaced by the pieces; rows with fewer pieces get empty cells. -- **Merge Columns…** joins the column with the one to its right into a single column, placing a separator you choose between the two values. +- **Rename Column…** prompts for a name and leaves the column where it is. +- **Insert Column Left / Right** adds a column beside the one you clicked. +- **Split Column…** splits each value into new columns at every match of a delimiter or a regular expression. The original is replaced by the pieces, and rows with fewer pieces get empty cells. +- **Merge Columns…** joins the column with the one to its right, placing a separator you choose between the values. - **Change Type ▸** overrides the inferred type as Text, Integer, Real, Boolean, or Date. **Reset to Inferred** drops the override. -- **Delete Column** removes the column. It sits last in the menu and is undoable with Cmd+Z. Deleting a column that holds data asks you to confirm first. - -Cmd-click several column headers to select whole columns, and the menu reads **Delete Columns**, removing all of them in one undoable step. Insert Column Left / Right, Split Column…, Merge Columns…, and Delete Column also appear in the Edit menu, where they act on the selected columns. - -In the toolbar `Columns` menu, **Add Column…** at the top appends a new column at the end, and every column is listed below it with its current type and the same submenu. +- **Delete Column** removes it, and asks first when the column holds data. -## Header row +`Cmd`-click several headers to select whole columns; the menu then reads **Delete Columns** and removes them in one undoable step. **Add Column…** at the top of the toolbar menu appends a column at the end, and every column is listed below it with its current type. -TablePro guesses whether the first row is a header when it opens the file. When it guesses wrong, choose **Edit > Switch First Row Between Header and Data** (`Cmd+Shift+H`) to flip it. Turning the header off moves the first row down into the data and names the columns `Column 1`, `Column 2`, and so on. Turning it back on promotes the first data row to the header. The change is undoable, and Save writes the file with or without a header row to match. +A type is inferred from the first 200 rows of the file, skipping empty values. It controls sort order and nothing else: every cell is still a string on disk, whatever the type says. ## Filter and sort @@ -72,35 +62,18 @@ TablePro guesses whether the first row is a header when it opens the file. When CSV Inspector Filter Bar -- `Cmd+F` toggles the filter bar. Each row is one condition: column, operator (`contains`, `equals`, `does not equal`, `starts with`, `ends with`, `is empty`, `is not empty`), and value. -- Use `+ Add filter` for additional conditions. All conditions are combined with AND. -- `Clear all` resets to a single empty condition. -- Click a column header to sort by that column. Shift-click another column to add it as a secondary sort key (and so on for tie-breakers). -- Numeric-typed columns sort numerically; text columns sort with natural ordering (so `Item 2` comes before `Item 10`). -- Filter and sort run off the main thread, so the UI stays responsive on large files. An "Updating…" indicator shows in the status bar while a recompute is in flight. - -## Copy and paste +`Cmd+F` toggles the filter bar. Each row is one condition: a column, an operator (contains, equals, does not equal, starts with, ends with, is empty, is not empty), and a value. **Add filter** adds another, every condition is joined with AND, and **Clear all** goes back to one empty row. -- `Cmd+C` copies the focused cell's value, or whole selected rows as TSV (tab-separated values). -- `Cmd+V` parses TSV from the pasteboard and appends each line as a new row. A block narrower than the table pastes into cells at the focused cell instead. A whole paste is one undo step. +Click a column header to sort by it, and `Shift`-click another to add a tie-breaker. A numeric-typed column sorts numerically; a text column sorts naturally, so `Item 2` comes before `Item 10`. An **Updating…** indicator appears in the status bar while a filter or sort recomputes. -## Type inference +## Copy and paste -TablePro samples the first 200 rows of the file and infers `Integer`, `Real`, `Boolean`, `Date`, or `Text` per column, skipping empty values. The inferred type controls numeric sorting. Inferred types do not modify the stored data: every cell is still a string on disk. Override per-column from the `Columns` menu. +`Cmd+C` copies the focused cell, or whole selected rows as TSV. `Cmd+V` parses TSV from the pasteboard and appends each line as a new row; a block narrower than the table pastes into the cells at the focused cell instead. A whole paste is one undo step. ## Saving -- Cmd+S saves to the original path, preserving the detected delimiter, encoding, line ending, and BOM. Unedited rows are written back byte-for-byte. -- Cmd+Shift+S brings up Save As, where you can re-pick the destination. -- The window close button shows the standard unsaved-changes dot until you save. - -## External changes - -If another application writes the file while you have it open in TablePro: - -- If you have no unsaved edits, TablePro reloads the file. -- If you have unsaved edits, TablePro asks whether to reload or keep your changes. +`Cmd+S` writes back to the original path, keeping the detected delimiter, encoding, line ending, and BOM, and every row you did not touch goes back byte for byte. `Cmd+Shift+S` opens Save As. The window's close button carries the standard unsaved-changes dot until you save. -## Window tabbing +If another application writes the file while it is open here, it reloads when you have no unsaved edits, and asks whether to reload or keep your changes when you do. -CSV windows form their own native window tab group, separate from connection windows. Drag a tab out to detach it into its own window, or drop one back in to re-attach. +CSV windows form their own native tab group, apart from connection windows. Drag a tab out to detach it, or drop one back in to re-attach. diff --git a/docs/features/data-grid.mdx b/docs/features/data-grid.mdx index 4e7d1167a..b7729c745 100644 --- a/docs/features/data-grid.mdx +++ b/docs/features/data-grid.mdx @@ -1,174 +1,74 @@ --- title: Data Grid -description: Sort, filter, edit, and copy query results in a spreadsheet-style grid +description: Sort, size, hide, chart and copy the rows a table or a query puts in the grid --- -A cell you change is not saved yet. Changes queue in the tab: the toolbar counts them, `Cmd+Shift+P` shows the statements they will produce, and nothing reaches the database until you press `Cmd+S`. The same grid holds a table's rows and a query's output, and both accept changes when the rows come from one table. +Most of the grid's controls sit in the status bar beneath it. The view switcher is at the leading edge, the row count in the middle, then the columns, filter and page buttons at the trailing edge. Data grid Data grid -## View modes - -Switch between **Data**, **Structure**, **JSON**, and **Chart** with the switcher at the left of the status bar, or from **View > Result View**. Query tabs show Data, JSON, and Chart only. The mode is remembered per tab. - -JSON mode shows the rows as a JSON array, with **Text** and **Tree** views. It follows the grid: the same rows in the same order, with a column value filter applied and hidden columns left out. Select rows in Data mode first to limit what JSON shows, then use **Copy JSON**. - -Rows marked for deletion are held back, since a Save would remove them, and the count line says how many. **Copy as JSON** in the grid's context menu copies every row you selected, including those. +## Sorting -### Chart mode - -Chart mode is a Starter feature. Pick a bar, line, area, or scatter chart, then choose a numeric Y column. X can use row numbers, another numeric column, a date or timestamp column, or a categorical column; an optional text, boolean, enum, or set column splits the result into series. Date and timestamp columns plot on a real time axis, so points sit at their true spacing whatever order the rows arrive in. Hover the plot to inspect exact X, Y and series values. Controls stack vertically when the result pane is narrow. +Click a header to cycle through ascending, descending, and off. `Shift`-click a second header to sort by both. Sorting re-runs the query with a new `ORDER BY`, so it orders the whole table rather than the page in front of you. -Your chart type and axis choices belong to the tab and follow the column names, so they survive a page turn, a sort, a refresh and a re-run. A column that is missing from the next result is remembered rather than cleared, and comes back when the column does. - -Charts draw the active result's loaded rows, up to 2,000 points, 20 series, and 50,000 inspected rows. Past a limit the chart still draws what fits and the toolbar says how much, for example "Showing the first 2,000 points of 8,431 loaded rows". Null, binary and unrepresentable axis values are skipped and counted. The status bar keeps the row count, the pagination controls and **Fetch All** in Chart mode, so you can load the rest without leaving the chart. Grid selection, Find, hidden columns and value filters do not narrow the chart. - -## Columns - -### Sort - -Click a header to cycle through ascending, descending, and off. `Shift`-click another header to add it to the sort, so you can order by several columns at once. Sort re-runs the query with `ORDER BY` appended, replacing any existing one. - -To sort on open, set **Settings > Data > Default row sort** to **Primary key** or **First column** (default is **No sorting**). A click on any header still overrides it. +To sort on open, set **Default row sort** in [Settings > Data](/customization/data-settings) to **Primary key** or **First column**; out of the box the order is the server's. A click on any header overrides it. -If the sort column can't be ordered on the server (a `BLOB`, `JSON`, or spatial column), the query fails. Pick another column or set the default back to No sorting. +Sorting on a `BLOB`, `JSON`, or spatial column the server cannot order on fails the query. Sort on another column, or set **Default row sort** back to the engine's order. -### Filter +## Filtering -The header gives you two filters: +The header menu carries two filters that answer different questions. -- **Filter Values…** (or the funnel icon on hover) filters the loaded rows by picking values, without re-querying. The popover lists each value with its count; search to narrow, check the values to keep, and click **Apply**. NULL and empty values appear as their own entries. `Return` applies, `Escape` cancels. Filter several columns at once and rows must match every filter. Clear from the header menu (**Clear Value Filter**, **Clear All Value Filters**). The filter belongs to the tab, so it survives switching to JSON or Structure mode and back, and switching to another tab and back. Replacing the result clears it, since the values you picked came from the rows being replaced: running a query, turning a page, refreshing, or moving to another result of the same script. **Fetch All** keeps it and applies it to the rows it loads, because that extends the same result. -- **Filter with column** adds a filter row for that column to the [Filter Panel](/features/filtering) and opens it. Use this to filter the whole table on the server. +| Header menu item | What it filters | +|---|---| +| **Filter Values…**, or the funnel icon on hover | The rows already loaded, without a re-query | +| **Filter with column** | The whole table. It adds a row to the [filter bar](/features/filtering) and opens it | + +**Filter Values…** lists every value in the column with its count, NULL and empty among them. Select the values to keep and click **Apply**. Filter several columns and a row has to match all of them; **Clear Value Filter** and **Clear All Value Filters** are on the same menu. Value filter popover Value filter popover - -Value filters run on the rows already loaded, so they cover the current page only. - - -### Resize and hide - -Drag a column border to resize, or double-click it to fit the content. The header menu has **Size to Fit** for one column and **Size All Columns to Fit** for all of them. Fitting reads the loaded page rather than a sample of it, and leaves room for any action button the cells draw, such as the arrows on a date. Sizing every column of a very wide result reads fewer rows per column so the command stays quick. Fitted columns stop at half the visible grid width, so one long-text column can't push the rest off screen. - -Hide columns from the columns button in the status bar or the header menu. A hidden column isn't fetched, so hiding a large column makes the table load faster. The primary key is always fetched, so editing still works, and so is the column you are sorting by, so hiding it leaves the order alone. - -On a schemaless store such as MongoDB, the popover lists every field the grid can draw, including ones that only turn up in later documents, so **Hide All** covers all of them. - -Column widths, order, and which columns are hidden are remembered per table, scoped to the connection, database, and schema. Hiding a column keeps its place in that order, so showing it again puts it back where it was. Click **Reset Columns** in the columns popover to put everything back to defaults. - -A result with hundreds of columns only builds the ones near the viewport, so scrolling stays smooth on very wide tables. The horizontal scroller still covers the whole result, and columns are built as you reach them. +A value filter lives as long as the result does: switching view mode or tab keeps it, replacing the result clears it. **Fetch All** keeps it and applies it to the rows it loads. -### Display format - -UUIDs and Unix timestamps render in a readable form when the column type and name match (for example a `BINARY(16)` column named `uuid`). Right-click a header and choose **Display As** to set the format per column: Raw Value, UUID, Unix Timestamp (seconds or milliseconds), JSON, or PHP Serialized. See [Cell and Row Viewers](/features/json-viewer) for what each format does, and for how MongoDB binary columns differ. Toggle the automatic detection in **Settings > Data > Smart value detection**. - -## Editing - - -Editing works when the results come from one table, with or without an alias, as in `SELECT * FROM orders o WHERE o.id = 1`. Comments, `DISTINCT`, `ORDER BY`, `LIMIT` and `FOR UPDATE` are fine. Joins, comma joins, subqueries in `FROM`, CTEs, `UNION`, `EXCEPT`, `INTERSECT` and `FROM ONLY` are read-only, and so is any connection set to the Read-Only [safe mode](/features/safe-mode) level. - -You can write the schema in front of the table, as in `SELECT * FROM public.orders o`, as long as it is the schema you are browsing. Naming a different schema keeps the results read-only: the saved edit does not repeat the schema, so it would go to the browsed one instead of the table you queried. Switch to that schema first if you want to edit through it. - -Two shapes look editable but refuse to save. A column renamed with `AS` cannot be written back, though the other columns in the same row still can. A query that renames or omits the primary key blocks the whole save, because there is nothing to match the row on. - +## Columns -Double-click a cell to edit it. Press `Enter` to confirm or `Escape` to cancel. Some types open a dedicated editor: +Drag a border to resize a column, or double-click it to fit the content; **Size to Fit** and **Size All Columns to Fit** on the header menu do the same. A fitted column stops at half the visible grid width. -| Column type | Editor | -|---|---| -| Date, time, datetime, timestamp | Calendar and time picker, or edit as text | -| Foreign key | Searchable list of referenced values | -| Boolean, `BIT`, `TINYINT(1)` | Checkbox, with a third state for nullable columns | -| `ENUM` | Searchable value list | -| `SET` (MySQL/MariaDB) | Multi-select checkboxes | -| Array of a simple type (PostgreSQL) | Ordered list, one row per element | -| `JSON`, `JSONB`, `BLOB`, binary | [Cell viewers](/features/json-viewer); hex editor is read-only over 10 KB | - - - Cell editor - Cell editor - +Hide columns from the columns button in the status bar or from the header menu. A hidden column is not fetched, so hiding a large one makes the table load faster; the primary key and the sorted column are fetched either way. On a schemaless store the popover lists every field the grid can draw, including ones that turn up only in later documents. -Right-click an editable cell and open **Set Value** for common values without typing: **Empty**, **NULL** (nullable columns), **Default** (columns with a default), and date functions like `NOW()` or `CURRENT_DATE` on date columns. +Widths, order, and hidden columns are remembered per table, scoped to the connection, database, and schema; **Reset Columns** in the popover puts them back. Right-click a header and choose **Display As** to render a column as a UUID, a Unix timestamp, JSON, or PHP serialized data; see [Cell and Row Viewers](/features/json-viewer). -### Foreign Keys +## Foreign keys -Foreign key cells show an arrow on the right edge. Click it to open the referenced table filtered to the matching row. If the current tab holds a query or unsaved edits, the reference opens in its own tab instead of replacing it, and clicking the same reference again returns to that tab. `Cmd`-click always opens a new tab. Right-click for **Preview Referenced Row**, which shows the row in a popover. +A foreign key cell carries an arrow on its right edge. Click it to open the referenced table filtered to the matching row, or right-click for **Preview Referenced Row**, which shows that row in a popover. `Cmd`-click always opens a new tab; otherwise the reference takes over the current tab unless that tab holds a query or unsaved edits. -Step back through the references you followed with the Back and Forward buttons at the leading edge of the toolbar, or **View > Back** (`Ctrl+Cmd+[`) and **View > Forward** (`Ctrl+Cmd+]`). Back puts the table you came from back as you left it, with the same filters, sort, page and selected row. Opening a table from the object browser over the current tab is recorded the same way. Each tab keeps its own history, so a reference you opened in its own tab has nothing to go back to, and Back never closes a tab. Back is unavailable while a tab holds unsaved edits, since going back would discard them. +Step back with the Back and Forward buttons at the leading edge of the toolbar, or **View > Back** (`Ctrl+Cmd+[`) and **View > Forward** (`Ctrl+Cmd+]`). Back restores the table you came from as you left it: same filters, sort, page, and selected row. Each tab keeps its own history, Back never closes a tab, and it is unavailable while a tab holds unsaved edits. Foreign key lookup Foreign key lookup -### Row operations - -- **Add** (`Cmd+Shift+N`): click **+** in the toolbar, use **Edit > Add Row**, or press the shortcut. -- **Duplicate** (`Cmd+Shift+D`): right-click a row and choose **Duplicate**. The primary key resets to `DEFAULT` so the database assigns a new one. -- **Delete**: select rows by their row number (`Shift`-click for a range, `Cmd`-click for separate rows) and press `Delete`. -- **Paste**: copy rows, then right-click and choose **Paste** to insert them as new rows. -- **Fill Column**: right-click a header to set one value across all loaded rows. It skips primary key columns. - -### Saving changes - -Edits are queued, not applied right away. The toolbar shows the pending count, modified cells are highlighted, and new and deleted rows are marked. - -- **Preview SQL** (`Cmd+Shift+P`): review the statements before applying. -- **Save Changes** (`Cmd+S`): apply all pending changes. -- **Undo / Redo** (`Cmd+Z` / `Cmd+Shift+Z`): step through edits before saving. - -There is no discard button. Undo your edits, or let the confirmation prompt drop them when you refresh, re-sort, change filters, or close the tab. See [Change Tracking](/features/change-tracking) for how the queue works. - ## Inspector -Toggle the inspector with `Cmd+Option+I`, or with the button at the right end of the toolbar. With a row selected, it lists every column value with full editors for long text and JSON; see [Cell and Row Viewers](/features/json-viewer) for details. Turn on **Settings > Data > Auto-show inspector on row select** to open it automatically. - -With no row selected, it shows table statistics: data, index, and total size, row count, average row size, engine, collation, and created and updated dates (fields vary by database). - -In the Structure tab, the inspector follows the structure grid instead: select a column, index, or foreign key and it lists that row's own fields. The fields are editable where the grid is, with the same type list and YES/NO menus, and an edit joins the pending changes you apply with Save. A row you have marked for deletion is read-only. +`Cmd+Option+I`, or the button at the trailing end of the toolbar, opens the inspector beside the grid. With a row selected it lists every column with an editor matched to the value; with no row selected it shows the table's statistics. [Cell and Row Viewers](/features/json-viewer) covers both. **Auto-show inspector on row select** in [Settings > Data](/customization/data-settings) opens it whenever you pick a row. Row inspector Row inspector -## Select and copy - -Click a cell to select it, drag or `Shift`-click for a range, and click row numbers for whole rows. Copy, **Copy as**, and Delete act on the whole current selection, whether that is a cell range, a column (`Cmd`-click its header), or rows picked by their row numbers. - -- `Cmd+C`: the focused cell, or the selected rows as TSV. -- `Cmd+Shift+C`: the selected rows as TSV. -- `Cmd+Option+J`: the selected rows as JSON. - -Right-click a row and choose **Copy as** for more formats: +## Find in the loaded rows -| Format | Output | -|---|---| -| With Headers | TSV with a header row | -| JSON | Array of row objects | -| CSV / CSV with Headers | RFC 4180 CSV | -| Markdown | Markdown table | -| IN Clause | `('a', 'b', 'c')` for `WHERE col IN (…)` | -| INSERT Statement(s) / UPDATE Statement(s) | SQL statements per row (SQL databases) | - -Copies follow the grid as shown: hidden columns are left out and columns keep their current order. When you select specific cells, **Copy as** (JSON, CSV, Markdown, INSERT, and UPDATE) includes only the columns you selected; select whole rows to include every column. The UPDATE format still keys its WHERE clause on the primary key, even when the primary key cell is not part of the selection. Right-click a header and choose **Copy Column Values** to copy a whole column, one value per line. - -## Find in results - -`Cmd+F` on a table tab opens a find bar above the grid. Type a term and the matching cell is highlighted and scrolled into view. `Return` and `Cmd+G` step forward, `Cmd+Shift+G` steps back, and `Escape` clears the term, then closes the bar. - -Matching is case and accent insensitive, and it runs over the text you see, so a value reads the same in the find bar as it does in the cell. Binary and spatial columns are skipped, since they render as hex rather than as text. - -**The find bar searches the rows already loaded, not the whole table.** Results are paginated at the database, so the counter always says which it means: +`Cmd+F` opens the find bar above the grid. `Return` and `Cmd+G` step forward, `Cmd+Shift+G` back, `Escape` clears the term and then closes the bar. Matching ignores case and accents and runs over the text as displayed; binary and spatial columns are skipped. The counter says which rows were searched: | What you see | What it means | |---|---| @@ -177,30 +77,45 @@ Matching is case and accent insensitive, and it runs over the text you see, so a | `No matches` | Every row is loaded and none match | | `Not on this page` | None of the loaded rows match, and more rows exist | -When nothing matches on the page and more rows exist, **Search All Rows** turns the term into a filter that runs on the server, so the answer covers the whole table. That button replaces the filters on the tab, so it only appears when you have none applied. With filters already set, add your search to the filter bar instead. +**Search All Rows** turns the term into a filter that runs at the database, covering the whole table. It replaces the tab's filters, so it appears only when none are applied; with a filter set, add the term to the filter bar instead. + +## Pages and row counts -## Pagination and limits +A table tab pages. The status bar offers 5, 10, 20, 100, 500 or 1,000 rows per page, **Custom…** up to 1,000,000, and **All rows…**, plus First, Previous, Next and Last; `Cmd+[` and `Cmd+]` step pages, and clicking the page indicator (`3 / 12`) jumps to a page number. The size a table opens with is a [data setting](/customization/data-settings). -**Table tabs** page through data. The status bar has a rows-per-page menu (5 to 1,000, a custom size up to 1,000,000, or **All rows**) and First / Previous / Next / Last controls. Click the page indicator (for example `3 / 12`) to type a page number and jump to it. Set the default page size in **Settings > Data**. +A large table shows an estimated total prefixed with `~` instead of running a slow `COUNT(*)`. **Count Exactly** beside it runs the real count in the background, and `Cmd+.` cancels that. While a total is an estimate, **Last** and **All rows…** stay unavailable and **Next** works as long as the last page came back full. -These controls stay on a table tab whatever it holds. On a table that fits in one page they are dimmed rather than hidden, and while a table is loading they dim until its rows arrive, so the bar keeps the same shape as you move between tables. +A query tab does not page. It stops at the [row cap](/customization/data-settings) instead and offers **Fetch All** to load the rest, and a query carrying its own `LIMIT`, `FETCH FIRST`, or `TOP` is never capped. `Cmd+.` cancels a running query or a Fetch All. -Large tables show an estimated total instead of running a slow `COUNT(*)`. **Settings > Data > Count rows if estimate less than** sets the threshold below which TablePro counts exactly. An estimated total is prefixed with `~`. **Count Exactly** next to the total replaces an estimate, or fills in a total shown as `?`, with a real count. That count can take a while on a large table, so it runs in the background and `Cmd+.` cancels it. **Count Exactly** appears once the total has settled, so it does not flicker in and out while a table loads, and a real count stays put: the automatic estimate that runs after each page turn will not overwrite it. **Refresh** works the total out again. +## Copying -An estimate of zero from a table the server has never analyzed is treated as unknown rather than as an empty table, so you get no total instead of a wrong one. +Click a cell to select it, drag or `Shift`-click for a range, and click a row number for a whole row. Copy acts on the whole selection. -While the total is an estimate it never bounds navigation: the page indicator marks it (`3 / ~12`), **Last** and **All rows** stay unavailable, and **Next** keeps working as long as the page came back full. Run **Count Exactly** to turn the estimate into a real count and get all four back. +| Shortcut | Copies | +|---|---| +| `Cmd+C` | The focused cell, or the selected rows as TSV | +| `Cmd+Shift+C` | The selected rows as TSV | +| `Cmd+Option+C` | The same, with a header row | +| `Cmd+Option+J` | The selected rows as JSON | + +Right-click and open **Copy as** for CSV, CSV with Headers, Markdown, and **IN Clause**, which writes `('a', 'b', 'c')` for a `WHERE col IN (…)`. **INSERT Statement(s)** and **UPDATE Statement(s)** appear when the result comes from a single table, and never on MongoDB or Redis. **Copy Column Values** on the header menu copies one column, one value per line. -**Query tabs** cap results at 10,000 rows by default. Your query is sent exactly as you wrote it; TablePro stops reading once it reaches the cap. A query with its own `LIMIT`, `FETCH FIRST`, or `TOP` is not capped. When the cap trims a result, the status bar shows **Fetch All** to load the rest (with a confirmation, since large results use a lot of memory). Run one query without the cap via **Execute Without Limit** (`Option+Cmd+Enter`). Adjust the cap in **Settings > Data** (**Truncate query results**, **Row cap**). +Every copy follows the grid as shown: hidden columns are left out, columns keep their current order, and over a cell selection only those columns go out. UPDATE keys its `WHERE` clause on the primary key even when that cell is outside the selection. + +## View modes + +Switch between **Data**, **Structure**, **JSON**, and **Chart** with the switcher at the leading edge of the status bar, or from **View > Result View**. Query tabs have no Structure mode, the mode is remembered per tab, and [Cell and Row Viewers](/features/json-viewer) covers JSON mode. + +### Chart mode -Press `Cmd+.` to cancel a running query or a Fetch All. +Needs a [Starter license](/features/licensing). -## Display settings +Pick a bar, line, area, or scatter chart, then a numeric Y column. X takes row numbers, another numeric column, a date or timestamp column, or a categorical column; an optional text, boolean, enum, or set column splits the result into series. Date and timestamp columns plot on a real time axis, whatever order the rows arrive in. Hover the plot for exact values. -NULL shows as styled `NULL` text. Set the NULL display, date format, row height, row numbers, and alternate row backgrounds in **Settings > Data**. +A chart draws the loaded rows, up to 2,000 points, 20 series, and 50,000 inspected rows, and the toolbar says when it hit one: "Showing the first 2,000 points of 8,431 loaded rows". Null, binary, and unrepresentable axis values are skipped and counted. Grid selection, Find, hidden columns, and value filters do not narrow a chart, and the page controls and **Fetch All** stay in the status bar. Chart type and axes belong to the tab and follow the column names, so they survive a page turn, a sort, and a re-run. -## MongoDB collections +## Editing and display -MongoDB has no fixed schema, so TablePro infers columns by sampling up to 50 documents. Missing fields show as NULL, and the `_id` column is read-only (delete and re-insert to change it). BSON values display as `ObjectId("…")`, ISO 8601 dates, `BinData(…)`, decimal strings, and `DBRef(…)`. +Double-click a cell to edit it. Nothing reaches the database until you save, and [Change Tracking](/features/change-tracking) covers the type-specific editors, the row operations, and what a Save runs. -Changes preview as shell commands: `insertMany`, `updateOne` with `$set`, and `deleteMany`. +NULL renders as styled `NULL` text. That text, the date format, row height, row numbers, and alternate row backgrounds are [data settings](/customization/data-settings). Every shortcut the grid answers to, and how to rebind it, is in [Keyboard Shortcuts](/features/keyboard-shortcuts). diff --git a/docs/features/er-diagram.mdx b/docs/features/er-diagram.mdx index b0b820e27..3024f84a2 100644 --- a/docs/features/er-diagram.mdx +++ b/docs/features/er-diagram.mdx @@ -3,7 +3,7 @@ title: ER Diagram description: View table relationships as an interactive entity-relationship diagram --- -The diagram is built from foreign keys, so a driver that reports none opens with every table unconnected. Right-click a table in the sidebar and choose **View ER Diagram**, or use **Database > View ER Diagram** in the menu bar. What you get is the current schema: tables outside it are not drawn. +Right-click a table in the sidebar and choose **View ER Diagram**, or use **Database > View ER Diagram**. What opens is the current schema, drawn entirely from the foreign keys the driver reports: tables outside that schema are left out, and a foreign key pointing at one of them is not drawn. -## Layout +The diagram is a snapshot taken when the tab opens, and it does not follow later schema changes. After a migration, close the tab and open it again. -Tables are arranged automatically so the diagram fills the canvas in both directions instead of stacking into one tall column. TablePro finds groups of tables connected by foreign keys, places each group together, and packs the groups across the plane. Tables with no relationships are collected into their own grid block. +## Reading the diagram -Each group of connected tables gets its own header color. Single tables and tables without foreign keys stay uncolored. When the system **Differentiate Without Color** accessibility setting is on, the tint is dropped and grouping is shown by position alone. +Each node lists the table's columns with their types and badges the primary and foreign keys. Tables connected by foreign keys share a header color; a table with no relationships stays uncolored. With the system **Differentiate Without Color** setting on, the tint goes and grouping is left to position. -Each table node shows: +Edges use crow's foot notation. The referenced end always carries a single bar, "exactly one". The foreign key end shows the cardinality inferred from that column's primary key and unique index data, so an edge from `orders.user_id` to `users.id` forks at `orders` and bars at `users`. -- **Header**: table name with icon, tinted by its relationship group -- **Columns**: name and data type, with badges for primary keys and foreign keys -- **Edges**: lines connecting FK columns to their referenced tables +| Foreign key column | Marker | Meaning | +|--------------------|--------|---------| +| Unique and NOT NULL | bar + bar | One-to-one | +| Unique and nullable | bar + circle | Zero-or-one-to-one | +| Not unique, NOT NULL | fork + bar | Many-to-one | +| Not unique, nullable | fork + circle | Zero-or-many-to-one | ## Navigation @@ -41,58 +44,22 @@ Each table node shows: | Reset to 100% | Click the zoom percentage label | | Copy diagram | `Cmd+C` | -## Moving tables - -Click and drag any table node to reposition it. Positions are saved automatically and persist across sessions. Drag a table toward the edge of the viewport to auto-scroll the canvas in that direction. - -Click **Reset Layout** (the circular arrow button) to return all tables to their computed positions. - -## Compact Mode - -Toggle compact mode with the compress button in the toolbar. In compact mode, each table shows only primary key and foreign key columns. +Drag a node to move it, toward the edge of the viewport to auto-scroll. Positions are saved and survive across sessions; **Reset Layout**, the circular arrow button, puts every table back where the automatic layout had it. -## Edge notation +## Compact mode -Edges use crow's foot notation. The referenced table always carries a single **bar** ("exactly one"). The foreign key side shows the relationship's cardinality, inferred from the column's primary key and unique index data: - -| Foreign key column | Marker | Meaning | -|--------------------|--------|---------| -| Unique and NOT NULL | bar + bar | One-to-one | -| Unique and nullable | bar + circle | Zero-or-one-to-one | -| Not unique, NOT NULL | fork + bar | Many-to-one | -| Not unique, nullable | fork + circle | Zero-or-many-to-one | - -A **fork** (three lines) marks the "many" side, a **bar** marks "one", and a **circle** marks an optional (nullable) side. An edge from `orders.user_id` to `users.id` shows a fork at `orders` and a bar at `users`. +The compress button in the toolbar cuts every table down to its primary key and foreign key columns, which is what makes a wide schema fit on screen. ## Junction tables -When a table's primary key is made of two foreign keys (a junction table like `user_roles`), TablePro treats it as a many-to-many relationship. By default the junction table is hidden and a single many-to-many edge is drawn between the two related tables, with a fork at each end. - -Use the **Collapse Junction Tables** toggle in the toolbar (shown only when the schema has junction tables) to expand them back to the underlying table and its two one-to-many edges. Junction tables carry a distinct header icon when expanded. +A table whose primary key is made of two foreign keys, `user_roles` and its like, is read as a many-to-many join. **Collapse Junction Tables** is on by default: the junction table is hidden and one many-to-many edge, forked at both ends, joins the two related tables. Turn it off to draw the junction table itself, with its two one-to-many edges and a distinct header icon. The toggle appears only when the schema has junction tables. ## Export -- **Export as PNG**: saves the diagram as an image. `Cmd+C` copies it to the clipboard instead. -- **Export as SQL**: opens a new query tab with `CREATE TABLE` and foreign key statements for the diagram's tables, written in the connection's SQL dialect. SQLite inlines foreign keys in each `CREATE TABLE`; other databases add them with `ALTER TABLE … ADD CONSTRAINT` after the tables are created, so the script handles circular references. +**Export as PNG** renders the whole diagram at twice screen resolution with 40 points of padding, whatever the current zoom and scroll position, and saves it as `er-diagram.png`. `Cmd+C` puts the same image on the clipboard. Compact mode and expanded junction tables carry into the render, so set the view up first. + +**Export as SQL** opens a query tab with `CREATE TABLE` and foreign key statements for the diagram's tables in the connection's dialect. SQLite inlines the foreign keys; other engines add them afterwards with `ALTER TABLE … ADD CONSTRAINT`, so the script survives circular references. ## Database support -These drivers report foreign keys, so their diagrams draw edges: - -| Database | Support | -|----------|---------| -| MySQL / MariaDB | Full | -| PostgreSQL / Redshift / CockroachDB / PGlite | Full | -| SQLite | Full | -| SQL Server | Full | -| Oracle | Full | -| DuckDB | Full | -| Snowflake | Full | -| LibSQL / Turso | Full | -| Cloudflare D1 | Full | - -Every other driver reports no foreign keys: ClickHouse, BigQuery, Trino, Teradata, Cassandra / ScyllaDB, MongoDB, Redis, DynamoDB, Elasticsearch, etcd, SurrealDB, and Beancount. **View ER Diagram** still works on those connections, so the diagram opens with every table unconnected. - - -The diagram loads columns and foreign keys for the current schema. Foreign keys that reference a table outside the loaded schema are not drawn. - +Edges are drawn on MySQL, MariaDB, PostgreSQL, Redshift, CockroachDB, PGlite, SQLite, SQL Server, Oracle, DuckDB, Snowflake, libSQL, and Cloudflare D1. Every other driver reports no foreign keys at all, so the command still opens a diagram and every table in it arrives unconnected. diff --git a/docs/features/explain-visualization.mdx b/docs/features/explain-visualization.mdx index bc2b133d8..2f058657e 100644 --- a/docs/features/explain-visualization.mdx +++ b/docs/features/explain-visualization.mdx @@ -3,13 +3,9 @@ title: EXPLAIN Visualization description: View query execution plans as diagrams, trees, or raw output --- -Click **Explain** in the query editor toolbar, or use **Query > Explain Query** (default shortcut `Cmd+Option+E`), to get the execution plan for the current query. +**EXPLAIN** estimates. **EXPLAIN ANALYZE** runs the query and reports what actually happened. Choose between them from the dropdown beside **Explain** in the query editor toolbar, or use **Query > Explain Query** (`Cmd+Option+E`); engines with a single variant show a plain button instead. -Databases with multiple EXPLAIN variants show a dropdown: PostgreSQL offers **EXPLAIN** (estimated plan) and **EXPLAIN ANALYZE** (runs the query and shows actual timing); MySQL and MariaDB offer **EXPLAIN** and **EXPLAIN (JSON)**. Databases with a single variant, like SQLite or DuckDB, show a plain Explain button. - -Typing an `EXPLAIN`, `EXPLAIN ANALYZE`, `EXPLAIN FORMAT=JSON`, or MariaDB's `ANALYZE FORMAT=JSON` statement in the editor and running it opens the same plan viewer. MySQL's TREE and ANALYZE text output is parsed into the diagram and tree views. Multi-column plans like MySQL's plain `EXPLAIN` table stay in the results grid, but a multi-column plan the driver itself declares, such as SQLite's `EXPLAIN QUERY PLAN`, still opens the viewer. - -The plan arrives as a result tab next to your query results, so you can switch back to the data without running the query again, and you can pin a plan to keep it while you try another query. +The plan arrives as a result tab next to your query results, so going back to the data costs nothing, and pinning that tab keeps a plan while you try another query. Typing an `EXPLAIN` statement in the editor and running it opens the same viewer. +## Finding the expensive step + +Every node carries a badge for its own share of the plan's total cost, taken after its children's cost is subtracted, so it marks where the work sits rather than where it accumulates. + +| Badge | Share of the plan's cost | +|---|---| +| Green circle | up to 5% | +| Yellow diamond | over 5% | +| Orange triangle | over 20% | +| Red warning triangle | over 50% | + +Shape escalates alongside color, so the badge still reads with the system **Differentiate Without Color** setting on. + +Then compare estimate against measurement. A node whose **Rows** estimate is orders of magnitude from its **Actual Rows** is why the planner chose that shape, so refresh the table's statistics before rewriting the query. On PostgreSQL a **Loops** value above 1 means the node ran once per row of its parent, which makes **Actual Time** the per-loop figure rather than the total. + -**Tree** shows the plan as an expandable outline with **Operation**, **Cost**, **Rows** and **Actual Time** columns. Drag a column edge to resize it and TablePro remembers the width. Click a column header to sort, which reorders the steps under each parent without flattening the plan. Arrow keys move between steps, and right-click copies a step. The detail panel below the outline can be resized by dragging the divider. +### Tree -**Raw** shows the original EXPLAIN output as text, with a copy button and a font size stepper in the toolbar. When the plan includes timing, planning and execution times are shown next to the view switcher. +An expandable outline with **Operation**, **Cost**, **Rows**, and **Actual Time** columns. Drag a column edge to resize it and the width is remembered. Clicking a header sorts the steps under each parent without flattening the plan. Arrow keys move between steps, right-click copies one, and the divider resizes the detail panel. -## Database support +### Raw -| Database | Variants | Output | -|----------|----------|--------| -| PostgreSQL | EXPLAIN, EXPLAIN ANALYZE | JSON, parsed into diagram and tree | -| PGlite | EXPLAIN, EXPLAIN ANALYZE | PostgreSQL's `EXPLAIN (FORMAT JSON)`, parsed into diagram and tree | -| Redshift | EXPLAIN, EXPLAIN ANALYZE | PostgreSQL's `EXPLAIN (FORMAT JSON)`, read by the PostgreSQL plan parser | -| CockroachDB | EXPLAIN, EXPLAIN ANALYZE | Text plan, parsed into diagram and tree | -| MySQL | EXPLAIN, EXPLAIN (JSON); typed EXPLAIN FORMAT=TREE or EXPLAIN ANALYZE | JSON and TREE output parsed into diagram and tree; plain EXPLAIN stays in the results grid | -| MariaDB | EXPLAIN, EXPLAIN (JSON); typed ANALYZE FORMAT=JSON | JSON output parsed into diagram and tree; plain EXPLAIN stays in the results grid | -| SQLite | Explain | EXPLAIN QUERY PLAN, parsed into diagram and tree | -| ClickHouse | Plan, Pipeline, AST, Syntax, Estimate | Indented text, parsed into diagram and tree | -| DuckDB | Explain | Indented text, parsed into diagram and tree | -| Cloudflare D1 | Query Plan | EXPLAIN QUERY PLAN, parsed into diagram and tree | -| LibSQL / Turso | Query Plan | EXPLAIN QUERY PLAN, parsed into diagram and tree | -| Snowflake | Explain (Text) | Raw text | -| Trino | Explain (Logical), Explain (Distributed), Explain (IO), Explain (Validate), Explain Analyze | Raw text | -| SurrealDB | Explain, Explain Full | Raw text | -| BigQuery | Dry Run (Cost) | Dry run cost estimate | -| MongoDB | Explain | `explain` runCommand with execution stats | -| Redis | Explain | `DEBUG OBJECT` for the command's key | +The original EXPLAIN output as text, with a copy button and a font size stepper. Planning and execution times, where the plan reports them, sit beside the view switcher in every mode. + +## Node details -## Plan details +Selecting a node in either view fills the detail panel: -Each node in the plan shows: +| Field | What it holds | +|---|---| +| **Table** | The relation this step reads | +| **Cost** | `startup..total`, or the total alone when that is all the plan carries | +| **Rows**, **Width** | Estimated row count and row width | +| **Actual Time**, **Actual Rows**, **Loops** | Present only on a measured plan | +| **Details** | Everything else the driver reported: join type, index name, filter conditions, sort keys | -- **Operation**: Seq Scan, Index Scan, Hash Join, Nested Loop, Sort, etc. -- **Table**: which table the operation accesses -- **Cost**: startup and total cost estimates (PostgreSQL format: 0.00..45.18) -- **Rows**: estimated number of rows -- **Actual time**: real execution time per node (EXPLAIN ANALYZE only) +Properties the driver reported as false or zero are left out of **Details**. -Click a node to see all properties including join type, index name, filter conditions, and sort keys. +## Database support + +| Database | Variants | Diagram and tree | +|---|---|---| +| PostgreSQL, PGlite, Redshift | EXPLAIN, EXPLAIN ANALYZE | From `EXPLAIN (FORMAT JSON)` | +| CockroachDB | EXPLAIN, EXPLAIN ANALYZE | From the text plan | +| MySQL | EXPLAIN, EXPLAIN (JSON) | From JSON, and from a typed `EXPLAIN FORMAT=TREE` or `EXPLAIN ANALYZE`. Plain EXPLAIN stays in the results grid | +| MariaDB | EXPLAIN, EXPLAIN (JSON) | From JSON, including a typed `ANALYZE FORMAT=JSON`. Plain EXPLAIN stays in the results grid | +| SQLite, Cloudflare D1, libSQL / Turso | Query Plan | From `EXPLAIN QUERY PLAN` | +| ClickHouse | Plan, Pipeline, AST, Syntax, Estimate | From the indented text | +| DuckDB | Explain | From the indented text | +| Snowflake | Explain (Text) | Raw text only | +| Trino | Logical, Distributed, IO, Validate, Analyze | Raw text only | +| SurrealDB | Explain, Explain Full | Raw text only | +| BigQuery | Dry Run (Cost) | A dry run cost estimate, no plan | +| MongoDB | Explain | The `explain` runCommand with execution stats | +| Redis | Explain | `DEBUG OBJECT` for the command's key | `EXPLAIN` does not run the query. `EXPLAIN ANALYZE` does, so on a production server it costs whatever the query costs. Safe Mode asks before an EXPLAIN too, whichever way you started it, and the Stop button cancels one that is taking too long. diff --git a/docs/features/favorites.mdx b/docs/features/favorites.mdx index fdef30175..f5e69a747 100644 --- a/docs/features/favorites.mdx +++ b/docs/features/favorites.mdx @@ -3,52 +3,54 @@ title: Favorites description: Mark tables as favorites and save frequently used queries with optional keyword shortcuts --- -The Favorites tab in the sidebar has two sections: **Tables** for pinned tables and **Queries** for saved SQL. The sidebar can also show a **Recent** section with the last tables you opened (off by default). +The query you retype every Monday is worth saving once. Give it the keyword `dau`, and from then on typing `dau` in the editor expands the whole statement, with the cursor already where you need to fill something in. + +The Favorites tab in the sidebar holds two sections, **Tables** for pinned tables and **Queries** for saved SQL, plus a **Team Library** on a Team license and any folders of `.sql` files you have linked. ## Table favorites -Every table row in the sidebar has a star button at the end. Click it to add or remove the table from favorites. A filled yellow star marks a favorite. Favorites move to the top of their section and appear in the **Tables** group of the Favorites tab. +Hover a table row in the sidebar and a star appears at the end of it. Click the star to pin the table; a filled yellow star marks one that is already pinned, so it stays visible without hovering. Favorites move to the top of their section and appear under **Tables** in the Favorites tab. -Double-click a table in the Favorites tab to open it. Right-click it to open the table, open the database's ER diagram, or remove it. +Double-click a favorite there to open the table. Its right-click menu has **Open Table**, **Show ER Diagram**, and **Remove from Favorites**. -Favorites are scoped to the connection, database, and schema, and sync through iCloud. A favorite is hidden when its table doesn't exist in the database you're viewing. +Favorites are scoped to the connection, database, and schema, and sync through [iCloud](/features/icloud-sync). One whose table does not exist in the database you are viewing is hidden rather than shown broken. ## Recent tables -Turn on **Settings > General > Sidebar > Show recent tables** to add a **Recent** section at the top of the sidebar. It appears in every sidebar layout. Each table you open is recorded, up to 10 per connection and database, most recent first. Arrowing through preview tabs does not count. Click a row to reopen the table, or right-click to remove one entry or clear the list. Recents persist between launches. - -## SQL favorites - -### Creating a favorite +Turn on **Settings > General > Sidebar > Show recent tables** for a **Recent** section at the top of the sidebar. It records the last 10 tables you opened per connection and database, most recent first; arrowing through preview tabs does not count. Click a row to reopen the table, or right-click to remove one entry or clear the list. Recents stay device-local and survive relaunches. -- **From the editor toolbar**: Click the star button above the editor, or press `Cmd+D` -- **From the editor**: Right-click selected SQL > **Save as Favorite** -- **From query history**: Right-click an entry > **Save as Favorite** -- **From the sidebar**: Click **+** in the Favorites tab, then **New Favorite** +## Saving a query -Enter a name, the SQL text, and optionally a keyword and scope. +| From | How | +|------|-----| +| The editor toolbar | Click the star above the editor, or press `Cmd+D` | +| Selected SQL | Right-click > **Save as Favorite** | +| [Query history](/features/query-history) | Right-click an entry > **Save as Favorite** | +| The sidebar | **+** in the Favorites tab, then **New Favorite…** | -The **+** menu also has **New Query** (opens an empty SQL query tab, same as `Cmd+T`), **New Folder**, and **Add Linked SQL Folder…** +Give it a name, the SQL, and optionally a keyword and a scope. The **+** menu also carries **New Query**, **New Folder**, and **Add Linked SQL Folder…** Creating a new SQL favorite Creating a new SQL favorite -### Keyword expansion +A new favorite belongs to the connection you created it in. Select **Global** in the dialog to make it available in all connections, which is what you want for anything that does not name objects only one database has. Deleting a connection deletes the favorites scoped to it. -Assign a unique keyword to a favorite (e.g., `selall`). Start typing the keyword in the SQL editor and it shows up in the autocomplete popup as a starred suggestion with the favorite's name. Press Tab or Enter to insert the full SQL. +## Keywords -Keywords must be unique across all favorites in the same scope. Typing a keyword in [Open Quickly](/features/open-quickly) (`Cmd+Shift+O`) also finds the saved query. +A keyword is a shorthand you type in the editor. Type its first letters, case does not matter, and it appears in the completion popup as a starred entry with the favorite's name; `Tab` or `Return` inserts the full SQL. Keywords must be unique inside their scope. Keyword expansion in autocomplete Keyword expansion in autocomplete +Typing a keyword in [Open Quickly](/features/open-quickly) finds the same query without touching the sidebar. + ### Cursor placement -Put `;;` in a favorite's SQL to set where the cursor lands after expansion. The marker is removed on insert and the cursor is placed at its position: +Put `;;` in the SQL to say where the cursor lands after expansion. The marker is removed on insert: ```sql SELECT COUNT(*) @@ -56,47 +58,39 @@ FROM orders WHERE orders.;; ``` -Accepting the keyword inserts the query without the `;;` and leaves the cursor right after `orders.`, ready for a column name. Only the first `;;` counts as a marker. Without one, the cursor lands at the end of the inserted SQL. - -### Scope - -Each favorite is either **global** (visible in all connections) or **connection-scoped** (visible only in the connection where it was created). Set the scope when creating or editing a favorite. Deleting a connection deletes its scoped favorites. +Accepting that keyword leaves the cursor right after `orders.`, ready for a column name. Only the first `;;` counts; without one the cursor lands at the end. -### Managing favorites +## Working with saved queries -Double-click a favorite in the Favorites tab, or select it and press Return, to insert it into the editor. Arrow keys move between rows and typing jumps to a name. `Delete` removes the selected favorite. The right-click menu has **Insert in Editor**, **Run in New Tab**, **Copy Query**, **Edit…**, **Move to** (a folder), and **Delete**. +Double-click a favorite, or select it and press `Return`, to insert it into the editor. That is the default; **Run in New Tab** on the right-click menu runs it instead, and the same menu has **Copy Query**, **Edit…**, **Move to** for filing it in a folder, and **Delete**. Arrow keys move between rows, typing jumps to a name, and `Delete` removes the selection. -Organize favorites into folders: create them from the **+** menu, right-click a folder to rename or delete it, and use **Move to** on a favorite to file it. Renaming happens in place, on the row. - -Drag a saved query or a linked `.sql` file out of the sidebar to drop its SQL into the editor or another app. +Create folders from the **+** menu, rename or delete them by right-clicking, and rename in place on the row. Dragging a saved query or a linked file out of the sidebar drops its SQL into the editor or another app. ## Team Library -On a Team license, the Favorites tab shows a **Team Library** section with queries shared by your team. Double-click one, or select it and press Return, to run it in a new tab. To share yours, click **+ > Publish Saved Queries to Team…** See [Team Plan](/features/team). +On a Team license the Favorites tab gains a **Team Library** section holding queries your team shared. Double-click one, or select it and press `Return`, to run it in a new tab. To share yours, click **+ > Publish Saved Queries to Team…** See [Team Plan](/features/team). ## Linked SQL folders -Link a folder of `.sql` files on disk and they show up in the Favorites sidebar live. Useful for a Git repo of shared queries: clone the repo, link the folder, the team's queries appear next to your DB-stored favorites. - -This is a different feature from [Linked Folders](/features/connection-sharing#linked-folders), which watches a folder of shared `.tablepro` connection files. +Link a folder of `.sql` files on disk and they appear in the Favorites sidebar next to your saved queries. The point is a Git repo of shared queries: clone it, link it, and the team's queries stay one click away and current with `git pull`. -### Adding a folder + +This is not [Linked Folders](/features/connection-sharing#linked-folders), which watches a folder of shared `.tablepro` connection files. + -In the Favorites sidebar, click **+** and choose **Add Linked SQL Folder…** Pick any folder. Subfolders nest in the sidebar in the same shape as on disk. You can link as many folders as you want; all linked folders are global and show in every connection's Favorites tab. +Click **+** in the Favorites sidebar and choose **Add Linked SQL Folder…** Pick any folder; `.sql`, `.psql`, and `.pgsql` files are indexed, subfolders nest the way they do on disk, and hidden files are skipped. Folders are global, so every connection's Favorites tab shows them, and a large repo is fine: only the first 4 KB of each file is read to build the sidebar. ### Editing files -Click a linked file to open it as a regular editor tab. `Cmd+S` writes back to disk in the file's original encoding. UTF-8, UTF-16, ISO Latin-1 and a few others are auto-detected on load and preserved on save. - -If the file was modified outside TablePro since you opened it, a yellow banner appears above the editor with a one-click **Reload from Disk**. If you save anyway, TablePro shows a side-by-side diff sheet with **Keep My Changes**, **Reload from Disk**, and **Cancel**. +Click a linked file to open it as an ordinary editor tab. `Cmd+S` writes back in the file's original encoding, detected on load from UTF-8, UTF-16, ISO Latin-1, and a few others. -External edits propagate to the sidebar within about a second via FSEvents: new files appear, deleted files disappear, and `git pull` triggers the same refresh. +A file changed on disk after you opened it, by a `git pull` or a merge, shows a yellow banner above the editor with **Reload from Disk**. Save anyway and a side-by-side diff sheet offers **Keep My Changes**, **Reload from Disk**, and **Cancel**. Files added and removed outside TablePro reach the sidebar a second or two later on their own. -Non-UTF-8 files show a yellow warning triangle in the sidebar. Saving works in their native encoding; a character that doesn't fit (e.g., an emoji into ISO Latin-1) fails the save with an error instead of silently losing data. +A non-UTF-8 file carries a yellow warning triangle in the sidebar. Saving keeps its encoding, and a character that does not fit, an emoji into ISO Latin-1, fails the save with an error rather than losing the character quietly. ### Frontmatter -Top-of-file SQL comments set the display name, autocomplete keyword, and tooltip: +Leading SQL comments set the display name, the keyword, and the tooltip: ```sql -- @name: Active Users (24h) @@ -109,22 +103,16 @@ WHERE last_seen > NOW() - INTERVAL 24 HOUR; | Key | Effect | |-----|--------| -| `@name` | Display name in the sidebar. Falls back to the filename without `.sql`. | -| `@keyword` | Autocomplete trigger. Type the keyword in the editor and the file content expands as a query. | -| `@description` | Optional. Shown in tooltips. | - -The parser stops at the first non-frontmatter line, so put these at the very top. A UTF-8 BOM is handled. Files without frontmatter still appear, with the filename as the display name and no keyword. The `;;` [cursor marker](#cursor-placement) works in linked files too. - -To edit frontmatter without opening the file, right-click a linked row and choose **Edit Metadata…** The dialog rewrites only the leading comment block and preserves the rest of the file plus its original encoding. - -### Managing linked files and folders +| `@name` | Display name in the sidebar. Falls back to the filename without its extension. | +| `@keyword` | Autocomplete trigger, exactly as for a saved favorite. | +| `@description` | Shown in tooltips. Optional. | -Drag any row from the Favorites sidebar (linked or DB-stored) onto the SQL editor to insert its content at the cursor. +The parser stops at the first line that is not frontmatter, so these go at the very top; a UTF-8 BOM is handled. A file without frontmatter still appears, under its filename and with no keyword. The `;;` [cursor marker](#cursor-placement) works in linked files too. -Press Delete on a linked file or right-click > **Move File to Trash**; the file goes to the macOS Trash and stays recoverable. Right-click a linked folder root for **Disable**, **Reload**, **Copy Path**, **Show in Finder**, **Add Another SQL Folder…**, or **Remove from Sidebar**. Removing only unlinks the folder; files on disk stay where they are. +To change frontmatter without opening the file, right-click the row and choose **Edit Metadata…** The dialog rewrites the leading comment block only and keeps the rest of the file and its encoding. -## Storage +### Managing linked files -Favorites live in a SQLite database (`sql_favorites.db`) in `~/Library/Application Support/TablePro/`. Search covers name, keyword, and query text. +Press `Delete` on a linked file, or right-click > **Move File to Trash**: it goes to the macOS Trash and stays recoverable. Right-click a folder's root row for **Disable**, **Reload**, **Copy Path**, **Show in Finder**, **Add Another SQL Folder…**, and **Remove from Sidebar**. Removing unlinks the folder; the files stay where they are. -Linked folder paths live in UserDefaults under `com.TablePro.linkedSQLFolders`, and parsed metadata (name, keyword, mtime, size, encoding) is cached in `linked_sql_index.db` in the same directory so the sidebar renders without re-reading every file. File content always lives on disk. Linked folder paths are not part of iCloud Sync; each Mac links its own copy of a shared repo. +Linked folder paths are the one part of Favorites that does not sync, so each Mac links its own copy of a shared repo. diff --git a/docs/features/filtering.mdx b/docs/features/filtering.mdx index 990d21bed..d3947f462 100644 --- a/docs/features/filtering.mdx +++ b/docs/features/filtering.mdx @@ -1,6 +1,6 @@ --- title: Filtering -description: Filter table data with 18 operators, raw SQL, and saved presets +description: Build a WHERE clause from the filter bar, or write one by hand, and save it as a preset --- A filter is a WHERE clause, and it runs at the database. That is the difference between filtering and the find bar: matches come from every row in the table, not from the page you are looking at. Open the filter bar with `Cmd+Option+F`, type a condition into the row that appears, and press `Enter`. @@ -10,124 +10,104 @@ A filter is a WHERE clause, and it runs at the database. That is the difference Filter panel above the data grid -Each row has a drag grip, a checkbox, a column picker, operator, value field, and **+**/**−** buttons. The **Match all** / **Match any** picker in the header combines rows with AND or OR. You can also right-click a column header in the data grid and choose **Filter with column** to pre-fill a filter row for that column. +Each row has a drag grip, a checkbox, a column picker, an operator, a value field, and **+**/**−** buttons. Right-clicking a column header in the grid and choosing **Filter with column** pre-fills a row for that column. | Control | What it does | |---------|--------------| -| Header **Apply** (or `Enter` in a value field, or `Cmd+Return`) | Runs all checked filter rows | -| Row checkbox | Turns that filter off without deleting it; unchecked rows stay dimmed. Toggling does not re-run the query, press **Apply** when ready | -| Row **Apply** | Filters by just that row, leaving all checkboxes as they are, so you can test one condition and return to the checked set with the header **Apply** | -| **Clear** | Removes the applied filter without deleting the filter rows | -| Row grip (left edge) | Drag it onto another row to move the filter into that row's position. The whole condition moves, so the column, operator, and value stay as they were. The grip appears once there are two or more rows | -| Right-click > **Move Up** / **Move Down** | Reorders a row without dragging | +| Header **Apply**, `Enter` in a value field, or `Cmd+Enter` | Runs every checked row | +| Row checkbox | Turns that row off without deleting it. Press **Apply** to re-run | +| Row **Apply** | Filters by that row alone, without touching the checkboxes, so the header **Apply** brings the checked set back | +| **Clear** | Drops the applied filter and keeps the rows | +| Row grip, or right-click > **Move Up** / **Move Down** | Reorders the list. Order never changes the result | +| ⋯ > **Remove All Filters** | Deletes every saved row for this table | -Row order is for organizing the list. Every row is joined by the same **Match all** or **Match any** setting, so moving a row never changes which rows the query returns. - -When you reopen a table, TablePro restores the filters you last applied to it, including the ones you left unchecked, after you quit and relaunch. Filters are saved per connection, database, schema, and table, so two tables on the same connection keep independent filters. If you cleared the filter with **Clear** or removed its rows with **−**, the table reopens unfiltered. Tables with active filters open in a new tab when you click another table. - -To delete every saved filter row for the current table, open the **⋯** menu and choose **Remove All Filters**. - -The filter bar queries the database. To narrow rows already loaded in the grid without re-querying, use the column value filter in the [Data Grid](/features/data-grid). +Filters are saved per connection, database, schema, and table, and survive a relaunch, unchecked rows included. A table with an active filter opens in a new tab rather than replacing the current one. -Redis shows a key-pattern search bar (for example `user:*`) with a key-type picker instead of the filter bar. +Redis shows a key-pattern search bar (`user:*` and the like) with a key-type picker instead of the filter bar. ## Operators -18 operators with SQL symbols shown inline: `=`, `!=`, `LIKE %..%`, `NOT LIKE %..%`, `LIKE ..%`, `LIKE %..`, `>`, `>=`, `<`, `<=`, IS NULL, IS NOT NULL, IS EMPTY, IS NOT EMPTY, `IN (..)`, `NOT IN (..)`, `BETWEEN`, `~` (regex). +Eighteen of them, and the menu shows the SQL each one produces. -BETWEEN shows two value fields. IN/NOT IN takes comma-separated values. +| Operators | SQL | +|---|---| +| equals, not equals | `=`, `!=` | +| contains, not contains | `LIKE %..%`, `NOT LIKE %..%` | +| starts with, ends with | `LIKE ..%`, `LIKE %..` | +| greater than, greater or equal, less than, less or equal | `>`, `>=`, `<`, `<=` | +| is NULL, is not NULL | `IS NULL`, `IS NOT NULL` | +| is empty, is not empty | On a text column, NULL or `''`. On any other column, the same as is NULL | +| in list, not in list | `IN (..)`, `NOT IN (..)`, from comma-separated values | +| between | `BETWEEN`, with two value fields | +| matches regex | `~`, or the engine's own regex function. Where the engine has none, it falls back to contains | ## Case sensitivity -Contains, not contains, starts with, and ends with ignore case. Equals, not equals, IN, NOT IN, and regex match case. Open the operator menu and use **Match Case** to change either one for that row. A small `Aa` icon on the operator button marks a row that is not using its usual setting. - -TablePro picks the fastest form each database offers: `ILIKE` on PostgreSQL, DuckDB, CockroachDB, and Snowflake, and `LOWER()` on both sides for Oracle, BigQuery, ClickHouse, and Redshift, whose `ILIKE` only folds ASCII. Trino has no `ILIKE`, so it uses `regexp_like` with the `i` flag. +Contains, not contains, starts with, and ends with ignore case. Equals, not equals, IN, NOT IN, and regex respect it. Open the operator menu and use **Match Case** to flip either one for that row; an `Aa` badge marks a row that is not on its usual setting. -On MySQL, MariaDB, SQL Server, SQLite, libSQL, and Cloudflare D1 the column's collation decides case sensitivity and TablePro cannot override it per query, so **Match Case** is dimmed. The default collation on these databases already ignores case. Cassandra and Redis have no case-insensitive matching at all, so the option is dimmed there too. +On MySQL, MariaDB, SQL Server, SQLite, libSQL, and Cloudflare D1 the column's collation decides, and no query can override it, so **Match Case** is dimmed. Their default collations already ignore case. Cassandra and Redis have no case-insensitive matching at all, so it is dimmed there too. -Filters saved before this option existed follow the same rules, so a saved "contains" filter now ignores case on PostgreSQL, DuckDB, Oracle, and BigQuery. Turn **Match Case** on to get the old behavior back. +## Combining conditions + +**Match all** and **Match any** in the header join every row with AND or OR. There is no per-row choice and no grouping, so a mixed expression cannot be built out of rows. Put the parentheses in a raw SQL row instead, which enters the WHERE clause as a single parenthesized condition: + +```sql +(status = 'open' AND priority > 3) OR escalated +``` + +⋯ > **Preview Query** shows the WHERE clause the rows produce, with a copy button. + +An applied filter reaches an export only through the grid. **File > Export > Export Results…** writes the rows the grid holds; the toolbar export reads the table itself and ignores the filter bar. See [Import & Export](/features/import-export). ## Nested fields -On a document store such as MongoDB, a field holding an object or an array of objects is one column in the grid, showing its JSON. The column picker also lists the paths inside it, so you can filter on `customer.country` or `items.sku` directly. +On a document store such as MongoDB, a field holding an object or an array of objects is one column showing its JSON. The column picker also lists the paths inside it, so `customer.country` and `items.sku` filter directly. Filter column picker with nested paths grouped under their top-level field Filter column picker with nested paths grouped under their top-level field -TablePro reads the paths from a sample of the collection, so the picker lists the fields the sample actually contained, grouped under their top-level parent. Only the first two levels go in the picker. The button beside it opens a searchable list of every path found, and typing a path there that no document in the sample had uses it anyway, which is how you reach a field that only some documents carry. - -A field whose name contains a literal dot is left out. MongoDB reads the dot in a query as a path separator, so a filter naming such a field would match a different field. +The paths come from a sample of the collection and only the first two levels reach the picker. The button beside it opens a searchable list of every path found, and a path typed there that no sampled document carried is used anyway, which is how to reach a field only some documents have. See [MongoDB](/databases/mongodb) for the fields this leaves out. ### Fields inside an array -A row on a field inside an array gets one more control, because MongoDB has two different answers here: - - - Filter rows on an array field showing the any element and same element control - Filter rows on an array field showing the any element and same element control - +A row on a field inside an array gets one more control: | Setting | What it matches | |---------|-----------------| -| **any element** | Each row is satisfied on its own by any entry in the array. Two rows can be satisfied by two different entries | +| **any element** | The row is satisfied by any entry in the array. Two rows can be satisfied by two different entries | | **same element** | Every row set to **same element** for that array must be satisfied by one single entry | -Filtering `items.price` greater than 500 and `items.name` equal to `Laptop` with **any element** returns an order that has an expensive item and, separately, an item called Laptop. With **same element** on both rows it returns only orders holding one item that is both. **any element** is the default, and it is what dot notation means in MongoDB. +Filtering `items.price` greater than 500 and `items.name` equal to `Laptop` with **any element** returns an order holding an expensive item and, separately, an item called Laptop. With **same element** on both rows it returns only orders holding one item that is both. **any element** is the default. -Rows sharing an array and set to **same element** are grouped together automatically, and the header's **Match all** / **Match any** setting applies inside the group too: with **Match any**, one array entry satisfying any of those rows is enough. Open **SQL Preview** in the **⋯** menu to see the query this produces. + + Filter rows on an array field showing the any element and same element control + Filter rows on an array field showing the any element and same element control + -Sorting still works on the grid's own columns only, so a nested path can be filtered but not sorted. +Rows sharing an array and set to **same element** group together, and **Match all** / **Match any** applies inside the group too. ## Raw SQL -The default mode. On a database that does not speak SQL the row is labelled **Raw Filter** and takes that database's own filter syntax: on MongoDB, a filter document such as `{"customer.country": "US"}`. - -Type any WHERE condition directly: +A new row starts in raw SQL, unless **Default Column** in the settings says otherwise, and picking a column from its picker switches it to column mode. Type any WHERE condition: ```sql created_at > NOW() - INTERVAL 7 DAY ``` -```sql -price * quantity > 1000 -``` - -As you type, autocomplete suggests the table's columns and SQL keywords at every position in the expression, including after AND and OR. Use the arrow keys to pick a suggestion and Tab or Return to insert it. Suggestions only appear once you start a word, not on an empty field or after a space. Press Escape to dismiss the suggestions; the filter bar stays open. - -To switch a row to column mode, select a column from the picker. +Autocomplete offers the table's columns and SQL keywords at every position, after AND and OR included. Arrow keys pick a suggestion, `Tab` or `Return` inserts it, `Escape` dismisses the list without closing the filter bar. On a database that does not speak SQL the row is labeled **Raw Filter** and takes that database's own syntax: on MongoDB, a filter document such as `{"customer.country": "US"}`. -Raw SQL is injected directly into the WHERE clause. Ensure syntax matches your database type. +The text goes into the WHERE clause as written. A condition whose syntax does not match the database fails the query. -## Presets - -Save and load filter configurations via the **⋯** menu in the header. - -| Action | How | -|--------|-----| -| Save | ⋯ > **Save as Preset…** | -| Load | ⋯ > click preset name | -| Delete | ⋯ > **Delete Preset** > click name | - -## SQL Preview - -⋯ > **Preview Query** shows the generated WHERE clause with a copy button. - -## Settings - -⋯ > **Filter Settings** to configure: +## Presets and settings -| Setting | Options | -|---------|---------| -| Default Column | Raw SQL, Primary Key, Any Column | -| Default Operator | Equal, Contains | -| Panel State | Always Hide, Always Show, Restore Last Filter | +Both live in the ⋯ menu. **Save as Preset…** stores the current rows, clicking a preset name loads it, and **Delete Preset** removes one. -**Panel State** controls what happens when you reopen a table. **Restore Last Filter** (the default) brings back the filter you last applied. **Always Hide** reopens tables unfiltered with the panel closed. **Always Show** keeps the filter bar visible even when no filter is set. +**Filter Settings** sets what a new row starts as (**Default Column**: Raw SQL, Primary Key, or Any Column; **Default Operator**: Equal or Contains) and what happens when you reopen a table. **Restore Last Filter**, the default, brings back the filter you last applied; **Always Hide** reopens the table unfiltered with the bar closed; **Always Show** keeps the bar visible with no filter set. diff --git a/docs/features/handoff.mdx b/docs/features/handoff.mdx index aff4e9bb1..7bb5ca721 100644 --- a/docs/features/handoff.mdx +++ b/docs/features/handoff.mdx @@ -1,28 +1,34 @@ --- title: Handoff -description: Resume the active connection or table on another Mac signed into the same iCloud account +description: Resume the active connection or table on another device signed into the same iCloud account --- -TablePro publishes an `NSUserActivity` for the key window so Apple Continuity can hand off the current context to another Mac. +Continuity carries which connection you have open, never the connection's settings, so the receiving device needs that connection saved already. [iCloud Sync](/features/icloud-sync) is what puts it there. + + + Handoff icon in the Dock + Handoff icon in the Dock + ## Requirements -- Both Macs signed into the same iCloud account -- Both Macs on the same Wi-Fi network with Bluetooth on -- Handoff enabled in **System Settings > General > AirDrop & Handoff** -- TablePro running on both devices +- Both devices signed into the same iCloud account +- Both on the same Wi-Fi network, with Bluetooth on +- Handoff turned on in **System Settings > General > AirDrop & Handoff** +- TablePro running on both, and the connection saved on both + +## What carries over -## What gets handed off +The connection in front of you, and the table too when the frontmost tab is a table tab. Query text, scroll position, and unsaved edits stay put. [TablePro for iPhone and iPad](/ios) takes part on both sides. -The activity tracks the focused window and switches between two types based on what is selected: +## Picking it up -- **`com.TablePro.viewConnection`**: connection ID. Receiving Mac opens that connection. -- **`com.TablePro.viewTable`**: connection ID and table name. Receiving Mac opens the connection and selects the table. +The TablePro Handoff icon appears in the receiving Mac's Dock and app switcher (`Cmd+Tab`), and at the bottom of the app switcher on iPhone and iPad. Open it to get the same connection, and the table if there was one. -Switching between a query tab and a table tab updates the activity automatically. The activity title shows the table name when viewing a table, otherwise the connection name. +## When nothing arrives -Query text, scroll position, and unsaved edits are not included. +Handoff is a system service, so the app cannot report why it stayed quiet. Check both devices: same iCloud account, awake and unlocked, Bluetooth and Wi-Fi on, Handoff enabled. -## Use it +### No saved connection with ID "…". -On the receiving Mac, the TablePro Handoff icon appears in the Dock and in the Cmd-Tab app switcher. Click it to open the same connection (and table, if any). +The icon arrived, so Handoff worked; the connection itself is not on this device. Turn on [iCloud Sync](/features/icloud-sync) on both, or export the connection and import it here. diff --git a/docs/features/icloud-sync.mdx b/docs/features/icloud-sync.mdx index c1708c94c..677745577 100644 --- a/docs/features/icloud-sync.mdx +++ b/docs/features/icloud-sync.mdx @@ -3,54 +3,45 @@ title: iCloud Sync description: Sync connections, table favorites, settings, and SSH profiles across Macs and iOS via iCloud --- -Sync is off by default, and it needs an active license, Starter or Team (see [Licensing](/features/licensing)). Every category has its own toggle, and individual connections can be marked **Local only**. The iPhone and iPad app shares the same CloudKit data, but carries connections, groups, and tags only. +Sync needs a Starter or Team [license](/features/licensing) and starts off. Press `Cmd+,`, open **Settings > Account**, turn on **iCloud Sync**, pick the categories, and click **Sync Now**. The iPhone and iPad app shares the same CloudKit data but carries connections, groups, and tags only. -## What syncs (and what doesn't) + + iCloud Sync settings + iCloud Sync settings + + +## What syncs + +Each synced category has its own toggle under **Sync Categories**. | Data | Synced | Notes | |------|--------|-------| | **Connections** | Yes | Host, port, username, database type, SSH/SSL config | -| **Passwords** | Opt-in | Via iCloud Keychain (end-to-end encrypted), nested under the Connections toggle | -| **Groups & Tags** | Yes | Full connection organization, including nested group hierarchy and sort order | -| **SSH Profiles** | Yes | Named [SSH profiles](/connections/ssh-profiles). Has its own toggle. | -| **Table Favorites** | Yes | Favorited table names shown in the Favorites tab and pinned in table lists | -| **Saved Queries** | Yes | Saved SQL queries and their folders. Has its own toggle. | +| **Passwords** | Opt-in | Nested under Connections, and carried by Apple's iCloud Keychain rather than TablePro's own records. Turning it on affects new saves only, so re-save a password to sync it. On iPhone and iPad the same switch is **Settings > Sync > Sync Passwords** | +| **Groups & Tags** | Yes | Nested group hierarchy and sort order included | +| **SSH Profiles** | Yes | Named [SSH profiles](/connections/ssh-profiles) | +| **Table Favorites** | Yes | The names shown in the Favorites tab and pinned in table lists | +| **Saved Queries** | Yes | Saved SQL and their folders | | **Settings** | Yes | General, Appearance, Editor, Data Grid, History, Tabs, Keyboard, and AI settings, plus custom AI slash commands and saved per-table column widths and order | -| **Linked SQL Folders** | No | Folder paths are per-Mac. Link the same Git repo on each Mac after cloning. Cached file metadata (`linked_sql_index.db`) is also local. | +| **Linked SQL Folders** | No | Folder paths are per-Mac. Link the same Git repo on each Mac after cloning. The cached file index stays local too | | **Sidebar state** | No | The sidebar's database filter and recent tables lists stay on each Mac | - -Passwords are not synced by default. Enable the **Passwords** toggle under Connections to sync them via Apple's iCloud Keychain. Enabling it only affects new saves: re-save a password to update its sync. With password sync off, enter the password once on each new Mac. +A sync runs at launch, when you switch back to the app, and 2 seconds after you change anything synced. -On iPhone and iPad the same opt-in lives in **Settings > Sync > Sync Passwords**, under the iCloud Sync toggle. It is off by default and, like the Mac, only affects new saves. - +## Keeping a connection off iCloud -## Enabling iCloud Sync +A localhost or throwaway database is rarely worth a round trip. Mark it **Local only** in the connection form's **Advanced** pane, or right-click it and choose **Exclude from iCloud Sync**. They show a struck-through cloud icon in the connection list, and the flag survives duplicating and exporting. -Press `Cmd+,`, open **Settings > Account**, toggle iCloud Sync on, choose which categories to sync, and click **Sync Now**. Sync is off by default. +## Checking that it worked - - iCloud Sync settings - iCloud Sync settings - - -Connections (with a nested Passwords opt-in), Groups & Tags, SSH Profiles, Settings, Table Favorites, and Saved Queries each have their own toggle. Custom AI slash commands and saved column layouts ride along with the Settings toggle. - -## Excluding individual connections - -Some connections (e.g., localhost, dev databases) don't make sense on other devices. Mark them as **Local only** to keep them off iCloud: +The **Sync Status** section answers this: **Account** reads iCloud Connected or Not Available, **Last Synced** gives the relative time of the last successful run, and a failure prints in red under **Sync Now**. -- **Connection form**: open the **Advanced** pane and toggle **Local only** -- **Context menu**: right-click a connection and choose **Exclude from iCloud Sync** +When the same connection changed on two devices, TablePro merges field by field, so a name edited on one Mac and a port edited on the other both survive. When one field changed in both places, the device that syncs last wins. -Local-only connections show an `icloud.slash` icon in the sidebar. The flag is preserved when duplicating or exporting connections. - -TablePro auto-syncs on app launch, when you switch back to it, and 2 seconds after you modify synced data. - -When the same connection changes on two devices, TablePro merges them field by field: a name changed on one device and a port changed on the other both survive. If the same field changed in both places, the device that syncs last wins. +## Troubleshooting -When a license expires, sync stops but local data remains. Re-activate to resume. +If nothing syncs, confirm iCloud is signed in and iCloud Drive is enabled, then click **Sync Now**. For "iCloud account unavailable", sign in through **System Settings > Apple Account**. -## Troubleshooting +Signing into a different iCloud account clears TablePro's sync metadata, so the next sync is a full fetch rather than a delta. Expect it to take longer once. -If no records sync, confirm iCloud is signed in and iCloud Drive is enabled, then click **Sync Now**. For "iCloud account unavailable," sign in via **System Settings > Apple Account**. +When a license expires, sync stops and local data stays where it is. Re-activate to resume. diff --git a/docs/features/import-export.mdx b/docs/features/import-export.mdx index 9948dbded..95edbe63e 100644 --- a/docs/features/import-export.mdx +++ b/docs/features/import-export.mdx @@ -3,29 +3,36 @@ title: Import & Export description: Export to CSV, JSON, SQL, MQL, or XLSX. Import SQL, JSON, and CSV files with column mapping and transaction safety --- -Where you start the export decides what you get. From the toolbar (`Cmd+Shift+E`) rows stream out of the database straight to disk, so table size does not matter and there is no row limit. From a results grid, **Export Results…** on the right-click menu writes only the rows already in memory. Import is `Cmd+Shift+I`. +The toolbar's **Export** (`Cmd+Shift+E`) re-reads every table you tick with `SELECT * FROM`, so what is on screen has no bearing on the file: filters, sort, hidden columns, and the page you are on are all ignored, and table size does not matter. **Export Results…** on the grid's right-click menu writes what the tab has loaded instead. Import is `Cmd+Shift+I`. -## Export Data +## Export data -1. Open a table, or run a query and use its results -2. Click **Export** in the toolbar (`Cmd+Shift+E`), or right-click the results grid and choose **Export Results…** -3. Choose a format, select tables in the tree view, configure options -4. Click **Export** - -TablePro remembers the last format and options you exported with. Option changes stick only after a successful export; cancelling the dialog discards them. **Reset to Defaults** under the options restores the stock settings for the current format. - - -**MongoDB**: SQL export is not available. Use MQL, which generates `db.collection.insertMany([…])` scripts for `mongosh`. **Redis**: SQL and MQL exports are not available. + + + Open a table or run a query, then click **Export** in the toolbar (`Cmd+Shift+E`), or right-click the results grid and choose **Export Results…**. + + + Pick a format, tick tables in the tree, and set the options for that format. + + + Click **Export**. The destination file appears only on success: a failed or cancelled export removes its partial file. + + -MQL writes top-level typed values as the constructors mongosh understands, so `_id` comes back as an `ObjectId`, a date as an `ISODate`, and binary as `BinData` with its real subtype. A typed value **nested inside a subdocument or array** is written as a string, because the parent is serialized to JSON before the exporter sees it. Re-importing such a script gives you a string where the original held an ObjectId, date, or binary. Export those collections as JSON, or use `mongodump`. - +The last format and its options come back next time, but only after an export succeeds; cancelling discards the changes. **Reset to Defaults** restores the stock settings for the current format. Export dialog Export dialog -### Export Formats +### What ends up in the file + +A whole-table export streams from the database at constant memory, with no row-count limit, and can be cancelled from the progress dialog. + +**Export Results…** writes what the tab holds in memory, which is the output of the query that filled it. The filter bar and a sorted header are part of that query, and so is column visibility on a table tab: hiding a column re-queries without it, keeping only the primary key and anything being sorted on. Two things are not part of it. A column value filter narrows the loaded rows in the grid afterwards, so it never reaches the file. And a truncated result with more rows behind it is re-run and streamed in full rather than written as far as it got. + +### Formats @@ -38,10 +45,12 @@ MQL writes top-level typed values as the constructors mongosh understands, so `_ | Line breaks in values to spaces | No | | Line ending (LF, CRLF, CR) | LF | | Decimal separator (period, comma) | Period | - | Formula sanitization | Yes | + | Sanitize formula-like values | Yes | + + Sanitizing prefixes a value starting with `=`, `+`, `-`, or `@` with a single quote, so a spreadsheet treats it as text. - Exports as an array of objects. + An array of objects. | Option | Default | |--------|---------| @@ -50,31 +59,30 @@ MQL writes top-level typed values as the constructors mongosh understands, so `_ | Preserve all values as strings | No | - Global options: - | Option | Default | |--------|---------| | Compress with gzip (`.sql.gz`) | No | - | Batch size (rows per INSERT) | 500 | + | Batch size (rows per INSERT: 1, 100, 500, 1,000) | 500 | + + Structure (CREATE TABLE), Drop (DROP TABLE IF EXISTS), and Data (INSERT statements) are per-table checkboxes, and a multi-table export can mix them. - Per-table checkboxes, set individually for multi-table exports: + + All three are on by default, so an SQL export carries `DROP TABLE IF EXISTS` unless you untick **Drop**. Run that file against the wrong database and it drops the tables first. + - | Column | Includes | Default | - |--------|----------|---------| - | Structure | CREATE TABLE | Yes | - | Drop | DROP TABLE IF EXISTS | Yes | - | Data | INSERT statements | Yes | + Not available on MongoDB or Redis. - MongoDB only. Generates `insertMany()` scripts that run directly in `mongosh`. Batch size defaults to 500 documents per `insertMany`. Per-collection checkboxes cover drop, indexes, and data. + MongoDB only. Writes a `.js` file of `insertMany()` calls that runs in `mongosh`, 500 documents per call by default, with per-collection checkboxes for drop, indexes, and data. - Output is a `.js` file: ```javascript db.users.insertMany([ {"_id": {"$oid": "507f1f77bcf86cd799439011"}, "name": "Alice", "age": 30}, {"_id": {"$oid": "507f1f77bcf86cd799439012"}, "name": "Bob", "age": 25} ]); ``` + + Top-level values keep their type: `ObjectId`, `ISODate`, `BinData` with its real subtype. A typed value nested inside a subdocument or array is written as a string, so re-importing gives you a string where the original held an ObjectId, date, or binary. Export those collections as JSON, or use `mongodump`. | Option | Default | @@ -82,39 +90,27 @@ MQL writes top-level typed values as the constructors mongosh understands, so `_ | Include headers (bold first row) | Yes | | NULL as empty cells | Yes | - Each table exports as a separate worksheet. Numbers are stored as numeric cells. Tables exceeding 1,048,576 rows (Excel's limit) auto-split into multiple sheets. + Each table becomes its own worksheet and numbers are stored as numeric cells. A table over 1,048,576 rows, Excel's limit, splits across sheets. -### Streaming export - -Whole-table exports stream rows from the database straight to disk: - -- Constant memory regardless of table size, no row-count limit -- Atomic write: the destination file appears only on success, partial files are removed on failure -- Cancellable from the progress dialog; cancelling removes the partial file - -Result-grid exports use the in-memory result set. Use LIMIT in your query to control their size. - ## Clipboard paste (CSV/TSV) -Paste tabular data directly into the data grid. Press `Cmd+V` after selecting a row. Format is auto-detected: tabs parse as TSV, commas as CSV. - -## Import Data +Select a row in the data grid and press `Cmd+V` to paste tabular data straight in. Tabs parse as TSV, commas as CSV. -Import `.sql` and `.sql.gz` files (statements execute directly against your database: backups, migrations, seed data), `.json` / `.jsonl` files into a table, or `.csv` / `.tsv` files into a table with column mapping. +## Import data -### Import SQL +**File > Import > Import Data…** (`Cmd+Shift+I`) takes `.sql` and `.sql.gz` files, whose statements execute directly against the database, and `.json`, `.jsonl`, `.ndjson`, `.csv`, and `.tsv` files, which load into a table you pick or one TablePro creates. - - Click **File > Import > Import Data…** (`Cmd+Shift+I`) and pick a `.sql` file. + + Choose **File > Import > Import Data…** and select the file. The sheet that opens depends on what you picked. - - Set encoding, transaction wrapping, and foreign key check options. TablePro remembers the options from your last successful import; cancelling the dialog discards changes, and **Reset to Defaults** restores the stock settings. + + Encoding, transaction wrapping, and error handling for SQL; parsing and column mapping for CSV and JSON. TablePro remembers the options from your last successful import, and **Reset to Defaults** restores the stock ones. - - Review the SQL preview, statement count, and file size. Click **Import** to execute. + + Check the preview, statement count, and file size, then click **Import**. @@ -123,62 +119,61 @@ Import `.sql` and `.sql.gz` files (statements execute directly against your data Import dialog -### Import options +### Options -| Option | Description | Default | +| Option | What it does | Default | |--------|-------------|---------| -| On error | How to handle failed statements (see below) | Stop and Rollback | -| Encoding | File encoding: UTF-8, UTF-16, Latin1, or ASCII | UTF-8 | -| Wrap in transaction | Execute all statements within a single transaction | Yes | -| Disable foreign key checks | Temporarily disable FK constraints during import | Yes | - -For PostgreSQL the checkbox runs `SET session_replication_role = replica`, which requires superuser (or, on PostgreSQL 15+, a `GRANT SET` on the parameter). If the server rejects the statement, the import stops with that error; uncheck the option to proceed. TablePro's SQL exports emit foreign key constraints with `ALTER TABLE … ADD CONSTRAINT` after data load, so the dump imports in order without needing the privilege. +| On error | Stop and Rollback, Stop and Commit, or Skip and Continue | Stop and Rollback | +| Encoding | UTF-8, UTF-16, Latin1, or ASCII | UTF-8 | +| Wrap in transaction | Runs every statement inside one transaction. Dimmed in Skip and Continue | Yes | +| Disable foreign key checks | Suspends constraint checks for the import | Yes | -For MySQL the checkbox runs `SET FOREIGN_KEY_CHECKS = 0` and works on standard accounts. SQLite uses `PRAGMA foreign_keys = OFF`. Drivers without an equivalent (most NoSQL drivers) ignore the option. +| Mode | What happens on a failed statement | +|------|-----------| +| **Stop and Rollback** | Stops there. With the transaction on, everything rolls back | +| **Stop and Commit** | Stops there, keeping what already succeeded | +| **Skip and Continue** | Logs it and carries on, including a line the parser cannot read. No transaction | -### Error handling modes +Skip and Continue collects up to 1,000 failures with their line numbers and messages, and the summary counts successes against failures behind a **Copy Details** button. A stop shows the line, the database's own message, and the failing statement, with the dialog still open behind it, ready for a changed setting and another run. -| Mode | Behavior | -|------|----------| -| **Stop and Rollback** | Stops on first error. If transaction is enabled, rolls back all changes. Default. | -| **Stop and Commit** | Stops on first error. Commits statements that succeeded before the error. | -| **Skip and Continue** | Logs failed statements and continues. A line the parser cannot read is logged the same way. Transaction wrapping is disabled in this mode. | +### Disabling foreign key checks -In **Skip and Continue** mode, failed statements are collected (up to 1,000) with line numbers and error messages. After import completes, a summary shows how many succeeded vs failed, with a scrollable error list and a **Copy Details** button. +The checkbox runs a different statement per engine, and one of them needs a privilege. -When an import stops on an error, the same panel shows the line it stopped at, the database's own message, and the statement that failed. The dialog stays open behind it so you can change a setting and run the import again. +| Engine | Statement | Needs | +|---|---|---| +| PostgreSQL, Redshift, CockroachDB, PGlite | `SET session_replication_role = replica` | Superuser, or `GRANT SET` on the parameter from PostgreSQL 15 | +| MySQL, MariaDB | `SET FOREIGN_KEY_CHECKS=0` | Nothing | +| SQLite, libSQL, Cloudflare D1 | `PRAGMA foreign_keys = OFF` | Nothing | +| Everything else | Nothing. The option is ignored | | -### Import JSON data +A server that rejects the statement stops the import with that error, so clear the checkbox and run it again. A dump TablePro exported needs no privilege: it adds foreign keys with `ALTER TABLE … ADD CONSTRAINT` after the data. -Choose **File > Import > Import Data…** and pick a `.json`, `.jsonl`, or `.ndjson` file. The sheet accepts an array of objects `[{…}, {…}]`, newline-delimited JSON (one object per line, streamed for large files), and TablePro's own JSON export shape `{ "table": [ {…} ] }`, so an export round-trips back in. +### Import JSON -Choose a destination: +The sheet accepts an array of objects `[{…}, {…}]`, newline-delimited JSON streamed a line at a time, and TablePro's own export shape `{ "table": [ {…} ] }`, so an export round-trips. Then choose a destination: -- **Existing table**: pick the table, then map each JSON field to a column. Fields are auto-matched by name; toggle any field off to skip it, or remap it. Columns with no matching field keep their default or NULL. -- **New table**: name the table and review the columns TablePro infers from the data. Each column's name, type, primary key, nullable flag, and default are editable before the table is created. +- **Existing table**: map each JSON field to a column. Fields auto-match by name; switch one off to skip it. A column with no matching field keeps its default or NULL. +- **New table**: name it and review the columns TablePro infers. Name, type, primary key, nullable flag, and default are all editable before the table is created. -Rows insert through parameterized statements, so JSON values are never concatenated into SQL. Nested objects and arrays are stored as JSON text. +Rows insert through parameterized statements, so a JSON value is never concatenated into SQL. Nested objects and arrays are stored as JSON text. -### Import CSV data +### Import CSV -Choose **File > Import > Import Data…** and pick a `.csv` or `.tsv` file. TablePro opens the same row import sheet used for JSON, with CSV-specific parsing options. The delimiter and encoding are auto-detected; the quote character defaults to a double quote. Change any parsing option and the field mapping re-reads the file. Destination options match JSON import: map into an existing table or create a new one. +CSV and TSV open the same sheet as JSON, with parsing options in front of the mapping. The delimiter and encoding are detected from the file; change any option and the mapping re-reads it. -| Option | Description | Default | +| Option | What it does | Default | |--------|-------------|---------| | Delimiter | Comma, semicolon, tab, or pipe | Auto-detect | | Quote character | Double or single quote | Double quote (`"`) | | Encoding | UTF-8, ISO Latin 1, or Windows-1252 | Auto-detect | -| First row is a header | Use row 1 as column names; off imports every row as data | Yes | -| Trim leading and trailing spaces | Trim each field before import | No | -| Treat empty values as NULL | Insert NULL for empty fields instead of empty text | Yes | -| NULL text | An extra value imported as NULL, for example `\N` | None | - -Quoted fields keep embedded commas and newlines (RFC 4180), and doubled quotes (`""`) decode to a single quote. - -### Row import options +| First row is a header | Row 1 becomes the column names; off imports it as data | Yes | +| Trim leading and trailing spaces | Trims each field before import | No | +| Treat empty values as NULL | Inserts NULL instead of empty text | Yes | +| NULL text | One more value to import as NULL, `\N` for instance | None | -A row where none of the fields match a mapped column writes nothing, so it is reported rather than counted as imported. +Quoted fields keep embedded commas and newlines (RFC 4180), and a doubled quote (`""`) decodes to one. -CSV and JSON imports insert rows in batches through parameterized statements. The on-error and transaction options work the same as SQL import. They add one option of their own: **Delete existing rows before import**, which clears the target table first. The delete runs inside the import transaction, so a failed import in the default Stop and Rollback mode restores the deleted rows. +### Row imports -During import, a progress bar shows rows processed and overall completion. +CSV and JSON insert in batches, and the on-error and transaction options behave as they do for SQL. They add **Delete existing rows before import**, which clears the target table inside the import transaction, so a failure in the default Stop and Rollback mode brings the rows back. A row where no field matches a mapped column writes nothing and is reported rather than counted. diff --git a/docs/features/json-viewer.mdx b/docs/features/json-viewer.mdx index 340ad4118..697e2a2a0 100644 --- a/docs/features/json-viewer.mdx +++ b/docs/features/json-viewer.mdx @@ -3,11 +3,13 @@ title: Cell and Row Viewers description: View and edit JSON, PHP serialized, and binary cell values, set per-column display formats, and inspect whole rows in the sidebar --- -An editable JSON or blob cell carries a chevron, and clicking it opens the value in a popover anchored to that cell. Which viewer opens follows the column: its declared type, or the **Display As** choice on its header. That choice changes rendering only, never what is stored. +Click the chevron on a JSON or blob cell to open its value in a popover anchored to that cell. Which viewer opens follows the column: its declared type, or the **Display As** choice on its header, and that choice changes rendering only, never what is stored. + +Whether it opens for editing follows the row and the column. A row marked for deletion opens nothing at all. A result the app cannot write back to, a generated column, and a column the driver marks immutable such as MongoDB's `_id` all open read-only; [Change Tracking](/features/change-tracking) covers which results are writable. PHP serialized values, and binary past the hex cap, are read-only for everyone. ## Display as formats -Right-click a column header and pick **Display As**. The submenu lists the formats that fit the column type, with a checkmark on the active one. MongoDB binary columns are the one exception: they never offer UUID, because the driver already decodes them under its own Legacy UUID Encoding setting. +Right-click a column header and pick **Display As**. The submenu lists the formats that fit the column type, with a checkmark on the active one. | Format | Column types | Effect | | --- | --- | --- | @@ -18,75 +20,65 @@ Right-click a column header and pick **Display As**. The submenu lists the forma | JSON | text | Opens the cell in the JSON viewer | | PHP Serialized | text | Opens the cell in the PHP serialized viewer | -Formats are display only: they never change the stored value. Overrides persist per column, scoped to the connection and table. A column with a format set copies in that format when you copy a single cell; copying a block of cells or a whole row always copies the stored value. +An override persists per column, scoped to the connection and table. A column with a format set copies in that format when you copy the single cell; a block of cells or a whole row always copies the stored value. -TablePro also auto-detects two cases: `BINARY(16)` columns with id-like names and `CHAR(32)`/`CHAR(36)` columns named `uuid` or `guid` render as UUIDs, and integer columns named `*_at`, `*_time`, `*_timestamp`, `created`, `updated`, `modified`, or `timestamp` render as dates when sample values fall in a plausible range. A manual Display As choice always wins over detection, including **Raw Value**: picking it pins the column to its stored value instead of handing the column back to detection. +Id-like binary and character columns, and integer columns whose names read as timestamps, pick up UUID and date rendering on their own. **Smart value detection** in [Settings > Data](/customization/data-settings) turns that off, and a manual choice always wins over it, **Raw Value** included. MongoDB binary columns never offer UUID, since the driver decodes them under its own Legacy UUID Encoding setting; see [MongoDB](/databases/mongodb). ## JSON viewer -- **JSON-typed column**: click the chevron in the cell. Double-click or `Enter` edits the value inline instead. -- **Text column holding JSON**: set **Display As > JSON**, then double-click or `Enter`. - -The viewer opens as a popover anchored to the cell, with two modes: +A JSON-typed column opens on the chevron; double-click or `Enter` edits it inline instead. A text column holding JSON needs **Display As > JSON** first. -- **Text**: syntax-highlighted JSON, pretty-printed. Editable when the cell is editable. -- **Tree**: collapsible tree with a search field. Read-only navigation. See [Filtering a tree](#filtering-a-tree). +The popover has two modes. **Text** is syntax-highlighted and pretty-printed, and editable when the cell is. **Tree** is collapsible, with a search field, and navigation only. Both keep your key order and exact number values, integers larger than JavaScript can represent included. JSON editor popover JSON editor popover -Both modes keep your original key order and exact number values, including integers larger than JavaScript can represent. Pretty-printing caps at 500 KB; larger values show as stored. Tree mode parses documents up to 100 KB, and above that shows a "JSON Too Large" placeholder so you read the value in Text mode. A document that parses but runs past 5,000 nodes loads only the first 5,000 and marks the cut with a `…` row; filtering then says so, because the rows past the cut were never searched. - -### Filtering a tree - -The search field above the tree filters keys and values as you type. Both the JSON tree and the [PHP serialized tree](#php-serialized-viewer) work the same way. - -- Matching ignores case and accents, so `cafe` finds `café`. It does not ignore full-width and half-width differences: `ABC` does not find `ABC`. -- A row matches on its whole value, not the shortened form shown in the row, so a match deep inside a long string still finds it. -- Matches nested under collapsed parents open on their own, so you see the match rather than the parent hiding it. -- A key that matches keeps its full contents and stays expandable, so you can search for a key and then read what is under it. -- Rows you open or close while filtering stay that way for as long as the field has text. Clearing the field restores the layout you had before you started, which is why **Expand All** during a filter applies to the filtered view rather than to the whole document. -- A filter that matches nothing says so. If the document was cut off at 5,000 nodes, it says that too instead of claiming the value does not contain what you typed. +The mode you switch to becomes the default for the next value, which is the same preference as **JSON Viewer > Default view** in Settings. The pop-out button detaches the viewer into its own resizable window. -The mode you switch to becomes the default for the next open. The same preference lives in **Settings > Data > JSON Viewer > Default view** (`Cmd+,`). +Opening a value and seeing it pretty-printed is not an edit: the row is marked changed only once you alter the content. **Save** commits a compact value, key order and numbers preserved, through the [change tracking](/features/change-tracking) queue. Text that is not valid JSON prompts first. -Click the pop-out button in the toolbar to detach the viewer into its own resizable window. The window supports fullscreen. +### Filtering a tree -### Editing +The search field above the tree filters keys and values as you type. The [PHP serialized tree](#php-serialized-viewer) works the same way. -Opening a value and seeing it pretty-printed is not an edit. The row is marked changed only when you alter the content, so browsing JSONB row by row stays clean. **Save** commits a compact value, with key order and numbers preserved, through the standard [change tracking](/features/change-tracking) flow. If the text is not valid JSON, the viewer asks before saving. +- Matching ignores case and accents, so `cafe` finds `café`. Full-width and half-width differ: `ABC` does not find `ABC`. +- A row matches on its whole value, not the shortened form shown, so a match deep inside a long string counts. A match under a collapsed parent opens its parents, and a key that matches keeps its contents and stays expandable. +- A filter that matches nothing says so, and says separately when the document was cut at 5,000 nodes, since the rows past the cut were never searched. ## PHP serialized viewer -Set **Display As > PHP Serialized** on a text column, then double-click or `Enter`. The viewer is read-only: PHP serialized values round-trip through PHP itself, so TablePro does not write them back. - -- **Tree**: collapsible tree with search, filtered the same way as the JSON tree. See [Filtering a tree](#filtering-a-tree). Nodes show type badges (`str`, `int`, `arr`, `obj`, `ser`, `ref`). Protected object members show a `protected` badge; private members show `private (ClassName)`. Custom-serialized classes (the `C:` token) appear as a single opaque leaf; references (`r:`/`R:`) show as `→ #N` and are not followed. -- **Raw**: the original serialized string with text selection. +Set **Display As > PHP Serialized** on a text column, then double-click or press `Enter`. The viewer is read-only: PHP serialized values round-trip through PHP itself. -Values above 5 MB are not parsed; the tree shows a placeholder and Raw mode still works. Trees cap at 5,000 nodes and 256 nesting levels, with a truncation marker beyond that. The toolbar pop-out button opens the value in its own window. +**Tree** mode filters the same way as the JSON tree, with type badges on the nodes (`str`, `int`, `arr`, `obj`, `ser`, `ref`) and visibility badges on object members (`protected`, `private (ClassName)`). A custom-serialized class (the `C:` token) is one opaque leaf, and a reference (`r:`/`R:`) shows as `→ #N` and is not followed. **Raw** mode shows the original string with text selection. ## Blob and hex -Blob cells render in the grid as compact hex (`0x48656C…`, first 64 bytes). Click the chevron or double-click to open the hex popover: +A blob cell renders in the grid as compact hex (`0x48656C…`). Click the chevron or double-click for the popover: a classic hex dump with offset, byte columns, and ASCII, plus an editable hex field under it on an editable table. That field takes spaced or continuous hex, with or without a `0x` prefix, and invalid hex disables **Save**. -- A classic hex dump (offset, byte columns, ASCII), capped at 10 KB. -- On editable tables, an editable hex field below it. It accepts spaced or continuous hex, with or without a `0x` prefix. Invalid hex disables **Save**; values over 10 KB open read-only. +## Limits -## Row details inspector +A value that refuses to open in one mode still opens in another. -**View > Show Inspector** (`Cmd+Option+I`) opens the right sidebar. Select a row and every field appears with an editor matched to its content: +| Viewer | Cap | Past the cap | +| --- | --- | --- | +| JSON pretty-printing | 500,000 characters | The value shows as stored | +| JSON tree | 100,000 characters | "JSON Too Large". Read it in Text mode | +| JSON and PHP trees | 5,000 nodes | The first 5,000 load, and a `…` row marks the cut | +| PHP serialized | 5,000,000 characters, 256 levels deep | Tree shows a placeholder, Raw still works | +| Hex dump and hex editor | 10 KB | The dump is truncated, and the editor opens read-only | +| Blob cell in the grid | First 64 bytes | Rendered as `0x48656C…` | + +## Row details inspector -- JSON columns, and text values that parse as JSON, get the JSON editor. No Display As step needed. -- PHP serialized values are auto-detected the same way and get the read-only tree. -- Blob columns get the hex editor; enum, set, and boolean columns get pickers. +**View > Show Inspector** (`Cmd+Option+I`) opens the right sidebar. Select a row and every field appears with an editor matched to its content, with no **Display As** step: JSON columns and text values that parse as JSON get the JSON editor, PHP serialized values the read-only tree, blob columns the hex editor, and enum, set, and boolean columns get pickers. Row details inspector Row details inspector -JSON and PHP fields have buttons to expand inside the sidebar or pop out to a window. Hover over an editable field for a menu that sets NULL, the column default, an empty value, or a SQL function. With no row selected, the inspector shows table info instead. +JSON and PHP fields carry buttons to expand inside the sidebar or pop out to a window. Hover an editable field for a menu that sets NULL, the column default, an empty value, or a SQL function. With no row selected, the inspector lists table statistics instead: data, index and total size, row count, average row size, engine, collation, and creation and update dates, as far as the database reports them. On a Structure tab it follows the structure grid instead of the rows. -For whole-row JSON across a result set, switch the results pane to its JSON view mode with the switcher at the left of the status bar, or from **View > Result View**. Table tabs show Data / Structure / JSON; query tabs show Data / JSON. +For whole-row JSON, switch the result to JSON mode with the switcher at the leading edge of the status bar, or from **View > Result View**. It shows what the [grid](/features/data-grid) shows, in the same order, minus hidden columns and rows marked for deletion, which the count line reports separately. Select rows in Data mode first to narrow it, then click **Copy JSON**. diff --git a/docs/features/keyboard-shortcuts.mdx b/docs/features/keyboard-shortcuts.mdx index 9be158c6f..4c59a02d1 100644 --- a/docs/features/keyboard-shortcuts.mdx +++ b/docs/features/keyboard-shortcuts.mdx @@ -3,7 +3,18 @@ title: Keyboard Shortcuts description: Every keyboard shortcut in TablePro, plus how to rebind them in Settings > Keyboard --- -Defaults are listed below. Most shortcuts are rebindable in **Settings > Keyboard**; see [Customizing shortcuts](#customizing-shortcuts). The same key can do different things depending on focus: `Cmd+[` indents in the editor and pages back in the data grid. +Focus decides what a key does: `Cmd+[` indents in the SQL editor and pages back in the data grid, and neither one shadows the other. Everything here is the shipped default, and most of it is rebindable in **Settings > Keyboard**. + +## Same key, different pane + +| Key | In the SQL editor | In the data grid | +|-----|-------------------|------------------| +| `Cmd+[` and `Cmd+]` | Indent and outdent | Previous and next page | +| `Cmd+F` | Find in the text | Find in the loaded rows | +| `Cmd+Delete` | Delete to the start of the line | Delete the selected rows | +| `Option+Delete` | Delete the previous word | Truncate the tables selected in the sidebar | + +The grid meanings come back the moment you click into the grid, and `Delete` on its own always deletes the selected rows. `Cmd+W` follows the same rule across the whole window; see [Tabs and windows](#tabs-and-windows). ## Essential @@ -99,11 +110,7 @@ Every row except Find Next and Find Previous is built into the editor and cannot | Undo change | `Cmd+Z` | | Redo change | `Cmd+Shift+Z` | -Truncate table opens a dialog with **Cascade** and **Ignore foreign key checks**, then marks the tables as a pending truncate that `Cmd+S` runs. - - -`Cmd+Delete` and `Option+Delete` step aside while the SQL editor or a text field has focus. There they keep their standard macOS meaning: delete to the start of the line, and delete the previous word. The grid commands come back as soon as you click into the grid, and `Delete` on its own still deletes the selected rows. - +Truncate table opens a dialog with **Cascade** and **Ignore foreign key checks**, then stages a pending truncate that `Cmd+S` runs. ### Clipboard @@ -116,7 +123,7 @@ Truncate table opens a dialog with **Cascade** and **Ignore foreign key checks** | Copy as JSON | `Cmd+Option+J` | | Paste | `Cmd+V` | -With a drag-selected cell range, `Cmd+C` copies the range as TSV. With a single focused cell it copies that value. Otherwise it copies the selected rows as TSV. +`Cmd+C` copies a drag-selected range as TSV, a single focused cell as its value, and otherwise the selected rows as TSV. ### Pagination @@ -127,15 +134,11 @@ With a drag-selected cell range, `Cmd+C` copies the range as TSV. With a single | First page | `Cmd+Option+Up` | | Last page | `Cmd+Option+Down` | - -`Cmd+[` and `Cmd+]` page the data grid. In the SQL editor the same keys indent and outdent. Focus decides which fires. - - ### Filtering | Action | Shortcut | |--------|----------| -| Find, which searches the loaded rows in a table tab and the text in the editor | `Cmd+F` | +| Find | `Cmd+F` | | Next match | `Return` or `Cmd+G` | | Previous match | `Cmd+Shift+G` | | Close the find bar | `Escape` | @@ -153,7 +156,7 @@ See [Filtering](/features/filtering) for the filter bar itself. ## Tabs and windows -`Cmd+W` closes whatever is in front. In a connection window it closes the current editor tab, and the File menu names it **Close Tab**. In Settings, Integrations, a JSON or PHP viewer, or the welcome window there are no tabs, so it closes the window and the menu reads **Close Window**. While the connections strip has keyboard focus it closes the highlighted connection, and the menu names that connection. +`Cmd+W` closes whatever is in front. In a connection window that is the current editor tab, and the File menu reads **Close Tab**. Settings, Integrations, a JSON or PHP viewer, and the welcome window have no tabs, so it closes the window and the menu reads **Close Window**. While the connections strip holds the keyboard it closes the highlighted connection, and the menu names it. | Action | Shortcut | |--------|----------| @@ -187,24 +190,25 @@ See [Filtering](/features/filtering) for the filter bar itself. |--------|----------| | Back | `Ctrl+Cmd+[` | | Forward | `Ctrl+Cmd+]` | -| Toggle sidebar (listed as Toggle Table Browser in **Settings > Keyboard**) | `Cmd+0` | +| Toggle sidebar | `Cmd+0` | | Toggle connections | `Cmd+Option+0` | | Show previous connection | `Ctrl+Cmd+Up` | | Show next connection | `Ctrl+Cmd+Down` | | Focus sidebar filter | `Ctrl+Cmd+Option+F` | | Move from the sidebar filter into the object list | `Down` | | Move between objects in the sidebar | `Up` / `Down` | -| Jump to an object by name | type its first letters with the sidebar focused | | Keep the selected table's tab (same as double-clicking it) | `Return` | | Expand or collapse the selected container | `Right` / `Left` | | Clear the sidebar selection | `Escape` | -| Toggle inspector panel | `Cmd+Option+I` | +| Toggle inspector | `Cmd+Option+I` | | Toggle results | `Cmd+Option+R` | -| Query history | `Cmd+Y` | +| Toggle history | `Cmd+Y` | | Increase text size (editor font) | `Cmd+=` | | Decrease text size (editor font) | `Cmd+-` | | Toggle full screen | `Ctrl+Cmd+F` | +With the sidebar focused, typing the first letters of an object's name jumps to it. + ### Result tabs | Action | Shortcut | @@ -216,7 +220,7 @@ See [Filtering](/features/filtering) for the filter bar itself. ### Query history drawer -With the drawer open (`Cmd+Y`) and its entry list focused: `Return` loads the selected entry in the editor, `Delete` removes it, `Cmd+C` copies its query. `Cmd+Y` again hides the drawer. `Return` anywhere else in the window belongs to whatever you are typing in. +With the drawer open (`Cmd+Y`) and its entry list focused, `Return` loads the selected entry into the editor, `Delete` removes it, and `Cmd+C` copies its query. `Cmd+Y` hides the drawer again. ## Welcome window @@ -231,7 +235,7 @@ With the drawer open (`Cmd+Y`) and its entry list focused: `Return` loads the se | Collapse group | `Ctrl+H` | | Expand group | `Ctrl+L` | -`Ctrl+J`/`Ctrl+N` and `Ctrl+K`/`Ctrl+P` also move the selection in [Open Quickly](/features/open-quickly), where `Cmd+Return` and `Option+Return` open the selected item in a new tab. +`Ctrl+J`/`Ctrl+N` and `Ctrl+K`/`Ctrl+P` also move the selection in [Open Quickly](/features/open-quickly), where `Cmd+Enter` and `Option+Enter` open the selected item in a new tab. ## CSV inspector @@ -254,23 +258,23 @@ See [CSV Inspector](/features/csv-inspector). Turn on Vim mode in **Settings > Editor**. Vim keys apply only in the SQL editor; the data grid keeps its shortcuts, and `Cmd+Enter` runs the query in any mode. See [Vim Mode](/features/vim-mode) for the key reference. -## Customizing Shortcuts +## Customizing shortcuts -Open **Settings > Keyboard** (`Cmd+,`). Each action has a recorder field: click it and press the new combination. The menu bar updates immediately. Use the search field to filter actions, press `Delete` in a recorder field to clear a binding, click the curved-arrow button next to a changed shortcut to restore that one action, or **Reset to Defaults** to restore all. See [Settings](/customization/settings) for the other tabs. +Open **Settings > Keyboard** (`Cmd+,`). Each action has a recorder field: click it and press the new combination, and the menu bar updates immediately. Actions are listed under their own names there: the sidebar toggle is Toggle Table Browser. Filter with the search field, press `Delete` in a recorder field to clear a binding, click the curved-arrow button beside a changed shortcut to restore that one, or **Reset to Defaults** to restore all. Keyboard settings Keyboard settings -If a combination is already used by another action in the same context, TablePro shows a dialog naming that action: **Cancel** keeps the existing assignment, **Reassign** moves the shortcut and clears it from the other action. Editor and data grid shortcuts can share a key without conflict because focus decides which fires. +A combination already taken by another action in the same context raises a dialog naming it: **Cancel** keeps the existing assignment, **Reassign** moves the shortcut and clears it from the other action. Editor and grid actions can share a key without conflict. Menu actions need a modifier (`Cmd`, `Option`, `Ctrl`, or `Shift`); function keys `F1` through `F12` work bare, as do grid actions that read the key directly, like Preview FK Reference (`Space`). -Some shortcuts cannot be reassigned: editor built-ins (like `Cmd+/`), tab selection (`Cmd+1` through `Cmd+9`), text size (`Cmd+=`, `Cmd+-`), Find (`Cmd+F`), and macOS system shortcuts, which TablePro reads live from System Settings. The recorder warns if you try. +Some shortcuts cannot be reassigned: editor built-ins such as `Cmd+/`, tab selection (`Cmd+1` through `Cmd+9`), text size (`Cmd+=`, `Cmd+-`), Find (`Cmd+F`), and macOS system shortcuts, which are read live from System Settings. The recorder warns if you try. ## Outside the app -TablePro can be driven without app shortcuts at all: the Raycast extension, the `tablepro://` URL scheme, and MCP clients are covered in the [External API](/external-api) section. +Drive the app with no keyboard at all: the Raycast extension, the `tablepro://` URL scheme, and MCP clients are covered in the [External API](/external-api) section. diff --git a/docs/features/licensing.mdx b/docs/features/licensing.mdx index a22afaac9..38e1d445f 100644 --- a/docs/features/licensing.mdx +++ b/docs/features/licensing.mdx @@ -3,7 +3,7 @@ title: Licensing description: Activate and deactivate a license key, what each tier includes, and how offline validation works --- -TablePro works without a license and has no trial countdown. A license adds the features below. Purchase, renewal, and billing are managed on [tablepro.app](https://tablepro.app/#pricing), not in the app. +You never have to buy TablePro to keep using it, and nothing counts down. A license adds the features in the table, and purchase, renewal, and billing all happen on [tablepro.app](https://tablepro.app/#pricing) rather than in the app. ## Tiers @@ -20,38 +20,39 @@ There are two tiers, Starter and Team. Team includes everything in Starter. | [Team Catalog](/features/connection-sharing) | Team | | [Team Library](/features/team) | Team | -Without a license, these screens show an overlay with **Activate License…** and a purchase link. On a Starter license, Team features show an **Upgrade Plan** link instead. Everything else in the app works. +Without a license, those screens show an overlay with **Activate License…** and a purchase link. On a Starter license, the two Team features show an **Upgrade Plan** link instead. Everything else in the app works. ## Activate -1. Open **Settings > Account** (`Cmd+,`). -2. Paste your license key (`XXXXX-XXXXX-XXXXX-XXXXX-XXXXX`) and click **Activate**. +Open **Settings > Account** (`Cmd+,`), paste your license key (`XXXXX-XXXXX-XXXXX-XXXXX-XXXXX`), and click **Activate**. -The welcome window has an **Activate License** link that opens an activation dialog, and shows a green **Pro** badge once a license is active. The badge reads Pro on both tiers, Starter and Team. The activation field also accepts a team invite code; TablePro detects which one you entered. See [Team Plan](/features/team). +The welcome window has an **Activate License** link to the same dialog, and shows a green **Pro** badge once a license is active. The field also takes a team invite code, which TablePro tells apart from a key on its own. See [Team Plan](/features/team). License settings showing email, masked key, status, tier, and activations License settings showing email, masked key, status, tier, and activations -Once active, the Account tab shows your email, a masked license key, status, expiry date (or Lifetime), tier, and billing cycle. +Once active, the Account tab shows your email, a masked license key, status, expiry date or Lifetime, tier, and billing cycle. ## Activations -A license is bound to each Mac it is activated on, using a hashed hardware ID. The **Activations** section lists every activated Mac with its app and macOS version, marks this Mac, and shows the count against your plan's limit (for example "Activations (2 of 3)"). The limit comes from your plan on tablepro.app. +A license binds to each Mac it is activated on through a hashed hardware ID. The **Activations** section lists every activated Mac with its app and macOS version, marks this one, and counts them against the limit your plan sets on tablepro.app. -To move the license to another Mac, click **Deactivate…** in **Settings > Account** first. This frees the activation slot. If the license server can't be reached, the license is still removed from this Mac, but the slot may stay occupied until it expires on the server. +Activating one Mac too many fails with "This license has reached its activation limit. Deactivate another Mac first." Freeing a slot means clicking **Deactivate…** in **Settings > Account** on the Mac you are giving up, before you wipe or sell it: the list in the app is read-only, so no Mac can release another. If the machine is already gone, ask for the slot back through [GitHub issues](https://github.com/TableProApp/TablePro/issues). + +Deactivating with the license server unreachable still removes the license from that Mac, but the slot may stay occupied until it expires on the server. ## Validation and offline use -Activation stores a signed license on your Mac. The license key goes in the macOS Keychain. +Activation stores a signed license on your Mac, and the license key goes to the macOS Keychain. -- On every launch, TablePro re-verifies the license signature offline. No network needed. Your tier, status, and expiry all come from the signed license, so editing the copy on disk changes nothing. -- Every 7 days, the app re-validates with the license server in the background. -- If the server is unreachable, the license keeps working for 30 days after the last successful validation. After that, the status changes to Validation Failed and Pro features pause until a validation succeeds. -- If the server replies that the license is suspended, expired, or no longer activated on this Mac, Pro features pause at that check. The offline grace period only covers a server TablePro could not reach. Reopening the app runs on the signed license again until the next check, which is never more than a week after the last successful validation. +- Every launch re-verifies the signature offline, with no network. Tier, status, and expiry all come from the signed license, so editing the copy on disk changes nothing. +- Every 7 days the app re-validates with the license server in the background. +- If the server is unreachable, the license keeps working for 30 days after the last successful validation. After that the status becomes Validation Failed and Pro features pause until a validation succeeds. +- If the server answers that the license is suspended, expired, or no longer activated on this Mac, Pro features pause at that check. The 30-day grace period only covers a server TablePro could not reach. -**Check Status** in **Settings > Account** re-validates on demand, and gated screens offer **Retry Validation**. When a license expires within 7 days, the Account tab shows a renewal warning with a link to tablepro.app. +**Check Status** in **Settings > Account** re-validates on demand, and a gated screen offers **Retry Validation**. When a license expires within 7 days, the Account tab shows a renewal warning linking to tablepro.app. ## What syncs diff --git a/docs/features/mcp.mdx b/docs/features/mcp.mdx index 8a968306d..bd2dc5d17 100644 --- a/docs/features/mcp.mdx +++ b/docs/features/mcp.mdx @@ -5,58 +5,52 @@ description: Built-in Model Context Protocol server that lets AI clients query y The [MCP](https://modelcontextprotocol.io) server binds `127.0.0.1` and nothing else: no remote mode, no TLS certificate, no setting that opens it to your network. Claude Desktop, Claude Code, Cursor, and Zed reach your databases through the connections you already saved, and never see a password. -This page covers the **Settings > Integrations** pane. The Model Context Protocol itself, the 46 tools, the prompts and the token model live in the [External API](/external-api) section. +This page covers the **Settings > Integrations** pane. The protocol itself, the tool catalog, the prompts and the token model live in the [External API](/external-api) section. TablePro MCP settings TablePro MCP settings -## Enabling the server +## Starting and stopping -Open **Settings > Integrations** and turn on **Enable MCP Server**. A status row shows the state and the port. +Open **Settings > Integrations** and turn on **Enable MCP Server**. The **Status** row underneath is how you check it before touching a client config: it reads **Running on port 23508**, or **Stopped**, or the reason it failed. -You can also skip the toggle. The server starts on demand: the bundled `tablepro-mcp` bridge fires `tablepro://integrations/start-mcp` when a client launches it, and pairing an extension starts it too. +Leaving the toggle off does not keep the server down. It starts on demand when a client launches the bundled `tablepro-mcp` bridge and when you pair an extension, so turning the toggle off stops the listener but not those paths. -TablePro stays out of the Dock and the app switcher while it is only answering a client. It appears in both, with its menu bar, whenever it has a window on screen, including a window a client asked it to open. +While it is only answering a client, TablePro stays out of the Dock and the app switcher. It appears in both, with its menu bar, once it has a window on screen, including one a client asked it to open. -The server binds `127.0.0.1` on the configured port (default **23508**) and speaks plain HTTP. If that port is taken, TablePro asks the kernel for a free one instead. Either way the real port is written to `~/Library/Application Support/TablePro/mcp-handshake.json`, which the bridge reads, so client configs never carry a port. +The default port is **23508**. If it is taken, TablePro asks the kernel for a free one instead. Either way the real port goes to `~/Library/Application Support/TablePro/mcp-handshake.json`, which the bridge reads, so a client config never carries a port. ## Server configuration -| Setting | Default | -|---------|---------| -| Port | 23508 | -| Default row limit | 500 | -| Maximum row limit | 10,000 | -| Query timeout | 30 seconds | -| Log MCP queries in history | on | +| Setting | Default | Range | +|---------|---------|-------| +| Port | 23508 | | +| Default row limit | 500 | 1 to 500,000 | +| Maximum row limit | 10,000 | 1 to 500,000 | +| Query timeout | 30 seconds | 1 to 300 seconds | +| Log MCP queries in history | on | | -Row limits clamp to 1 to 500,000 and the timeout to 1 to 300 seconds. A tool can ask for fewer rows or a shorter timeout, never more than the maximum. +A tool can ask for fewer rows or a shorter timeout, never for more than the maximum. ## Connecting a client -Click **Connect a Client…**. The sheet has a **Client** picker with four choices: **Claude Code**, **Claude Desktop**, **Cursor**, and **Zed**. Each shows numbered steps and a copyable snippet or command pointing at the bundled bridge. You paste it into the client's own config; TablePro does not write to other apps' files. +Click **Connect a Client…** and pick **Claude Code**, **Claude Desktop**, **Cursor**, or **Zed**. Each shows numbered steps and a copyable snippet pointing at the bundled bridge. Paste it into the client's own config; TablePro does not write to other apps' files. -For VS Code, Cline, Continue, Windsurf, Antigravity, Goose, the HTTP transport, and troubleshooting, see [MCP Clients](/external-api/mcp-clients). +For VS Code, Cline, Continue, Windsurf, Antigravity, Goose, the HTTP transport, and a client that connects but lists no tools, see [MCP Clients](/external-api/mcp-clients). ## Authentication -**Require authentication** is on by default. Turning it on for the first time, with no tokens yet, generates a full-access "Default token" and shows the plaintext once so you can copy it. Create, scope, allowlist, expire and revoke tokens in the same section; the details are in [Tokens](/external-api/tokens). +**Require authentication** is on by default. Turning it on for the first time with no tokens yet generates a full-access "Default token" and shows the plaintext once, so copy it then. Create, scope, allowlist, expire and revoke tokens in the same section, described in [Tokens](/external-api/tokens). -With the toggle off, a caller on your own machine is accepted without a token, but only as a read-only caller. It can read schema and run `SELECT`. It can never write, drop, or administer, whatever it asks for. Anything beyond reading needs a token you issued. - -## Local only - -The server listens on `127.0.0.1` and nothing else. There is no remote mode, no TLS certificate, and no setting that opens it to your network. The MCP specification allows plain HTTP for a server on the same machine, which is what this is. - -If you need to reach it from another machine, forward the port over SSH yourself and understand what that exposes. +With the toggle off, a caller on your own machine is accepted without a token, but only as a read-only caller: schema reads and `SELECT`. Anything beyond reading needs a token you issued. ## Activity and connected clients **View Activity…** opens a separate window with two sections: -- **Activity Log**: every authentication, tool call, resource read and query, with the token behind it, the category, the connection, and the outcome. Filter by time, category or token, search, and export the filtered view to CSV. Kept for 90 days. Statements are recorded as a SHA-256 digest, not as text. +- **Activity Log**: every authentication, tool call, resource read and query, with the token behind it, the category, the connection, and the outcome. Filter by time, category or token, search, and export the filtered view to CSV. Retained for 90 days. Statements are recorded as a SHA-256 digest, not as text. - **Connected Clients**: clients that have called in the last five minutes, with the client name, the token, and when they were first and last seen. **Disconnect** revokes that client's token, so the client has to pair again. ## Security model @@ -64,10 +58,10 @@ If you need to reach it from another machine, forward the port over SSH yourself - Clients work through your saved connections. Passwords never leave the Keychain and are not readable over MCP. - AI access is set per connection, including blocking a connection from external clients entirely. A blocked connection is not even listed. - Tokens carry scopes and can be limited to named connections. An unauthenticated local caller is read-only and can never be more. -- Writes go through the connection's Safe Mode. `DROP` and `TRUNCATE` need a full-access token and the user's approval every time. +- Writes go through the connection's [Safe Mode](/features/safe-mode#external-clients). `DROP` and `TRUNCATE` need a full-access token and the user's approval every time. - Statements that read or write files, or run server-side code, are refused before they reach the driver. - The reachable surface is the [tool catalog](/external-api/mcp-tools), the [resources](/external-api/mcp-resources) and the [prompts](/external-api/mcp-prompts). Nothing else. -- Tool calls do not move the app around. `list_tables`, `describe_table`, `browse_table` and `execute_query` read whatever database and schema you pass them and leave the sidebar selection and open tabs alone. `switch_database` and `switch_schema` are the only tools that move the selection, which is what they are for. +- Reaching the server from another machine means forwarding the port over SSH yourself, and owning what that exposes. ## Reference @@ -76,7 +70,7 @@ If you need to reach it from another machine, forward the port over SSH yourself Versions, required metadata, headers, and error codes. - All 46 tools with arguments and result shapes. + Every tool with its arguments and result shapes. Prompts rendered from the live schema, and completions. diff --git a/docs/features/open-quickly.mdx b/docs/features/open-quickly.mdx index 22d70a03a..09a8444bd 100644 --- a/docs/features/open-quickly.mdx +++ b/docs/features/open-quickly.mdx @@ -3,7 +3,7 @@ title: Open Quickly description: Search objects and queries in one connection or across every open connection --- -Press `Cmd+Shift+O` (or **File > Open Quickly**) in a connection window to open a floating search panel. Type a few characters and press `Return` to jump to any table, view, system table, database, schema, saved query, or recent query. Matching is fuzzy: `usr` finds `users` and `user_settings`, and matched characters are shown in bold. +Press `Cmd+Shift+O` in a connection window, type a few characters, and press `Return`. Matching is fuzzy, so `usr` finds `users` and `user_settings`, and the characters that matched are bold. **File > Open Quickly…** opens the same panel. Open Quickly panel @@ -16,56 +16,57 @@ Press `Cmd+Shift+O` (or **File > Open Quickly**) in a connection window to open |--------|----------| | Open Quickly | `Cmd+Shift+O` | | Move selection | `Up` / `Down`, or `Ctrl+J`/`Ctrl+N` and `Ctrl+K`/`Ctrl+P` | -| Open selected item | `Return` (double-click also works) | -| Open in a new tab | `Cmd+Return` or `Option+Return` | +| Open selected item | `Return`, or double-click | +| Open in a new tab | `Cmd+Enter` or `Option+Enter` | | Switch scope | `Cmd+1` to `Cmd+5` | | Clear the search text | `Escape` | -| Dismiss | `Escape` on an empty field, `Cmd+Shift+O` again, or click outside | +| Dismiss | `Escape` on an empty field, `Cmd+Shift+O` again, or a click outside | -The footer along the bottom of the panel lists these as you go. Its `Escape` hint reads **Clear** while you have something typed and **Close** once the field is empty, so you can see which of the two the next press will do. - -The opening shortcut is rebindable in **Settings > Keyboard**; see [Keyboard Shortcuts](/features/keyboard-shortcuts). +The footer lists these as you go, and its `Escape` hint reads **Clear** while you have text and **Close** once the field is empty. Rebind the opening shortcut in **Settings > Keyboard**. ## Scopes -Five scopes limit what the search covers: **All** (`Cmd+1`), **Tables** (`Cmd+2`, includes views and system tables), **Databases** (`Cmd+3`, includes schemas), **Queries** (`Cmd+4`, saved queries and recent queries), and **Connections** (`Cmd+5`). A segmented control under the search field sets the scope. It stays on screen the whole time, so you can change scope after typing without deleting what you typed. With an empty search in a scope other than All, everything in that scope is listed under section headers. The database list follows the sidebar's database filter. +The segmented control under the search field sets the scope. It stays on screen, so switching scope after typing keeps what you typed. -Saved queries come from your [favorites](/features/favorites) and also match on their keyword. In the **All** scope, recent queries are the last 50 entries from [query history](/features/query-history) for the current connection. +| Scope | Key | Covers | +|-------|-----|--------| +| **All** | `Cmd+1` | Tables, views, databases, schemas, and saved queries in this connection, plus its last 50 queries | +| **Tables** | `Cmd+2` | Tables, views, and system tables | +| **Databases** | `Cmd+3` | Databases and schemas | +| **Queries** | `Cmd+4` | Saved queries and recent queries, from every connected window | +| **Connections** | `Cmd+5` | Tables, views, and system tables in every connected window | -## Search across connections +Leave the search empty in any scope other than All and everything in it is listed under section headers. Saved queries come from your [favorites](/features/favorites) and match on their keyword as well as their name, so a keyword you use in the editor reaches the query from here too. -### Objects +Opening a table or view opens a table tab, a database or schema switches the connection to it, and a query loads into the SQL editor without running it. -The **Connections** scope searches tables, views, and system tables in every connected window. With an empty search the list is grouped under each connection's name. Once you type, results are ranked across all connections in one list, and every row shows the connection, database, and schema it belongs to. Search matches both the object name and that path, so typing a connection name limits the results to that connection. +## Across connections -TablePro loads this catalog only when you open the Connections scope. It searches the database or schema each connection is currently browsing. Opening a result in another connection brings that connection's window forward and opens the table there. +The **Connections** and **Queries** scopes reach into every connected window, and that catalog is read the first time you open one of them, not before. -### Queries +An empty search groups results under each connection's name. Once you type, everything is ranked in one list and each row names the connection, database, and schema it belongs to; the path is matched too, so typing a connection's name narrows the list to it. Opening a result brings that connection's window forward. -The **Queries** scope searches saved queries and up to 200 recent query-history entries drawn from every connected window. Each connection contributes its own most recent entries, so a connection you ran hundreds of queries on today cannot push another one off the list. Each row shows its connection and database. Recent queries also show their execution time. Global saved queries open in the connection where you opened the panel. The connection the panel was opened from stays listed even while it is reconnecting. +**Queries** holds your saved queries plus up to 200 recent ones, drawn evenly across connections rather than by raw recency, with execution times on the recent rows. Global saved queries open in the connection you opened the panel from, which stays listed even while it is reconnecting. Open Quickly showing query history from Chinook and Analytics connections -Opening a query brings its connection forward and loads the SQL without running it. The tab you opened the panel from is used whenever it is on the right connection and database, so a query never replaces an editor you were not looking at. Query-history entries keep their recorded database context. If no open tab uses that database, TablePro opens a new tab instead of loading the SQL into a tab bound to another database. `Cmd+Return` and `Option+Return` always open a new tab. - -## Ranking +A query loads into the tab you opened the panel from when that tab is on the right connection and database. If no open tab uses the database the query was recorded against, it gets a new tab. `Cmd+Enter` and `Option+Enter` always open a new tab. -With an empty search, the panel shows a **Recent** section: the last 10 items you opened through the panel on this connection. With a query, each result's fuzzy match score is weighted by: +## Open badge -- **Object kind**: tables rank above views, databases, schemas, and saved queries; query history ranks last. -- **Frecency**: each item's last 10 opens through the panel, weighted by how recent they are, tracked per connection. Opening another connection's result counts towards that connection. -- **Open tab boost**: tables already open in a tab rank higher. -- **Where the match landed**: a match on the name ranks highest, then a saved query's keyword, then the connection and database path shown beside it. +A table already open in a tab shows an **Open** badge, and the hint on the selected row reads **Switch to Tab**. Committing goes to that tab instead of opening a duplicate; `Cmd+Enter` and `Option+Enter` force a new one. -The list shows at most 200 results. +## Ranking -## Open badge +An empty search shows **Recent**: the last 10 items you opened through the panel on this connection. Once you type, a match on the name beats a match on the connection and database path beside it, tables rank above other kinds, and anything you open often through the panel or already have open in a tab moves up. The list stops at 200 results. -Tables already open in a tab show an **Open** badge, and the selected row's hint reads **Switch to Tab**. Committing switches to the existing tab instead of opening a duplicate. `Cmd+Return` and `Option+Return` force a new tab. +## When a table is not listed -What opening does depends on the item: tables and views open a table tab, databases and schemas switch the active database or schema, and queries load into the SQL editor. +- The list covers the database the connection is browsing. To reach a table somewhere else, switch database first, then search again. +- The **Databases** scope offers only databases the sidebar's database filter shows. Clear the filter with **View > Show All Databases**. +- The **Connections** scope covers connected windows only, and only the database each one is browsing. ## Right-click actions @@ -73,8 +74,6 @@ What opening does depends on the item: tables and views open a table tab, databa |--------|------------| | Open | All items | | Open in New Tab | Tables, views, system tables, saved queries, recent queries | -| Open Structure | Tables, views, system tables | +| Open Structure | Tables, views, and system tables in the current connection | | Copy Name | All items | | Copy Query | Saved and recent queries | - -Open Structure is available only for objects in the current connection. diff --git a/docs/features/overview.mdx b/docs/features/overview.mdx index 20288e47f..185199f83 100644 --- a/docs/features/overview.mdx +++ b/docs/features/overview.mdx @@ -3,15 +3,39 @@ title: Features Overview description: Every feature page, grouped the way the sidebar groups them --- -Every feature in TablePro, grouped so you can jump straight to what you need. +First time here: [Quick Start](/quickstart) gets you from install to a query without a database server of your own, using the bundled Chinook sample. -TablePro opens with a welcome window: an actions panel on the left and your saved connections on the right. The panel has **Create Connection…** and an **Add from Existing** menu holding Import from URL, Import from Other App, Open Project Folder, and Import Connections. Search sits in the connections panel header; press `Cmd+F` to focus it. With nothing saved yet, the connections panel offers **Try Sample Database**, a bundled Chinook SQLite database. Reset it any time from **Help > Reset Sample Database…**. +## Start here + + + + Homebrew or DMG, on macOS 14.0+. + + + Connect and run a query, sample database included. + + + Import from TablePlus, Sequel Ace, DBeaver, and others. + + + Licensing, privacy, offline use, coverage. + + + Symptom-first checks for launch, connect, and save failures. + + ## Connections + + Create, organize, and switch connections. + + + Paste a URL, get a connection. + - Reach a database behind a bastion. Built-in libssh2, no `ssh` binary required. + Reach a database behind a bastion, no `ssh` binary needed. Reusable SSH tunnel configurations. @@ -23,15 +47,18 @@ TablePro opens with a welcome window: an actions panel on the left and your save Export and import connection definitions. - Create a connection from the database settings in a project's config files. + A connection from the settings in a project's config files. ## Databases + + Every engine, and where its driver comes from. + - Install drivers, import and export formats, and themes from the registry. + Drivers, import and export formats, and themes from the registry. @@ -39,22 +66,28 @@ TablePro opens with a welcome window: an actions panel on the left and your save - Tree-sitter highlighting, multi-statement execution, formatting. + Highlighting, multi-statement execution, formatting. - Schema-aware completions backed by cached column metadata. + Schema-aware completions from cached column metadata. `:name` placeholders filled in a panel, run as prepared statements. - Modal editing in the SQL editor with motions, operators, text objects, and macros. + Motions, operators, text objects, and macros in the editor. SQLite FTS5-backed history with full-text search. - What your own query history says about which statements are slow and which repeat. Starter feature. + Which of your statements are slow and which repeat. Starter feature. + + + Collapse statements, CTEs, subqueries and `BEGIN` blocks from the gutter. + + + Open a `.sql` file as a query tab and save back to it. @@ -62,7 +95,7 @@ TablePro opens with a welcome window: an actions panel on the left and your save - Inline editing, sorting, filtering, change tracking with undo. + Inline editing, sorting, filtering, undo. Per-column filters, presets, persisted per table. @@ -71,10 +104,13 @@ TablePro opens with a welcome window: an actions panel on the left and your save Stage edits, preview SQL, save or discard. - JSON, PHP serialized, and binary viewers, per-column display formats, row details inspector. + JSON, PHP serialized, and binary viewers, plus row details. - Open and edit CSV and TSV files without importing them. + Edit CSV and TSV files without importing them. + + + Result tabs, pinning, the row cap, and what a failed statement shows. @@ -85,7 +121,7 @@ TablePro opens with a welcome window: an actions panel on the left and your save Alter columns, indexes, foreign keys, primary keys. - Truncate, drop, rename, duplicate, copy DDL. + Truncate, drop, maintenance, views, databases. Visualize tables and foreign keys. @@ -97,7 +133,7 @@ TablePro opens with a welcome window: an actions panel on the left and your save Live server stats and activity. - Manage users, roles, and privileges on MySQL and PostgreSQL. + Users, roles, and privileges on MySQL and PostgreSQL. @@ -122,10 +158,10 @@ TablePro opens with a welcome window: an actions panel on the left and your save Expose TablePro to Claude and other MCP clients. - URL scheme, MCP, and pairing for Raycast, Cursor, Claude Desktop. + URL scheme, MCP, and pairing for other apps. - Search connections, run queries, focus tabs from Raycast. + Search connections, run queries, focus tabs. `tablepro://` URLs to open connections, tables, and queries. @@ -139,16 +175,16 @@ TablePro opens with a welcome window: an actions panel on the left and your save A tab strip per connection, in one window. - The narrow column listing every connection and database you have open. + Every connection and database you have open, one click away. - Cmd+Shift+O fuzzy search across tables, databases, and saved queries. + Fuzzy search over tables, databases, and saved queries. - Pin tables and save reusable queries. + Pin tables, save queries, link a folder of `.sql` files. - Full shortcut reference. + Every default binding, and how to rebind it. @@ -156,23 +192,54 @@ TablePro opens with a welcome window: an actions panel on the left and your save - Sync connections, favorites, and settings across Macs. + Connections, favorites, and settings across Macs. - Resume the active connection or table on another Mac. + Resume the active connection or table on another device. - Activate a license key, tier comparison, offline validation. + License keys, tiers, offline validation. - Share one license, manage seats on the web, join with an invite code. + Share one license, manage seats, invite codes. + + + +## Customization + + + + Every tab, its controls, and their defaults. + + + Themes, fonts, colors, connection colors. + + + Editor toggles, Vim mode, indentation. + + + Page size, result cap, JSON viewer, history. + + + +## iPhone and iPad + + + + Browse, query, and edit rows on a synced connection list. ## Security and privacy + + Every outbound request and where its off switch is. + Prompt before destructive statements on production. + + The open source libraries in the app, and their licenses. + diff --git a/docs/features/plugins.mdx b/docs/features/plugins.mdx index 974a04100..936e7b415 100644 --- a/docs/features/plugins.mdx +++ b/docs/features/plugins.mdx @@ -3,7 +3,9 @@ title: Plugins & Themes description: Install database drivers, import and export formats, and themes from the plugin registry, and keep them updated --- -Most drivers arrive on their own. Pick a database type badged **Not Installed** in the chooser and the download runs before the connection opens, with no restart. Five drivers ship inside the app and cover 9 databases; the other 17 come from the registry. +import DriverCounts from "/snippets/driver-counts.mdx"; + +Pick a database type badged **Not Installed** in the chooser and the download runs before the connection opens, with no restart. ## Bundled plugins @@ -46,40 +48,36 @@ These install from the registry when you need them: ## Installing plugins -You usually don't install drivers by hand. The database type chooser marks registry drivers you don't have with a **Not Installed** badge; pick one anyway and TablePro downloads the plugin before connecting. No restart needed. - -To browse the full catalog, open **Settings > Plugins > Browse**. Search, filter by category (Database Drivers, Export Formats, Import Formats, Themes, Other), and click **Install**. +To browse the catalog instead of waiting for a connection to need something, open **Settings > Plugins > Browse** and filter by category: Database Drivers, Export Formats, Import Formats, Themes, Other. Plugin registry browser in Settings Plugin registry browser in Settings -To install from a file, drag a `.tableplugin` or `.zip` onto **Settings > Plugins > Installed**, or click the **+** button there. User-installed plugins live in `~/Library/Application Support/TablePro/Plugins`. +On a Mac with no network, install from a file: drag a `.tableplugin` or `.zip` onto **Installed**, or click the **+** button there. User-installed plugins live in `~/Library/Application Support/TablePro/Plugins`. -Every plugin is verified before it loads. Registry downloads must match the SHA-256 checksum in the registry manifest, and the bundle must carry a valid code signature. +Every plugin is verified before it loads. A registry download must match the SHA-256 checksum in the registry manifest, and the bundle must carry a valid code signature. ### Plugins from other developers -A plugin TablePro signed itself installs with no extra step. - -A plugin signed by someone else installs only after you agree to trust that developer. TablePro asks once, naming the developer and their Apple Team ID, and the plugin is discarded if you decline. The bundle must be signed with an Apple Developer ID to get that far; unsigned and ad-hoc signed bundles are refused outright and there is no way to override that. A Developer ID signature says Apple issued that certificate to a named developer and that nobody has altered the bundle since. It is not a review of what the plugin does, which is why the decision to trust the developer is yours and not something TablePro makes for you. +A plugin TablePro signed itself installs with no extra step. A plugin signed by someone else installs only after you agree to trust that developer: TablePro asks once, naming the developer and their Apple Team ID, and discards the plugin if you decline. Unsigned and ad-hoc signed bundles are refused outright, with no override. Say yes only if you would give that developer your database credentials, because that is what you are granting. A driver plugin runs as part of TablePro and can read the credentials of every connection you open. -Trust is recorded per developer rather than per plugin, so later plugins and updates from the same developer install without asking again. Withdraw it in **Settings > Plugins**, which stops every plugin signed by that developer from loading. +Trust is per developer, not per plugin, so later plugins and updates from the same developer install without asking again. Withdraw it in **Settings > Plugins** to stop every plugin signed by that developer from loading. Themes are different: they are JSON with no executable code, so they need no signature and never ask. They are still checked against their SHA-256 checksum. ## Updates -TablePro compares installed plugin versions against the registry and shows a badge on the Plugins settings tab counting pending updates and plugins that failed to load. Select the plugin under **Installed** and click **Update**. +The Plugins settings tab carries a badge counting pending updates and plugins that failed to load. Select a plugin under **Installed** to see its installed version, then click **Update**. If a driver has open connections, the update is staged instead of applied. The staged version activates when the last connection using the driver closes, or click **Activate Now** after closing them. Quit before it activates and the update is offered again on next launch. ## After an app update -An app update can raise the plugin interface version. Plugins built against the old version are rejected at load instead of crashing, and TablePro repairs them on its own: at launch it fetches the registry and reinstalls a compatible build of each outdated plugin, retrying after 30 seconds, then 5 and 10 minutes (up to five attempts), when the registry is unreachable or a download fails. It tries again when the network comes back, and connecting to a database whose plugin is outdated updates that plugin first. +An app update can raise the plugin interface version. Plugins built against the old version are rejected at load instead of crashing, and TablePro repairs them itself: at launch it reinstalls a compatible build of each outdated plugin, retrying in the background if the registry is unreachable. Connecting to a database whose plugin is outdated updates that plugin first. Until a plugin is repaired, it appears in a banner in **Settings > Plugins > Installed** with the reason and an action: @@ -94,11 +92,13 @@ Until a plugin is repaired, it appears in a banner in **Settings > Plugins > Ins In **Settings > Plugins > Installed**: -- Toggle a plugin off to unload its drivers and formats without deleting it; toggle it back on to reload. -- Click **Uninstall** to remove a user-installed plugin from disk. Bundled plugins can't be uninstalled. +- The switch beside a plugin's name unloads its drivers and formats without deleting it. Switch it back on to reload. +- **Uninstall** removes a user-installed plugin from disk. Bundled plugins cannot be uninstalled. - The detail pane shows version, bundle ID, capabilities, database type, and default port. Plugins with their own settings show them here. -macOS can't fully unload a plugin's code until relaunch, so after removing one a banner offers **Quit & Reopen**. +macOS cannot fully unload a plugin's code until relaunch, so a banner offers **Quit & Reopen** after you remove one. + +Saved connections survive either way. A connection whose driver is switched off or uninstalled stays in the list, and opening it lands on "Could not connect", with **Install Plugin…** as the button. ## Themes @@ -106,4 +106,4 @@ Themes are plugins too: install registry themes from **Settings > Plugins > Brow ## Writing plugins -Want to build your own driver, format, or theme? See [Plugin Development](/development/plugin-development). +To build your own driver, format, or theme, see [Plugin Development](/development/plugin-development). diff --git a/docs/features/project-folder-import.mdx b/docs/features/project-folder-import.mdx index 8da863cd0..8097be54c 100644 --- a/docs/features/project-folder-import.mdx +++ b/docs/features/project-folder-import.mdx @@ -3,22 +3,30 @@ title: Open Project Folder description: Create a connection from the database settings already in your project's config files --- -Each set of credentials found in the folder you pick becomes one row, showing the file and key behind it. Nothing is saved or connected until you review the filled-in form and click **Save**. The scan goes four levels deep and ignores `node_modules`, `.git`, `vendor`, similar directories, and any file over 1 MB. +Point TablePro at a checkout and it reads the database settings out of the config files already in it. Each set of credentials becomes one row, showing the file and key it came from. -## Open a folder + + Project folder scan sheet + Project folder scan sheet + -- **Open Project Folder…**, in the welcome window's **Add from Existing** menu -- **Add from Existing > Open Project Folder…** on the welcome window + + + On the welcome window, choose **Add from Existing > Open Project Folder…** and pick the folder. **Choose Another Folder…** in the sheet points the scan somewhere else without starting over. + + + Each row shows the database type, host, port, user, and database, plus the file and key behind it. Select one and click **Continue**. + + + The connection form opens filled in. Nothing is saved and nothing connects until you click **Save**. + + -Pick the folder in the panel that appears. TablePro scans it and lists what it found. +One pass imports one row. Run it again for the next service, and again after you add a service to a compose file. Nothing is matched against the connections you already have, so importing one row twice gives you two connections. -## Pick a connection +## What a row can tell you -Each row shows the database type, host, port, user, and database, plus the file and key it came from. Select one and click **Continue**. The connection form opens filled in, and nothing is saved or connected until you review it and click **Save**. - -Passwords are never shown. A row says **Password found** when the file has one, and the value goes straight to your Keychain when you save. - -Rows can carry extra notes: +Passwords are never displayed. A row says **Password found** when the file has one, and the value goes straight to your Keychain when you save. | Note | Meaning | |------|---------| @@ -26,9 +34,9 @@ Rows can carry extra notes: | Looks like a placeholder value | The value matches a known template, such as `!ChangeMe!` or `password_here`. | | Container service name, may be unreachable | The host is a name like `db` or `postgres`, which usually only resolves inside Docker. | | Host and port assumed | The file had credentials but no address, so `127.0.0.1` and the default port were used. | -| No published port, may be unreachable | A Compose service with no `ports:` mapping, so it may not be reachable from your Mac. | +| No published port, may be unreachable | A Compose service with no `ports:` mapping, so your Mac may not reach it. | -A connection starts at the **Alert** [Safe Mode](/features/safe-mode) level instead of Silent when `prod`, `production`, or `live` shows up as a whole word in the file path, the host, or the database name. +A connection starts at the **Alert** [Safe Mode](/features/safe-mode) level rather than Silent when `prod`, `production`, or `live` appears as a whole word in the file path, the host, or the database name. ## Files it reads @@ -42,13 +50,17 @@ A connection starts at the **Alert** [Safe Mode](/features/safe-mode) level inst | `application.properties`, `application.yml` | `spring.datasource.url`, username, and password | | `appsettings.json` | The `ConnectionStrings` entries | -Template files are skipped, so `.env.example`, `.env.sample`, `.env.template`, and `.env.dist` never appear. `.envrc` is a shell script, so TablePro never reads or runs it. +Template files are skipped, so `.env.example`, `.env.sample`, `.env.template`, and `.env.dist` never appear. `.envrc` is a shell script, so TablePro neither reads nor runs it. + +## What the scan skips + +The walk goes four levels deep, ignores files over 1 MB, follows no symlinks, and stops after 20,000 entries. It skips `node_modules`, `.git`, `vendor`, `dist`, `build` and around twenty more directories of that kind, and never reads inside `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.docker`, `~/Library/Keychains`, or `/etc`. -The scan does not follow symlinks, skips `node_modules`, `.git`, `vendor`, `dist`, `build` and similar directories, goes four levels deep, and ignores files over 1 MB. It reads only the folder you picked, with one exception: WordPress allows `wp-config.php` to sit one level above the site, so that single file is checked in the parent folder too. When it comes from there, the row shows the path as `../wp-config.php`. +The one file it reads outside the folder you picked is `wp-config.php` one level up, where many WordPress sites keep it. That row shows its path as `../wp-config.php`. ## When a value cannot be read -Some settings point somewhere else instead of holding a value, like `${{Postgres.DATABASE_URL}}` on Railway or a Rails credential lookup. TablePro leaves those out rather than guessing, so a row you expected may be missing. Fill those connections in by hand, or paste the URL with [Import from URL](/connections/urls). +Some settings point elsewhere instead of holding a value, like `${{Postgres.DATABASE_URL}}` on Railway or a Rails credential lookup. Those are left out rather than guessed at, so a row you expected can be missing. Fill those in by hand, or paste the URL with [Import from URL](/connections/urls). ## Related diff --git a/docs/features/query-history.mdx b/docs/features/query-history.mdx index 993b8b551..753f209f0 100644 --- a/docs/features/query-history.mdx +++ b/docs/features/query-history.mdx @@ -3,9 +3,9 @@ title: Query History description: Every executed query is saved to a local SQLite database with full-text search --- -Every query you run is saved automatically, whether it succeeded or failed. History persists across sessions and is searchable. +By default the drawer shows only the SQL you wrote yourself. The SELECTs behind table browsing, the UPDATEs behind grid edits, the DDL behind a structure change and everything an MCP client ran are all recorded too, waiting behind the **Source** menu. -Open the drawer with `Cmd+Y` or **View > Show Query History**. There is also a **History** toolbar button, which you add through **View > Customize Toolbar**. +Open the drawer with `Cmd+Y` or **View > Show Query History**. There is also a **History** toolbar button, added through **View > Customize Toolbar…**. Query history drawer @@ -14,19 +14,13 @@ Open the drawer with `Cmd+Y` or **View > Show Query History**. There is also a * ## The drawer -The drawer opens under the editor and you can drag its divider to resize it. It remembers its height, its filters, and whether it was open, per connection. +The drawer opens under the editor and its divider resizes it. Height, filters and whether it was open are remembered per connection. -Entries are grouped by day, newest first, under **Today**, **Yesterday**, or the date. Each row shows whether the query succeeded, the query text, the database, the time it ran, and how long it took. A query that ran in under a millisecond shows `<1 ms`; a step whose duration was never measured shows `–` rather than `0 ms`. +Entries group by day, newest first, under **Today**, **Yesterday**, or the date. A row carries the outcome, the query text, the database, the time it ran and how long it took. Under a millisecond reads `<1 ms`; a step whose duration was never measured reads `–` rather than `0 ms`. -The right pane shows the selected query with syntax highlighting for the database it actually ran against, plus its connection, database and schema, timestamp, duration, row count, source, and the error message for failed queries. +Select a row and the right pane shows the full query, highlighted for the database it ran against, with its connection, database and schema, timestamp, duration, row count, source, and the error when it failed. The keyboard stays in the list, so arrow keys keep moving. -Recent queries also appear in [Open Quickly](/features/open-quickly). For a summary of what this history adds up to, rather than a list of it, see [Query Insights](/features/query-insights). - -## Scope - -History is scoped to the connection you are looking at. Switch the first popup to **All Connections** to search across every connection you have; rows then show which connection each query came from, so you can tell two databases named `app` apart. - -Loading a query that belongs to another connection opens it in a new tab in that connection's own window, rather than running it against the connection in front of you. **Run in New Tab** is dimmed for those entries, because only the connection that recorded a query can run it. Load it there and run it from that window. +Recent queries also appear in [Open Quickly](/features/open-quickly). For the summary rather than the list, see [Query Insights](/features/query-insights). ## Filtering @@ -38,16 +32,12 @@ Loading a query that belongs to another connection opens it in a new tab in that | Outcome | Any, succeeded, or failed | | Search | Full-text search across query text, matching as you type | -Search matches partial words, so typing `cust` finds `customers`. Several words all have to match, but they do not have to be next to each other: `select customers` finds `SELECT id, name FROM customers`. +Search matches partial words, so `cust` finds `customers`. Several words must all match but need not be adjacent: `select customers` finds `SELECT id, name FROM customers`. -## Pausing - -The pause button stops recording new queries on this Mac. Nothing is recorded from any source while it is paused, including row edits, structure changes, imports and AI clients, and the drawer says so until you resume. Pausing is local to the Mac you press it on, and it survives relaunch. +**All Connections** searches everything you have, and rows then name their connection, so two databases both called `app` stay apart. **Reset Filters** puts the drawer back to its defaults. ### Sources -TablePro runs SQL on your behalf in several places, and the **Source** menu decides which of those you see. It defaults to **My Queries**, which is the SQL you wrote yourself. - | Source | What it covers | |--------|----------------| | Editor | Queries you ran from a query tab | @@ -58,32 +48,38 @@ TablePro runs SQL on your behalf in several places, and the **Source** menu deci | Imports | Import runs | | AI and MCP | Queries run by an AI assistant or an MCP client | -Turn on **Table Browsing** to see exactly what the app sent while you clicked around a table. Turn on **Structure Changes** to review what altered a schema and when. +**My Queries**, the default, is Editor and Explain. Add **Table Browsing** to see what the app sent while you clicked around a table, **Structure Changes** to review what altered a schema and when. ## Working with entries | Action | How | |--------|-----| -| Load into the editor | Select an entry and click **Load in Editor**, or press `Return` | -| Run it again | **Run in New Tab**, or right-click > **Run in New Tab**. It opens a new tab and runs the query there | +| Load into the editor | `Return`, double-click the row, or click **Load in Editor** | +| Run it again | **Run in New Tab**, or right-click > **Run in New Tab** | | Copy the query | `Cmd+C`, the **Copy** button, or right-click > **Copy Query** | | Save as favorite | Right-click > **Save as Favorite…** (see [Favorites](/features/favorites)) | -| Delete one entry | `Delete` key or right-click > **Delete** | +| Delete one entry | `Delete`, or right-click > **Delete** | | Load older entries | **Load More** at the bottom of the list | -Click a row to select it and the details appear beside it; the keyboard stays in the list, so arrow keys keep moving through entries. Loading a query into the editor is a separate, deliberate step: press `Return` with the list focused, double-click the row, or click **Load in Editor**. +**Run in New Tab** goes to the server, so an entry that writes asks for confirmation whatever the connection's [safe mode](/features/safe-mode) is set to. Reads run straight away, and an entry using [query parameters](/features/query-parameters) raises the parameter panel instead. -**Run in New Tab** sends the query to the server, so an entry that writes (INSERT, UPDATE, DELETE, or a schema change) asks for confirmation first, whatever the connection's [safe mode](/features/safe-mode) is set to. Reads run straight away. An entry that uses [query parameters](/features/query-parameters) opens the parameter panel instead of running, because parameter values are never recorded. +An entry belonging to another connection loads into a new tab in that connection's own window rather than running against the connection in front of you. **Run in New Tab** is dimmed for those entries: load it there and run it from that window. + +## Pausing + +The pause button stops recording on this Mac, from every source: row edits, structure changes, imports and AI clients included. The drawer says so until you resume. Pausing is local to the Mac you press it on and survives relaunch. ## Clearing history -The trash button clears exactly what the drawer is showing, including whatever the filters are hiding. Scoped to one connection it leaves every other connection alone; with a date range selected it only clears that range; and with the default **My Queries** source it leaves table browsing, row edits, imports and AI queries in place. It asks first, and the confirmation says which of those two it is about to do. There is no undo. +The trash button deletes exactly the entries the drawer is listing. Anything the source, date, outcome or search filters are hiding stays, which at the default **My Queries** source spares table browsing, row edits, imports and AI queries. The confirmation names what it is about to delete. There is no undo. **Settings > Data > Query History > Clear History…** clears everything, for every connection. ## Storage and retention -History lives in `~/Library/Application Support/TablePro/query_history.db`, a local SQLite database with a full-text index. +History lives in `~/Library/Application Support/TablePro/query_history.db`, a local SQLite database with a full-text index. It never leaves the Mac that recorded it: query history is not one of the categories [iCloud sync](/features/icloud-sync) carries, so two Macs keep two separate histories. + +There is no export command. The file is an ordinary SQLite database, so open it as a SQLite connection and query it like anything else. Configure retention in **Settings > Data > Query History**: @@ -91,14 +87,12 @@ Configure retention in **Settings > Data > Query History**: |---------|---------|---------| | Maximum entries | 10,000 | 100 to 10,000, or Unlimited | | Keep entries for | 90 days | 7 days to 1 year, or Forever | -| Auto cleanup | On | Applies the two limits above. Turn it off and nothing is pruned. | +| Auto cleanup | On | Enforces the entry count and the age limit. Turn it off and nothing is pruned. | Deleting a connection deletes its query history with it. -Query parameter values are never written to disk. The user and role editor never records a statement that carries a password, such as `CREATE USER … IDENTIFIED BY`. A query you write yourself is recorded as you wrote it, so use the pause button before running one that contains a secret. +Parameter values are never recorded here, and the user and role editor never records a statement carrying a password, such as `CREATE USER … IDENTIFIED BY`. A query you wrote yourself is recorded as you wrote it, so pause before running one that contains a secret. ## Search from external clients -History is searchable from MCP clients. The `search_query_history` tool returns matching entries with timestamp, connection, query text, source, and outcome. The Raycast extension wraps this in a **Search Query History** command. - -See [`search_query_history`](/external-api/mcp-tools) and [Raycast commands](/external-api/raycast#commands). +The [`search_query_history`](/external-api/mcp-tools) MCP tool returns matching entries with timestamp, connection, query text, source and outcome. The Raycast extension wraps it in a **Search Query History** command; see [Raycast commands](/external-api/raycast#commands). diff --git a/docs/features/query-insights.mdx b/docs/features/query-insights.mdx index 164f9a30f..0fb4c4e32 100644 --- a/docs/features/query-insights.mdx +++ b/docs/features/query-insights.mdx @@ -3,58 +3,74 @@ title: Query Insights description: Which queries you run most, which cost the most time, and which got slower --- - -Query Insights needs a [Starter license](/features/licensing). Everything it shows is computed on your Mac from the [query history](/features/query-history) already stored there. Nothing is uploaded. - +A query you ran 400 times with 400 different ids is one row here, not 400. That regrouping is what turns a list of what you ran into a ranking of what is worth looking at. Every number on the page is computed on your Mac from the [query history](/features/query-history) already stored there, and none of it is uploaded. -Query history answers "what did I run". Insights answers "what is worth my attention". It reads the same local SQLite database and summarizes it. +Needs a [Starter license](/features/licensing). -Open it from **Database > Query Insights**. It opens as a tab, one per connection, and reuses the existing tab if you already have one open. +Open it from **Database > Query Insights**. It opens as a tab, one per connection, and reuses the tab you already have open. Query Insights tab Query Insights tab -## Queries are grouped by shape +## The panels -`SELECT * FROM users WHERE id = 1` and `SELECT * FROM users WHERE id = 2` are the same query run twice, not two queries run once. Insights strips the values out of every statement and groups by what is left: +Each ranked panel lists the top 10 shapes. Right-click a row for **Copy Query** and **Load in Editor**: both give you the most recent real query in the group, values and all, from a run the row is actually counting, so the connection, sources and date range you picked all apply. In **Failures** it comes from the run that produced the error beside it. -| Written | Counted as | -|---------|------------| -| `WHERE id = 1` and `WHERE id = 2` | `WHERE id = ?` | -| `IN (1, 2)` and `IN (1, 2, 3, 4)` | `IN (…)` | -| `VALUES (1, 'a')` and `VALUES (1, 'a'), (2, 'b')` | `VALUES (…)` | -| `select * from t` and `SELECT * FROM t` | one shape | -| the same query with and without comments | one shape | +### Summary -This is the same idea as PostgreSQL's `pg_stat_statements` and MySQL's statement digest, so the numbers mean what they mean there. +Across the top: how many queries ran, what share failed, the average duration, and the total time spent waiting. -Two things are deliberately **not** merged. Table and column names keep their capitalization, because on a case-sensitive server `Orders` and `orders` are different tables and merging them would report a wrong count. And numbers inside a name stay part of the name, so `events_2025` and `events_2026` are counted separately. +### Activity -Each row shows the shape, not the literal query you ran. **Copy Query** and **Load in Editor** in the right-click menu give you the most recent real query in the group, values and all. It always comes from a run the row is actually counting: the connection, the sources and the date range you picked all apply to it, so a matching query run somewhere you filtered out never supplies the values. In **Failures** it comes from a run that failed, which is the one that produced the error shown beside it. +Queries per day, split into succeeded and failed. A range of two days or less is charted by hour instead. -## The panels +### Most Run -**Summary** across the top: how many queries ran, what share failed, the average duration, and the total time spent waiting. +Shapes ranked by how many times they ran. -**Activity** charts queries per day, split into succeeded and failed. Ranges of two days or less are charted by hour instead. +### Slowest -**Most Run** ranks shapes by how many times they ran. +Ranked by **Total Time** by default, which finds the query that actually costs you time: usually a quick one you run constantly rather than a slow one you ran once. Switch **Rank By** to **Average Time** for the query that is slow every time. Average Time lists only shapes that ran at least 3 times. -**Slowest** ranks by **Total Time** by default, which is what `pg_stat_statements` sorts by: it finds the query that actually costs you time, which is usually a quick query you run constantly rather than one slow query you ran once. Switch it to **Average Time** to find the query that is slow every single time. Average Time only lists shapes that ran at least 3 times, because the average of one run is just that run. +### Got Slower -**Got Slower** compares the range you picked against the range immediately before it. Pick **Last 7 Days** and it compares against the 7 days before that. A shape is reported when all of these hold: +Compares the range you picked against the range immediately before it, so **Last 7 Days** compares against the 7 days before that. An unbounded range compares the last 7 days. A shape is reported when all three hold: - it ran at least 5 times in **both** periods - its average got at least 50% slower - its average grew by at least 25 ms -Those floors are there because without them the panel reports noise. Ordinary variation in a query that runs hundreds of times a day will cross a 20% threshold regularly, and a query going from 1 ms to 2 ms has doubled without costing you anything. Only successful runs count, since a query that failed fast is not a query that got quicker. +Only successful runs count. Rows are ordered by the time the slowdown actually costs, so a query that got twice as slow and runs all day ranks above one that got ten times slower and runs twice. + +### Failures + +Shapes ranked by how many times they failed, with the most recent error message for each. + +## What to do with a finding + +A row in **Slowest** or **Got Slower** is a shape, not a diagnosis. **Load in Editor** puts a real example in a query tab, where `Cmd+Option+E` gives you the execution plan: see [Explain Visualization](/features/explain-visualization). + +Durations are measured on this side of the wire, from sending the query to getting the result, so they cover network time and any [SSH tunnel](/connections/ssh-profiles) as well as the server. For server-side timings, locks and running queries, see the [Server Dashboard](/features/server-dashboard). -Rows are ordered by the time the slowdown actually costs, so a query that got twice as slow and runs all day ranks above one that got ten times slower and runs twice. +A shape ranked high under **Table Browsing** came from the app paging or sorting a table, not from anything you wrote. -**Failures** ranks shapes by how many times they failed, with the most recent error message for each. +## How shapes are grouped + +Values are stripped out of every statement and what is left is the shape: + +| Written | Counted as | +|---------|------------| +| `WHERE id = 1` and `WHERE id = 2` | `WHERE id = ?` | +| `IN (1, 2)` and `IN (1, 2, 3, 4)` | `IN (…)` | +| `VALUES (1, 'a')` and `VALUES (1, 'a'), (2, 'b')` | `VALUES (…)` | +| `select * from t` and `SELECT * FROM t` | one shape | +| the same query with and without comments | one shape | + +This is the same idea as PostgreSQL's `pg_stat_statements` and MySQL's statement digest, so the numbers mean what they mean there. + +Two things stay apart: table and column names keep their capitalization, since `Orders` and `orders` are different tables on a case-sensitive server, and a number inside a name stays part of it, so `events_2025` and `events_2026` are counted separately. ## Filtering @@ -64,16 +80,10 @@ Rows are ordered by the time the slowdown actually costs, so a query that got tw | Source | Which parts of the app the queries came from | | Date | Last hour, today, last 7 days, last 4 weeks, all time | -Source defaults to **My Queries**, the SQL you wrote yourself. Turn on **Table Browsing** to see what the app sent while you clicked around, which is usually where the highest run counts hide. The sources are the same ones the [history drawer](/features/query-history) uses. +Source defaults to **My Queries**, the SQL you wrote yourself, and uses the same source list as the [history drawer](/features/query-history#sources). Date defaults to **Last 4 Weeks** rather than All Time, since "got slower than before" needs a before to compare against. There is no outcome filter. -Date defaults to **Last 4 Weeks** rather than All Time, because "got slower than before" needs a before to compare against. - -There is no outcome filter, on purpose. Hiding failed queries would make the failure panel report that nothing ever fails. - -Insights refreshes itself as you run queries. The refresh button is there for when you want it now. +Insights refreshes as you run queries. The refresh button is for when you want it now. ## What it cannot tell you -It only knows what your Mac recorded. Queries run before you installed TablePro, run by other people, or run while [capture was paused](/features/query-history#pausing) are not in it. Queries pruned by your [retention settings](/features/query-history#storage-and-retention), 10,000 entries and 90 days by default, are gone too, so a long **All Time** range does not reach further back than retention allows. - -Durations are measured by TablePro, from sending the query to getting the result. That includes network time and any [SSH tunnel](/connections/ssh-profiles), so a query is "slower" here if your connection got slower, not only if the server did. For server-side timings see the [Server Dashboard](/features/server-dashboard) and [EXPLAIN](/features/explain-visualization). +It knows what your Mac recorded and nothing else. Queries run before you installed the app, run by other people, or run while [capture was paused](/features/query-history#pausing) are not in it, and neither are queries already pruned by your [retention settings](/features/query-history#storage-and-retention). **All Time** never reaches further back than retention allows. diff --git a/docs/features/query-parameters.mdx b/docs/features/query-parameters.mdx index 97cbe6027..9161d107e 100644 --- a/docs/features/query-parameters.mdx +++ b/docs/features/query-parameters.mdx @@ -3,7 +3,7 @@ title: Query Parameters description: Use :name placeholders in SQL queries, fill values in a panel, execute with prepared statements --- -A `:word` in your SQL becomes a field you fill in before the query runs, unless it sits inside a string, a comment, or a PostgreSQL cast. Press `Cmd+Enter` once to raise the fields and `Cmd+Enter` again to run. Where the driver has a parameter API, what you typed is bound, never pasted into the text. +Write `:id` where the value goes, then press `Cmd+Enter` twice: once to raise the fields, once to run. A `:word` inside a string, a comment, or a PostgreSQL cast is left alone. -## Usage - -Write a query with `:name` placeholders: - ```sql SELECT * FROM orders @@ -30,33 +26,18 @@ WHERE customer_id = :customer_id AND created_at > :since; ``` -Press `Cmd+Enter`. A panel appears with a field for each parameter. Fill in values, press `Cmd+Enter` again. Done. - -Works with **Execute All Statements** (`Cmd+Shift+Enter`) too. - -## The panel - -Each row has: - -| Control | What it does | -|---------|-------------| -| **Name** | Shows the parameter name from your query | -| **Value** | Where you type the value | -| **Type** | String, Integer, Decimal, Date, or Boolean | -| **NULL** | Check this to bind NULL | +That query raises three rows, in the order the names first appear. Each row is the parameter name, a value field, a type popup (String, Integer, Decimal, Date or Boolean), and a **NULL** checkbox that binds NULL and disables the field. -Rows appear in the order the names first appear in your SQL. +Every parameter needs a value or NULL. Running with one empty stops with "Missing value for parameter: :name" instead of going to the server. **Clear All** empties every value field, and the X button hides the panel until the next run. -**Clear All** empties all value fields. The X button hides the panel. It comes back on the next execute if the query still has parameters. +Where the driver has a parameter API the value is bound, never pasted into the text; everywhere else it is escaped. Either way you never quote a value yourself, and a `'` in a value is not an injection. -Every parameter needs a value or the NULL checkbox. Executing with an empty value shows "Missing value for parameter: :name" instead of running. +## What counts as a parameter -## What gets detected +`:name` is detected when `name` starts with a letter or underscore. These are not: -`:name` is detected when `name` starts with a letter or underscore. These are ignored: - -| Pattern | Why it's ignored | -|---------|-----------------| +| Pattern | Why | +|---------|-----| | `':name'` | Inside a string | | `-- :name` | Inside a comment | | `/* :name */` | Inside a block comment | @@ -64,32 +45,29 @@ Every parameter needs a value or the NULL checkbox. Executing with an empty valu | `$$ :name $$` | Dollar-quoted string | | `:123` | Starts with a digit | -If the same name appears twice (`:id = :id`), both get the same value. +The same name twice (`:id = :id`) gets one field and one value. -## How binding works +## LIKE patterns and IN lists -TablePro converts your `:name` placeholders to the database's native format before executing: +One placeholder is one value, which is where both of these go wrong. -- MySQL, SQLite: `?` placeholders via `mysql_stmt_bind_param` / `sqlite3_bind_text` -- PostgreSQL: `$1`, `$2` via `PQexecParams` -- DuckDB: `$1`, `$2` via prepared statements -- ClickHouse: `{p1:String}` server-side query parameters -- SQL Server: `sp_executesql` with declared parameters -- Drivers with no parameter API: TablePro substitutes the values with escaping +For a `LIKE`, put the wildcards in the value rather than the SQL. `LIKE '%:term%'` is a string literal and no parameter at all; write `LIKE :term` and type `%acme%` into the field. - -Where the driver has a parameter API, values are bound, not pasted into your SQL. Everywhere else TablePro escapes them. Either way, you never quote a value yourself. - +For an `IN` list, a single `:ids` binds the whole thing as one value and matches nothing. Write one placeholder per value: -## Values stick around +```sql +SELECT * FROM orders WHERE status IN (:a, :b, :c); +``` -Parameter values are saved with the tab. They survive tab switches, app restarts, and show up in [query history](/features/query-history). +## Values are kept with the tab -When you edit the query and change parameter names, new names get empty fields and old ones disappear. Names that still match keep their values. +Values are saved with the tab and survive tab switches and app restarts. Editing the query keeps the values whose names still match, empties the fields for new names, and drops the ones that are gone. -## Multiple statements +Values are never recorded in [query history](/features/query-history), so loading a parameterized entry from there raises the panel rather than running it. -For multi-statement scripts, all unique parameter names across all statements appear in one panel. Each statement only binds the parameters it uses. +## Several statements at once + +**Execute All Statements** (`Cmd+Shift+Enter`) collects every unique name across the whole script into one panel. Each statement binds only the parameters it uses. ```sql INSERT INTO users (id, name) VALUES (:id, :name); @@ -98,12 +76,10 @@ SELECT * FROM users WHERE id = :id; Both use `:id`. Only the INSERT uses `:name`. -## Safe Mode +## Where it applies -Parameterized queries still go through [safe mode](/features/safe-mode) checks. DROP, DELETE without WHERE, and TRUNCATE still ask for confirmation. +Parameters are read when a query tab runs: `Cmd+Enter`, **Execute All Statements**, and the [run buttons in the gutter](/features/sql-editor#statement-markers). A parameterized query still goes through [safe mode](/features/safe-mode), so DROP, TRUNCATE, and DELETE without WHERE still ask first. ## Settings -**Settings > Editor > Query parameters (:name syntax)**. On by default. - -Turn it off if you don't want parameter detection (`:name` will be sent to the database as-is). +**Settings > Editor > Query parameters (:name syntax)**, on by default. Turn it off and `:name` is sent to the database exactly as written. diff --git a/docs/features/query-results.mdx b/docs/features/query-results.mdx new file mode 100644 index 000000000..308e891f4 --- /dev/null +++ b/docs/features/query-results.mdx @@ -0,0 +1,49 @@ +--- +title: Query Results +description: Result tabs, pinning, the row cap, and what a failed or non-SELECT statement shows +--- + +import RowCap from "/snippets/row-cap.mdx"; + +Every statement you run gets its own result tab. The next run reuses an unpinned one rather than adding to the pile, so a tab survives only as long as you leave it unpinned. + +## Naming + +A result is named after the table it came from. When there is no single table, the statement names itself, and a line comment written above the statement wins over the SQL: + +```sql +-- monthly totals +SELECT count(*) FROM orders; +``` + +names its result tab `monthly totals`. Names longer than 28 characters are truncated with an ellipsis, and a result no statement stands behind falls back to `Result 1`. Rest the pointer on a tab to see the query that produced it, or the error message if it failed. + +## Working with the strip + +| Action | How | +|---|---| +| Switch result | `Cmd+Option+[` and `Cmd+Option+]`, or click the tab | +| Pin | The pin on the right of the tab, `Cmd+Option+P`, or right-click > **Pin Result** | +| Close | `Cmd+Shift+W`, or right-click > **Close** | +| Close the rest | Right-click > **Close Others** | +| Show or hide the panel | `Cmd+Option+R`, or the toolbar button | + +A pinned result moves to the front of the strip, and the next query opens a new tab instead of overwriting it. Pinned tabs cannot be closed or cleared until you unpin them, which is what makes them useful for comparing two runs side by side. + +Picking a result moves the editor cursor to the statement that produced it and unfolds that statement if it was collapsed. Picking the result already showing does nothing, so clicking between pinned results never moves the editor under you. A statement you have since edited away leaves the cursor where it is. + +The results panel expands itself when a query runs. The toolbar's trash button clears the query and the results together; to keep the query, right-click the results and choose **Clear Results**. + +## The row cap + +A `SELECT` or `WITH` query with no `LIMIT`, `FETCH FIRST` or `TOP` of its own stops at the row cap. , and `EXPLAIN`, `SHOW`, writes and DDL are never capped. + +When the cap trims a result the status bar reads **Showing N rows** and offers **Fetch All**, which extends the result in place. To skip the cap for one run, press `Cmd+Option+Enter` or choose **Execute Without Limit** from the Execute button's menu. + +The cap and its off switch are in [Settings > Data](/customization/data-settings#query-result-row-cap). + +## Statements that return no rows + +INSERT, UPDATE, DELETE and DDL show a success view with the affected row count and the execution time. + +A failed statement shows a red banner above the results with the database's own error message and a **Fix with AI** button. See [AI Assistant](/features/ai-assistant). diff --git a/docs/features/safe-mode.mdx b/docs/features/safe-mode.mdx index 7ceae90eb..38d027983 100644 --- a/docs/features/safe-mode.mdx +++ b/docs/features/safe-mode.mdx @@ -3,13 +3,11 @@ title: Safe Mode description: Per-connection query execution controls, from no restrictions to full read-only lockdown --- -Each connection carries its own Safe Mode level, from no restrictions to full write protection. The level decides what happens before a query runs. - -Set the Safe Mode level in the **Customization** pane of the connection form. +Six levels, one per connection, set in the **Customization** pane of its edit form. The level decides what stands between a query and the server: nothing, a confirmation, Touch ID, or a refusal. ## Levels -| Level | Write Queries | Read Queries | Authentication | +| Level | Write queries | Read queries | Authentication | |-------|--------------|--------------|----------------| | **Silent** | Execute immediately | Execute immediately | None | | **Alert** | Confirmation dialog | Execute immediately | None | @@ -18,73 +16,40 @@ Set the Safe Mode level in the **Customization** pane of the connection form. | **Safe Mode (Full)** | Confirmation + Touch ID | Confirmation + Touch ID | Touch ID / password | | **Read-Only** | Blocked entirely | Execute immediately | None | -New connections default to **Silent**. - -## How it works - -### Silent - -No restrictions. Queries execute immediately. TablePro still shows its built-in dangerous query warning for DROP, TRUNCATE, and DELETE-without-WHERE statements. - -### Alert - -A confirmation dialog appears before executing write queries (INSERT, UPDATE, DELETE, DROP, TRUNCATE, ALTER, etc.). The dialog shows a preview of the SQL to be executed. Read queries run without prompts. - -### Alert (Full) - -Same as Alert, but the confirmation dialog appears for ALL queries, including SELECT statements. Useful when you want to review every query before execution. - -### Safe Mode - -Like Alert, but after confirming the dialog, you must also authenticate with Touch ID. Falls back to your macOS password if Touch ID is unavailable. - -### Safe Mode (Full) +New connections start at **Silent**, which is the right choice for a local database you own. Move a shared staging connection to **Alert** and anything with real customer data in it to **Safe Mode** or **Read-Only**. -Combines Alert (Full) and Safe Mode: every query requires both a confirmation dialog and Touch ID/password authentication. +Four things the table cannot carry. The confirmation dialog previews the SQL it is about to run. Touch ID falls back to your macOS password on a Mac without it. **Silent** is not a free pass: `DROP`, `TRUNCATE`, and a `DELETE` with no `WHERE` still raise the built-in dangerous query warning even there. And **Read-Only** goes past queries to the interface itself, disabling inline cell editing, adding, deleting and duplicating rows, table truncate and drop, and import. -### Read-Only +## What the level gates -All write operations are blocked. The UI disables: +Safe Mode sits in front of query execution, saving cell edits, structure and table changes, sidebar operations, imports, and maintenance jobs, including the ones the [AI assistant](/features/ai-assistant) and the MCP tools ask for. -- Inline cell editing -- Adding, deleting, and duplicating rows -- Table truncate and drop operations -- Import functionality - -Read queries (SELECT) execute normally. +It does not sit in front of reading metadata. Loading the sidebar, opening a table's structure, and exporting data are never confirmed, which is why a backup stays available on a Read-Only connection. ## Drivers without read-only support -The Redis, MongoDB, and etcd drivers do not support read-only mode, so Safe Mode treats every query on these connections as a write. Alert and Safe Mode levels confirm every query; Read-Only blocks everything. Other non-SQL drivers (Cassandra, Elasticsearch, DynamoDB) classify reads and writes normally. +The Redis, MongoDB, and etcd drivers cannot open a read-only session, so Safe Mode treats every query on those connections as a write: the Alert and Safe Mode levels confirm everything, and Read-Only blocks everything. Every other driver classifies reads and writes normally. ## Toolbar badge -The current Safe Mode level appears as a badge in the toolbar (orange for Alert levels, red for Safe Mode and Read-Only). Click it to change levels. +The level appears as a badge in the toolbar, orange for the Alert levels and red for Safe Mode and Read-Only. Click it to change level. Safe Mode level badge and picker in the toolbar Safe Mode level badge and picker in the toolbar -Changing the level from the badge applies to the whole connection and stays set as you open other tables and tabs. It writes back to the saved connection, so the Customization pane shows the new level and the change reaches your other Macs when iCloud Sync is on. There is no session-only override. - -Editing the level in the connection form works the same way round: the change reaches an open connection right away, so the badge and the form always agree. - -Safe Mode gates apply to query execution, saving cell edits, table operations, and sidebar changes. +There is no session-only override. A change from the badge writes back to the saved connection, so it holds across tables and tabs, shows up in the Customization pane, and reaches your other Macs when iCloud Sync is on. Editing the level in the form works the same way round and reaches an open connection right away. -## Server read-only is not safe mode +## Server read-only is not Safe Mode -Safe Mode runs inside TablePro. It never changes anything on the database server, and it cannot make the server accept a write the server itself refuses. - -If a save fails with a read-only error while Safe Mode is set to anything other than Read-Only, the database server is the one refusing the write. Common causes: +Safe Mode runs inside TablePro. It cannot make a server accept a write the server itself refuses, and the wording tells you which one refused. "Cannot execute write queries: TablePro's Safe Mode is set to read-only for this connection" is the app. Anything else is the server, and the usual causes are these: - You are connected to a read replica or a reader endpoint rather than the primary. - The server runs with `read_only` or `super_read_only` turned on. -- The server or the session sets new transactions to read-only. - -TablePro tells the transactions it opens for a write that they are read-write, so a server that only defaults new transactions to read-only accepts the save. A server that is genuinely read-only still refuses, and TablePro says the server refused it. +- The server or the session opens new transactions read-only. -On MySQL and MariaDB you can check which one it is: +TablePro opens its write transactions as read-write, so a server that only defaults new transactions to read-only accepts the save anyway. On MySQL and MariaDB this narrows down the rest: ```sql SHOW SESSION VARIABLES WHERE Variable_name IN @@ -95,44 +60,32 @@ SHOW SESSION VARIABLES WHERE Variable_name IN ## Execution log -Every database operation goes through one authorization step, including the ones the AI assistant and the MCP tools ask for. TablePro records each decision, allowed or refused, to a local log. - -A record holds the time, the connection, the kind of operation, whether it was a write, and the outcome. It holds a SHA-256 digest of the statement, never the statement itself: a query contains customer data, and an audit trail that stored it would be a second copy of the database. - -Each record carries the hash of the one before it. Recomputing the chain shows whether any record was edited, reordered or removed. +TablePro records each authorization decision, allowed or refused, to a local log: the time, the connection, the kind of operation, whether it was a write, and the outcome. The statement is stored as a SHA-256 digest, so the log is not a second copy of your data. Each record carries the hash of the one before it, so recomputing the chain shows whether a record was edited, reordered or removed. -This is tamper evident, not tamper proof. Anyone who can write the file can also recompute every hash after the record they changed, and truncating the end of the log leaves a chain that still verifies. It turns a silent edit into a visible one; it does not prevent one. Somewhere the person being audited cannot write is the only thing that would. +This is tamper evident, not tamper proof. Anyone who can write the file can recompute every hash after the record they changed, and truncating the end leaves a chain that still verifies. Only storing it somewhere the audited person cannot write would fix that. -The log is local, is not synced, and is not sent anywhere. +The log is local. It is not synced and not sent anywhere. ## Managed by an organization -An administrator can set a minimum Safe Mode level through a macOS configuration profile, delivered by an MDM such as Jamf or Kandji. A connection set below that level is raised to it. A stricter choice is left alone, so the policy is a floor and never a ceiling: someone who wants Read-Only on a production connection still gets it. - -The profile targets the `com.TablePro` preference domain with a flat key: +An administrator can impose a minimum level through a macOS configuration profile, delivered by an MDM such as Jamf or Kandji. The profile targets the `com.TablePro` preference domain with one flat key: | Key | Type | Value | |---|---|---| | `com.TablePro.policy.minimumSafeModeLevel` | String | `silent`, `alert`, `alertFull`, `safeMode`, `safeModeFull`, or `readOnly` | -A value TablePro does not recognize imposes no floor at all. It never falls back to Read-Only, which would lock people out of a typo, and never to Silent, which would quietly drop the policy. - -TablePro reads this the way macOS intends: a configuration profile already outranks the app's own preferences, so a forced key wins, and the app asks `CFPreferencesAppValueIsForced` only to know that the matching control should be disabled rather than merely preset. - - -This is a floor on TablePro's own behavior, not on the database. It stops the app issuing a write; it does not stop the same person connecting with `psql`. Pair it with server-side privileges for anything that has to hold. See [Server Read-Only Is Not Safe Mode](#server-read-only-is-not-safe-mode). - +A connection set below the floor is raised to it, and a stricter choice is left alone: the policy is a floor, never a ceiling. A value TablePro does not recognize imposes no floor at all. While the policy is in force the matching control appears dimmed. -## External Clients +This is a floor on TablePro's own behavior, not on the database. It stops the app issuing a write; it does not stop the same person connecting with `psql`. Pair it with server-side privileges for anything that has to hold. -Safe Mode runs inside the app on every query you execute. External clients (Raycast, Cursor, Claude Desktop, and other MCP clients) hit a separate gate first. +## External clients -A write request from an external client clears three locks in this order: +A write from an external client (Raycast, Cursor, Claude Desktop, any MCP client) clears three locks in order: -1. **External Clients** (per-connection: **Blocked** / **Read Only** / **Read & Write**). Set in the connection form's **Advanced** pane. A Read Only connection rejects any write before the request reaches the database. -2. **Token scope** (per-integration, `readOnly` / `readWrite` / `fullAccess`). Issued by the [pairing flow](/external-api/pairing) and bounded by External Access: effective permission is `MIN(token.scope, connection.externalAccess)`. -3. **Safe Mode** (per-query). The same rules on this page apply once the request has been routed to the connection. Touch ID prompts and confirmation dialogs still appear, even for queries originating from an external client. +1. **External Clients** (per connection: **Blocked** / **Read Only** / **Read & Write**), in the connection form's **Advanced** pane. A Read Only connection rejects a write before the request reaches the database. +2. **Token scope** (per integration: `readOnly` / `readWrite` / `fullAccess`), issued by the [pairing flow](/external-api/pairing). The effective permission is `MIN(token.scope, connection.externalAccess)`. +3. **Safe Mode**, per query. Everything on this page then applies, Touch ID prompts and confirmation dialogs included. -DROP and TRUNCATE always go through the `confirm_destructive_operation` tool, which needs a `fullAccess` token and the user's approval every time: through the client's own elicitation prompt when it supports one, otherwise through TablePro's confirmation dialog. No token scope skips it. See [External API security model](/external-api/index#security-model). +`DROP` and `TRUNCATE` always go through the `confirm_destructive_operation` tool, which needs a `fullAccess` token and the user's approval every time: through the client's own elicitation prompt where it has one, otherwise through TablePro's dialog. No token scope skips it. See the [External API security model](/external-api/index#security-model). diff --git a/docs/features/server-dashboard.mdx b/docs/features/server-dashboard.mdx index 158e28f0c..c21ef8d76 100644 --- a/docs/features/server-dashboard.mdx +++ b/docs/features/server-dashboard.mdx @@ -3,9 +3,9 @@ title: Server Dashboard description: Monitor active sessions, server metrics, and slow queries in real time --- -Open this when the question is about the server rather than about a table. It shows who is connected, whatever metrics the engine reports, and any query still running after a second, refreshing every 5 seconds until you change the interval or turn it off. +What this screen shows is what your account is allowed to see. A MySQL user without the `PROCESS` privilege gets only its own connections in the session list. A PostgreSQL role that is neither a superuser nor a member of `pg_monitor` gets everyone's backends with the Query column blank. An empty or half-blank dashboard is nearly always a privilege, not a fault. -Open it from the menu bar **Database > Server Dashboard**. A Dashboard toolbar button is also available: right-click the toolbar, choose **Customize Toolbar**, and drag it in. +Open it from **Database > Server Dashboard**. A Dashboard toolbar button is available too: right-click the toolbar, choose **Customize Toolbar**, and drag it in. Server Dashboard**. A Dashboard toolbar b /> -The panels are stacked top to bottom. Drag the dividers between Active Sessions, Server Metrics, and Slow Queries to resize each section; positions are remembered across launches. +The panels stack top to bottom. Drag the dividers between Active Sessions, Server Metrics, and Slow Queries to resize them; the positions are remembered across launches. ## Active Sessions -A sortable table of all connections to the server, showing: +Every connection to the server except this one, sortable, one row each. -- **PID**: process or session ID -- **User**: connected user -- **Database**: target database -- **State**: current status (active, idle, sleeping) -- **Duration**: how long the current operation has been running -- **Query**: the SQL statement being executed (truncated, hover for full text) +| Column | Holds | +|---|---| +| PID | Process or session ID | +| User, Database | Who is connected, and to what | +| State | active, idle or sleeping, in the engine's own wording | +| Duration | How long the current operation has been running | +| Query | The statement, truncated at 1,000 characters. Hover for the full text | -### Kill and cancel - -Each session row has action buttons: - -- **Cancel Query** (stop icon): cancels the running query without terminating the connection (`pg_cancel_backend` on PostgreSQL, `KILL QUERY` on MySQL) -- **Terminate Session** (x icon): kills the entire connection - -Both actions show a confirmation alert before executing. If the server rejects the action, an Action Failed alert shows the error. - -## Server Metrics - -A horizontal strip of metric cards. What is shown depends on the database: - -| Database | Metrics shown | -|----------|--------------| -| PostgreSQL / Redshift / CockroachDB | Connections, cache hit ratio, database size, uptime, active queries | -| MySQL / MariaDB | Connected threads, running threads, uptime, total queries, slow queries, max connections, bytes received, bytes sent | -| MSSQL | User sessions, uptime, database size | -| ClickHouse | Active queries, active merges, part mutations, disk usage | -| DuckDB | Database size, block size, total blocks, memory limit, threads | -| SQLite | Database size, page count, page size, journal mode, cache size | +Two buttons sit on each row. **Cancel Query** stops the running statement and leaves the connection up: `pg_cancel_backend` on PostgreSQL, `KILL QUERY` on MySQL. **Terminate Session** ends the connection itself with `pg_terminate_backend` or `KILL`, and the server rolls back any transaction that session had open. Both confirm first, and a refusal from the server comes back as an **Action Failed** alert. ## Slow Queries -A list of queries running longer than 1 second, sorted by duration. Each entry shows the elapsed time, SQL text, user, and database. +Queries still running after 1 second, longest first, with elapsed time, SQL, user, and database. -ClickHouse is different: the list comes from `system.query_log`, so it shows the 20 most recent finished queries that took over a second, newest first, with no database. +ClickHouse is the exception: its list comes from `system.query_log`, so it holds the 20 most recent *finished* queries that took over a second, newest first, and no database. -## Auto-refresh +## Refreshing -- **Interval menu**: 1s, 2s, 5s (default), 10s, 30s, or Off -- **Pause/Resume**: stops refreshing without changing the interval (disabled when the interval is Off) -- **Refresh Now**: runs one refresh -- **Last refresh time**: shown on the right +The interval menu offers 1s, 2s, 5s (the default), 10s, 30s, and Off. **Pause** stops the cycle without changing the interval and is dimmed while the interval is Off. **Refresh Now** runs a single pass. The time of the last refresh sits on the right. -## Database support +## What each engine reports -| Database | Sessions | Metrics | Slow Queries | Kill | Cancel | -|----------|:--------:|:-------:|:------------:|:----:|:------:| -| PostgreSQL / Redshift / CockroachDB | Yes | Yes | Yes | Yes | Yes | -| MySQL / MariaDB | Yes | Yes | Yes | Yes | Yes | -| MSSQL | Yes | Yes | Yes | Yes | - | -| ClickHouse | Yes | Yes | Yes | Yes | - | -| DuckDB | - | Yes | - | - | - | -| SQLite | - | Yes | - | - | - | +| Database | Sessions | Metrics | Slow queries | Cancel | Terminate | +|----------|:--------:|---------|:------------:|:------:|:---------:| +| PostgreSQL / Redshift / CockroachDB | Yes | Connections, cache hit ratio, database size, uptime, active queries | Yes | Yes | Yes | +| MySQL / MariaDB | Yes | Connected threads, running threads, uptime, total queries, slow queries, max connections, bytes received, bytes sent | Yes | Yes | Yes | +| SQL Server | Yes | User sessions, uptime, database size | Yes | - | Yes | +| ClickHouse | Yes | Active queries, active merges, part mutations, disk usage | Yes | - | Yes | +| DuckDB | - | Database size, block size, total blocks, memory limit, threads | - | - | - | +| SQLite | - | Database size, page count, page size, journal mode, cache size | - | - | - | -For databases without a dashboard provider (Redis, MongoDB, and others), the menu item and toolbar button are disabled. +Every other engine has no dashboard provider, so the menu item and the toolbar button are dimmed. diff --git a/docs/features/sql-editor.mdx b/docs/features/sql-editor.mdx index 7840f1eba..a4ce53c7f 100644 --- a/docs/features/sql-editor.mdx +++ b/docs/features/sql-editor.mdx @@ -3,149 +3,101 @@ title: SQL Editor description: Write and run SQL with syntax highlighting, multi-statement execution, find and replace, and a built-in formatter --- -Put the cursor in a statement and press `Cmd+Enter` to run just that one. Semicolons separate statements, so a single tab can hold a whole script. Highlighting comes from tree-sitter. +`Cmd+Enter` sends one statement: the one the cursor is in. Semicolons separate statements, so a single tab holds a whole script and you run it a piece at a time. SQL Editor SQL Editor -## Inline diagnostics - -The editor underlines a structural mistake in red while you type, and clears it as soon as you fix it. It reports only what more typing cannot fix: - -| Reported | Not reported | -|----------|--------------| -| A closing bracket with no opener | A bracket you have not closed yet | -| A bracket that closes the wrong kind | A string you are still typing | -| An unterminated block comment | An incomplete statement | - -A half-written statement is never flagged, which is the usual complaint about editors that check as you type. Brackets inside a string literal or a comment are ignored. - -On MongoDB connections the query parser runs too, so an unknown collection method or a query that does not start with `db.` is underlined with the reason. The method name itself is underlined where the parser names one. - -Checking runs 500ms after you stop typing, and is skipped on very large documents. - -## Writing queries - -Separate statements with semicolons. Place the cursor in any statement and press `Cmd+Enter` to run just that one. - -```sql -SELECT * FROM users LIMIT 10; -SELECT COUNT(*) FROM orders; -``` - -To run the whole tab, choose **Execute All Statements** from the Execute button's menu, press `Cmd+Shift+Enter`, or use **Query > Execute All Statements**. Nothing needs to be selected first. - -Select text and press `Cmd+Enter` to run only the selection. Multiple statements, whether from a selection or from Execute All Statements, run sequentially: - -- On databases with transaction support, the batch runs in a transaction: if a statement fails, execution stops and all changes roll back. Databases without transactions run each statement as-is, with no rollback. -- The error names the failing statement ("Statement 3/5 failed: …"). -- The last `SELECT` result appears in the data grid. -- Each statement is recorded individually in query history. - -The editor supports multiple cursors for editing several places at once. +## Run your first query -Autocomplete suggests tables, columns, and keywords as you type, and resolves aliases. See [Autocomplete](/features/autocomplete). + + + Press `Cmd+T`, or choose **File > New Tab**. + + + The toolbar picker binds this tab to one database. Changing it later affects this tab only. + + + Suggestions arrive as you type: tables after FROM and JOIN, a table's columns after its alias and a dot. See [Autocomplete](/features/autocomplete). + + + The statement under the cursor runs. Rows land in the grid below, under a tab named after the table they came from. See [Query Results](/features/query-results). + + -Use `:name` placeholders instead of hardcoding values; they bind via prepared statements. See [Query Parameters](/features/query-parameters). +Instead of hardcoding a value, write `:name` and fill it in when the query runs. See [Query Parameters](/features/query-parameters). -## Per-tab database picker - -The editor toolbar shows a database picker (or schema picker, depending on the engine). Each SQL tab binds to its own database: - -- Changing the picker affects only that tab. Switching the active database elsewhere keeps existing tabs on their bound database. -- The picker lists the databases visible in the sidebar, so it follows the sidebar database filter. -- Databases whose driver reconnects the session to switch, PostgreSQL, Redshift, and CockroachDB, show a lock instead of a menu: there is no in-place way to change database from this picker. The tab still keeps the database it was opened with; on these engines a tab bound to a database other than the connection's active one runs on a separate connection for that database. See [Cross-Database Tabs](/databases/postgresql#cross-database-tabs). - -## Find and replace - -Press `Cmd+F` in a query tab to open the find panel. Switch the panel to Replace mode to replace the current match or all matches. +`Cmd+O` opens a `.sql`, `.psql` or `.pgsql` file as a query tab that `Cmd+S` writes back to. See [SQL Files](/features/sql-files). -| Action | Shortcut | -|--------|----------| -| Find | `Cmd+F` | -| Find next | `Cmd+G` | -| Find previous | `Cmd+Shift+G` | +## Running several statements -Match modes: contains, matches word, starts with, ends with, or regular expression, plus match case and wrap around toggles. +To run the whole tab, press `Cmd+Shift+Enter`, choose **Execute All Statements** from the Execute button's menu, or use **Query > Execute All Statements**. Nothing needs to be selected first. Select text and `Cmd+Enter` runs the selection instead. -## Query execution +A batch runs top to bottom: -| Action | Shortcut | Description | -|--------|----------|-------------| -| Execute query | `Cmd+Enter` | Runs query at cursor, or all selected statements | -| Execute all statements | `Cmd+Shift+Enter` | Runs every statement in the tab, top to bottom | -| Execute without limit | `Cmd+Option+Enter` | Runs the query at cursor without the row cap | -| Cancel query | `Cmd+.` | Stops the running query | -| Explain query | `Cmd+Option+E` | Shows the execution plan for the query at cursor as a tree or diagram (see [Explain Visualization](/features/explain-visualization)) | -| Format query | `Cmd+Shift+L` | Formats the current query | +- On engines with transactions, the batch runs inside one. A failure stops it and rolls back everything before it. Engines without transactions run each statement as-is, with nothing to roll back. +- The error names its place in the run: "Statement 3/5 failed: …". +- Each statement gets its own result tab, and each is recorded separately in [query history](/features/query-history). -### Statement markers +## Statement markers -A tab can hold many statements, and the editor marks which one it is about to run. +The editor marks which statement it is about to send. -- The statement your cursor is in gets a faint background, so you can see what `Cmd+Enter` will send before you press it. It is a decoration, not a selection: your next keystroke does not replace it. -- Move the pointer over the gutter and a run button appears beside every statement. Clicking one runs that statement, even when your cursor is somewhere else. The column the buttons sit in is reserved at all times, so revealing them never shifts the text. -- A run button takes the same [Safe Mode](/features/safe-mode) checks and [query parameter](/features/query-parameters) prompts as any other run, and goes dim while a query is running. +- The statement holding the cursor gets a faint band behind it. It is a decoration, not a selection, so your next keystroke does not replace it. +- Move the pointer over the gutter and a run button appears beside every statement. Click one to run that statement while the cursor is elsewhere. Their column is reserved at all times, so revealing them never shifts the text. +- A run button takes the same [safe mode](/features/safe-mode) checks and [parameter](/features/query-parameters) prompts as any other run, and dims while a query is running. - VoiceOver reads each button as a button naming the line its statement starts on. - A `BEGIN … END` body counts as one statement, so a trigger or stored procedure gets one button rather than one per line inside it. `BEGIN;` and `BEGIN TRANSACTION;` are statements of their own. -- Turn either off in **Settings > Editor** with **Highlight current statement** and **Run button beside each statement**. - -### Moving between statements -- `Ctrl+Cmd+Left` and `Ctrl+Cmd+Right` move the cursor to the previous and next statement. From part-way through a statement, the first press back lands on that statement's own start. Neither wraps: at the last statement, next does nothing. -- `Ctrl+Cmd+Enter` runs the statement the cursor is in and then moves to the next one, so you can work down a script without reaching for the mouse. The cursor moves first, so you can see what is queued while the current one runs. -- `Option+Shift+Up` and `Option+Shift+Down` extend the selection by a statement. macOS assigns those keys to paragraph selection, and a statement is the paragraph of a SQL file. -- A statement hidden inside a collapsed fold is unfolded when the cursor lands on it. -- All three commands are in the Query menu and rebindable in **Settings > Keyboard**. +Turn either marker off in **Settings > Editor** with **Highlight current statement** and **Run button beside each statement**. Statement run controls in the editor gutter Statement run controls in the editor gutter -### Automatic row limit +### Moving between statements + +`Ctrl+Cmd+Left` and `Ctrl+Cmd+Right` step to the previous and next statement. From part-way through a statement, the first press back lands on that statement's own start. Neither wraps. + +`Ctrl+Cmd+Enter` runs the statement the cursor is in and then moves to the next one, so a script can be worked through without reaching for the pointer. The cursor moves first, so the next statement is visible while the current one runs. + +`Option+Shift+Up` and `Option+Shift+Down` extend the selection by a statement, the way macOS extends a selection by a paragraph. + +A statement hidden inside a [collapsed fold](/features/code-folding) unfolds when the cursor lands on it. All three commands are in the Query menu and rebindable in **Settings > Keyboard**. + +## Inline diagnostics + +A structural mistake is underlined in red 500ms after you stop typing, and the underline clears as soon as you fix it. Only two things are reported, both of them problems more typing cannot fix: -SELECT and WITH queries without their own `LIMIT` or `FETCH FIRST` run with the configured row cap: +| Underlined | Message | +|---|---| +| A closing bracket with no opener, or one closing the wrong kind | No matching opening bracket | +| A `/*` with no `*/` after it | Unterminated comment | -- The default cap is 10,000 rows. Pick 100 to 500,000 in **Settings > Data** (**Row cap**), or turn off **Truncate query results** to disable it. -- Your query is sent exactly as you wrote it. TablePro never changes the SQL, it stops reading once it reaches the cap. -- Your own `LIMIT`, `FETCH FIRST`, or `TOP` always wins: the query is not capped for that run. -- `EXPLAIN`, `SHOW`, writes, and DDL are never limited. +A half-written statement is never flagged. An opener you have not closed yet, a string you are still typing, and brackets inside a string or a comment are all left alone. Documents over 100,000 characters are not checked at all. -When the cap trims a result, the status bar reads **Showing N rows** with a **Fetch All** button. To skip the cap for one run, press `Cmd+Option+Enter` or use the Execute button's menu or the Query menu. +On MongoDB connections the query parser runs as well, so an unknown collection method or a query that does not start with `db.` is underlined with the reason. Where the parser names the method, the method name itself is what gets marked. -### Results +## Per-tab database picker -Results appear in the data grid below the editor with row count and execution time. Large result sets are paginated. +The editor toolbar carries a database picker, or a schema picker depending on the engine. Each tab binds to its own database, and switching the active database elsewhere leaves existing tabs on theirs. The picker lists what the sidebar lists, so it follows the sidebar's database filter. -- **Result tabs**: every result gets a tab above the grid; multi-statement runs produce one tab per statement. Tabs show in JSON view too. Switch with `Cmd+Option+[` / `Cmd+Option+]`, close with `Cmd+Shift+W`. Hover a tab to see its query. -- **Tab names**: a result is named after the table it came from. When there is no single table, it is named after the statement instead, and a comment written above the statement is used as the name in preference to the SQL: +PostgreSQL, Redshift and CockroachDB reconnect the session to change database, so their picker shows a lock instead of a menu. The tab keeps the database it opened with, and a tab bound to anything other than the connection's active database runs on a separate connection. See [Cross-Database Tabs](/databases/postgresql#cross-database-tabs). - ```sql - -- monthly totals - SELECT count(*) FROM orders; - ``` +## Find and replace - names its result tab `monthly totals`. -- **Back to the statement**: picking a result moves the editor cursor to the statement that produced it, and unfolds it if you had it collapsed. Picking the result already showing does nothing, so you can click between pinned results to compare them without the editor moving under you. A statement you have since edited away leaves the cursor where it is. -- **Pinning**: click the pin on the right of a result tab, press `Cmd+Option+P`, or right-click the tab and choose **Pin Result**. The next query lands in a new tab instead of overwriting the pinned one. Pinned results move to the front of the strip and cannot be closed or cleared until unpinned. -- **Panel**: toggle the results panel with `Cmd+Option+R` or the toolbar button; it auto-expands when a query executes. The toolbar trash button clears the query and results together; to keep the query, right-click the results and choose **Clear Results**. -- **Errors**: a red banner above the results shows the database error message with a **Fix with AI** button. -- **Non-SELECT statements** (INSERT, UPDATE, DELETE, DDL) show a success view with affected row count and execution time. +Press `Cmd+F` to open the find panel, `Cmd+G` and `Cmd+Shift+G` to walk the matches. Switch the panel to Replace mode to replace the current match or all of them. Match modes are contains, matches word, starts with, ends with, and regular expression, each with match case and wrap around toggles. -## SQL formatting +The editor also carries multiple cursors, for editing several places at once. -Press `Cmd+Shift+L` to format the current query. You can also click the format button in the editor toolbar (its tooltip reads Format Query) or use **Query > Format Query**; the shortcut is rebindable in **Settings > Keyboard**. The token-based formatter: +## Formatting -- Breaks lines per clause (`SELECT`, `FROM`, `WHERE`, `JOIN`) and indents 2 spaces -- Uppercases keywords -- Preserves comments, string literals, and your cursor position -- Keeps dialect identifier quoting (MySQL backticks, PostgreSQL double quotes) -- Handles JOINs, subqueries, CASE expressions, CTEs (including recursive), window functions, and set operations +Press `Cmd+Shift+L` to format the statement at the cursor. The toolbar's format button and **Query > Format Query** do the same, and the shortcut is rebindable in **Settings > Keyboard**. -Procedural blocks (PL/pgSQL `DO`, stored procedures, T-SQL `BEGIN`/`END`) pass through with minimal changes. +The token-based formatter breaks a line per clause, indents two spaces, uppercases keywords, and keeps your comments, string literals, cursor position and dialect identifier quoting (MySQL backticks, PostgreSQL double quotes). JOINs, subqueries, CASE expressions, recursive CTEs, window functions and set operations are all handled. Procedural blocks pass through with minimal changes: PL/pgSQL `DO`, stored procedures, T-SQL `BEGIN`/`END`. **Before**: ```sql @@ -166,69 +118,14 @@ HAVING COUNT(o.id) > 5 ORDER BY order_count DESC; ``` -## Vim mode - -Enable Vim keybindings in **Settings > Editor > Vim mode**. Normal, Insert, Replace, Visual, Visual Line, and Command-line modes are supported, with standard motions, operators, count prefixes, and text objects. `:w` executes the query and `:q` closes the tab. Jump to a line with `10G` or `10gg` (`:10` is not supported). Control-key bindings other than `Ctrl+R` are not intercepted. Full key reference: [Vim Mode](/features/vim-mode). - -## Code folding - -Move the pointer over the gutter and a chevron appears next to every region that spans more than one line: whole statements, table bodies, CTEs, subqueries, `BEGIN` blocks, and block comments. The chevron points down while the region is showing and right while it is folded, the way a disclosure control works everywhere else in macOS. Resting on a chevron marks how far its region reaches. Click to collapse, click again to expand. - -A folded region keeps its chevron whether or not the pointer is in the gutter, so nothing is ever hidden without a way back to it. Move the pointer away and a gutter with nothing folded is just line numbers. - -Where a statement and the block inside it open on the same line, the chevron folds the larger of the two. - -A collapsed region is replaced by a chip showing the start of what it hides and the number of hidden lines, so a folded `CREATE TABLE` still tells you what is inside it. Click the chip to bring the region back. In the query editor, resting the pointer on a chip peeks at the whole block, opening line included and syntax highlighted, without expanding it. Pointing at a chip marks its chevron in the gutter too: the chip and the chevron are two ways to reach the same fold. - -The chevrons are reachable with VoiceOver. Each one reports as a disclosure control that says whether its block is folded, and pressing it folds or unfolds the block. - -Rest the pointer on the chip to peek at the block without expanding it. The peek shows the whole block, opening line included, with the same syntax highlighting and theme as the editor, and reports how many lines it left out when the block is longer than it can show. Moving away, scrolling, typing, or clicking closes it, and the caret never leaves the editor. - -**Toggle Fold** (`Cmd+Option+Left`), **Fold All** (`Cmd+Option+Shift+Left`) and **Unfold All** (`Cmd+Option+Shift+Right`) are in the Query menu and can be rebound in **Settings > Keyboard**. Fold All collapses top-level statements; fold the regions inside them individually. **Fold** and **Unfold** are also on the editor's right-click menu. - -Collapsed regions are remembered when you close and reopen a tab. If the file changed while the tab was closed, the folds that no longer fit are dropped. - -Strings, comments, and PostgreSQL dollar-quoted bodies are never treated as structure, so a `(` inside a string does not open a fold. Folding is skipped on documents over 2 million characters, the same limit that applies to syntax highlighting. - -Turn folding off in **Settings > Editor**. It also works on the DDL and trigger views, the SQL import preview, the AI review sheet, AI chat code blocks, and the JSON cell viewer. Those views number their lines while folding is on, because a gutter holding nothing but the chevrons reads as code that failed to line up. - -Turning line numbers off in **Settings > Editor** removes the gutter, and the chevrons with it. Toggle Fold, Fold All, Unfold All and the collapsed chips still work. - -## Editor settings - -**Settings > Editor** covers line numbers, current-line highlight, current-statement highlight, word wrap, code folding, the per-statement run button, tab width, auto-uppercase keywords as you type, query parameters, and Vim mode. Editor font family and size are set per theme in **Settings > Appearance**. - -Editor windows remember their size, position, and font zoom between launches. See [Query Tabs](/features/tabs#windows-and-connections). - ## AI assistance -**Explain with AI** (`Cmd+L`) explains the query at cursor, **Optimize with AI** (`Cmd+Option+L`) suggests performance improvements, and **Fix with AI** in the error banner rewrites a failing query. Inline AI suggestions (ghost text) complete your SQL as you type: Tab accepts, Escape dismisses. See [AI Assistant](/features/ai-assistant) for setup. - -## SQL files - -TablePro opens `.sql`, `.psql`, and `.pgsql` files: +**Explain with AI** (`Cmd+L`) explains the query at the cursor and **Optimize with AI** (`Cmd+Option+L`) suggests improvements. When a query fails, **Fix with AI** in the error banner rewrites it. Inline suggestions complete your SQL as ghost text: `Tab` accepts, `Escape` dismisses. See [AI Assistant](/features/ai-assistant) for setup. -- Double-click in Finder (or **Open With > TablePro**) -- **File > Open File…** (`Cmd+O`) -- Drag files onto the dock icon +For the execution plan rather than an opinion, press `Cmd+Option+E` and see [Explain Visualization](/features/explain-visualization). -Files open in a new tab. Without a connection they queue and open on connect. Opening the same file twice focuses the existing tab. - -### Saving - -- `Cmd+S` saves back to the source file. Untitled tabs trigger Save As. -- `Cmd+Shift+S` opens Save As. - -The title bar shows the filename for file-backed tabs, with the standard macOS unsaved-changes dot on the close button. `Cmd+click` the title for the standard path menu, then pick a folder to open it in Finder. Closing a query tab keeps its SQL: reopen with `Cmd+Shift+T` or **File > Reopen Closed Tab**. See [Tabs](/features/tabs). - - -When a tab has both unsaved file changes and pending data grid edits, `Cmd+S` saves the grid changes first. Save the file after the grid save completes. - - -### External modifications - -If a file changes on disk while open (a `git pull`, an edit in another editor), a yellow banner offers **Reload from Disk**, which replaces your tab edits with the new content. Saving over an externally changed file shows a side-by-side diff with three actions: **Keep My Changes**, **Reload from Disk**, or **Cancel**. +## Editor settings -### Linked Folders +**Settings > Editor** holds line numbers, current-line and current-statement highlighting, word wrap, [code folding](/features/code-folding), the per-statement run button, tab width, auto-uppercase keywords, [query parameters](/features/query-parameters), and [vim mode](/features/vim-mode). Editor font family and size are per theme, in **Settings > Appearance**. -To watch a whole folder of `.sql` files, use [Linked SQL Folders](/features/favorites#linked-sql-folders) instead of opening files one by one. +Editor windows remember their size, position and font zoom between launches. See [Query Tabs](/features/tabs#switching-tabs). diff --git a/docs/features/sql-files.mdx b/docs/features/sql-files.mdx new file mode 100644 index 000000000..13cb2820e --- /dev/null +++ b/docs/features/sql-files.mdx @@ -0,0 +1,32 @@ +--- +title: SQL Files +description: Open .sql files as query tabs, save back to them, and resolve edits made outside TablePro +--- + +Three ways in: double-click in Finder, **File > Open File…** (`Cmd+O`), or drag onto the Dock icon. `.sql`, `.psql` and `.pgsql` all work, and what you get is an ordinary query tab with a file behind it. + +## Opening + +Each file opens in a new tab. Opening one that is already open focuses its existing tab instead of making a second, and files opened before any connection exists wait in a queue until you connect. + +The title bar carries the filename, and the close button gets the standard macOS unsaved-changes dot. + +## Saving + +`Cmd+S` saves back to the source file, and does a Save As when the tab has no file yet. `Cmd+Shift+S` always opens Save As. + +`Cmd+click` the title bar for the standard path menu, then pick a folder to open it in Finder. + +When a tab has both unsaved file changes and pending data grid edits, `Cmd+S` saves the grid changes first. Press it again once that finishes to save the file. + +Closing a query tab keeps its SQL either way. Reopen it with `Cmd+Shift+T` or **File > Reopen Closed Tab**. See [Tabs](/features/tabs). + +## When the file changes underneath you + +A `git pull` or an edit in another editor puts a yellow banner across the top of the tab saying the file was modified on disk, with a **Reload** button that replaces your tab's contents with what is now on disk. + +Saving over a file that changed since you opened it opens a diff sheet instead of overwriting. **Your Changes** and **On Disk** sit side by side, and three buttons resolve it: **Keep My Changes**, **Reload from Disk**, or **Cancel**. + +## A folder at a time + +To watch a whole directory of `.sql` files rather than opening them one by one, use [Linked SQL Folders](/features/favorites#linked-sql-folders). diff --git a/docs/features/table-operations.mdx b/docs/features/table-operations.mdx index b0328856f..4391291d2 100644 --- a/docs/features/table-operations.mdx +++ b/docs/features/table-operations.mdx @@ -3,24 +3,22 @@ title: Table Operations description: Drop, truncate, run maintenance, manage views, and switch databases from the sidebar --- -Dropping a table cannot be undone, and the confirmation dialog is the only step between the menu item and the server. Check its **Cascade** box and the drop takes tables linked by foreign keys with it. +Dropping a table cannot be undone, and **Cascade** in the confirmation dialog widens it to every table that depends on this one. The dialog runs nothing on its own, though: the drop is staged on the table's row and reaches the server when you save. -## Create table or view +## Create a table or view -Click the plus button in the bottom-left of the sidebar and choose **New Table** or **New View**. New Table opens the visual structure editor (see [Table Structure](/features/table-structure)). New View opens a SQL editor tab with a CREATE VIEW template for your database type. The button is disabled while [safe mode](/features/safe-mode) blocks writes. +**Database > New Table…** opens the visual structure editor (see [Table Structure](/features/table-structure)). **Database > New View…** opens a query tab holding a `CREATE VIEW` template for the engine. Both are also on the sidebar's right-click menu over empty space, and both are dimmed while [safe mode](/features/safe-mode) blocks writes. ## Drop and truncate -Right-click a table and choose **Delete** to drop it, or **Truncate** to remove all rows while keeping the structure. Hold Cmd and click to select multiple tables; the same options apply to all of them. - -A confirmation dialog shows two checkboxes: +Right-click a table and choose **Delete** to drop it, or **Truncate** to empty it and keep the structure. **Database > Truncate Table** and `Option+Delete` do the same. Cmd-click to select several tables first and one confirmation covers all of them, with the same options applied to each. | Option | Effect | |--------|--------| -| Ignore foreign key checks | Skips foreign key constraint checks. Grayed out on engines without it (PostgreSQL suggests Cascade instead). | -| Cascade | Drops or truncates tables linked by foreign keys. Only enabled on engines that support it, such as PostgreSQL. | +| Ignore foreign key checks | Skips constraint checks. Dimmed where the engine has no equivalent, and PostgreSQL's dialog points at Cascade instead | +| Cascade | On a drop, takes every table that depends on this one. On a truncate, takes every table linked by a foreign key. Dimmed where the engine has no CASCADE | -Click **Drop** or **Truncate** to execute, or **Cancel**. +Confirming with **Drop** or **Truncate** stages the operation. The row picks up a red minus badge for a pending drop, an orange exclamation badge for a pending truncate. **Preview SQL** (`Cmd+Shift+P`) shows the statements, **Save Changes** (`Cmd+S`) runs them alongside any pending cell edits, and choosing the same menu item again takes the table back out of the queue. Drop table dialog @@ -33,79 +31,49 @@ Dropping is irreversible. On MySQL and MariaDB, truncate also resets the auto-in ## Maintenance -Right-click a table > **Maintenance** and pick an operation. A sheet shows the operation's options and a SQL preview before executing. +Right-click a table, choose **Maintenance**, and pick an operation. A sheet shows the operation's options and the exact SQL before it runs. | Database | Operations | |----------|-----------| | PostgreSQL | VACUUM, ANALYZE, REINDEX, CLUSTER | -| MySQL/MariaDB | OPTIMIZE TABLE, ANALYZE TABLE, CHECK TABLE, REPAIR TABLE | +| MySQL / MariaDB | OPTIMIZE TABLE, ANALYZE TABLE, CHECK TABLE, REPAIR TABLE | | SQLite | VACUUM, ANALYZE, REINDEX, Integrity Check | -PostgreSQL VACUUM has FULL (rewrites the table, blocks access), ANALYZE (updates statistics), and VERBOSE (prints progress) toggles. MySQL CHECK TABLE offers QUICK, FAST, MEDIUM, EXTENDED, and CHANGED modes. - - -The Maintenance submenu is hidden in read-only safe mode and on databases with no maintenance operations. - +PostgreSQL VACUUM carries FULL (rewrites the table and blocks access), ANALYZE, and VERBOSE toggles. MySQL CHECK TABLE offers QUICK, FAST, MEDIUM (the default), EXTENDED, and CHANGED. No other engine reports maintenance operations, so the submenu is hidden there, and in read-only safe mode. ## Views -Views appear in the sidebar with an eye icon in your accent color. Materialized views and foreign tables get their own icons. All three are read-only kinds: their context menus hide **Truncate** and **Import**, and the drop item is labeled **Drop View**, **Drop Materialized View**, or **Drop Foreign Table**. - -| Action | Tables | Views | -|--------|--------|-------| -| Show Structure | Yes | Yes | -| Export | Yes | Yes | -| Import | Yes | No | -| Truncate | Yes | No | -| Edit View Definition | No | Yes | -| Delete / Drop View | Yes | Yes | - -- **Create**: right-click in the sidebar > **Create New View…** (or **Database > New View…**). A SQL editor opens with a CREATE VIEW template; edit the name and SELECT, then execute. -- **Edit**: right-click a view > **Edit View Definition**. The current definition opens in a SQL editor tab; modify and execute to update. -- **Drop**: right-click a view > **Drop View**. Only the definition is removed; underlying table data stays intact. +Views carry an eye icon in the sidebar; materialized views and foreign tables get their own. All three are read-only kinds, so their menus drop **Truncate** and **Import**, and the drop item reads **Drop View**, **Drop Materialized View**, or **Drop Foreign Table**. Dropping removes the definition only, and the data in the underlying tables stays. -## Database operations +Right-click empty space and choose **Create New View…** to write a new one; right-click an existing view and choose **Edit View Definition** to open the current definition in a query tab. Either way, execute the statement to apply it. -### Switch databases +## Databases and schemas -Click the database name in the toolbar or press `Cmd+K`. Type to filter, move with the arrow keys, then press Return or double-click to switch. `Cmd+R` refreshes the list. The switcher adapts to the engine's vocabulary: Schema on Oracle, Keyspace on Cassandra, Dataset on BigQuery, Namespace on SurrealDB. SQLite is file-based, so the switcher points you to the Welcome window to open a different file. +`Cmd+K` switches database, covered in [Managing Connections](/connections#switch-connections-and-databases). The switcher takes the engine's own vocabulary: Schema on Oracle, Keyspace on Cassandra, Dataset on BigQuery, Namespace on SurrealDB. SQLite is file-based, so it points at the welcome window to open a different file. Database switcher popover Database switcher popover -### Create Database +**New Database…** at the bottom of the switcher creates one. Its fields come from the driver: MySQL and MariaDB ask for a name, character set, and collation; other engines show their own. The button is absent where the engine cannot create databases. -Click **New Database…** at the bottom of the switcher. The form fields come from the driver: MySQL/MariaDB ask for a name, character set, and collation; PostgreSQL, ClickHouse, MongoDB, and others show their own fields. The button only appears on engines that support creating databases. - -### Drop database - -Right-click a database in the switcher or in the sidebar tree and choose **Drop Database…**. The item is hidden for system databases and for the database you are currently connected to; switch to another one first. A confirmation dialog shows the database name in its title with a red **Drop Database** button. +To drop one, right-click it in the switcher or the sidebar tree and choose **Drop Database “name”…**. The item is missing for system databases and for the database you are connected to, so switch away first. -Dropping a database permanently deletes all its tables and data. The server rejects the drop if your account lacks the required privilege. +Dropping a database permanently deletes every table in it. The server rejects the drop if your account lacks the privilege. -### Work on several databases at once - -Select more than one database in the sidebar tree or in the switcher: Shift-click for a range, Cmd-click to add or remove one. The right-click menu then acts on the whole selection and says how many it covers, for example **Drop 3 Databases…**. - -| Action | What it does | -|--------|--------------| -| Refresh | Reloads the object list for every selected database. | -| Copy Names | Copies the selected names to the clipboard, comma-separated. | -| Export… | Opens the export dialog with every table of the selected database already ticked, reading from that database rather than the one you are browsing. It covers one database at a time, and is left out on DuckDB and PGlite for a database other than the active one, because those cannot open a second connection to it. | -| Drop | Drops all of them after one confirmation that lists every name. | +### Several at once -The menu follows the row you right-click: if it is part of the selection, the action covers the selection; if it is not, the action covers just that row. **Use as Active Database** stays single-target and is disabled while more than one row is selected. +Shift-click for a range, Cmd-click to add or remove one, and the menu counts what it covers: **Drop 3 Databases…**, **Copy 3 Names**. **Refresh** reloads every selected database. **Export…** opens the export dialog with every table of the selected database already selected, one database at a time; it is absent on DuckDB and PGlite for any database other than the active one. **Use as Active Database** appears only when a single row is selected and it is not already active. -Databases the server will not let you drop are left out of the Drop item: system databases and the one you are connected to. If a drop fails partway, TablePro finishes the rest and then reports which ones failed and why. +Databases the server will not drop are left out of the drop item. A drop that fails partway finishes the rest, then reports which ones failed and why. -### Drop schema +### Schemas -On engines that group tables by schema, the same menu appears on a schema row. PostgreSQL, SQL Server and SurrealDB support it. PostgreSQL drops the schema with `CASCADE`, so objects that depend on it go too. SQL Server has no cascade, so it refuses to drop a schema that still holds objects; drop those first. +On engines that group tables by schema, the same menu appears on a schema row: PostgreSQL, SQL Server, SurrealDB, and Dameng. PostgreSQL and Dameng drop the schema with `CASCADE`. SQL Server has no cascade, so it refuses to drop a schema that still holds objects; drop those first. -Dropping a schema permanently deletes everything in it. On PostgreSQL it also drops objects in other schemas that depend on it. +Dropping a schema permanently deletes everything in it. On PostgreSQL and Dameng it also drops objects in other schemas that depend on it. diff --git a/docs/features/table-structure.mdx b/docs/features/table-structure.mdx index 255be2880..fc4776535 100644 --- a/docs/features/table-structure.mdx +++ b/docs/features/table-structure.mdx @@ -3,6 +3,8 @@ title: Table Structure description: Browse and edit columns, indexes, foreign keys, triggers, and DDL with a visual structure editor --- +import StagedUntilSave from "/snippets/staged-until-save.mdx"; + This is a DDL editor with a grid in front of it. Rename a column, add an index, change a key, and what you get is a pending `ALTER TABLE` you can read before it runs; the table on the server is untouched until you apply it. @@ -10,34 +12,26 @@ This is a DDL editor with a grid in front of it. Rename a column, add an index, Table Structure -Open a table, then pick **Structure** in the Data / Structure / JSON control at the bottom of the tab. Or right-click a table in the sidebar and choose **Show Structure**. +Open a table and switch the result view to **Structure**, or right-click it in the sidebar and choose **Show Structure**. -Tabs: **Columns**, **Indexes**, **Foreign Keys**, **Triggers**, **DDL**, and **Parts** (ClickHouse only). The Columns, Indexes, Foreign Keys, and Triggers labels show item counts. Tabs the database cannot support are hidden, such as Foreign Keys on ClickHouse or Triggers on Redshift. A filter field above the grid narrows rows by name; click a column header to sort. +The tabs are **Columns**, **Indexes**, **Foreign Keys**, **Triggers**, **DDL**, and **Parts** (ClickHouse only); the first four carry item counts. A tab the engine has no concept of is hidden: ClickHouse has no Foreign Keys, Redshift no Triggers. Every grid has a filter field, and clicking a header sorts. ## Columns tab -Columns are edited inline in the grid: +Columns are edited in place. **Nullable**, **Primary Key**, and **Auto Inc** are YES/NO dropdowns; **Primary Key** set to YES forces **Nullable** to NO and holds it there until the key comes back off. **Type** opens a picker of the engine's types by category: search to filter, or type a parametric value such as `VARCHAR(255)` and press Return to use it as written. -- **Nullable**, **Primary Key**, and **Auto Inc** are YES/NO dropdowns. -- Setting **Primary Key** to YES sets **Nullable** to NO. A primary key column cannot hold NULL, so Nullable stays NO until you turn the primary key off. -- **Type** opens a picker with the database's types grouped by category. Search to filter, or type a parametric value such as `VARCHAR(255)` or `DECIMAL(10,2)` and press Return to use it as written. -- **Charset** and **Collation** columns appear for MySQL and MariaDB. -- **On Update** appears for MySQL and MariaDB, next to **Default**. Set it to YES on a `TIMESTAMP` or `DATETIME` column to add `ON UPDATE CURRENT_TIMESTAMP`, so the column re-stamps itself on every write. TablePro matches the fractional-second precision to the column's own type, so a `TIMESTAMP(6)` column gets `ON UPDATE CURRENT_TIMESTAMP(6)`. +Which remaining columns appear is the driver's choice. **Comment** is there on most engines and writes the column comment. MySQL and MariaDB add **Charset**, **Collation**, and **On Update**; **On Update** set to YES on a `TIMESTAMP` or `DATETIME` column adds `ON UPDATE CURRENT_TIMESTAMP` at that column's own precision, so `TIMESTAMP(6)` gets `ON UPDATE CURRENT_TIMESTAMP(6)`. Type picker popover Type picker popover -Add a column with the **+** button at the right of the status bar or `Cmd+Shift+N`. Select rows and click **-** or press `Delete` to mark them for removal. - -### Primary keys +Add a column with **+** at the right of the status bar or `Cmd+Shift+N`. Select rows and click **-** or press `Delete` to mark them for removal. -Toggle the **Primary Key** flag on one or more columns; multiple flagged columns produce a composite key in a single `PRIMARY KEY (col1, col2)` clause. Changing the key on an existing table runs a drop-and-add sequence on MySQL, MariaDB, PostgreSQL, PGlite, SQL Server, DuckDB, and Snowflake. Other engines, SQLite, ClickHouse, Cassandra, ScyllaDB, and Oracle among them, cannot alter a primary key in place: the dropdown stays editable but the change generates no SQL. +Flag **Primary Key** on one column, or several for a composite key in one `PRIMARY KEY (col1, col2)` clause. On an existing table that becomes a drop of the old constraint followed by an add. -### Reordering columns - -Drag a column row to a new position (MySQL and MariaDB only). The reorder executes immediately as `ALTER TABLE … MODIFY COLUMN … AFTER` and is recorded in query history. Dragging is disabled while unsaved structure changes exist. +Drag a column row to reorder it (MySQL and MariaDB only). That one runs immediately as `ALTER TABLE … MODIFY COLUMN … AFTER` and goes to query history rather than the queue. Dragging is off while unsaved changes exist. ## Indexes tab @@ -57,75 +51,63 @@ Drag a column row to a new position (MySQL and MariaDB only). The reorder execut | **Ref Schema** | Referenced schema, for cross-schema references | | **On Delete / On Update** | Dropdowns: NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT | -Right-click a foreign key and choose **Open [table]** to jump to the referenced table. +Right-click a foreign key and choose **Open [table]** to jump to the referenced table. Right-click any row in these three grids for **Copy Name**, **Copy Definition**, **Copy As** (CSV, JSON, SQL), **Duplicate** (`Cmd+D`), and **Delete**. A row already marked for deletion offers **Undo Delete**. ## Saving changes -Structure edits queue locally; nothing runs until you save. See [Change Tracking](/features/change-tracking) for how the queue, undo (`Cmd+Z`), and redo (`Cmd+Shift+Z`) work. Saving runs on the tab's own connection, database, and schema, the ones it was opened on. It never moves the sidebar's selected database or the toolbar. - -- **Save Changes** (`Cmd+S` or the toolbar checkmark) applies the queued changes. Changes that can lose data (dropping a column, changing a type, adding NOT NULL, changing the primary key) first show a confirmation that lists each risky change. -- **Preview SQL** (`Cmd+Shift+P`) shows the generated DDL statements without executing them. +; see [Change Tracking](/features/change-tracking) for the queue, undo (`Cmd+Z`) and redo (`Cmd+Shift+Z`). A save runs on the tab's own connection, database, and schema, the ones it was opened on, and never moves the sidebar or the toolbar. -Queued changes stay queued while you look at something else. Switching to another tab, or switching the same tab between Data and Structure, leaves them alone, and so does coming back. Each tab also keeps its own place in the editor: the sub-tab you were on, the filter you typed and the sort you set, so opening one table in two tabs gives you two independent editors rather than one shared between them. +- **Save Changes** (`Cmd+S` or the toolbar checkmark) applies the queue. Changes that can lose data, dropping a column, changing a type, adding NOT NULL, changing the primary key, first show a confirmation listing each one. +- **Preview SQL** (`Cmd+Shift+P`) shows the generated statements without executing them. -They are only discarded when you discard them: closing the tab, closing the window or quitting asks first, and **Refresh** asks before replacing them with what is on the server. Answering **Save** on that prompt applies the queued changes wherever the tab happens to be, including from the Data view, from the read-only DDL, Parts and Triggers tabs, and from a background tab caught by **Close All Tabs** or **Close Other Tabs**. If the save cannot run, because Safe Mode is read-only, because you cancelled the data-loss confirmation, or because the server rejected the statement, the tab stays open with the changes still queued. - -The same holds for a table you are still defining in a Create Table tab. The name, the options and every column you have added survive switching away and back. +The queue outlives everything short of an explicit discard: closing the tab, closing the window, quitting, and **Refresh** all ask first. A save that never reaches the server leaves the tab open with its queue intact. Schema change preview with ALTER TABLE statements Schema change preview with ALTER TABLE statements -### Row context menu +### When one statement fails -Right-click rows in the Columns, Indexes, or Foreign Keys tabs for **Copy Name**, **Copy Definition**, **Copy As** (CSV, JSON, SQL), **Duplicate** (`Cmd+D`), and **Delete**. Rows already marked for deletion offer **Undo Delete** instead. Multi-select to act on several rows at once. +A save is often several statements, run in order. Engines with transactional DDL roll the whole set back; MySQL, MariaDB, and Oracle commit each one as it runs, so everything before the failure has landed while the queue still holds all of it. An **Error Applying Changes** sheet reports what the server said: refresh before saving again, or the second save replays work the server already did. ## Triggers tab -Lists the table's triggers with **Name**, **Timing** (BEFORE, AFTER, INSTEAD OF), **Event** (INSERT, UPDATE, DELETE), and **Enabled** where the database reports it. Select a trigger to see its full `CREATE TRIGGER` statement, fetched from the engine's catalog, in a syntax-highlighted viewer with **Copy** and **Open in Editor**. - -**New Trigger**, **Edit**, and **Delete** in the tab's toolbar manage triggers. The editor opens the trigger's actual DDL so the whole definition round-trips; for PostgreSQL it includes the trigger function so you can edit the logic. Saving runs the right statements for the engine, wrapped in a transaction where the engine supports transactional DDL. Deleting asks for confirmation. +Lists **Name**, **Timing** (BEFORE, AFTER, INSTEAD OF), **Event** (INSERT, UPDATE, DELETE), and **Enabled** where the engine reports it. Select one to read its `CREATE TRIGGER` statement from the catalog, with **Copy** and **Open in Editor**. **New Trigger**, **Edit**, and **Delete** sit in the action bar; the editor opens the trigger's real DDL, including the trigger function on PostgreSQL. -Triggers are available for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, libSQL, and Cloudflare D1; the tab is hidden elsewhere. Oracle does not return the trigger body, so editing starts from the trigger header. +Triggers are available for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, libSQL, and Cloudflare D1; the tab is hidden elsewhere. Oracle does not return the trigger body, so the viewer and the editor start from the trigger header alone. ## DDL tab -Read-only `CREATE TABLE` statement with syntax highlighting and font size controls. Toolbar buttons: **Copy**, **Export** as a `.sql` file, and **Open in Editor** to send the DDL to a new query tab. +Read-only `CREATE TABLE` with syntax highlighting and font size controls. **Copy**, **Export** as a `.sql` file, and **Open in Editor** send it onward. On PostgreSQL the `CREATE SEQUENCE` and `CREATE TYPE … AS ENUM` statements the table depends on are prepended, so the script runs on an empty database. ## Parts tab (ClickHouse) -Lists the table's partitions and parts from `system.parts`. Toolbar actions: **Optimize** (merge parts), **Drop Partition**, and **Detach Partition** for the selected partition. +Lists partitions and parts from `system.parts`. **Optimize** merges parts; **Drop Partition** and **Detach Partition** act on the selected partition. Detached data stays on disk and is unreadable until it is re-attached. ## Creating a new table -Choose **Database > New Table…**, or right-click the empty space in the sidebar and choose **New Table…**. The editor opens as a tab with: - -- **Table Name** field, plus Engine, Charset, and Collation pickers for MySQL and MariaDB -- **Columns**, **Indexes**, and **Foreign Keys** tabs using the same grid editing as the structure view -- **SQL Preview** tab with the live-generated `CREATE TABLE` DDL - -Click **Create Table** (`Cmd+Return` or `Cmd+S`) to execute. TablePro opens the new table and refreshes the sidebar. +Choose **Database > New Table…**, or right-click empty space in the sidebar and choose **New Table…**. The tab holds a **Table Name** field (with Engine, Charset, and Collation pickers on MySQL and MariaDB), the same three editing grids, and a **SQL Preview** tab carrying the live `CREATE TABLE`. **Create Table** (`Cmd+Enter` or `Cmd+S`) executes it and opens the new table. -Visual table creation is supported for MySQL, MariaDB, PostgreSQL, PGlite, SQLite, SQL Server, ClickHouse, DuckDB, Oracle, Snowflake, libSQL, Cloudflare D1, Trino, and Teradata. +Visual table creation is supported for MySQL, MariaDB, PostgreSQL, PGlite, SQLite, SQL Server, ClickHouse, DuckDB, Oracle, Snowflake, libSQL, Cloudflare D1, Trino, Teradata, and Dameng. -## Limitations - -TablePro disables operations the database cannot perform: +## MongoDB collections -- **SQLite**: columns can be renamed but not otherwise modified. -- **Cassandra / ScyllaDB**: add and drop column only; no index editing. -- **Redshift, CockroachDB, Cloudflare D1, Beancount**: structure is read-only. -- **Redis, etcd, DynamoDB**: no table schema to edit. +MongoDB structure is read-only, and inferred from the collection's first 200 documents: top-level field names are unioned across the sample, and each field takes its most common BSON type. `_id` comes first, marked as the primary key. The DDL tab shows indexes as `createIndex()` commands for `mongosh`, plus the validator and capped-collection options where present. -## MongoDB collections +## Limitations -MongoDB structure is read-only. TablePro infers the schema from the first 50 documents of the collection: it unions the top-level field names across the sample and picks each field's most common BSON type. `_id` is listed first and marked as the primary key. The DDL tab shows indexes as `createIndex()` commands you can run in `mongosh`, plus the collection's validator and capped-collection options when present. Edit documents in the data grid instead. +- **Changing a primary key on an existing table** works on MySQL, MariaDB, PostgreSQL, PGlite, SQL Server, DuckDB, Snowflake, and Dameng. Elsewhere the dropdown accepts the edit and the save produces nothing for it: rebuild the table by hand. +- **Check constraints and generated columns** have no field in the grid. Both show in the DDL tab; change them by running DDL yourself. +- **SQLite**: a column can be added, dropped, and renamed. No other column change generates SQL. +- **Cassandra / ScyllaDB**: add and drop column only, no index editing, no visual table creation. +- **Redshift, CockroachDB, BigQuery, Elasticsearch, SurrealDB, Beancount**: structure is read-only. +- **Redis, etcd, DynamoDB**: no table schema to edit. ## Refreshing -Use **Database > Refresh** (`Cmd+R`) or the toolbar refresh button to reload structure from the database. Changes made through TablePro refresh automatically. Refresh always reads from the tab's own database and schema, not the sidebar's current selection. +**Database > Refresh** (`Cmd+R`) or the toolbar refresh button reloads structure from the server; changes made through TablePro refresh on their own. A refresh reads the tab's own database and schema, not the sidebar's current selection. diff --git a/docs/features/tabs.mdx b/docs/features/tabs.mdx index 90cea5ebf..b77edf408 100644 --- a/docs/features/tabs.mdx +++ b/docs/features/tabs.mdx @@ -3,11 +3,11 @@ title: Query Tabs description: Each tab keeps its own SQL, results, sorting, and filters, and comes back after a restart --- -Every open connection lives in one window, and each connection has its own set of tabs. A tab strip appears in the window chrome, below the toolbar and lined up with the content pane, as soon as a connection holds more than one tab, and lists only that connection's tabs. With a single tab there is no strip, so a window that behaves the way it always did gains no chrome. +Open a table while the sidebar is on `app` and that tab keeps querying `app`, and its schema, for the rest of its life. Changing the database in the sidebar moves what the sidebar lists and where the next tab opens; it retargets nothing already open. -Each tab keeps its own SQL, results, sorting, and filter state. Tabs persist across app restarts. +The tab strip sits below the toolbar and lists only the current connection's tabs. One tab, no strip. -A tab is named after the object it shows. When two tabs would carry the same name from different databases, both names gain the database: two tabs called `orders` become `app.orders` and `staging.orders`. A name no other tab uses stays short. Hovering a tab names its database either way, and VoiceOver reads the same. +Tabs are named after the object they show. When two would draw the same name, both gain their database: two tables called `orders` become `app.orders` and `staging.orders`. A name nothing else uses stays short, and hovering names the database either way. Tab strip with query and table tabs @@ -21,148 +21,99 @@ A tab is named after the object it shows. When two tabs would carry the same nam | **Query tab** | Write and run SQL | | **Table tab** | Browse table data with pagination, sorting, and [filters](/features/filtering) | -Table tabs track cell edits as pending changes by default. Query tabs support [change tracking](/features/change-tracking) when the query is a SELECT from one table, with or without `WHERE`, `ORDER BY`, or `LIMIT`. Joins, table aliases, and schema-qualified names like `public.users` come back read-only. +Both stage cell edits as pending changes. A query tab does it only when its query is a SELECT from one table; joins, aliases, and schema-qualified names like `public.users` come back read-only. See [Change Tracking](/features/change-tracking). -## Preview tabs - -Clicking a table opens it right away, in a preview tab that is reused when you click a different table, like VS Code's preview tabs. A tab with unsaved edits, an applied filter, or sorting is never replaced: the click opens a new tab instead. A preview tab still open at quit is restored as a permanent tab. +## Keys -Double-click a table, or select it and press `Return`, to keep its tab. The tab stops being a preview and the next table you click opens in its own tab, so double-clicking your way down a list gives you one tab each. Double-clicking a table that is already open switches to its tab rather than opening a second one. A preview tab also becomes permanent on its own once you interact with it: sort it, filter it, or edit data. +| Action | Shortcut | +|--------|----------| +| New query tab | `Cmd+T` | +| Close the current tab | `Cmd+W` | +| Reopen the last closed tab | `Cmd+Shift+T` | +| Go to a tab by position | `Cmd+1` through `Cmd+9` | +| Previous tab | `Cmd+Shift+[` | +| Next tab | `Cmd+Shift+]` | -Opening a table from Favorites always gives it a permanent tab, since selecting a favorite does not open it and the double-click, `Return`, or **Open Table** that does is deliberate. +**Close Other Tabs**, **Close All Tabs**, and **Close Tabs for Other Databases** ship unbound. Bind them in **Settings > Keyboard** under Navigation. -Turn preview tabs off with **Settings > General > Tabs > Enable preview tabs** if you prefer every click to open a permanent tab. +## Preview tabs -## Two tabs on one table +Clicking a table opens it immediately in a preview tab, which the next click reuses. Double-click a table, or select it and press `Return`, to keep its tab: the next table then opens in one of its own, so double-clicking down a list gives you a tab each. Double-clicking a table already open switches to its tab. -**Open in New Tab**, on the table's contextual menu in the object browser, opens another tab for a table you already have open. The two tabs are independent, so you can filter, sort, and page one without touching the other, which is how you compare a table under two sets of conditions side by side. +A preview tab turns permanent as soon as you sort it, filter it, or edit data, and a tab in any of those states is never replaced by a click. One still open at quit comes back permanent, and a table opened from Favorites is permanent from the start. Turn the behavior off in **Settings > General > Tabs > Enable preview tabs**. -Per-table filters are saved per connection, database, schema, and table rather than per tab, so the filter that comes back when you next open the table is whichever of the two you set last. +## Two tabs on one table -## Managing tabs +**Open in New Tab**, on the table's contextual menu in the sidebar, opens a second tab for a table already open, and the two are independent: filter, sort, and page one without touching the other. Per-table filters are stored per table rather than per tab, so the one that comes back next time is whichever of the two you set last. -### Creating and closing +## Closing tabs | Action | How | |--------|-----| -| New query tab | `Cmd+T` or the **+** button at the end of the tab strip | -| New table tab | Click a table in the sidebar (the preview tab is reused), double-click it or press `Return` to keep its tab, or **Open in New Tab** on its contextual menu for a second tab on a table already open | -| Close tab | `Cmd+W`, the tab's close button, or **Close Tab** on the tab's contextual menu | -| Close other tabs | **File > Close Other Tabs**, or **Close Other Tabs** on the tab's contextual menu | -| Close every tab for the connection | **File > Close All Tabs**, or **Close All Tabs** on the tab's contextual menu | -| Close tabs belonging to other databases | **File > Close Tabs for Other Databases** | - -Right-clicking a tab gives you the three close commands for that tab without leaving the strip. +| Close one tab | `Cmd+W`, the tab's close button, or **Close Tab** on its contextual menu | +| Close the rest | **File > Close Other Tabs**, also on the tab's contextual menu | +| Close every tab for this connection | **File > Close All Tabs**, also on the tab's contextual menu | +| Close tabs bound to another database | **File > Close Tabs for Other Databases** | -The three bulk commands have no shortcut out of the box. Bind them in **Settings > Keyboard** under Navigation. +Closing the last tab leaves the connection open on its empty state. `Cmd+W` again closes the connection; [Keyboard Shortcuts](/features/keyboard-shortcuts#tabs-and-windows) has the rest of that cascade. -Closing the last tab leaves the connection open on its empty state rather than closing anything. `Cmd+W` again closes the connection, and closes the window once that was the last connection open. So the shortcut reads the same way it does everywhere else: it closes the smallest thing in front of you first. +**Close Tabs for Other Databases** takes only tabs opened against a database other than the one the connection is browsing now, never the tab in front of you, and never another connection's tabs. On engines that switch schemas instead, such as BigQuery and Oracle, it reads **Close Tabs for Other Schemas**. -**Close Tabs for Other Databases** closes only tabs opened against a database other than the one the connection is on now. It never touches the tab you are looking at, and it is limited to the current connection. Tabs from different databases are meant to coexist, so switching databases never closes anything on its own. On engines that switch schemas instead of databases, such as BigQuery and Oracle, the command reads **Close Tabs for Other Schemas**. +Closing asks **Do you want to save changes?** when the tab holds unsaved cell edits, a `.sql` file that differs from disk, or staged structure, Create Table, or user and role changes. Save closes the tab once the save lands, except for user and role changes, where it opens the review sheet instead and leaves the tab open. A bulk close asks once, and Cancel stops the rest. A tab you are not looking at is selected first, putting the work in front of you before you answer. -Closing a query tab keeps its SQL, so an accidental close costs nothing, including when you close a whole group at once. That is why a tab holding only typed query text closes without asking: the text comes back. +A dot on the tab marks unsaved text or unsaved edits. It is deliberately wider than the prompt: a scratch query tab shows the dot and still closes without asking. -TablePro asks before closing when unsaved work would be lost, and the prompt offers Save, Cancel and Don't Save. It covers unsaved cell edits in a table tab, a `.sql` file that differs from what is on disk, staged structure and Create Table changes, and staged user and role changes. Save closes the tab once the save lands, except for user and role changes, where Save opens the review sheet instead and leaves the tab open until you execute or discard there. A bulk close asks once, and cancelling stops the rest. +## Reopening closed tabs -Closing a tab you are not looking at selects it first, so the prompt is about work you can see before you answer it. +`Cmd+Shift+T`, or **File > Reopen Closed Tab**, brings back the last query or table tab you closed, with its SQL, cursor position, and database context. The last 20 are kept for 30 days, and deleting a connection deletes its closed tabs with it. Server Dashboard, Users & Roles, and structure tabs are not candidates. -A tab whose work would raise that prompt shows an unsaved dot on the tab, so nothing is ever marked clean and then asks to be saved. +Pending cell edits are the one thing that does not come back: they are never written to disk, so **Don't Save** on the close prompt is final. -### Reordering tabs +## Reordering tabs -Drag a tab along the strip to move it. The other tabs slide out of the way as you drag, and the tab lands where you drop it. - -Right-click a tab for **Move Tab Left** and **Move Tab Right**, which move it one place at a time and dim at the ends of the strip. VoiceOver is offered the same two actions on each tab, since dragging is the only way to reorder that needs a pointer. +Drag a tab along the strip to move it; the others slide out of the way. Right-click a tab for **Move Tab Left** and **Move Tab Right**, which move it one place at a time, dim at the ends of the strip, and are offered to VoiceOver as actions on the tab. -Tabs cannot be pinned. Pinning exists for result tabs inside a query tab (`Cmd+Option+P`, see [Keyboard Shortcuts](/features/keyboard-shortcuts)). +Tabs cannot be pinned. Pinning exists for result tabs inside a query tab (`Cmd+Option+P`). -### Reopening closed tabs - -| Action | How | -|--------|-----| -| Reopen the last closed tab | `Cmd+Shift+T` or **File > Reopen Closed Tab** | -| Reopen the last closed tab | **File > Reopen Closed Tab** (`Cmd+Shift+T`) | - -The last 20 closed query and table tabs are kept for 30 days. A reopened tab comes back with its SQL, cursor position, and database context. Deleting a connection also deletes its closed tabs. - -### Switching tabs - -- `Cmd+1` through `Cmd+9` jump to a tab by position -- `Cmd+Shift+[` / `Cmd+Shift+]` for previous and next tab -- **Window > Show Previous Tab** and **Window > Show Next Tab** do the same from the menu - -Each tab keeps its full state when you switch away: SQL, cursor position, results, scroll position, sort and filter state, and pending changes. Selecting a tab that is scrolled out of sight pulls it back into view. - -## Windows and connections +## Switching tabs -One window hosts every connection you have open. Picking a connection in the [connections strip](/features/workspace-rail) switches that window to it and returns you to the tab you last used there, rather than raising a second window. Opening a table or query on a connection you already have open adds a tab to that connection instead of opening another window. +`Cmd+1` through `Cmd+9` jump by position, `Cmd+Shift+[` and `Cmd+Shift+]` step, and **Window > Show Previous Tab** and **Window > Show Next Tab** do the same from the menu bar. A tab scrolled out of sight is pulled back into view. Switching keeps SQL, cursor, results, scroll position, sort, filters, and pending changes. -New windows open at 1200x800; size and position are remembered across launches. +Loaded rows are the exception, and only in bulk. Background table tabs keep their rows up to a budget read from the Mac's memory: two under 8 GB, three at 8 GB, five at 16 GB, eight at 32 GB or more, halved under memory pressure. Past that, the tabs you touched longest ago drop their rows and re-query when you return. Thirty table tabs cost one query on return, not thirty resident result sets. -### Separate windows +## Where a tab points -You can still have more than one window, for putting two connections side by side. TablePro follows your **Prefer tabs when opening documents** setting in System Settings > Desktop & Dock rather than forcing a choice. When that setting is **Always**, new windows join a tab group and the standard commands apply: +A tab is bound to one connection, database, and schema, and every query, refresh, filter, sort, structure read, and structure save it runs uses that binding. Switching tabs never changes the connection's saved default database. To point an existing tab somewhere else, use the database picker in the query editor's toolbar: it rebinds that one tab and reruns it. -| Action | How | -|--------|-----| -| Move the current window out of its tab group | **Window > Move Tab to New Window** | -| Gather every window into one tab group | **Window > Merge All Windows** | - -Both dim when the window is not part of a tab group. These are macOS window tabs, which is a different thing from the editor tab strip below the toolbar: window tabs hold whole windows, the strip holds one connection's editors. - -### Disconnecting - -**Database > Disconnect** ends the session without closing the window. The window shows a Reconnect screen in place of its tabs, and the tabs are saved before the session ends. Use **Database > Reconnect**, or the Reconnect button on the screen itself. - -Reconnecting puts the tabs back as they were. - -Disconnecting asks first only when there are unsaved changes or a query still running. It applies to the whole connection, so every window and entry on it shows the same Reconnect screen. Nothing reconnects on its own afterwards: a connection you disconnected is not reopened at the next launch, and clicking back into it leaves it disconnected until you ask. - -You can also disconnect by right-clicking an entry in the [connections strip](/features/workspace-rail), or a connection in the connection list. - -## Database binding - -A tab is bound to the connection, database, and schema it was opened on, fixed for the life of the tab. Every query, refresh, filter, sort, structure read, and structure save the tab performs uses that binding, not whatever the sidebar shows at the time. - -The sidebar's database selection only controls two things: what the sidebar lists, and which database a new tab opens into. Changing it does not touch tabs that are already open, and switching between tabs does not change the connection's saved default database either. - -To point an existing tab somewhere else, use the database picker in the query editor's toolbar. It rebinds that one tab and reruns it, and leaves the sidebar and every other tab alone. - -The window subtitle shows the database, and the schema on engines that have one, that the frontmost tab is bound to. Tabs for the same connection can be bound to different databases, so the subtitle is how you tell them apart. +The window subtitle names the frontmost tab's database, and its schema on engines that have one. That is how you tell two tabs on one connection apart. -PostgreSQL, Redshift, and CockroachDB can only change database by reconnecting. A tab bound to a database other than the connection's active one runs its queries on a separate connection for that database, so it does not share temp tables, session variables, or an open transaction with the query editor on the main connection. See [PostgreSQL](/databases/postgresql#cross-database-tabs). +PostgreSQL, Redshift, and CockroachDB change database only by reconnecting, so a tab bound to a second database runs on its own connection: no shared temp tables, session variables, or open transaction. See [PostgreSQL](/databases/postgresql#cross-database-tabs). -## Tab persistence +## What survives a restart | Saved | Not saved | |-------|-----------| -| SQL and cursor position | Query results (re-queried on reopen) | +| SQL and cursor position | Query results, re-queried on reopen | | Tab type, table name, database, and schema | Pending data changes | | Applied sort and current page | Selected rows | -| Column widths | | +| Per-table filters, column widths | | | Tab order and which tab was frontmost | | -Per-table filters are stored separately per connection, database, schema, and table, and come back when the table reopens. - -Tab state saves when you switch tabs and every 30 seconds in the background, so a crash or force quit keeps recent work. On relaunch, only the frontmost tab loads its data right away; other restored tabs load the first time you switch to them, so launch stays fast and the database never sees a connection storm. If a restored tab fails to load, the error shows inline in that tab, not as a dialog. The same applies one level up: if the connection itself cannot be reached, the window shows why, with Try Again and Manage Connections, and keeps the connection in the next session's list so a server that was briefly down is not forgotten. - -Whether the last session reopens at launch is controlled by [Startup Behavior](/customization/settings#general). +The tab set is written when you switch tabs, a second after you stop typing, and every 30 seconds otherwise, so a crash costs at most the last half minute. On relaunch only the frontmost tab runs its query; the others load the first time you switch to them, and one that fails shows the error inline. A connection that cannot be reached shows the driver's error with **Try Again**, **Manage Connections…**, and **Copy Details**, and stays in the next session's list. Whether the last session reopens at all is [Startup Behavior](/customization/general-settings#startup). -## Pagination +## Row limits -Table tabs load one page at a time. The default page size is 1,000 rows, set in **Settings > Data > Pagination** (100, 500, 1,000, 5,000, or 10,000 rows). Query tabs are not paginated: **Truncate query results** (on by default) caps results at 10,000 rows, and queries with their own `LIMIT` are sent as written. Adjust the cap in the same settings pane. See [Data Grid](/features/data-grid#pagination-and-limits) for the pagination controls. +Table tabs page; query tabs stop at a row cap. Both limits, and what **Fetch All** does about them, are in [Settings > Data](/customization/data-settings). ## From external clients -Raycast, Cursor, Claude Desktop, and other MCP clients can list and focus tabs: +MCP clients list open tabs with `list_recent_tabs`, bring one forward by id with `focus_query_tab`, and open new ones with `open_table_tab` and `open_connection_window`. See [MCP Tools](/external-api/mcp-tools) for the schemas, and Raycast's [Recent Tabs](/external-api/raycast#commands) command. -- `list_recent_tabs` enumerates open tabs across every connection. -- `focus_query_tab` brings an existing tab to the front by id. -- `open_connection_window` opens a saved connection. -- `open_table_tab` opens a specific table. +## Related -The Raycast extension's [Recent Tabs command](/external-api/raycast#commands) wraps these tools. See [MCP Tools](/external-api/mcp-tools) for input and output schemas. +- [Connections strip](/features/workspace-rail) for switching connection and for disconnecting +- [Keyboard Shortcuts](/features/keyboard-shortcuts) for every default binding diff --git a/docs/features/team.mdx b/docs/features/team.mdx index ccd5031f4..3881d1b16 100644 --- a/docs/features/team.mdx +++ b/docs/features/team.mdx @@ -3,11 +3,11 @@ title: Team Plan description: Share one license across your team, manage seats on the web, and join with an invite code --- -Each activated Mac takes a seat. Seats are handed out and taken back from your account at [tablepro.app/account](https://tablepro.app/account), and the app's only part is redeeming the invite code that arrives by email. +Buy seats, hand them out from your account at [tablepro.app/account](https://tablepro.app/account), and each person redeems the invite code that arrives by email. One seat covers one Mac, and the app's only part is the redeeming. ## Seats -A Team license has a fixed number of seats. Each activated Mac takes one seat. Members have a role: +A Team license has a fixed number of seats, and each activated Mac takes one. Members carry a role, set and shown on the web only: | Role | Meaning | |------|---------| @@ -15,30 +15,27 @@ A Team license has a fixed number of seats. Each activated Mac takes one seat. M | Admin | Invited with the Admin role. | | Member | A regular seat. | -Roles are set and shown on the web. The app does not display them. - ## Manage your team on the web -Sign in to your account at [tablepro.app/account](https://tablepro.app/account) with the email on your license. You get a one-time sign-in link by email, so there is no separate password to remember. - -From the team panel you can: +Sign in at [tablepro.app/account](https://tablepro.app/account) with the email on your license, using the one-time link sent to that address. The team panel shows seats used against seats total, and holds the controls to invite a member by email with a role, cancel an invite nobody has accepted, and remove a member. -- See seats used and seats total. -- Invite a member by email and pick their role (Admin or Member). -- Cancel an invite that has not been accepted yet. -- Remove a member. Removing a member frees their seat. - -An invited member gets an email with an invite code. +Removing a member frees the seat on the server at once. Their copy keeps running on its signed license until its next check, at most 7 days later, and Pro features pause then. See [Licensing](/features/licensing) for the validation schedule and what each tier includes. ## Join a team -You join from the invite email. You do not need a license key of your own. - -1. Install TablePro. Open **Settings > Account** with `Cmd+,`, or click **Activate License** on the welcome window. -2. Paste the **invite code** from your email into the same field where a license key goes. -3. Click **Activate**. +You join from the invite email, with no license key of your own. -TablePro detects that the code is an invite rather than a license key, takes a seat on the team, and activates. From then on your copy validates and renews like any other license. See [Licensing](/features/licensing) for activation, validation, and offline behavior. + + + Install TablePro, then open **Settings > Account** with `Cmd+,`, or click **Activate License** on the welcome window. + + + It goes in the same field as a license key, and TablePro tells the two apart. + + + Click **Activate**. TablePro takes a seat on the team, and from then on your copy validates and renews like any other license. + + License activation sheet accepting a team invite code @@ -46,17 +43,17 @@ TablePro detects that the code is an invite rather than a license key, takes a s -An invite code is single use. It seats the first Mac that redeems it. To add a second Mac, ask your team owner for another invite. To move your seat to a new Mac, deactivate the old one under **Settings > Account** and redeem a fresh invite. +An invite code is single use and seats the first Mac that redeems it. To add a second Mac, ask your team owner for another invite. To move your seat, deactivate the old Mac under **Settings > Account** and redeem a fresh invite. ## Share connections with your team -Two ways, depending on whether your team already shares a folder. +Two routes, and the choice is whether your team already shares a folder. If it does, publish connection definitions into it with [Team Catalog](/features/connection-sharing#team-catalog). If it does not, use Team Library, which keeps the shared set in your account instead. -**Team Library** keeps the shared set in your account, so nothing on disk needs to be shared. In the app, right-click a connection and choose **Share > Publish to Team Library…**, or publish your saved queries from the Favorites sidebar. Passwords are never sent: only the connection definition and the query text. Teammates on the team see shared queries in their Favorites sidebar, and their app pulls the library when its license revalidates. You review and remove shared items from your account at [tablepro.app/account](https://tablepro.app/account). +### Team Library -If your team already shares a Git repo, Dropbox folder, or network drive, publish connection definitions there instead with [Team Catalog](/features/connection-sharing#team-catalog). +Right-click a connection and choose **Share > Publish to Team Library…**, or publish saved queries from the Favorites sidebar with **Publish Saved Queries to Team…**. Only the connection definition and the query text are sent, never passwords. -## What a seat includes +Teammates get the connections in their list the way a Linked Folder delivers them, and shared queries in their Favorites sidebar labeled with whoever published each one. Their app pulls the library every 7 days. A pulled connection arrives without its startup SQL, because it lands with no confirmation step and nothing that would run by itself travels with it. -Every seat carries the Team tier. That is everything in Starter (iCloud Sync, encrypted connection export, Linked Folders) plus the two Team-only features, Team Catalog and Team Library. See [Licensing](/features/licensing) for the full tier table. +You review and remove what you published from your account at [tablepro.app/account](https://tablepro.app/account). diff --git a/docs/features/users-roles.mdx b/docs/features/users-roles.mdx index 7737d33c4..4918c8861 100644 --- a/docs/features/users-roles.mdx +++ b/docs/features/users-roles.mdx @@ -3,90 +3,77 @@ title: "Users & Roles" description: "Manage database users, roles, and privileges without writing GRANT statements by hand" --- -TablePro can manage the users and roles on a server and the privileges they hold. Changes are staged, so nothing reaches the server until you review the SQL and apply it. +Nothing here touches the server on its own. A privilege you select, a password you set, an account you drop: all of it stages until you open the review sheet, read the exact `GRANT` and `REVOKE` statements, and press **Execute**. -Available on **MySQL**, **MariaDB**, **PostgreSQL**, and **PGlite** connections. Open it from **Database > Users & Roles**. The command is disabled on connections whose driver does not support it. +Available on MySQL, MariaDB, PostgreSQL, and PGlite. Open it from **Database > Users & Roles**; the command is dimmed on every other connection. Users and Roles tab with the account list, object tree, and privilege checklist Users and Roles tab with the account list, object tree, and privilege checklist -## The layout +## Create a read-only account -The tab has three panes: + + +Click **+** under the account list. The sheet asks for a kind (User or Role), a name, a host on MySQL and MariaDB, and a password; **Generate** produces a random 20-character one with the lookalike characters left out and reveals it for copying. + + +Select the account, then the database in the Objects pane, and select **Connect** under Administration. On PostgreSQL, select the schema as well and its **Usage** privilege. + + +Expand the schema, select its tables (Shift-click for a range, Cmd-click to add one), and select the **Select** privilege under Data. A privilege set against several objects of one kind is granted on all of them. + + +**Review & Apply…** in the bottom bar lists the statements. **Execute** runs them; **Open in Query Editor** hands them to a query tab instead. + + -- **Users and roles** on the left. Filter, sort, and select an account. **+** and **-** below the list create and drop accounts. -- **Objects** in the middle: the server, its databases, and, as you expand them, schemas, tables, and columns. The **Privileges** column tells you what the selected account has on each object without having to click it. -- **Privileges** on the right: the privileges that can actually be granted on the object you selected, grouped into Data, Structure, Administration, and Dynamic. +## The three panes -Select several objects of the same kind to grant a privilege on all of them at once. +Accounts sit on the left, with a filter field and the **+** / **-** buttons. Objects sit in the middle: the server, its databases, and, as you expand them, schemas, tables, and columns, each row carrying a **Privileges** summary of what the selected account holds there. Privileges sit on the right, listing only what the selected object can be granted, grouped into Data, Structure, Administration, and Dynamic. The last two start collapsed, and the bulk menu offers **Grant All** and **Revoke All**. ## Effective privileges -A checkbox tells you what is granted **directly** on that object. The **Effective** column tells you where access actually comes from: +A selected checkbox says what is granted **directly** on that object. The **Effective** column says where access actually comes from: -- **Inherited from `role`** means the account holds the privilege through a role it is a member of. PostgreSQL roles are followed transitively. If the account has `NOINHERIT`, the privilege only applies after `SET ROLE`, and TablePro says so. -- **Granted on `object`** means a privilege on a parent object already covers this one. This is engine-specific: on MySQL a database-level grant covers the tables inside it, while on PostgreSQL it does not. TablePro asks the driver rather than guessing. +- **Inherited from `role`**: the account holds it through a role it belongs to. PostgreSQL roles are followed transitively, and an account marked `NOINHERIT` only gets the privilege after `SET ROLE`, which the column says. +- **Granted on `object`**: a privilege on a parent object already covers this one. Whether it does is the engine's rule: a MySQL database-level grant covers the tables inside it, a PostgreSQL one does not. ## Granting and revoking -Toggling a checkbox stages a change. The account, the object, and the privilege each show that they have unsaved edits, so a change is findable even in a server with hundreds of tables. - -Changes are always a diff. Unchecking a box you just checked cancels out instead of generating a redundant `REVOKE`, and a privilege that was grantable to others stays grantable. - -To give an account the same privileges as another, choose **Copy Privileges From** in the account's context menu. The copied grants are staged like any other change. +Toggling a checkbox stages a change, marked on the account, the object, and the privilege. Toggling the same box twice cancels the pair out instead of generating a redundant `REVOKE`, and a privilege that was grantable to others stays grantable. **Copy Privileges From** in an account's context menu stages the whole set another account holds. ## Account attributes -The detail pane has a **Privileges / Attributes** switch. The Attributes form edits the account itself: - -- Whether the account can log in. -- Role attributes such as `SUPERUSER` or `CREATEDB` on PostgreSQL. -- Role membership, where the engine has roles: **Member of** lists the account's roles, and **Edit** opens a checklist. -- The connection limit. - -Attribute edits are staged and applied with everything else. - -## Passwords - -**Change Password**, in the account's context menu and on the Attributes form, sets a new password. The sheet has a **Generate** button that produces a random 20-character password and reveals it so you can copy it somewhere safe. +The **Privileges / Attributes** switch opens a form for the account itself: whether it can log in, role attributes such as `SUPERUSER` or `CREATEDB` on PostgreSQL, role membership (**Member of**, with **Edit…** for a checklist), and the connection limit. These stage and apply with everything else. -The password is staged until you apply. Statements that carry a password, such as `CREATE USER` and `ALTER USER`, are never written to [query history](/features/query-history), which is stored unencrypted on disk. +**Change Password**, in the context menu and on the Attributes form, sets a new one. Statements that carry a password, `CREATE USER` and `ALTER USER` among them, are never written to [query history](/features/query-history), which is stored unencrypted on disk. ## Applying changes -The bar at the bottom counts staged changes. **Discard** throws the whole staged set away. **Review & Apply** shows the exact statements, including the ones that remove access. From the review sheet you can **Copy All**, open the SQL in a query editor, or **Execute** it. +The bar at the bottom counts staged changes. **Discard** throws the set away; **Review & Apply…** shows every statement, including the ones that remove access, with **Copy All**, **Open in Query Editor**, and **Execute**. -On PostgreSQL the statements run in a single transaction and roll back together if one fails. MySQL commits user management statements implicitly, so a transaction cannot undo them; if one fails, TablePro shows the error and how many statements had already been applied. +PostgreSQL runs them in one transaction, so a failure rolls the set back. MySQL commits user management statements implicitly, so a failure there leaves the earlier ones applied and reports how many. A successful apply re-reads the server. Changes take the same guards as any other write: a read-only connection blocks them, and [Safe Mode](/features/safe-mode) asks for confirmation or authentication as configured. -After a successful apply, TablePro re-reads the server so the tab reflects what is actually granted. - -Changes run through the same safeguards as any other write: a read-only connection blocks them, and [Safe Mode](/features/safe-mode) asks for confirmation or authentication as configured. - - -If a change would remove access for the account your connection is using, TablePro warns you in the review sheet before the SQL runs. It does not block the change, since revoking your own admin privileges can be deliberate. - + +Dropping the account this connection uses, altering it, or revoking every privilege from it is called out in the review sheet before the SQL runs. It is not blocked: revoking your own admin rights can be deliberate. + ## Dropping an account -Select it and press Delete, or use **-**, or the context menu. The drop is staged and struck through, and Cmd+Z undoes it. Nothing runs until you apply. +Select it and press Delete, click **-**, or use the context menu. The drop is staged and struck through, and `Cmd+Z` undoes it. -On PostgreSQL, a role that owns objects cannot be dropped outright. TablePro detects this and asks whether to reassign the owned objects to another role or drop them, generating `REASSIGN OWNED` or `DROP OWNED` alongside the `DROP ROLE`. +On PostgreSQL a role that owns objects cannot be dropped outright, so a sheet asks whether to reassign the owned objects to another role or drop them, generating `REASSIGN OWNED` or `DROP OWNED` alongside the `DROP ROLE`. -`REASSIGN OWNED` and `DROP OWNED` only affect the database you are connected to, plus shared objects. A role that owns objects in several databases needs the same step run in each. This is PostgreSQL behavior, not a TablePro limitation. +`REASSIGN OWNED` and `DROP OWNED` reach only the database you are connected to, plus shared objects. A role that owns objects in several databases needs the same step run in each. That is PostgreSQL behavior, not a TablePro limitation. -## What PostgreSQL cannot show you - -PostgreSQL stores schema, table, and column privileges per database and cannot read them for a database it is not connected to. Those databases still appear, still show their cluster-wide database-level grants, and are marked as not browsable. They are never shown as though the account had no privileges there. - -MySQL has no such restriction. - ## Limitations -- Editing `WITH GRANT OPTION`. It is displayed, and never silently removed, but it is not editable. -- `ALTER DEFAULT PRIVILEGES` and `GRANT … ON ALL TABLES IN SCHEMA`. -- MySQL 8 roles. MySQL accounts are managed as users. +- `WITH GRANT OPTION` is displayed, and never silently removed, but it cannot be edited. +- `ALTER DEFAULT PRIVILEGES` and `GRANT … ON ALL TABLES IN SCHEMA` are out of scope. +- MySQL 8 roles: MySQL accounts are managed as users. - Renaming an account. +- PostgreSQL keeps schema, table, and column privileges per database and cannot read them for a database it is not connected to. Those databases still appear, still show their cluster-wide grants, and are marked as not browsable rather than shown as empty. MySQL has no such restriction. diff --git a/docs/features/vim-mode.mdx b/docs/features/vim-mode.mdx index 144ffd1f1..8edf59cdc 100644 --- a/docs/features/vim-mode.mdx +++ b/docs/features/vim-mode.mdx @@ -3,7 +3,9 @@ title: Vim Mode description: Vim motions, text objects, registers, marks, and search inside the SQL editor --- -Turn on vim mode in **Settings > Editor > Vim mode**. A badge next to the editor shows the current mode. +Vim mode is scoped to the SQL editor. The data grid, the find field and every other control in the window keep their own keys, and a `Cmd` or `Option` chord passes through in any mode, so `Cmd+Enter` still runs the query. + +Turn it on in **Settings > Editor > Vim mode**, which is also the only way to turn it off: there is no `:set`. A badge next to the editor shows the current mode. Editor > Vim mode**. A badge next to the editor | `G` | Last line | | `5G` | Line 5 | | `H` `M` `L` | Top, middle, bottom of view | -| `gj` `gk` | Same as `j` `k` (logical lines, not wrapped display lines) | +| `gj` `gk` | The same lines as `j` `k`. Wrapped display lines have no motion of their own | | `%` | Matching `()`, `[]`, `{}` | ### Find a character @@ -191,19 +193,19 @@ The pattern is a literal, case-sensitive substring, not a regex. Searches wrap a | `@@` | Play back the last macro | | `3@a` | Play three times | -Recursion is capped at 50 to keep self-calling macros safe. +A macro that calls itself stops after 50 rounds. ## Control keys -`Ctrl+R` in normal mode is the only control combination vim mode intercepts. It redoes. - -Every other control combination passes straight through to the editor, which applies its own binding: +`Ctrl+R` in normal mode is the only control combination vim mode intercepts, and it redoes. Every other one reaches the editor and gets the editor's own binding, so these vim meanings are not available: -- No scrolling from the keyboard. `Ctrl+D`, `Ctrl+U`, `Ctrl+F`, `Ctrl+B`, `Ctrl+E`, and `Ctrl+Y` do not move the viewport. `Ctrl+D` deletes the character after the cursor and `Ctrl+E` jumps to the end of the paragraph. -- No increment or decrement. `Ctrl+A` and `Ctrl+X` leave the number under the cursor alone; `Ctrl+A` jumps to the start of the paragraph. -- No insert-mode editing keys. `Ctrl+W`, `Ctrl+U`, `Ctrl+T`, and `Ctrl+D` do not delete a word, delete to line start, indent, or outdent. `Ctrl+H` still backspaces, through the editor's binding rather than vim's. +| Keys | What you do not get | +|------|---------------------| +| `Ctrl+D` `Ctrl+U` `Ctrl+F` `Ctrl+B` `Ctrl+E` `Ctrl+Y` | Scrolling. `Ctrl+D` deletes the character after the cursor and `Ctrl+E` goes to the end of the paragraph instead | +| `Ctrl+A` `Ctrl+X` | Increment and decrement the number under the cursor. `Ctrl+A` goes to the start of the paragraph instead | +| `Ctrl+W` `Ctrl+U` `Ctrl+T` `Ctrl+D` | Insert-mode delete word, delete to line start, indent, outdent. `Ctrl+H` still backspaces, through the editor's binding rather than vim's | -`zt`, `zz`, and `zb` are swallowed without moving the viewport. +`zt`, `zz` and `zb` are read and discarded, so there is no way to move the viewport from the keyboard at all. ## Visual mode @@ -234,4 +236,4 @@ Indent and outdent work in normal mode only (`>>`, `<<`, `>{m}`, `<{m}`). | `:w` | Run the current query | | `:q` | Close the tab | -`/` and `?` are search. Other `:` commands, including `:wq` and `:x`, are parsed and ignored. +`/` and `?` are search. Every other `:` command is parsed and ignored, `:wq`, `:x` and `:set` among them. `:10` does not jump to line 10 either; use `10G` or `10gg`. diff --git a/docs/features/workspace-rail.mdx b/docs/features/workspace-rail.mdx index a12d7d21c..0bd8294ef 100644 --- a/docs/features/workspace-rail.mdx +++ b/docs/features/workspace-rail.mdx @@ -3,72 +3,66 @@ title: Connections strip description: Switch between every connection and database you have open from a narrow strip on the leading edge of the window --- -A click in the column beside the object browser puts you back where you left off in that connection or database, on the tab you last used there. No tab closes, and each one keeps querying the database it was opened against. - -Each entry is a connection plus the database you are browsing in it. Open two connections and you get two entries. Switch one of them to a second database and you get three. +Click an icon in the strip down the leading edge and you land back where you left off, on the tab you last used there. Each icon is one connection plus the database you are browsing in it, so two connections make two icons and moving one to a second database makes three. Clicking an entry returns you to it on the tab you last used there, and closes nothing. Connections strip Connections strip -The strip appears once you have a second entry open, since a list of one has nothing to switch to. It is on by default; turn it off from **View > Hide Connections**, with `Cmd+Option+0`, or in **Settings > General > Sidebar**. +The strip is on by default and appears once a second entry exists. Hide it with **View > Hide Connections**, `Cmd+Option+0`, or **Settings > General > Sidebar > Show connections**. -## What each entry shows +## What an entry shows -One icon per entry, top to bottom. The icon is the database engine's symbol, tinted with the connection's color, so you can tell staging from production at a glance without hovering. +The icon is the database engine's symbol, tinted with the connection's color, so staging and production are distinguishable without hovering. Under it is the database that entry browses, shortened in the middle when long. Hovering gives the full connection name, host, and database; VoiceOver reads that plus the connection's state. The entry you are in is highlighted, in each window separately. -The icon changes shape, not just color, when a connection is not healthy: a warning triangle when it failed, and a disconnected symbol when it has no live session. Color alone would be invisible to anyone who cannot distinguish red from grey. +On servers that group objects by schema, an entry stands for a schema. On a single-file or single-database engine, such as SQLite, DuckDB, or BigQuery, a connection has one entry, under the connection's name. -Under each icon is the database that entry browses. Long names are shortened in the middle, so both ends stay recognizable. Hover an icon for the full connection name, host, and database. VoiceOver reads the same information plus the connection's state. +## Icon states -The entry you are looking at is highlighted. Every window highlights its own, so the strip always tells you where you are. +Shape carries the state, not just color. -On servers that group objects by schema rather than by database, an entry stands for a schema. On a single-file or single-database engine, like SQLite, DuckDB, or BigQuery, a connection has one entry, labelled with the connection's name. +| Icon | State | What to do | +|------|-------|------------| +| Engine symbol | Connected, or connecting | Nothing | +| Filled warning triangle | The last connect failed | Click the entry: the pane carries the driver's own error, **Try Again**, and **Copy Details** | +| Bolt in a circle | The session ended, tabs kept | Click the entry, then **Reconnect** on the pane, or **Database > Reconnect** | ## When an entry appears -Switching database adds an entry only when the one you are leaving has work in it. An entry has work when it has a table open, a query you have typed, a file you opened, or edits you have not saved. +Switching database adds an entry only when the one you are leaving has work in it: a table open, a query you typed, a file you opened, or edits you have not saved. -| You are on | You switch to `logs` | Result | -|---|---|---| -| `app`, with a table open | | `app` stays, `logs` is added | -| `app`, with only an empty query tab | | `app` becomes `logs` | -| `app`, and `logs` is already listed | | the existing `logs` entry activates | +| You are on `app` and switch to `logs` | Result | +|---|---| +| A table is open in `app` | `app` stays, `logs` is added | +| Only an empty query tab is open | `app` becomes `logs` | +| `logs` is already listed | The existing `logs` entry activates | -An entry goes away when its last tab closes and you are no longer browsing it. Nothing is closed for you, and the strip never removes an entry you still have work in. The set is per session, so relaunching starts from the databases your restored tabs use. +An entry goes away when its last tab closes and you are no longer browsing it; one you still have work in is never removed. The set is per session, so relaunching starts from the databases your restored tabs use. ## Switching -Click an entry to go to it. Moving between two connections switches the window to that connection, returning you to the tab you last used there rather than an arbitrary one. Moving between two databases of one connection stays in the same window, moves what the sidebar lists, and returns you to the tab you last used in that database. An entry with nothing open just moves the sidebar, since there is no work to return to. - -One window hosts every connection you have open. Each connection keeps its own set of tabs, and the tab strip below the toolbar lists only the tabs of the connection you are in. Switching reads as the window changing content, because that is what it is. - -Open tabs are never closed or retargeted by a switch. A tab keeps the database it was opened against and keeps querying it, whichever entry you are in. +Between two connections, the window switches connection and returns you to the tab you last used there; between two databases of one connection, the sidebar moves and the tabs stay. An entry with nothing open moves the sidebar only, and clicking the entry you are in does nothing. No tab is ever closed or retargeted by a switch: each one keeps querying the database it was opened against. -Clicking the entry you are already in does nothing. +The strip takes the keyboard too. Click into it, then arrow keys move the highlight, typing jumps to a name, and `Return` opens the entry you land on. -The strip takes the keyboard too. Click into it and the arrow keys move the highlight, typing jumps to a name, and Return opens the entry you land on. +## Windows -## Moving to its own window +One window hosts every connection you have open, and the tab strip below the toolbar lists only the current connection's tabs. Opening a table or query on a connection already open adds a tab there rather than opening a second window. -Right-click a connection and choose **Open in New Window** to move it out of the shared window, carrying its tabs, its session and anything you have not saved. The item appears only when the window has another connection left to show, since moving the last one would close the window and open an identical one. +To put two connections side by side, right-click an entry and choose **Open in New Window**. It moves with its tabs, its session, and anything unsaved. The item is absent when the window has no other connection left to show. -## Closing +A first window opens at 1200x800 and goes no smaller than 720x480; later windows reuse the last size and position. Whether a new one joins a tab group follows **Prefer tabs when opening documents** in System Settings > Desktop & Dock. Inside a group, **Window > Move Tab to New Window** and **Window > Merge All Windows** apply; both dim outside one. Those are macOS window tabs, which hold whole windows, not the editor tab strip, which holds one connection's editors. -Right-click a connection and choose **Close "\"** to close it: every tab across every database it has open, its session, and every entry it has in the strip. Unsaved work is confirmed first, the same as closing a window. If the window has no other connection open, the window closes too. +## Closing and disconnecting -Middle-click an entry to close its connection without opening the menu. **File > Close Connection** does the same for the connection on screen. +Right-click an entry and choose **Close “\”** to close the connection: every tab across every database it has open, its session, and all of its entries. Unsaved work is confirmed first, and the window closes too if that was its last connection. Middle-clicking does the same without the menu, and **File > Close Connection** does it for the connection on screen. Nothing is lost for good: reopen the connection, from the connection list or **Database > Switch Connection…**, and its tabs come back as they were. -## Disconnecting - -Right-click an entry and choose **Disconnect** to end its session while keeping it open. The item only appears while the connection is live. Unlike Close, no window closes and nothing is removed from the strip: the connection shows a Reconnect screen in place of its tabs, with the tabs saved before the session ends. See [disconnecting](/features/tabs#disconnecting). +**Disconnect**, on the same menu while the connection is live, ends the session and keeps everything else. No window closes, no entry disappears, and the tabs are saved first; the connection shows a Reconnect pane in their place until **Database > Reconnect**, or the pane's own button, brings both back. **Database > Disconnect** does the same for the connection on screen, asking first when a query is running or changes are unsaved. Nothing reconnects on its own, and a connection you disconnected is not reopened at the next launch. ## Reordering -Drag entries to arrange them. The order is shared by every window and remembered across launches. - -Once you have arranged the strip, an entry keeps its position when you close it, and returns to the same slot when you reopen it. Until then, entries are listed in the order their connections opened, and the databases of one connection are grouped together by name. +Drag entries to arrange them. The order is shared by every window, kept across launches, and an entry returns to its slot when you reopen it. Until you drag anything, entries follow the order their connections opened, one connection's databases grouped by name. ## Keys @@ -78,9 +72,9 @@ Once you have arranged the strip, an entry keeps its position when you close it, | Show previous connection | `Ctrl+Cmd+Up` | | Show next connection | `Ctrl+Cmd+Down` | -Show Previous and Show Next Connection move through the strip in the order it displays, not the order things were opened. Change any of these in **Settings > Keyboard**. If you had already assigned `Ctrl+Cmd+Up` or `Ctrl+Cmd+Down` to something else, your assignment wins and the command stays unbound until you free the chord again. +Previous and next move through the strip in display order, not the order things opened. Rebind them in **Settings > Keyboard**; if either chord is already yours for something else, your assignment wins and the command stays unbound until you free it. ## Related -- [Tabs and Windows](/features/tabs) for how tabs and windows relate to connections -- [Open Quickly](/features/open-quickly) for searching within the current connection +- [Query Tabs](/features/tabs) for what a tab is bound to and what survives a restart +- [Open Quickly](/features/open-quickly) for searching inside one connection or across all of them diff --git a/docs/images/backup-dump-dialog-dark.png b/docs/images/backup-dump-dialog-dark.png new file mode 100644 index 000000000..d4f8e1b3d Binary files /dev/null and b/docs/images/backup-dump-dialog-dark.png differ diff --git a/docs/images/backup-dump-dialog.png b/docs/images/backup-dump-dialog.png new file mode 100644 index 000000000..25621696c Binary files /dev/null and b/docs/images/backup-dump-dialog.png differ diff --git a/docs/images/cloud-sql-proxy-pane-dark.png b/docs/images/cloud-sql-proxy-pane-dark.png new file mode 100644 index 000000000..0aceb1e58 Binary files /dev/null and b/docs/images/cloud-sql-proxy-pane-dark.png differ diff --git a/docs/images/cloud-sql-proxy-pane.png b/docs/images/cloud-sql-proxy-pane.png new file mode 100644 index 000000000..85eb9032b Binary files /dev/null and b/docs/images/cloud-sql-proxy-pane.png differ diff --git a/docs/images/cloudflare-tunnel-pane-dark.png b/docs/images/cloudflare-tunnel-pane-dark.png new file mode 100644 index 000000000..93d040c23 Binary files /dev/null and b/docs/images/cloudflare-tunnel-pane-dark.png differ diff --git a/docs/images/cloudflare-tunnel-pane.png b/docs/images/cloudflare-tunnel-pane.png new file mode 100644 index 000000000..467402a69 Binary files /dev/null and b/docs/images/cloudflare-tunnel-pane.png differ diff --git a/docs/images/cockroachdb-connection-form-dark.png b/docs/images/cockroachdb-connection-form-dark.png new file mode 100644 index 000000000..71abae655 Binary files /dev/null and b/docs/images/cockroachdb-connection-form-dark.png differ diff --git a/docs/images/cockroachdb-connection-form.png b/docs/images/cockroachdb-connection-form.png new file mode 100644 index 000000000..87223b12d Binary files /dev/null and b/docs/images/cockroachdb-connection-form.png differ diff --git a/docs/images/dameng-connection-form-dark.png b/docs/images/dameng-connection-form-dark.png new file mode 100644 index 000000000..254c19a36 Binary files /dev/null and b/docs/images/dameng-connection-form-dark.png differ diff --git a/docs/images/dameng-connection-form.png b/docs/images/dameng-connection-form.png new file mode 100644 index 000000000..c4df9095c Binary files /dev/null and b/docs/images/dameng-connection-form.png differ diff --git a/docs/images/dynamodb-connection-form-dark.png b/docs/images/dynamodb-connection-form-dark.png new file mode 100644 index 000000000..8be419106 Binary files /dev/null and b/docs/images/dynamodb-connection-form-dark.png differ diff --git a/docs/images/dynamodb-connection-form.png b/docs/images/dynamodb-connection-form.png new file mode 100644 index 000000000..a6d40f361 Binary files /dev/null and b/docs/images/dynamodb-connection-form.png differ diff --git a/docs/images/handoff-banner-dark.png b/docs/images/handoff-banner-dark.png new file mode 100644 index 000000000..600ee3469 Binary files /dev/null and b/docs/images/handoff-banner-dark.png differ diff --git a/docs/images/handoff-banner.png b/docs/images/handoff-banner.png new file mode 100644 index 000000000..e565f2e87 Binary files /dev/null and b/docs/images/handoff-banner.png differ diff --git a/docs/images/ios-connection-list-dark.png b/docs/images/ios-connection-list-dark.png new file mode 100644 index 000000000..61d965bdb Binary files /dev/null and b/docs/images/ios-connection-list-dark.png differ diff --git a/docs/images/ios-connection-list.png b/docs/images/ios-connection-list.png new file mode 100644 index 000000000..70a657a16 Binary files /dev/null and b/docs/images/ios-connection-list.png differ diff --git a/docs/images/pglite-connection-form-dark.png b/docs/images/pglite-connection-form-dark.png new file mode 100644 index 000000000..6569b66e8 Binary files /dev/null and b/docs/images/pglite-connection-form-dark.png differ diff --git a/docs/images/pglite-connection-form.png b/docs/images/pglite-connection-form.png new file mode 100644 index 000000000..1638968be Binary files /dev/null and b/docs/images/pglite-connection-form.png differ diff --git a/docs/images/project-folder-scan-dark.png b/docs/images/project-folder-scan-dark.png new file mode 100644 index 000000000..338ec8e4b Binary files /dev/null and b/docs/images/project-folder-scan-dark.png differ diff --git a/docs/images/project-folder-scan.png b/docs/images/project-folder-scan.png new file mode 100644 index 000000000..0dfea3207 Binary files /dev/null and b/docs/images/project-folder-scan.png differ diff --git a/docs/images/settings-data-dark.png b/docs/images/settings-data-dark.png new file mode 100644 index 000000000..96b2b7977 Binary files /dev/null and b/docs/images/settings-data-dark.png differ diff --git a/docs/images/settings-data.png b/docs/images/settings-data.png new file mode 100644 index 000000000..9b3e09484 Binary files /dev/null and b/docs/images/settings-data.png differ diff --git a/docs/images/snowflake-connection-form-dark.png b/docs/images/snowflake-connection-form-dark.png new file mode 100644 index 000000000..7836ef6cb Binary files /dev/null and b/docs/images/snowflake-connection-form-dark.png differ diff --git a/docs/images/snowflake-connection-form.png b/docs/images/snowflake-connection-form.png new file mode 100644 index 000000000..9ad30c6bf Binary files /dev/null and b/docs/images/snowflake-connection-form.png differ diff --git a/docs/images/socks-proxy-pane-dark.png b/docs/images/socks-proxy-pane-dark.png new file mode 100644 index 000000000..7c73e198a Binary files /dev/null and b/docs/images/socks-proxy-pane-dark.png differ diff --git a/docs/images/socks-proxy-pane.png b/docs/images/socks-proxy-pane.png new file mode 100644 index 000000000..150e93bb3 Binary files /dev/null and b/docs/images/socks-proxy-pane.png differ diff --git a/docs/images/teradata-connection-form-dark.png b/docs/images/teradata-connection-form-dark.png new file mode 100644 index 000000000..e1dec8f1a Binary files /dev/null and b/docs/images/teradata-connection-form-dark.png differ diff --git a/docs/images/teradata-connection-form.png b/docs/images/teradata-connection-form.png new file mode 100644 index 000000000..cbff4e96c Binary files /dev/null and b/docs/images/teradata-connection-form.png differ diff --git a/docs/images/trino-connection-form-dark.png b/docs/images/trino-connection-form-dark.png new file mode 100644 index 000000000..9e6141b1c Binary files /dev/null and b/docs/images/trino-connection-form-dark.png differ diff --git a/docs/images/trino-connection-form.png b/docs/images/trino-connection-form.png new file mode 100644 index 000000000..c667332de Binary files /dev/null and b/docs/images/trino-connection-form.png differ diff --git a/docs/index.mdx b/docs/index.mdx index fbb3576b0..129366340 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,7 +3,9 @@ title: Introduction description: Native macOS database client for MySQL, PostgreSQL, SQLite, MongoDB, Redis, and 22 more --- -Native macOS client for 27 databases. Built with SwiftUI and AppKit, no Electron. The download is about 20 MB. +import DriverCounts from "/snippets/driver-counts.mdx"; + +Using all 27 engines, the SQL editor, the data grid, import, export, and the AI assistant costs nothing, and there is no trial countdown. Paid tiers cover iCloud sync, encrypted connection export, environment variables in connection fields, Linked Folders, Query Insights, result charts, and team sharing ([Licensing](/features/licensing)). Mac is the full app, [iPhone and iPad](/ios) run a smaller one, and there is no Windows or Linux build. TablePro main interface @@ -22,61 +24,17 @@ Native macOS client for 27 databases. Built with SwiftUI and AppKit, no Electron -## Features - -**[SQL Editor](/features/sql-editor)**: Syntax highlighting, schema-aware autocomplete, Vim mode, multi-statement execution. -**[Data Grid](/features/data-grid)**: Inline editing, sorting, filtering, change tracking with undo/redo. -**[AI Assistant](/features/ai-assistant)**: Chat, inline suggestions, and Explain/Optimize across 14 providers, including GitHub Copilot, Claude, OpenAI, Gemini, xAI, and local llama.cpp, MLX, and Ollama. -**[MCP Server](/features/mcp)**: Expose your connections to Claude Desktop, Cursor, Raycast, and other MCP clients. The [External API](/external-api) adds a `tablepro://` URL scheme and one-click pairing. -**[Users & Roles](/features/users-roles)**: Manage database users, roles, and privileges on MySQL and PostgreSQL. Changes are staged, undoable, and shown as SQL before they run. -**[Secure Connections](/connections/ssh-tunneling)**: SSH tunnels authenticate with a password, a private key, an SSH agent, keyboard-interactive, or no credentials at all. Passwords stay in the Keychain, or [resolve at connect time](/features/connection-sharing#password-sources) from a file, an environment variable, a command, 1Password, HashiCorp Vault, or AWS Secrets Manager. -**[Safe Mode](/features/safe-mode)**: 6 per-connection protection levels, from no prompt at all to confirmation dialogs, Touch ID, and read-only. -**[Import & Export](/features/import-export)**: CSV, JSON, SQL, XLSX, MQL. Streaming export for large datasets. -**[CSV Inspector](/features/csv-inspector)**: Open `.csv` and `.tsv` files directly. Edit cells, insert and delete rows and columns, save in the original dialect. -**[Plugin System](/features/plugins)**: 5 bundled drivers covering 9 databases, plus 17 more drivers installable from the plugin registry. -**[iCloud Sync](/features/icloud-sync)**: Connections, groups, tags, settings, SSH profiles, saved queries and folders, favorite tables, and custom AI slash commands sync across Macs. -**[Themes](/customization/appearance)**: Light, dark, and custom editor themes. Per-connection color labels. - ## Supported databases -| Database | Default Port | Distribution | -|----------|--------------|--------------| -| [MySQL](/databases/mysql) | 3306 | Built-in | -| [MariaDB](/databases/mariadb) | 3306 | Built-in | -| [PostgreSQL](/databases/postgresql) | 5432 | Built-in | -| [SQLite](/databases/sqlite) | N/A (file-based) | Built-in | -| [Amazon Redshift](/databases/redshift) | 5439 | Built-in | -| [CockroachDB](/databases/cockroachdb) | 26257 | Built-in | -| [PGlite](/databases/pglite) | 5432 | Built-in | -| [Microsoft SQL Server](/databases/mssql) | 1433 | Plugin | -| [ClickHouse](/databases/clickhouse) | 8123 | Built-in | -| [Redis](/databases/redis) | 6379 | Built-in | -| [MongoDB](/databases/mongodb) | 27017 | Plugin | -| [Oracle Database](/databases/oracle) | 1521 | Plugin | -| [Dameng DM8](/databases/dameng) | 5236 | Plugin | -| [DuckDB](/databases/duckdb) | File, or 9494 remote | Plugin | -| [Beancount](/databases/beancount) | N/A (file-based) | Plugin | -| [Cassandra](/databases/cassandra) | 9042 | Plugin | -| [ScyllaDB](/databases/cassandra) | 9042 | Plugin | -| [etcd](/databases/etcd) | 2379 | Plugin | -| [Cloudflare D1](/databases/cloudflare-d1) | N/A (API-based) | Plugin | -| [DynamoDB](/databases/dynamodb) | N/A (API-based) | Plugin | -| [BigQuery](/databases/bigquery) | N/A (API-based) | Plugin | -| [Snowflake](/databases/snowflake) | 443 | Plugin | -| [Teradata](/databases/teradata) | 1025 | Plugin | -| [libSQL / Turso](/databases/libsql) | N/A (API-based) | Plugin | -| [Elasticsearch](/databases/elasticsearch) | 9200 | Plugin | -| [SurrealDB](/databases/surrealdb) | 8000 | Plugin | -| [Trino](/databases/trino) | 8080 | Plugin | - -## System requirements + -- **macOS**: 14.0 (Sonoma) or later -- **Architecture**: Apple Silicon (arm64) or Intel (x86_64). Downloads are per-architecture, and Homebrew picks the right one. +Twenty-seven engines, from MySQL and PostgreSQL to DynamoDB, BigQuery and Redis, each with its own +page. [Supported Databases](/databases) lists them all with their default ports and says which ones +arrive as a plugin. ## Open source -TablePro is free, open-source software licensed under the [GNU Affero General Public License v3.0 (AGPLv3)](https://www.gnu.org/licenses/agpl-3.0.html). The full source code is on GitHub. +The source, plugins included, is under the [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.html). A TablePro license key is a separate thing: it activates the paid features, not the app. View source code, report issues, and contribute to TablePro. diff --git a/docs/installation.mdx b/docs/installation.mdx index 0f4ab9616..0d3a0dee8 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -3,95 +3,64 @@ title: Installation description: Install TablePro via Homebrew or DMG on macOS 14.0+ --- +Homebrew is the short route: `brew install --cask tablepro` picks the build that matches your Mac. Everyone else takes the DMG, and that is the one route where you match the architecture yourself. + ## System requirements - **macOS**: 14.0 (Sonoma) or later -- **Processor**: Apple Silicon (M1+) or Intel x86_64 - -TablePro uses native Apple frameworks only. No Java, .NET, or other runtimes needed. The download is about 20 MB. - -## Install via Homebrew - -The fastest option: +- **Architecture**: Apple Silicon (M1 or later) or Intel x86_64 -```bash -brew install --cask tablepro -``` +Native Apple frameworks only, so there is no Java or .NET runtime to install first. The download is about 20 MB. -To update: +## Homebrew ```bash -brew upgrade tablepro +brew install --cask tablepro # install +brew upgrade tablepro # update +brew uninstall tablepro # remove the app +brew uninstall --zap tablepro # remove the app and its data ``` -To uninstall: - -```bash -brew uninstall tablepro -``` - -To also remove application data: - -```bash -brew uninstall --zap tablepro -``` - -## Download from GitHub releases - -1. Go to [GitHub Releases](https://github.com/TableProApp/TablePro/releases) -2. Download the DMG for your Mac. Asset names include the version and architecture, for example `TablePro--arm64.dmg`: - - **Apple Silicon (M1+)**: the `arm64` DMG - - **Intel**: the `x86_64` DMG - -Check your architecture: **Apple menu > About This Mac**, then look for Chip (Apple Silicon) or Processor (Intel). - -## Installation steps - -1. Open the DMG file (double-click) -2. Drag **TablePro** to the **Applications** folder shortcut - - - Drag to Applications - Drag to Applications - - -3. Eject the DMG (right-click > Eject) -4. Open **Finder > Applications** and launch **TablePro** - -## Updating TablePro - -TablePro checks for updates automatically via Sparkle. Check manually: **TablePro > Check for Updates…**. - -Connections and settings persist across updates. - -## Uninstallation - -1. Quit TablePro -2. Drag **TablePro** from **Finder > Applications** to Trash - -To delete local data (connections, query history, downloaded driver plugins): +## The DMG + + + + Release assets carry the version and the architecture: `TablePro--arm64.dmg` and `TablePro--x86_64.dmg`. Check which you need under **Apple menu > About This Mac**, where Chip means Apple Silicon and Processor means Intel. Both are on [GitHub Releases](https://github.com/TableProApp/TablePro/releases). + + + Open the DMG and drag **TablePro** onto the **Applications** shortcut. + + + Drag to Applications + Drag to Applications + + + + Right-click the mounted disk and choose **Eject**. Open **TablePro** from the Applications folder. + + + +## Updates + +Update checks run on their own. Force one with **TablePro > Check for Updates…**. Connections and settings survive an update. + +## Removing it + +Quit TablePro and drag it from the Applications folder to the Trash. To take the local data with it, which is connections, query history, and any driver plugins you installed: ```bash rm -rf ~/Library/Application\ Support/TablePro rm ~/Library/Preferences/com.TablePro.plist ``` - -Connection passwords are stored in the macOS Keychain and are not removed by these commands. Delete them in Keychain Access by searching for `com.TablePro`. Data synced through iCloud stays in your iCloud account. - - -## Troubleshooting - -**Crashes on launch**: Verify correct architecture, macOS 14.0+, and check Console.app for logs. - -**Connection issues**: Confirm the database server is running and not blocked by a firewall. +Connection passwords are in the macOS Keychain and survive both commands. Delete them in Keychain Access by searching for `com.TablePro`. Data already synced through iCloud stays in your iCloud account. -See [Development Setup](/development/setup) for building from source. +Building from source is in [Development Setup](/development/setup). A launch that fails or a connection that will not open is in [Troubleshooting](/troubleshooting). diff --git a/docs/ios/index.mdx b/docs/ios/index.mdx index 6796d24dd..e4cae737c 100644 --- a/docs/ios/index.mdx +++ b/docs/ios/index.mdx @@ -3,11 +3,16 @@ title: iPhone and iPad description: Browse tables, run queries, and edit rows on eight database engines from a synced connection list --- -The mobile app has its own version number, its own release schedule, and a smaller feature set than the Mac. It requires iOS 18. Connections saved on a Mac arrive over iCloud; the rest of your Mac setup stays on the Mac. +You need iOS 18 or later. Builds ship on their own schedule, so a feature in a Mac release note may not be on your phone yet. Your version and build are under **Settings > About**. Connections saved on a Mac come over iCloud; the rest of your Mac setup stays on the Mac. + + + TablePro connection list on iPhone + TablePro connection list on iPhone + ## Supported databases -There is no plugin system on iOS. Every driver is compiled into the app, so nothing is installed. The connection type picker offers eight: +Every driver is compiled in, so there is no plugin system and nothing to install. The type picker offers these eight: | Type | Notes | | --- | --- | @@ -23,58 +28,57 @@ There is no plugin system on iOS. Every driver is compiled into the app, so noth Redshift is not in the picker, but the driver is present: a Redshift connection created on the Mac connects once it syncs over. -Every other engine the Mac supports, MongoDB, ClickHouse, Cassandra, BigQuery, Snowflake and the rest, has no iOS driver. Those connections still sync and still appear in the list, but opening one fails. +MongoDB, ClickHouse, Cassandra, BigQuery, Snowflake and the rest have no iOS driver. Those connections still sync and still appear in the list, but opening one fails. ## What syncs over -The app reads the same CloudKit container as the Mac, and iCloud Sync is on by default here (it is off by default on the Mac). It carries three record types and ignores the rest of the zone: +The same CloudKit container as the Mac, with iCloud Sync on by default here and off by default there. Three record types cross: connections (host, port, SSH tunnel and SSL settings, color, safe mode level), groups with their nesting, and tags. SSH profiles, table favorites, saved queries, and app settings are skipped, and query history on the phone is only what you ran on the phone. + +Three things a connection depends on are per device and never sync: -- Connections, including host, port, SSH tunnel and SSL settings, color, and safe mode level -- Groups, with their nesting -- Tags +| Per device | What you see | What to do | +| --- | --- | --- | +| CA and client certificates | The SSL section reads **Not set** | Import them here. One PKCS#12 file covers the client certificate and its key | +| An SSH private key | It arrived as a path on the Mac, so the tunnel cannot find it | Pick the key on this device, or paste it into **Private Key** | +| SQLite and DuckDB file paths | The path points at the Mac's disk | Pick the file again here | -Nothing else crosses. SSH profiles, table favorites, saved queries, and app settings are skipped. Query history on the phone is only the queries you ran on the phone. +Passwords are a separate opt-in, off by default, at **Settings > Sync > Sync Passwords**. They ride iCloud Keychain and only new saves are affected, so re-save a password on the Mac to push it across. **Sync Now** sits in the same section, and a background refresh runs about every 30 minutes. -Three things a connection depends on are per device and never sync. Certificates live in the local keychain, so a connection using a CA or client certificate reports it missing until you import it again in the connection's SSL settings. An SSH private key syncs as a path, not as a file, and that path points at the Mac, so a key-authenticated tunnel does not open here. And SQLite and DuckDB paths point at the Mac's disk, so those connections need the file picked again. +Picking a SQLite file copies it into the app, and edits go to that copy, so the original never changes and the two drift apart. DuckDB writes back to the file you picked. -Passwords are a separate opt-in, off by default, under **Settings > Sync > Sync Passwords**. It routes them through iCloud Keychain and only affects new saves, so re-save a password on the Mac to push it across. **Sync Now** is in that same **Sync** section, **Refresh from iCloud** is in the one below it, and a background refresh runs about every 30 minutes. +## What works - -Picking a SQLite file copies it into the app. Edits go to that copy, so the file you picked is untouched and the two drift apart. DuckDB keeps a reference to the original instead and writes back to it. - +An open connection has four tabs: **Tables**, **Query**, **History**, **Info**. `Cmd+1` through `Cmd+4` switch between them on a keyboard, and a toolbar menu switches database and schema when the engine has more than one. -## What you can do +### Browsing -Opening a connection gives four tabs: **Tables**, **Query**, **History**, and **Info**. `Cmd+1` through `Cmd+4` switch between them on a keyboard. A toolbar menu switches database and schema when the engine has more than one. +Page a table at 50, 100, 200, or 500 rows, jump to a page number, sort by a column, search text columns, and stack filters with AND or OR. A foreign key value previews the row it points at. **Table Structure** lists columns, indexes, and foreign keys, read only. -**Browsing.** Page through a table at 50, 100, 200, or 500 rows, jump to a page number, sort by a column, search text columns, and stack filters with AND or OR. A foreign key value previews the row it points at. **Table Structure** lists columns, indexes, and foreign keys, read only. +### Editing -**Editing.** Tap a row to open it full screen, page between rows, edit values, toggle one to `NULL`, and save. You can insert a row, delete a row, and truncate or drop a table. Editing needs a primary key. +Tap a row to open it full screen, page between rows, edit values, toggle one to `NULL`, and save. Inserting and deleting rows, and truncating or dropping a table, are here too. Editing needs a primary key. -**Querying.** The editor highlights SQL, runs a statement, and stops one mid-flight. Results copy or export as JSON, CSV, or SQL `INSERT`. A running query shows in a Live Activity on the lock screen and Dynamic Island; **Settings > Privacy** hides the SQL text there. +### Querying + +The editor highlights SQL, runs a statement, and stops one mid-flight. Results copy or export as JSON, CSV, or SQL `INSERT`. A running query appears in a Live Activity on the lock screen and Dynamic Island; **Settings > Privacy** hides the SQL text there. ## What is missing -- **Schema changes.** Drop and truncate a table is the whole list. Creating or altering tables, columns, indexes, triggers, and views is Mac only. -- **AI.** No AI chat, no inline suggestions, no MCP server. None of it is in the iOS app. -- **Plugins.** The eight built-in drivers are all there is; no registry plugin installs on iOS. +- **Schema changes.** Truncating and dropping a table is the whole list; creating or altering tables, columns, indexes, triggers, and views is Mac only. +- **AI.** No chat, no inline suggestions, no MCP server. ## Security -Turn on Face ID, Touch ID, or Optic ID under **Settings > Security**. A cold launch always asks; after that the app relocks immediately or after 1, 5, 15, or 60 minutes idle. +Turn on Face ID, Touch ID, or Optic ID under **Settings > Security**. A cold launch always asks; after that **Auto-Lock** relocks immediately or after 1, 5, 15, or 60 minutes idle, starting at 5. -Each connection carries its own safe mode level, synced from the Mac and settable here: **Off**, **Confirm Writes**, or **Read-Only**. Read-only refuses writes outright; confirm mode prompts before a row edit, an insert, or a write query. **Settings > New Connections** sets the starting level. +Each connection carries its own [safe mode](/features/safe-mode) level, synced from the Mac and settable here. iOS has three: **Off**, **Confirm Writes**, and **Read-Only**, which refuses writes outright. **Settings > New Connections** sets the level a new connection starts at. -SSH tunnels work with a password, a private key, or no authentication, and an unknown host key prompts with its fingerprint first. +SSH tunnels authenticate with a password or a private key, and an unknown host key prompts with its fingerprint first. ## Shortcuts and widgets -Three App Intents put the app in Shortcuts, the Share Sheet, and Siri: **Open Connection**, **Add Row to Table**, and **Add Rows to Table**. See [iOS Shortcuts](/external-api/ios-shortcuts) for pickers, data formats, and limits. - -A **Quick Connect** home screen widget opens a saved connection directly. `tablepro://connect/` deep links work as on the Mac, Handoff carries an open connection between devices, and a `.tablepro` file from Files or AirDrop imports connections. - -## Updates +Three App Intents put the app in Shortcuts, the Share Sheet, and Siri: **Open Connection**, **Add Row to Table**, **Add Rows to Table**. See [iOS Shortcuts](/external-api/ios-shortcuts) for pickers, data formats, and limits. -Builds ship separately from the Mac app, so a Mac release note does not mean your phone has the feature yet. Check the version and build under **Settings > About**. +A **Quick Connect** home screen widget opens a saved connection directly, `tablepro://connect/` deep links work as on the Mac, Handoff carries an open connection between devices, and a `.tablepro` file from Files or AirDrop imports connections. diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index ab1694078..9a1ab6943 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -3,104 +3,94 @@ title: Quick Start description: Connect to your first database and run a query in a few minutes --- -You can finish this page without a database server. With no connections saved yet, the welcome window offers **Try Sample Database**, which opens a bundled Chinook SQLite file and puts you at step 4. Otherwise start at step 1. - -## Step 1: install - -Install via Homebrew (`brew install --cask tablepro`) or download the DMG from GitHub Releases. See [Installation](/installation). - -## Step 2: create your first connection - -The welcome window has an actions panel on the left with **Create Connection…** and an **Add from Existing** menu. Your saved connections are on the right. The `+` button, the group button, and the search field sit at the top of the connections panel. Press `Cmd+F` to focus search. Each connection shows its tags and group. - -- **Add from Existing** holds **Import from URL…**, **Import from Other App…** (TablePlus, DataGrip, DBeaver, Beekeeper Studio, Sequel Ace, and others), **Open Project Folder…**, and **Import Connections…**. -- With no connections saved yet, the right panel offers **Try Sample Database**. It opens the bundled Chinook SQLite database, no server needed. Reset it any time from **Help > Reset Sample Database…**. - -Click **Create Connection…**. A chooser sheet lists every supported database type grouped by category. Pick one and click **Continue**. - - - Database type chooser - Database type chooser - - - -Databases beyond the bundled drivers (MongoDB, Oracle, MSSQL, and others) run as plugins. The first time you pick one, TablePro asks to download the driver before opening the form. - - -## Step 3: fill in, test, and save - -The form pre-fills the host (`localhost`) and the default port. The username is optional; leave it empty and the database uses its own default, the same way `psql` and `mysql` do. Typical values: - -- **MySQL**: port `3306` -- **PostgreSQL**: port `5432` -- **MongoDB**: port `27017` -- **SQLite**: browse to a `.sqlite` or `.db` file, no auth needed - - - Connection form - Connection form - - - -Already have a connection URL? Click **Import from URL…** in the chooser footer. TablePro detects the database type and pre-fills the form for you. - - -Click **Test Connection** in the **Status** row at the bottom of the General pane. When it shows a green checkmark, click **Save & Connect** in the toolbar. - - - Connection test success - Connection test success - - -## Step 4: run your first query - -The main window has a sidebar on the left (databases and tables), the open tab in the middle, and an inspector on the right. A table tab shows the data grid. A query tab puts the SQL editor above the result grid. - - - Main interface - Main interface - - -Click a table in the sidebar to browse it. For SQL, press `Cmd+T` to open a query tab, type a query, and press `Cmd+Enter`: - -```sql -SELECT * FROM users LIMIT 10; -``` +With no connections saved, the welcome window offers **Try Sample Database**, a bundled Chinook SQLite file that needs no server and drops you at step 4. Reset it any time from **Help > Reset Sample Database…**. + + + + `brew install --cask tablepro`, or the DMG from GitHub Releases. See [Installation](/installation). + + + + Click **Create Connection…** in the welcome window, pick a type in the **Choose a Database** sheet, then click **Continue**. A type badged **Not Installed** downloads its driver before the form opens, with no restart. + + + Database type chooser + Database type chooser + + + + Already have a connection URL, another client's connections, or a repository with a `.env`? **Add from Existing** takes all three. See [Switching to TablePro](/switching). + + + + + The host pre-fills to `localhost` and the port to the engine's default. Username is optional: leave it empty and the database applies its own default. SQLite asks for a `.sqlite` or `.db` file instead of a host, with no credentials. + + + Connection form + Connection form + + + Click **Test Connection** in the **Status** row at the bottom of the General pane. It turns into **Connected** with a green checkmark. Now click **Save & Connect** in the toolbar. + + + Connection test success + Connection test success + + + + The window switches to the main layout: databases and tables in the sidebar, the open tab in the middle, the inspector on the right. + + + A failed attempt paints the window with the driver's own message and a **Try Again** button instead. [Troubleshooting](/troubleshooting) sorts refused from timed out. + + + + Click a table in the sidebar to browse it. For SQL, press `Cmd+T` for a query tab, type, and press `Cmd+Enter` to run the statement under the cursor. + + + Main interface + Main interface + + + ```sql + SELECT * FROM users LIMIT 10; + ``` + + ## What's next -Explore [connections](/connections), [SSH tunneling](/connections/ssh-tunneling), [keyboard shortcuts](/features/keyboard-shortcuts), or [AI features](/features/ai-assistant). - -Need help? [Open an issue](https://github.com/TableProApp/TablePro/issues) on GitHub. +[Managing connections](/connections), [SSH tunneling](/connections/ssh-tunneling), [keyboard shortcuts](/features/keyboard-shortcuts), and [AI features](/features/ai-assistant). Anything that goes wrong is worth an [issue](https://github.com/TableProApp/TablePro/issues). diff --git a/docs/scripts/check-docs-against-source.py b/docs/scripts/check-docs-against-source.py index bc1702d25..852665691 100755 --- a/docs/scripts/check-docs-against-source.py +++ b/docs/scripts/check-docs-against-source.py @@ -11,6 +11,7 @@ Run from the repository root or from docs/. """ +import collections import re import sys from pathlib import Path @@ -172,6 +173,18 @@ def check_shortcuts(root: Path, docs: Path) -> list[str]: f"`{written}`, the app binds the chord {spelled}" ) print(f" {checked} of {len(documented)} documented rows joined to a binding") + + spellings = collections.Counter() + for page in doc_pages(docs): + for line_no, line in enumerate(page.read_text().splitlines(), start=1): + for chord in re.findall(r"`((?:Cmd|Ctrl|Option|Shift)\+[A-Za-z0-9+]+)`", line): + for key, canonical in (("Return", "Enter"), ("Esc", "Escape"), ("Del", "Delete")): + if chord.endswith("+" + key): + spellings[(key, canonical)] += 1 + failures.append( + f"{page.relative_to(docs)}:{line_no}: `{chord}` spells the key {key}; " + f"the corpus spells it {canonical}" + ) return failures @@ -279,6 +292,64 @@ def check_heading_case(root: Path, docs: Path) -> list[str]: return failures +DOC_NAMES = { + "SQL Server": "Microsoft SQL Server", + "Oracle": "Oracle Database", + "Dameng": "Dameng DM8", + "Redshift": "Amazon Redshift", + "libSQL": "libSQL / Turso", +} + + +def registered_databases(root: Path) -> dict: + """Every database type the chooser offers, with its default port. + + The registry builds these as ("TypeId", PluginMetadataSnapshot(... defaultPort: N ...)) tuples, + spread over PluginMetadataRegistry.swift and its +*Defaults.swift extensions. A new engine adds + one tuple, so counting them is how the docs learn the engine exists. + """ + found = {} + for path in sorted((root / "TablePro/Core/Plugins").glob("PluginMetadataRegistry*.swift")): + source = path.read_text() + for match in re.finditer(r'\(\s*"([^"]+)",\s*PluginMetadataSnapshot\(', source): + tail = source[match.end():match.end() + 1500] + port = re.search(r"defaultPort:\s*([0-9_]+)", tail) + found[match.group(1)] = int(port.group(1).replace("_", "")) if port else None + return found + + +def check_database_table(root: Path, docs: Path) -> list[str]: + registered = registered_databases(root) + if len(registered) < 20: + return ["could not read the database types out of PluginMetadataRegistry"] + + page = docs / "databases/index.mdx" + rows = {} + for name, port in re.findall(r"^\| \[([^\]]+)\]\([^)]+\) \| ([^|]+?) \|", page.read_text(), re.M): + rows[name.strip()] = port.strip() + + failures = [] + expected = {DOC_NAMES.get(name, name) for name in registered} + for missing in sorted(expected - set(rows)): + failures.append(f"databases/index.mdx has no row for {missing}, which the chooser offers") + for extra in sorted(set(rows) - expected): + failures.append(f"databases/index.mdx lists {extra}, which is not a registered type") + + for type_id, port in registered.items(): + row = rows.get(DOC_NAMES.get(type_id, type_id)) + if row is None or port in (None, 0): + continue + if str(port) not in row: + failures.append( + f"databases/index.mdx gives {type_id} port {row}, the registry says {port}" + ) + + counted = re.search(r"\b(\d+|Twenty-seven)\b engines", page.read_text()) + if counted and counted.group(1) not in (str(len(registered)), "Twenty-seven"): + failures.append(f"databases/index.mdx says {counted.group(1)} engines, the registry has {len(registered)}") + return failures + + def main() -> int: root = repo_root() docs = root / "docs" @@ -290,6 +361,7 @@ def main() -> int: ("changelog parity", check_changelog_parity), ("changelog anchors", check_changelog_anchors), ("heading case", check_heading_case), + ("database table", check_database_table), ) total = 0 diff --git a/docs/scripts/check-links.py b/docs/scripts/check-links.py new file mode 100755 index 000000000..3d4a4d2eb --- /dev/null +++ b/docs/scripts/check-links.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Resolve every internal link, image and navigation entry in docs/ against the files on disk. + +The Mintlify CLI does this too, but it needs a working npm install with native bindings, and it +cannot answer the question that has broken this repo twice: a page that exists but sits in no +navigation group, so nothing ever links to it and the sidebar never shows it. +""" + +import json +import re +import sys +from pathlib import Path + +DOCS = Path(__file__).resolve().parent.parent +LINK = re.compile(r"\]\((/[^)\s]*?)(?:\s+\"[^\"]*\")?\)") +SRC = re.compile(r'src=\{?"(/[^"]+)"') +FENCE = re.compile(r"```.*?```", re.S) + + +def nav_pages(node, out, collecting=False): + if isinstance(node, dict): + for key, value in node.items(): + nav_pages(value, out, key == "pages") + elif isinstance(node, list): + for value in node: + nav_pages(value, out, collecting) + elif collecting and isinstance(node, str) and not node.startswith(("http", "#")): + out.add(node) + + +def main() -> int: + config = json.loads((DOCS / "docs.json").read_text()) + pages = set() + nav_pages(config["navigation"], pages) + redirects = {r["source"]: r["destination"] for r in config.get("redirects", [])} + + on_disk = { + str(p.relative_to(DOCS).with_suffix("")) + for p in DOCS.rglob("*.mdx") + if "snippets" not in p.parts and "node_modules" not in p.parts + } + + failures = [] + for page in sorted(pages - on_disk): + failures.append(f"docs.json lists {page}, which has no .mdx file") + for page in sorted(on_disk - pages): + failures.append(f"{page}.mdx is in no navigation group") + + anchors = {} + for path in DOCS.rglob("*.mdx"): + if "node_modules" in path.parts: + continue + slug = "/" + str(path.relative_to(DOCS).with_suffix("")) + body = FENCE.sub("", path.read_text()) + anchors[slug] = { + "#" + re.sub(r"[^a-z0-9]+", "-", h.lower()).strip("-") + for h in re.findall(r"^#{2,4} +(.+?)\s*$", body, re.M) + } + + for path in sorted(DOCS.rglob("*.mdx")): + if "node_modules" in path.parts: + continue + rel = path.relative_to(DOCS) + body = FENCE.sub("", path.read_text()) + for line_no, line in enumerate(body.splitlines(), 1): + for target in LINK.findall(line): + page, _, anchor = target.partition("#") + page = page.rstrip("/") or "/" + if page in redirects: + page = redirects[page] + if page == "/": + continue + bare = page.lstrip("/") + if bare not in on_disk and f"{bare}/index" in on_disk: + page = f"/{bare}/index" + bare = page.lstrip("/") + if bare not in on_disk: + failures.append(f"{rel}:{line_no} links to {target}, which does not resolve") + elif anchor and "#" + anchor not in anchors.get(page, set()): + failures.append(f"{rel}:{line_no} links to {target}, but that heading does not exist") + for asset in SRC.findall(line): + if not (DOCS / asset.lstrip("/")).exists(): + failures.append(f"{rel}:{line_no} references {asset}, which is not in docs/") + + if failures: + for failure in failures: + print(" FAIL", failure) + print(f"\n{len(failures)} unresolved references.") + return 1 + print(f" ok {len(pages)} navigation entries, {len(on_disk)} pages, every link resolves") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/scripts/make-placeholder.py b/docs/scripts/make-placeholder.py new file mode 100644 index 000000000..f7cfe0d14 --- /dev/null +++ b/docs/scripts/make-placeholder.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Draw the grey "Screenshot coming soon" card that stands in for a shot not taken yet. + +The card is a marker, not a defect. It reserves the filename the page already references, so +replacing it is dropping a PNG in with the same name and nothing else. Colours, size and layout +match the cards already in docs/images so a page never shows two different placeholder designs. + + python3 docs/scripts/make-placeholder.py "Data grid" data-grid + +writes docs/images/data-grid.png and docs/images/data-grid-dark.png. +""" + +import sys +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +SIZE = (1560, 960) +INSET = 24 +RADIUS = 16 +DASH, GAP = 12, 10 + +THEMES = { + "": {"bg": "#F5F5F5", "line": "#D1D1D1", "title": "#595959", "sub": "#8C8C8C"}, + "-dark": {"bg": "#1C1C1C", "line": "#2E2E2E", "title": "#BFBFBF", "sub": "#808080"}, +} + +FONTS = ("/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/Helvetica.ttc") + + +def font(size: int, weight: float): + for path in FONTS: + if Path(path).exists(): + try: + f = ImageFont.truetype(path, size) + try: + f.set_variation_by_axes([weight]) + except Exception: + pass + return f + except OSError: + continue + return ImageFont.load_default() + + +def dashed_rect(draw: ImageDraw.ImageDraw, box, colour: str, width: int = 3) -> None: + left, top, right, bottom = box + for x in range(left + RADIUS, right - RADIUS, DASH + GAP): + end = min(x + DASH, right - RADIUS) + draw.line([(x, top), (end, top)], fill=colour, width=width) + draw.line([(x, bottom), (end, bottom)], fill=colour, width=width) + for y in range(top + RADIUS, bottom - RADIUS, DASH + GAP): + end = min(y + DASH, bottom - RADIUS) + draw.line([(left, y), (left, end)], fill=colour, width=width) + draw.line([(right, y), (right, end)], fill=colour, width=width) + for xy, start in ( + ((left, top, left + RADIUS * 2, top + RADIUS * 2), 180), + ((right - RADIUS * 2, top, right, top + RADIUS * 2), 270), + ((right - RADIUS * 2, bottom - RADIUS * 2, right, bottom), 0), + ((left, bottom - RADIUS * 2, left + RADIUS * 2, bottom), 90), + ): + draw.arc(xy, start, start + 90, fill=colour, width=width) + + +def centred(draw: ImageDraw.ImageDraw, text: str, y: int, f, colour: str) -> None: + left, top, right, bottom = draw.textbbox((0, 0), text, font=f) + draw.text(((SIZE[0] - (right - left)) / 2 - left, y - (bottom - top) / 2 - top), + text, font=f, fill=colour) + + +def build(title: str, slug: str, out_dir: Path) -> list[Path]: + written = [] + for suffix, theme in THEMES.items(): + image = Image.new("RGB", SIZE, theme["bg"]) + draw = ImageDraw.Draw(image) + dashed_rect(draw, (INSET, INSET, SIZE[0] - INSET, SIZE[1] - INSET), theme["line"]) + centred(draw, title, 440, font(64, 700), theme["title"]) + centred(draw, "Screenshot coming soon", 525, font(38, 400), theme["sub"]) + path = out_dir / f"{slug}{suffix}.png" + image.save(path, optimize=True) + written.append(path) + return written + + +def main() -> int: + if len(sys.argv) != 3: + print(__doc__) + return 2 + out = Path(__file__).resolve().parent.parent / "images" + for path in build(sys.argv[1], sys.argv[2], out): + print(f"{path.relative_to(path.parent.parent)} {path.stat().st_size} bytes") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/scripts/proper-nouns.txt b/docs/scripts/proper-nouns.txt index bf730f787..d787c6d91 100644 --- a/docs/scripts/proper-nouns.txt +++ b/docs/scripts/proper-nouns.txt @@ -6,12 +6,14 @@ # second word is an ordinary English word, which no rule can tell from prose. # # One per line. Comments start with #. +Agent Atlas Authentication Beancount Cloud Connector Drive +Entra Finder Homebrew Insights @@ -20,8 +22,13 @@ Kerberos Keyspaces Library Local +Mac +Mac's +Microsoft +Mode Proxy Quack +Safe Shortcuts Studio Sync diff --git a/docs/security/privacy.mdx b/docs/security/privacy.mdx index f03f2412d..31c714778 100644 --- a/docs/security/privacy.mdx +++ b/docs/security/privacy.mdx @@ -3,7 +3,7 @@ title: Privacy description: "Every outbound request the app can make, what each one carries, and where its off switch is" --- -Three requests leave a stock install without you asking: one anonymous heartbeat a day, an update check, and a fetch of the plugin catalog. None of the three carries a query, a row, a hostname, or a password. Every other channel below is one you switch on yourself. +Three requests leave a stock install without you asking: one anonymous heartbeat a day, an update check, and a fetch of the plugin catalog. None of the three carries a query, a row, a hostname, or a password. Every other channel in this table is one you switch on yourself. | Channel | Goes to | On by default | Off switch | | --- | --- | --- | --- | @@ -36,17 +36,17 @@ The JSON body holds these fields and no others: No query text. No hostnames, ports, database names, or usernames. No email address, no file paths, no connection names. -The body is signed with HMAC-SHA256 using a secret compiled into the build (`ANALYTICS_HMAC_SECRET`) and sent as an `X-Signature` header. It is an integrity check over the body, not encryption, and it carries nothing about you. A build without the secret configured sends the payload with no signature at all. +An `X-Signature` header carries an HMAC-SHA256 of the body. It proves the payload arrived unaltered; it is not encryption, and it carries nothing about you. -`machine_id` is a hash of a hardware identifier, never of anything you typed, so two heartbeats from the same Mac count as one Mac. Where that identifier cannot be read, a random one is generated and stored instead, and it does not survive a reset. +`machine_id` hashes a hardware identifier, never anything you typed, so two heartbeats from one Mac count as one Mac. Where that identifier cannot be read, a random one is stored instead and does not survive a reset. **Share anonymous usage data** in **Settings > General** is on by default. With it off, the send returns before the payload is built, and nothing queues for later. ## AI providers -Nothing reaches a model until you add a provider. A fresh install has none, and with none configured every AI path returns before it builds a request. **Enable AI Features** in **Settings > AI** is on out of the box, but it gates an empty tab. +Nothing reaches a model until you add a provider, and a fresh install has none: with none configured, every AI path returns before it builds a request. **Enable AI Features** in **Settings > AI** is on out of the box, but it gates an empty tab. -One provider is marked active, and it answers inline suggestions and the editor actions. The chat panel has its own model picker, so a chat turn can go to any provider you have configured, not only the active one. Requests go to that provider's endpoint, `api.anthropic.com`, `api.openai.com`, `generativelanguage.googleapis.com`, `api.x.ai`, `openrouter.ai`, `api.cursor.com` and `opencode.ai` all ship as presets, and a custom provider goes wherever you point it. Ollama, llama.cpp, and MLX default to `http://localhost:11434` and `http://localhost:8080`, so their traffic stays on the machine, but the endpoint field is editable and points wherever you set it. +One provider is marked active and answers inline suggestions and the editor actions; the chat panel's own model picker can send a turn to any other provider you configured. Requests go to that provider's endpoint. The presets are `api.anthropic.com`, `api.openai.com`, `generativelanguage.googleapis.com`, `api.x.ai`, `openrouter.ai`, `api.cursor.com` and `opencode.ai`, plus `http://localhost:11434` for Ollama and `http://localhost:8080` for llama.cpp and MLX, which keeps their traffic on the machine. Every endpoint field is editable, and a custom provider goes wherever you point it. A chat turn carries: @@ -65,7 +65,7 @@ Per connection, the **AI Policy** picker in the connection form's Advanced pane **Ask Each Time** gates the chat panel only. Inline suggestions check the policy for **Never** and nothing else, so with **Enable inline suggestions while typing** on, the text before your cursor, the whole query, and the schema go to the provider after every typing pause with no prompt. That toggle is off by default. -Provider API keys live in the keychain under the `com.TablePro` service, never in `connections.json` and never in a CloudKit record. The provider list itself (type, name, endpoint, model) rides along with the Settings category of iCloud sync when sync is on. +Provider API keys live in the keychain, never in `connections.json` and never in a CloudKit record. The provider list itself (type, name, endpoint, model) rides along with the Settings category of iCloud sync when sync is on. Signing in to ChatGPT opens a second local listener on port 1455 for the OAuth redirect. It runs only during that sign-in and closes after it. @@ -102,9 +102,9 @@ Every call is written to the local activity log with the token behind it, statem ## Update checks -Sparkle fetches `https://raw.githubusercontent.com/TableProApp/TablePro/main/appcast.xml`, one day apart by default. The request is a plain `GET` with no query parameters: the app sets no system profiling key, so no profile is attached. The user agent names the app and its version, and GitHub sees your IP address as it would for any web request. +Sparkle fetches `https://raw.githubusercontent.com/TableProApp/TablePro/main/appcast.xml`, one day apart by default. It is a plain `GET` with no query parameters: system profiling is off, so nothing about your Mac rides along. The user agent names the app and its version, and GitHub sees your IP address as it would for any web request. -Updates download from the GitHub release named in the feed. Each build carries an EdDSA signature that Sparkle checks against the public key in the app's `Info.plist` before installing anything. +Updates download from the GitHub release named in the feed, and Sparkle checks each build's EdDSA signature against the public key inside the app before it installs anything. **Automatically check for updates** in **Settings > General** is on by default. With it off, **Check for Updates…** in the same section is the only thing that reaches the feed. @@ -128,9 +128,7 @@ Team Library is a Team-tier feature and it publishes only when you ask. **Share ## Crash reports -There are none. The app links no crash reporting SDK: its only remote packages are Sparkle, swift-certificates, and Yams. Nothing uploads a crash log, a stack trace, or a diagnostic file. - -Logging goes through OSLog, which stays in the system log store on your Mac. +There are none. The app links no crash reporting SDK, its only remote packages being Sparkle, swift-certificates, and Yams, so nothing uploads a crash log, a stack trace, or a diagnostic file. Logging goes through OSLog and stays in the system log store on your Mac. ## Your database connections @@ -140,26 +138,24 @@ Two helpers download on first use, and each is verified before it runs: the Clou ## What stays local -The execution log added in 0.67.0 records every authorization decision the app makes, from the UI, an MCP client, the AI assistant, the import pipeline, or background maintenance. Each record holds a sequence number, a timestamp, the connection ID, the operation kind, which of those five callers asked, allowed or denied, whether it counted as a write, a SHA-256 digest of the statement, and the previous record's hash. - -The statement text is never stored, and a caller-supplied label such as an MCP client name or an AI session ID is not stored either, only the channel. The hash chain is tamper evident, not tamper proof: anyone who can write the file can also recompute every hash after the record they changed. +The execution log records every authorization decision the app makes, from the UI, an MCP client, the AI assistant, the import pipeline, or background maintenance. Each record holds a sequence number, a timestamp, the connection ID, the operation kind, which of those five callers asked, allowed or denied, whether it counted as a write, a SHA-256 digest of the statement, and the previous record's hash. The statement text is never stored, and neither is a caller-supplied label such as an MCP client name or an AI session ID, only the channel it came through. -It is a JSON file in Application Support. No sync record type covers it, and no code path uploads it. It does not leave the Mac. +The hash chain is tamper evident, not tamper proof: anyone who can write the file can recompute every hash after the record they changed. It is a JSON file in Application Support, no sync record type covers it, and no code path uploads it. ### Where it lives on disk -Everything below is under `~/Library/Application Support/TablePro/` unless the path says otherwise. +Every path here is under `~/Library/Application Support/TablePro/` unless it says otherwise. | Path | What it holds | | --- | --- | | `connections.json` | Connection definitions: host, port, username, database, SSH and SSL settings. No passwords. | -| `connections.json.hmac` | The integrity tag for the file above. | +| `connections.json.hmac` | The integrity tag for `connections.json`. | | `known_hosts` | SSH host keys you accepted. | | `query_history.db` | The statements you ran, as text, with a full-text index. Parameter values are never recorded. | | `sql_favorites.db` | Saved queries and their folders. | | `ai_chats/` | One JSON file per AI conversation, including the context attached to each turn. | | `AIChatImages/` | Images pasted into chat. | -| `ExecutionAudit.json` | The execution log described above. | +| `ExecutionAudit.json` | The execution log described in this section. | | `mcp-audit.db` | The MCP activity log, 90 days, statements as digests. Mode `0600` inside a `0700` directory. | | `mcp-handshake.json` | The bound port and the bridge token, mode `0600`. | | `TabState/`, `RecentlyClosedTabs/` | Open and recently closed tabs, including their query text. | @@ -173,7 +169,7 @@ Everything below is under `~/Library/Application Support/TablePro/` unless the p ## Passwords and secrets -Secrets live in the keychain under the `com.TablePro` service, in the `D7HJ5TFYCU.com.TablePro.shared` access group, and the app asks for the data protection keychain: connection passwords, SSH passwords, key passphrases, SSL client key passphrases, TOTP secrets, plugin secure fields, AI provider keys, and the license key. MCP tokens are stored as a salted SHA-256 hash; the plaintext is shown once at creation and never written down. +Secrets live in the macOS keychain, under the `com.TablePro` service and in the data protection keychain: connection passwords, SSH passwords, key passphrases, SSL client key passphrases, TOTP secrets, plugin secure fields, AI provider keys, and the license key. MCP tokens are the exception, stored as a salted SHA-256 hash; the plaintext is shown once at creation and never written down. A connection can name a [password source](/features/connection-sharing#password-sources) instead of using the keychain: a file, an environment variable, a shell command, 1Password, HashiCorp Vault, or AWS Secrets Manager. Those resolve at connect time, on your machine, and are never synced, since a path or a command belongs to one Mac. diff --git a/docs/snippets/driver-counts.mdx b/docs/snippets/driver-counts.mdx new file mode 100644 index 000000000..f31eebad8 --- /dev/null +++ b/docs/snippets/driver-counts.mdx @@ -0,0 +1,2 @@ +Five drivers ship inside the app and cover nine databases. Another 17 come from the registry and +install on the first connection that needs them. See [Plugins & Themes](/features/plugins). diff --git a/docs/snippets/helper-port.mdx b/docs/snippets/helper-port.mdx new file mode 100644 index 000000000..e6a5d9336 --- /dev/null +++ b/docs/snippets/helper-port.mdx @@ -0,0 +1,7 @@ +| Option | What it does | Default | +| --- | --- | --- | +| **Choose port automatically** | Takes a free loopback port, and tries up to five times if one is claimed first | On | +| **Local port** | Pins a fixed port instead. There is no retry, so a port already in use fails the connect | - | + +TablePro polls that port and gives {binary} 30 seconds to answer on it. Past that the connect fails +and the error carries the last lines {binary} printed, which is where the real reason usually is. diff --git a/docs/snippets/mcp-gates.mdx b/docs/snippets/mcp-gates.mdx new file mode 100644 index 000000000..19d6616b5 --- /dev/null +++ b/docs/snippets/mcp-gates.mdx @@ -0,0 +1 @@ +Every {what} clears the same gates a tool call does: the token's `resources:read` scope, its connection allowlist, and the connection's **External Clients** level and AI policy. See [Scopes and gates](/external-api/mcp-tools#scopes-and-gates). diff --git a/docs/snippets/registry-plugin.mdx b/docs/snippets/registry-plugin.mdx new file mode 100644 index 000000000..edb93adae --- /dev/null +++ b/docs/snippets/registry-plugin.mdx @@ -0,0 +1,3 @@ +The {name} driver is not in the app. Picking {name} in the **Choose a Database** sheet offers the +download before the form opens, and opening a saved {name} connection installs it without asking. +**Settings > Plugins > Browse > {plugin}** installs it up front. See [Plugins](/features/plugins). diff --git a/docs/snippets/row-cap.mdx b/docs/snippets/row-cap.mdx new file mode 100644 index 000000000..8422af3e9 --- /dev/null +++ b/docs/snippets/row-cap.mdx @@ -0,0 +1,4 @@ +Your SQL reaches the server exactly as you wrote it. The cap applies to the rows read back, so a +query carrying its own `LIMIT`, `FETCH FIRST` or `TOP` is never touched, and `EXPLAIN`, `SHOW`, +writes and DDL are never capped. Both numbers are in **Settings > Data** +([Data settings](/customization/data-settings)). diff --git a/docs/snippets/staged-until-save.mdx b/docs/snippets/staged-until-save.mdx new file mode 100644 index 000000000..483490a95 --- /dev/null +++ b/docs/snippets/staged-until-save.mdx @@ -0,0 +1,2 @@ +Nothing here reaches the server on its own. {what} queue on the tab, the toolbar counts them, +**Preview SQL** (`Cmd+Shift+P`) shows the statements they will produce, and `Cmd+S` runs them. diff --git a/docs/switching.mdx b/docs/switching.mdx index 0ae20c5ec..bf335e99f 100644 --- a/docs/switching.mdx +++ b/docs/switching.mdx @@ -3,6 +3,8 @@ title: Switching to TablePro description: Move your connections from TablePlus, Sequel Ace, DBeaver, DataGrip, Beekeeper Studio or Navicat, what comes across, what does not, and how to get your shortcuts back --- +import DriverCounts from "/snippets/driver-counts.mdx"; + Most of a migration is one dialog. **File > Import from Other App…** reads the connections your current client already has, including groups and folders, and the source app does not need to be running. This page covers the rest: what does not come across, and what to do about it. @@ -14,10 +16,13 @@ This page covers the rest: what does not come across, and what to do about it. **File > Import from Other App…** - Navicat is the one exception: export first with **File > Export Connections** in Navicat, with **Export Password** turned on, then point TablePro at the `.ncx` file. + TablePlus, Sequel Ace, DBeaver, DataGrip and Beekeeper Studio are read in place. Navicat is the exception: export first with **File > Export Connections** in Navicat, with **Export Password** turned on, then point TablePro at the `.ncx` file. - Uncheck anything you don't want, resolve duplicates, click **Import**. + Deselect anything you do not want, resolve duplicates, click **Import**. + + + Connect to a connection that had a password. That is the only check that proves the credential came across, and deleting the source app is the one step you cannot undo. @@ -31,37 +36,25 @@ This page covers the rest: what does not come across, and what to do about it. Import preview listing the connections found -| Coming from | Databases | Passwords | -|-------------|-----------|-----------| -| TablePlus | MySQL, PostgreSQL, MongoDB, SQLite, Redis, and more | From Keychain | -| Sequel Ace | MySQL | From Keychain | -| DBeaver | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from config file | -| DataGrip | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | From Keychain or `c.kdbx` | -| Beekeeper Studio | MySQL, PostgreSQL, SQLite, SQL Server, Oracle, and more | Decrypted from its `app.db` store | -| Navicat | MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, Oracle, MongoDB | Decrypted from `.ncx` file | - -SSH tunnel and SSL settings come across with the connection. See [Connection Sharing](/features/connection-sharing) for the full detail on each source. +SSH tunnel and SSL settings come across with the connection. Which engines each source carries, and the quirks of each one, are in [Connection Sharing](/features/connection-sharing). ## About passwords -This is the part of any migration that goes wrong, so it is worth knowing which case you are in before you start. - -**Passwords in a file.** DBeaver, Beekeeper Studio and Navicat keep credentials in their own encrypted store. TablePro decrypts it and the import runs start to finish with nothing to click. - -**Passwords in the macOS Keychain.** TablePlus, Sequel Ace and DataGrip store each password as a separate Keychain item, and macOS asks permission per item. On a large connection list that is a lot of prompts. Choose **Always Allow** rather than **Allow** and the run finishes without asking again. +| Where your old client keeps them | What the import does | +|---|---| +| Its own encrypted store: DBeaver, Beekeeper Studio, Navicat | Decrypts it and runs start to finish with nothing to click | +| The macOS Keychain: TablePlus, Sequel Ace, DataGrip | macOS asks permission once per item. Choose **Always Allow** rather than **Allow** and the run stops asking | A DataGrip project protected by a master password cannot be read at all. Import those connections and set their passwords in TablePro. ## What does not come across -Be aware of these before you delete the old app. - | Not imported | What to do | |--------------|------------| | Saved queries and snippets | Re-save the ones you actually use as [Favorites](/features/favorites) with `Cmd+D`. To keep a whole folder of `.sql` files in sync instead, including one tracked in git, [Linked Folders](/features/connection-sharing) does that and needs a Starter license | | Query history | Starts fresh. TablePro records history per connection from the first query you run, searchable with `Cmd+Y` | | Column widths, sort order and per-table filters | Rebuilt as you browse. TablePro saves these per table on its own | -| Keyboard shortcuts | See below | +| Keyboard shortcuts | See the next section | ## Getting your shortcuts back @@ -74,7 +67,7 @@ Where yours differ, rebind them in **Settings > Keyboard** (`Cmd+,`). Record the Keyboard settings with the shortcut recorder -These are the ones worth setting up on your first day, because they are the jobs you do constantly: +Set these up on your first day: | Job | TablePro | |-----|----------| @@ -88,7 +81,7 @@ These are the ones worth setting up on your first day, because they are the jobs | Explain the query | `Cmd+Option+E` | | Save the query as a favorite | `Cmd+D` | -A few cannot be reassigned, so it is better to learn them than to fight them: editor built-ins such as `Cmd+/` for comment, `Cmd+[` and `Cmd+]` for indent, tab selection `Cmd+1` through `Cmd+9`, text size `Cmd+=` and `Cmd+-`, and `Cmd+F` for find. The recorder tells you when you hit one. +A few cannot be reassigned, so learn them rather than fight them: editor built-ins such as `Cmd+/` for comment, `Cmd+[` and `Cmd+]` for indent, tab selection `Cmd+1` through `Cmd+9`, text size `Cmd+=` and `Cmd+-`, and `Cmd+F` for find. The recorder tells you when you hit one. If you use Vim bindings, turn on [Vim Mode](/features/vim-mode) in **Settings > Editor** and most of this section stops mattering. @@ -109,7 +102,7 @@ Two other routes exist. ## If your database is not supported yet -TablePro ships 5 drivers inside the app and installs 17 more from the plugin registry on demand, so a database missing from the connection form may still be one click away in **Settings > Plugins > Browse**. + A database missing from the connection form may still be one click away in **Settings > Plugins > Browse**. Plugin registry browser diff --git a/docs/troubleshooting.mdx b/docs/troubleshooting.mdx index 88f88be27..2543d8666 100644 --- a/docs/troubleshooting.mdx +++ b/docs/troubleshooting.mdx @@ -3,42 +3,57 @@ title: Troubleshooting description: Symptom-first checks for launch, connection, plugin, tunnel and save failures --- -Everything on this page applies to every connection. A failure that only happens on one engine is on that engine's page, and each of those ends with its own Troubleshooting section. +import RowCap from "/snippets/row-cap.mdx"; + +Find your symptom, then match your error string against the headings in that section. Every heading in quotes is what the app or the server printed, unchanged, so searching for yours works. + +- Will not launch, quits at launch, Gatekeeper: [the app will not open](#the-app-will-not-open) +- Refused, timed out, stuck on Connecting: [a connection fails](#a-connection-fails) +- A certificate is rejected: [TLS and certificates](#tls-and-certificates) +- A driver is missing, rejected, or stale: [a plugin will not install or load](#a-plugin-will-not-install-or-load) +- The tunnel never comes up: [SSH tunnel failures](#ssh-tunnel-failures) +- Rows are missing, or a query is cancelled under you: [a query is slow](#a-query-is-slow-or-the-results-stop-short) +- A save is refused: [edits will not save](#edits-will-not-save) +- You need the log, or you are filing a bug: [logs and diagnostics](#logs-and-diagnostics) + +A failure that happens on one engine only is on that engine's page, which ends with its own Troubleshooting section. ## The app will not open -- **Wrong architecture**: release assets are per-architecture, `TablePro--arm64.dmg` and the `x86_64` one. Check yours under **Apple menu > About This Mac**. +- **Wrong architecture**: assets are per-architecture, `TablePro--arm64.dmg` and the `x86_64` one. Check yours under **Apple menu > About This Mac**. - **Too old a macOS**: 14.0 (Sonoma) is the minimum. -- **macOS refuses to open it**: the DMG and the app inside it are both signed, notarized and stapled, so a Gatekeeper block means the copy was damaged or stripped of its signature. Check with `spctl -a -vvv /Applications/TablePro.app`, then re-download or run `brew install --cask tablepro`. -- **Opens then quits**: look for the crash report in Console.app and attach it to an issue. +- **macOS refuses to open it**: the copy was damaged or stripped of its signature. Confirm with `spctl -a -vvv /Applications/TablePro.app`, then re-download or run `brew install --cask tablepro`. +- **Opens then quits**: attach the crash report from Console.app to an issue. +- **Opens straight into a connection**: startup is on **Reopen Last Session**. Switch to **Show Welcome Screen** in **Settings > General**. ## A connection fails -A failed connect paints the window with **Could not connect to ``** and the driver's own message below it, alongside **Try Again**, **Manage Connections…**, and **Copy Details**. +A failed connect paints the window with **Could not connect to `…`**, the driver's own message, and three buttons: **Try Again**, **Manage Connections…**, **Copy Details**. - **Refused** means something answered and said no: the server is not listening on that port, or it is bound to `127.0.0.1` only. **Timed out** means nothing answered, which points at a firewall, a security group or a VPN. Tell them apart with `nc -zv host port`. -- On macOS 15 and later, a database at a local network address (`192.168.x.x`, `.local`) needs the Local Network permission. Check TablePro under **System Settings > Privacy & Security > Local Network**. Loopback and internet hosts are not affected. +- **Still on Connecting**: after 12 seconds on one step the pane says "This is taking longer than usual." **Cancel** gives up at once and drops that connection from **Reopen Last Session**; a connect that fails on its own keeps its place there. +- On macOS 15 and later, a database at a local network address (`192.168.x.x`, `.local`) needs the Local Network permission. Grant it to TablePro under **System Settings > Privacy & Security > Local Network**. Loopback and internet hosts are unaffected. - Authentication failures are engine-specific. See [PostgreSQL](/databases/postgresql), [MySQL](/databases/mysql) or [SQL Server](/databases/mssql). ## TLS and certificates -A handshake failure is reported with the cause named and a specific SSL Mode to switch to, with the driver's original error underneath. The ladder is Disabled, Preferred, Required, Verify CA, Verify Identity. Every handshake error string and the mode that fixes it is on [SSL/TLS](/connections/ssl). +A handshake failure names the cause and the SSL Mode to switch to, with the driver's original error underneath. The ladder is Disabled, Preferred, Required, Verify CA, Verify Identity. Every handshake error string and the mode that fixes it is on [SSL/TLS](/connections/ssl). ## A plugin will not install or load -Every plugin is checked before it loads: a registry download must match the SHA-256 in the manifest, and the bundle must carry a valid Developer ID signature. A plugin that fails appears as a banner in **Settings > Plugins > Installed** with the reason. +A plugin that fails any of its checks appears as a banner in **Settings > Plugins > Installed** with the reason. ### "Plugin code signature verification failed: …" -The bundle is unsigned, ad-hoc signed, or was altered after signing. Unsigned and ad-hoc bundles are refused outright and there is no override. Download it again from the registry or from its developer. +The bundle is unsigned, ad-hoc signed, or altered after signing, and there is no override. Download it again from the registry or from its developer. ### "Plugin checksum does not match expected value" -The downloaded file does not match the SHA-256 the registry manifest lists. Retry the install. If it repeats, the registry entry and the file disagree, so file an issue. +The download does not match the SHA-256 in the registry manifest. Retry the install. A second failure means the manifest and the file disagree, so file an issue. ### "Plugin was built for PluginKit version …; this release of TablePro needs version …" -An app update raised the driver interface version and this plugin predates it. TablePro fetches a compatible build on its own, retrying after 30 seconds, then 5 and 10 minutes, up to five attempts. Click **Update Now** in the banner, or **Update TablePro** when the plugin needs a newer app. +This plugin predates the app's current driver interface. TablePro fetches a compatible build on its own, retrying after 30 seconds, then 5 and 10 minutes, five attempts in all. Click **Update Now** in the banner, or **Update TablePro** when the plugin needs a newer app. ### "Couldn't reach the plugin registry" @@ -48,61 +63,57 @@ A network problem, not a bad plugin. Retry once you are online. A saved connection's driver is missing. The failure screen offers **Install Plugin…**. -User-installed plugins live in `~/Library/Application Support/TablePro/Plugins`. macOS cannot fully unload plugin code, so after removing one a banner offers **Quit & Reopen**. See [Plugins & Themes](/features/plugins). +Removing a plugin needs a relaunch, offered as **Quit & Reopen**. Plugins you installed live in `~/Library/Application Support/TablePro/Plugins`. See [Plugins & Themes](/features/plugins). ## SSH tunnel failures ### "SSH host key verification failed" -The server presented a key that does not match the stored one, or you declined the prompt. Trusted keys live in `~/Library/Application Support/TablePro/known_hosts`. Accept a changed key only when you know why it changed, such as a rebuild or a key rotation. +The server presented a key that does not match the stored one, or you declined the prompt. Trusted keys are in `~/Library/Application Support/TablePro/known_hosts`. Accept a changed key only when you know what changed it, such as a rebuild or a rotation. ### "No available local port for SSH tunnel" -The tunnel binds a local port between 60000 and 65000 and found none free. List what is holding them with `lsof -nP -iTCP -sTCP:LISTEN`. Quit those processes and reconnect. +The tunnel binds a local port between 60000 and 65000 and found none free. List what holds them with `lsof -nP -iTCP -sTCP:LISTEN`, quit those processes, reconnect. Failures on the far side start with "The SSH server could not reach …". Those and the authentication rejections are in [SSH Tunneling](/connections/ssh-tunneling). ## A query is slow, or the results stop short -- A `SELECT` with no `LIMIT` of its own stops at 10,000 rows and shows a **Fetch All** button. **Execute Without Limit** skips the cap for one run. The SQL sent to the server is always what you typed; only the rows read back are capped. Change it in **Settings > Data**. -- Browsing a table pages 1,000 rows at a time, which is a separate setting on the same pane. -- A query is cancelled after 60 seconds by default. Cancel one yourself with `Cmd+.`. The limit is under **Settings > General**. + + +A capped result offers **Fetch All** in the status bar, and **Query > Execute Query Without Limit** runs one query with the cap off. Browsing a table pages instead of capping. - -The query timeout applies to new connections. Change it and reconnect, or the old value stays in force with nothing to say so. - +A query that runs past the timeout in **Settings > General** is cancelled ([Settings](/customization/settings)); cancel one yourself with `Cmd+.`. A changed timeout reaches new connections only, so reconnect after changing it or the old value stays in force with nothing to say so. ## Edits will not save - The connection is at the **Read-Only** [safe mode](/features/safe-mode) level, which disables cell editing, row changes and import. -- Query results are editable only when they come from one table. Joins, comma joins, subqueries in `FROM`, CTEs, `UNION`, `EXCEPT` and `INTERSECT` are read-only. -- A column renamed with `AS` cannot be written back. A query that renames or omits the primary key blocks the whole save, because there is nothing to match the row on. +- A result is editable only when it came from one table. Joins, comma joins, subqueries in `FROM`, CTEs, `UNION`, `EXCEPT` and `INTERSECT` are read-only. +- Select the primary key, unaliased. A key that is renamed with `AS` or left out blocks the whole save. - Generated columns are left out of every `INSERT` and `UPDATE`, so a value typed into one never reaches the server. -- A table with no primary key is matched on every original column value. The save fails when that still identifies no row, which usually means someone else changed it first. +- A table with no primary key is matched on every original column value, and the save fails when that matches no row. Someone else usually edited it first. See [Change Tracking](/features/change-tracking) for what the save actually runs. ## Logs and diagnostics -Logging goes to OSLog under the subsystem `com.TablePro`. Some drivers use their own: `com.TablePro.PostgreSQLDriver`, `com.TablePro.RedisDriver`, `com.TablePro.CassandraDriver`, `com.TablePro.OracleDriver`. Filter on the subsystem in Console.app, or read the last half hour in Terminal: +Logging goes to OSLog under `com.TablePro`, with four drivers on subsystems of their own: `com.TablePro.PostgreSQLDriver`, `com.TablePro.RedisDriver`, `com.TablePro.CassandraDriver`, `com.TablePro.OracleDriver`. Filter on the subsystem in Console.app, or read the last half hour in Terminal: ```bash log show --predicate 'subsystem BEGINSWITH "com.TablePro"' --last 30m ``` -Oracle connections get a diagnostic sheet on a recognized driver error, with **Copy Diagnostic Info** and **Open Issue Tracker**. The copied block names the target, the user and the error. No other driver builds one. +Oracle alone adds a diagnostic sheet on a recognized driver error, with **Copy Diagnostic Info** and **Open Issue Tracker**. Application data is in `~/Library/Application Support/TablePro`, preferences in `~/Library/Preferences/com.TablePro.plist`. **Settings > General** has **Reset All Settings to Defaults**. - -Neither button copies a password; those stay in the macOS Keychain. **Copy Details** carries the host, the database name, and the SSH host if there is one, plus the port when it is not the driver's default. **Copy Diagnostic Info** carries the user name as well. - +Neither copy button includes a password. **Copy Details** carries the host, the database name, the SSH host, and any non-default port; **Copy Diagnostic Info** adds the user name. ## Filing a bug report -**Help > Report an Issue** opens the [issue tracker](https://github.com/TableProApp/TablePro/issues). A report that gets fixed quickly has: +**Help > Report an Issue** opens the [issue tracker](https://github.com/TableProApp/TablePro/issues). A report that gets fixed quickly carries: -1. The TablePro version from **TablePro > About TablePro**, your macOS version, and whether the Mac is Apple Silicon or Intel. -2. The database engine and server version, plus the plugin version from **Settings > Plugins > Installed**. -3. The error text pasted from **Copy Details** or **Copy Diagnostic Info**, not retyped. -4. What you ran, what happened, and what you expected instead. Add the matching log lines when the failure left no dialog. +- The version from **TablePro > About TablePro**, your macOS version, and whether the Mac is Apple Silicon or Intel +- The engine and server version, plus the plugin version from **Settings > Plugins > Installed** +- The error pasted from **Copy Details** or **Copy Diagnostic Info**, not retyped +- What you ran, what happened, and what you expected. Add the log lines when the failure left no dialog