diff --git a/connections/connection-form.mdx b/connections/connection-form.mdx
index 929c3841..f36c71f0 100644
--- a/connections/connection-form.mdx
+++ b/connections/connection-form.mdx
@@ -171,6 +171,7 @@ Metadata connections are the extra ones opened to read a database's object list
| [Beancount](/databases/beancount) | File | No | No | No | No | No | No |
| [DynamoDB](/databases/dynamodb) | AWS API | No | No | No | No | No | No |
| [BigQuery](/databases/bigquery) | Cloud API | No | No | No | No | No | No |
+| [Bigtable](/databases/bigtable) | Cloud API | No | No | No | No | No | No |
| [Spanner](/databases/spanner) | Cloud API | No | No | No | No | No | No |
| [Firestore and Firebase Realtime Database](/databases/firebase) | Cloud API | No | No | No | No | No | No |
| [Cloudflare D1](/databases/cloudflare-d1) | Cloud API | No | No | No | No | No | No |
diff --git a/connections/ssh-tunneling.mdx b/connections/ssh-tunneling.mdx
index 42b51f3c..69add423 100644
--- a/connections/ssh-tunneling.mdx
+++ b/connections/ssh-tunneling.mdx
@@ -37,7 +37,7 @@ To share one SSH config across connections, save it with **Save Current as Profi
-**SSH Tunnel** is not offered on SQLite, DuckDB, PGlite, libSQL, Turso, Beancount, BigQuery, Spanner, Cloudflare D1, Cloudflare R2 SQL, DynamoDB, or Snowflake: each is reached over a local file, a loopback socket, or a vendor HTTP API. For SAP HANA, forward the port yourself, as its [Limitations](/databases/sap-hana#limitations) show.
+**SSH Tunnel** is not offered on SQLite, DuckDB, PGlite, libSQL, Turso, Beancount, BigQuery, Bigtable, Spanner, Cloudflare D1, Cloudflare R2 SQL, DynamoDB, or Snowflake: each is reached over a local file, a loopback socket, or a vendor HTTP API. For SAP HANA, forward the port yourself, as its [Limitations](/databases/sap-hana#limitations) show.
## Authentication methods
diff --git a/connections/ssl.mdx b/connections/ssl.mdx
index 58edea42..151534d4 100644
--- a/connections/ssl.mdx
+++ b/connections/ssl.mdx
@@ -48,7 +48,7 @@ A new connection starts on the mode that matches the driver's own default, and s
| Trino | Disabled | Sends every request over HTTPS with no fallback, again like Required |
| SAP HANA | Verify Identity | Nothing. No fallback exists, so Preferred forces TLS exactly like Required |
| Oracle | Disabled | Connects in plain TCP, so it behaves like Disabled. A red warning appears under the picker; use Required to enforce TCPS |
-| Snowflake, BigQuery, Spanner, Firestore, Firebase Realtime Database, DynamoDB, Cloudflare D1, Cloudflare R2 SQL, libSQL / Turso | Always encrypted | These drivers are HTTPS and manage TLS themselves. No **SSL Mode** |
+| Snowflake, BigQuery, Bigtable, Spanner, Firestore, Firebase Realtime Database, DynamoDB, Cloudflare D1, Cloudflare R2 SQL, libSQL / Turso | Always encrypted | These drivers are HTTPS and manage TLS themselves. No **SSL Mode** |
| SQLite, DuckDB, Beancount, PGlite | None | Local files or an in-process engine. No **SSL Mode** |
## Ports that mean TLS
diff --git a/customization/general-settings.mdx b/customization/general-settings.mdx
index 77134a7d..07f43d1a 100644
--- a/customization/general-settings.mdx
+++ b/customization/general-settings.mdx
@@ -48,7 +48,7 @@ How many seconds a query runs before it is cancelled. The default is 60; the pic
A connection can override this value under **Options > Query timeout**. Leave that field empty to inherit this setting, or enter `0` for no limit. The same section has a separate connect timeout for tunnels, proxies and database sign-in; it does not change how long a query can run.
-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, `ob_query_timeout` on OceanBase. On SQLite it bounds how long a statement waits for a locked database instead. Drivers that talk HTTP (BigQuery, Spanner, Cloudflare D1, LibSQL, Etcd, DynamoDB, Elasticsearch, Typesense, 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. SAP HANA ends the statement with the same `ALTER SYSTEM CANCEL SESSION` that **Stop** sends.
+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, `ob_query_timeout` on OceanBase. On SQLite it bounds how long a statement waits for a locked database instead. Drivers that talk HTTP (BigQuery, Bigtable, Spanner, Cloudflare D1, LibSQL, Etcd, DynamoDB, Elasticsearch, Typesense, 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. SAP HANA ends the statement with the same `ALTER SYSTEM CANCEL SESSION` that **Stop** sends.
Older servers have neither: MySQL before 5.7.8, MariaDB before 10.1.1. A statement that passes the timeout there is stopped with `KILL QUERY` from a second connection, which reaches a `SELECT` on MySQL and any statement at all on MariaDB. A server behind a proxy that reports a version it does not have still gets the right one of the two.
diff --git a/databases/bigtable.mdx b/databases/bigtable.mdx
new file mode 100644
index 00000000..3d0f231a
--- /dev/null
+++ b/databases/bigtable.mdx
@@ -0,0 +1,141 @@
+---
+title: Google Cloud Bigtable
+description: Connect to Google Cloud Bigtable, browse rows by key, filter on keys and cells, and edit cells
+---
+
+import RegistryPlugin from "/snippets/registry-plugin.mdx";
+
+Every row comes back in ascending key order, with its key in `_key` and one column per `family:qualifier`.
+
+
+
+## Quick setup
+
+Click **New Connection…**, select **Google Cloud Bigtable**, pick an **Auth Method**, then click **Save & Connect**.
+
+The toolbar's **Instance** switcher moves between the project's instances without reconnecting, and an open tab stays on the instance it opened on. See [Tabs](/features/tabs#where-a-tab-points).
+
+## Connection settings
+
+| Field | Description |
+|-------|-------------|
+| **Auth Method** | **Service Account Key**, the default, or **Application Default Credentials** |
+| **Service Account Key** | A path to the JSON key file, or the JSON pasted in. Stored in the Keychain |
+| **Project ID** | The Google Cloud project. Empty takes the project the key names. Under Application Default Credentials, empty takes the credentials file's project, then `GOOGLE_CLOUD_PROJECT` |
+| **Instance ID** | The Bigtable instance. Empty opens the instance whose ID sorts first |
+| **App Profile ID** | Options tab. Routes reads and writes through that app profile. The instance's default profile when empty |
+
+**Project ID** and **Instance ID** stand in for a host, a port and the Database field. Any instance connects, no connection URL opens one, and [SSH Tunnel](/connections/ssh-tunneling) is not offered.
+
+## Authentication
+
+| Auth Method | What is sent |
+|-------------|--------------|
+| **Service Account Key** | An OAuth token signed with the key |
+| **Application Default Credentials** | The token from `gcloud auth application-default login`, or from the file `GOOGLE_APPLICATION_CREDENTIALS` names |
+
+Reading needs the Bigtable Reader role and editing the Bigtable User role, granted on the project or on the instance.
+
+## Browsing tables
+
+The sidebar lists the instance's tables. A grid page has a column for each `family:qualifier` its rows hold, so two pages can show different columns; the **Structure** tab lists the columns of the first 50 rows. In a column where the row has no cell, the grid reads **No Field** ([Editing a cell](/features/change-tracking#editing-a-cell)). Each cell shows its newest version.
+
+Keys, qualifiers and values are bytes. UTF-8 text with no control characters other than tab and newline shows as itself. A key or qualifier that is anything else, or text that starts with `b64:`, shows as `b64:` and its base64, and that form reads back to the same bytes: type it that way in a filter, a request line or a new row's `_key`. A value of that kind shows as hex. To filter on it, type it as `b64:` and its base64.
+
+A table's grid keeps key order: clicking a column header does not sort it. A request's results in the editor sort on screen when you click a header. A later page starts after the last key of the page before, and a page jumped to directly skips the rows before it by reading their keys alone.
+
+The status bar's total stays blank until **Count Exactly** reads the key of every row the filters admit. See [Pages and row counts](/features/data-grid#pages-and-row-counts).
+
+## Filtering
+
+Most filters on `_key` read only the keys they name:
+
+| Operators on `_key` | What Bigtable reads |
+|----------|--------------------|
+| equals, in list | Exact keys. A list splits on commas, or takes a JSON array such as `["a,b", "c"]` |
+| starts with | The key range of that prefix |
+| greater than, greater or equal, less than, less or equal, between | A key range |
+| not equals, not in list, contains, not contains, ends with, matches regex | A row key regex, over every row |
+
+With **Match Case** off, equals, in list and starts with on `_key` run as a row key regex over every row too.
+
+A filter on a `family:qualifier` column keeps or drops whole rows by that cell's newest value. A row without the cell matches only is NULL, so not equals, not contains and not in list leave it out too:
+
+| Operators on a column | What Bigtable runs |
+|----------|--------------------|
+| equals, not equals, contains, not contains, starts with, ends with, in list, not in list | A value regex |
+| matches regex | The pattern as written, in RE2 syntax. It has to match the whole value: `.*ann.*` finds `ann` anywhere, `ann` only the value `ann` |
+| greater than, greater or equal, less than, less or equal, between | A value range, compared byte by byte |
+| is empty, is not empty | An empty value regex |
+| is NULL, is not NULL | Whether the row has the cell |
+
+Values are compared as bytes, so `9` sorts after `10`. Under **Match any**, filters that are all equals, in list, starts with or a range on `_key` read only the keys each one names. Mix in any other filter and a range on `_key` is refused, while equals, in list and starts with on `_key` run as a row key regex over every row.
+
+In a **Raw Filter** row, write a `RowFilter` as the Bigtable REST reference shows it, bytes in base64; it runs unchanged. A cell filter such as `valueRegexFilter` trims each row to the matching cells and drops rows left with none. To keep or drop whole rows, wrap it in a `condition` whose `trueFilter` is `{"passAllFilter": true}`.
+
+## Editing
+
+Each changed row is one request, and its cells change together. A save has no transaction: when one row fails, the rows written before it stay written.
+
+| Change | Request |
+|--------|---------|
+| New, duplicated or pasted row | `INSERT` with the key you typed in `_key` and every cell you filled. Refused when a row already has that key |
+| Edited cells | `PUT` with the changed cells |
+| Cell set to NULL, or **Remove Field** | The cell set to `null` in the `PUT`, which deletes every version of it |
+| Deleted row | `DELETE` on the key |
+
+A new row needs a `_key` and at least one cell. **Duplicate** and pasted rows copy every cell but `_key`, which shows DEFAULT until you type a key. The grid offers only the columns the rows on the page hold, so an empty table shows `_key` alone: write a table's first row, or a cell under a column the page does not show, with `INSERT` or `PUT` in the request editor. The column family must already exist, and the **DDL** tab of the Structure tab lists them under `columnFamilies`.
+
+An existing row's key cannot change. The save is refused, so duplicate the row, give the copy the new key, and delete the old one. **Set DEFAULT** in the row inspector is refused on an existing row too; set the cell to NULL to delete it, or enter a value. Text typed into a cell or into `_key` that starts with `b64:` is written as the bytes its base64 decodes to. Cells are written with the server's clock.
+
+## Request editor
+
+| Request | What it does |
+|---------|--------------|
+| `GET events/user#42` | One row |
+| `SCAN events/user#?limit=20` | Rows whose key starts with the prefix: 100, or the `?limit=` given. `SCAN events` reads from the table's first row |
+| `READ events {"rowsLimit": "10"}` | A ReadRows body from the REST reference |
+| `INSERT events/user#42 {"profile:name": "Ann"}` | Creates the row. Refused when the key already has a row, and refuses `null` |
+| `PUT events/user#42 {"profile:name": "Ann"}` | Sets cells, and creates the row when the key has none. `null` deletes a cell, and `b64:` text writes bytes |
+| `DELETE events/user#42` | Deletes the row |
+| `DELETE events/user#42 profile:name` | Deletes that cell, every version of it |
+
+The table goes before the first slash, and the key is everything after it up to a space or a `?`. A key holding either goes in JSON quotes, `GET events/"a b?c"`, and a binary key as `b64:`. A column after `DELETE` runs to the end of the line, so it may hold spaces.
+
+A request does not get the row cap in [Settings > Data](/customization/data-settings): a `READ` without `rowsLimit` returns up to 5,000,000 rows, so give it one.
+
+For [Safe Mode](/features/safe-mode), `GET`, `SCAN` and `READ` are reads and every other request is a write. A `DELETE`, or a `PUT` that sets a cell to `null`, also raises the dangerous query warning on **Silent**.
+
+## SSL/TLS
+
+Rows go to `bigtable.googleapis.com` and instances and tables to `bigtableadmin.googleapis.com`, both over HTTPS only. [Per-driver defaults](/connections/ssl#per-driver-defaults) lists Bigtable among the drivers with no **SSL Mode**.
+
+## Limitations
+
+- No GoogleSQL. Use the request editor.
+- No reverse scans. Rows come in ascending key order only; to start late in a table, filter `_key` with greater than.
+- Only the newest version of each cell shows, in a `READ` result too. An edit adds a new version; removing a cell deletes every version of it.
+- Tables and column families cannot be created, changed, truncated or dropped. Use the Google Cloud console or `cbt`.
+- Export fixes its columns from the first 500 rows. Columns that appear only later go into an `__extra__` column as JSON, with bytes as `b64:` and base64.
+- Import is not available. Write rows with `INSERT` or `PUT` in the request editor.
+- A Bigtable connection synced to iPhone or iPad appears in the list but does not open there; open it on the Mac. See [Supported databases](/ios#supported-databases).
+
+## Troubleshooting
+
+### Permission denied: ….
+
+The message names what was refused, such as "read events" or "list instances". Grant Bigtable Reader to read or Bigtable User to edit. A refused "list instances" means the role sits on one instance, not the project: fill in **Instance ID** so the connection skips the listing.
+
+### Project … has no Bigtable instance.
+
+The project in the message lists no Bigtable instance. With **Project ID** empty, that project came from the key or the Application Default Credentials, which need not be the one holding Bigtable. Set **Project ID** to the project with the instance.
+
+### Row … already exists. Edit that row, or give the new one another key.
+
+A new, duplicated or pasted row, or an `INSERT` line, names a key another row already has. Change `_key` on the new row, or edit the existing row instead.
+
+## Related
+
+- [Filtering](/features/filtering)
+- [Safe Mode](/features/safe-mode)
+- [Import & Export](/features/import-export)
diff --git a/databases/index.mdx b/databases/index.mdx
index 222b1484..0aa7970c 100644
--- a/databases/index.mdx
+++ b/databases/index.mdx
@@ -1,11 +1,11 @@
---
title: Supported Databases
-description: All 39 engines TablePro connects to, their default ports, and which ones need a plugin
+description: All 40 engines TablePro connects to, their default ports, and which ones need a plugin
---
import DriverCounts from "/snippets/driver-counts.mdx";
-Thirty-nine engines, and every one of them is free to use. What differs between them is where the
+Forty 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.
@@ -17,6 +17,7 @@ driver comes from, not what the license covers.
| [Amazon Redshift](/databases/redshift) | 5439 | Built-in |
| [Beancount](/databases/beancount) | File-based | Plugin |
| [BigQuery](/databases/bigquery) | API-based | Plugin |
+| [Bigtable](/databases/bigtable) | API-based | Plugin |
| [Cassandra](/databases/cassandra) | 9042 | Plugin |
| [ClickHouse](/databases/clickhouse) | 8123 | Built-in |
| [Cloudflare D1](/databases/cloudflare-d1) | API-based | Plugin |
diff --git a/docs.json b/docs.json
index 11970a68..78ec9d68 100644
--- a/docs.json
+++ b/docs.json
@@ -248,6 +248,7 @@
"databases/etcd",
"databases/firebase",
"databases/bigquery",
+ "databases/bigtable",
"databases/kafka",
"databases/libsql",
"databases/mariadb",
diff --git a/features/change-tracking.mdx b/features/change-tracking.mdx
index 7d161658..9560ce5d 100644
--- a/features/change-tracking.mdx
+++ b/features/change-tracking.mdx
@@ -38,7 +38,7 @@ Double-click a cell, or press `Enter` on it. `Enter` commits the edit to the que
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 the server fills in, and date functions such as `NOW()` or `CURRENT_DATE` on date columns.
-On MongoDB, Firestore and the Firebase Realtime Database a document can lack a field altogether, which the grid shows as **No Field**. **Remove Field** on the same right-click menu deletes the field rather than storing NULL. See [missing fields and null](/databases/mongodb#missing-fields-and-null).
+On MongoDB, Firestore and the Firebase Realtime Database a document can lack a field altogether, and on Bigtable a row can lack a cell; the grid shows either as **No Field**. **Remove Field** on the same right-click menu deletes the field rather than storing NULL. Bigtable stores no NULL, so **NULL** and **Remove Field** both delete the cell. See [missing fields and null](/databases/mongodb#missing-fields-and-null).
## Adding and deleting rows
diff --git a/features/copy-objects.mdx b/features/copy-objects.mdx
index beb1c3d8..2d038e01 100644
--- a/features/copy-objects.mdx
+++ b/features/copy-objects.mdx
@@ -255,7 +255,7 @@ have none, so the command does not appear on them.
## Limitations
-Copying is SQL only. MongoDB, Redis, DynamoDB, Firestore, the Firebase Realtime Database, Elasticsearch,
+Copying is SQL only. MongoDB, Redis, DynamoDB, Firestore, the Firebase Realtime Database, Bigtable, Elasticsearch,
Typesense, Kafka, etcd and SurrealDB have no
`CREATE TABLE` and no row writer to copy through, so neither command appears on them, and neither
end of a copy may be one of them.
diff --git a/features/data-grid.mdx b/features/data-grid.mdx
index d8af2a95..354ab4ee 100644
--- a/features/data-grid.mdx
+++ b/features/data-grid.mdx
@@ -16,7 +16,7 @@ A narrow window trims that bar rather than cutting it off. The Columns and Filte
Click a header to cycle through the first direction, its opposite, 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. In a query tab that query is the one statement behind the result, never the rest of the script.
-A Cassandra or ScyllaDB table has no header sort, and a query result there sorts the rows it holds. See [Cassandra](/databases/cassandra#browsing-and-editing).
+A Cassandra, ScyllaDB or Bigtable table has no header sort, and a query result there sorts the rows it holds. See [Cassandra](/databases/cassandra#browsing-and-editing) and [Bigtable](/databases/bigtable#browsing-tables).
**Sort direction** in [Settings > Data](/customization/data-settings) decides which way that first click goes. It is Ascending out of the box; set it to Descending to open on the newest rows and reach them in one click.
@@ -138,7 +138,7 @@ A table tab pages. The status bar offers 5, 10, 20, 100, 500 or 1,000 rows per p
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.
-Cassandra, ScyllaDB and the Firebase Realtime Database have no estimate, and their count reads every partition or key, so a table there shows no total until you click **Count Exactly**.
+Cassandra, ScyllaDB, Bigtable and the Firebase Realtime Database have no estimate, and their count reads every partition or key, so a table there shows no total until you click **Count Exactly**.
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.
diff --git a/features/plugins.mdx b/features/plugins.mdx
index 7f70a3c6..bb2636f1 100644
--- a/features/plugins.mdx
+++ b/features/plugins.mdx
@@ -41,6 +41,7 @@ These install from the registry when you need them:
| Cloudflare R2 SQL | Cloudflare R2 SQL |
| DynamoDB | DynamoDB |
| BigQuery | BigQuery |
+| Bigtable | Google Cloud Bigtable |
| Spanner | Spanner |
| Firebase | Cloud Firestore, Firebase Realtime Database |
| Snowflake | Snowflake |
diff --git a/features/routines-triggers.mdx b/features/routines-triggers.mdx
index 9a5225e5..582bc7e7 100644
--- a/features/routines-triggers.mdx
+++ b/features/routines-triggers.mdx
@@ -27,7 +27,7 @@ Expand a schema and **Procedures**, **Functions**, and **Triggers** sit beside *
| DuckDB | No | Macros | No |
| SQLite, LibSQL, Cloudflare D1 | No | No | Yes |
| Redshift, CockroachDB, PGlite, SAP HANA | No | No | No |
-| MongoDB, Redis, Elasticsearch, Typesense, DynamoDB, etcd, SurrealDB, Trino, Beancount, Spanner, Firestore, Firebase Realtime Database | No | No | No |
+| MongoDB, Redis, Elasticsearch, Typesense, DynamoDB, etcd, SurrealDB, Trino, Beancount, Spanner, Firestore, Firebase Realtime Database, Bigtable | No | No | No |
A section appears only where the engine has that kind of object. An engine that has them and currently holds none keeps the section and shows **No items** inside it, so a database with no tables still lists its procedures and functions. A section whose list is still on its way shows **Loading…**, and one the server refused shows the error in place of its rows.
diff --git a/features/table-operations.mdx b/features/table-operations.mdx
index bbfc66f3..6e93ea85 100644
--- a/features/table-operations.mdx
+++ b/features/table-operations.mdx
@@ -65,7 +65,7 @@ The statement runs at once instead of joining the drop and truncate queue, so **
| SAP HANA | No | No | No |
| MongoDB | Yes | No | No |
-No other engine has a rename, so the item never appears on Cassandra, DynamoDB, Elasticsearch, etcd, Firestore, the Firebase Realtime Database, Kafka, Redis, SurrealDB, Typesense or Beancount. What resembles one there is a copy followed by a delete, and it changes record identity.
+No other engine has a rename, so the item never appears on Bigtable, Cassandra, DynamoDB, Elasticsearch, etcd, Firestore, the Firebase Realtime Database, Kafka, Redis, SurrealDB, Typesense or Beancount. What resembles one there is a copy followed by a delete, and it changes record identity.
**Rename Database** and **Rename Schema** are absent on the container the connection is browsing. Switch to another one first, then rename the one you left.
@@ -119,7 +119,7 @@ A MariaDB table declared `WITH SYSTEM VERSIONING` stays an ordinary table: row e
## Databases and schemas
-`Cmd+K` switches database, covered in [Managing Connections](/connections#switch-connections-and-databases). The switcher takes the engine's own vocabulary: Schema on Oracle and Spanner, 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.
+`Cmd+K` switches database, covered in [Managing Connections](/connections#switch-connections-and-databases). The switcher takes the engine's own vocabulary: Schema on Oracle and Spanner, Keyspace on Cassandra, Dataset on BigQuery, Instance on Bigtable, Namespace on SurrealDB. SQLite is file-based, so it points at the welcome window to open a different file.
diff --git a/features/table-structure.mdx b/features/table-structure.mdx
index ea275a2b..22245b29 100644
--- a/features/table-structure.mdx
+++ b/features/table-structure.mdx
@@ -280,7 +280,7 @@ Edit a field's **Name** to rename it in every document, or remove its row to rem
- **Check constraints and generated columns** have no field in the grid. Both show in the DDL tab; change them by running DDL yourself.
- **Cassandra / ScyllaDB**: add and drop column only, no index editing, no visual table creation.
- **ClickHouse**: the Indexes tab drops a data-skipping index but cannot add or change one. A save or a new table that tries is refused before anything runs. Run `ALTER TABLE … ADD INDEX … TYPE bloom_filter` in the SQL editor instead.
-- **Redshift, CockroachDB, BigQuery, Spanner, Elasticsearch, Typesense, SurrealDB, Beancount, Firestore, Firebase Realtime Database**: structure is read-only.
+- **Redshift, CockroachDB, BigQuery, Bigtable, Spanner, Elasticsearch, Typesense, SurrealDB, Beancount, Firestore, Firebase Realtime Database**: structure is read-only.
- **Redis, etcd, DynamoDB**: no table schema to edit.
## Refreshing
diff --git a/index.mdx b/index.mdx
index f4260076..e0494588 100644
--- a/index.mdx
+++ b/index.mdx
@@ -1,6 +1,6 @@
---
title: Introduction
-description: Native macOS database client for MySQL, PostgreSQL, SQLite, MongoDB, Redis, and 34 more
+description: Native macOS database client for MySQL, PostgreSQL, SQLite, MongoDB, Redis, and 35 more
---
import DriverCounts from "/snippets/driver-counts.mdx";
diff --git a/scripts/check-docs-against-source.py b/scripts/check-docs-against-source.py
index 0c392403..0a77a620 100755
--- a/scripts/check-docs-against-source.py
+++ b/scripts/check-docs-against-source.py
@@ -265,7 +265,7 @@ def check_heading_case(root: Path, docs: Path) -> list[str]:
the app's own strings, so a renamed control never leaves a stale allowlist behind.
"""
ui = set()
- sources = [root / "TablePro", root / "Plugins", root / "Packages" / "TableProCore" / "Sources"]
+ sources = [root / "TablePro", root / "Plugins", *sorted((root / "Packages").glob("*/Sources"))]
for swift in (path for source in sources for path in source.rglob("*.swift")):
ui |= set(re.findall(r'String\(\s*localized:\s*"((?:[^"\\]|\\.)*)"', swift.read_text()))
ui |= {re.sub(r"%(?:\d+\$)?(?:@|l{0,2}d)", "…", s) for s in ui}
diff --git a/snippets/driver-counts.mdx b/snippets/driver-counts.mdx
index f399ca53..54055a3f 100644
--- a/snippets/driver-counts.mdx
+++ b/snippets/driver-counts.mdx
@@ -1,3 +1,3 @@
-Five drivers ship inside the app and cover twelve databases. Twenty-four registry plugins cover the
-other twenty-seven and install on the first connection that needs one. See
+Five drivers ship inside the app and cover twelve databases. Twenty-five registry plugins cover the
+other twenty-eight and install on the first connection that needs one. See
[Plugins](/features/plugins).