Using airtable2 with DBI

Introduction

airtable2 provides a DBI interface so you can use the familiar DBI::dbConnect(), DBI::dbReadTable(), DBI::dbWriteTable(), and related functions to interact with Airtable bases. This makes it easy to integrate Airtable into existing R workflows that expect standard database connections, and enables the RStudio and Positron connection pane for point-and-click table browsing.

The DBI interface covers the most common database operations, but Airtable is not an SQL database, though it has many relational database features. Some functionality is intentionally absent. Capabilities: reading tables, listing tables and fields, appending records, and overwrite-syncing records. Limitations: no SQL queries (use Airtable formula syntax instead), no table creation or deletion via DBI, no transactions, and no dbplyr lazy tbl() support (deferred). For the full feature set — type coercion, attachments, multi-select, upsert by key — use the air_* functions directly.

Connecting

The primary way to open a DBI connection to Airtable is air_connect(). You can connect to a single base by name or ID, or open a multi-base connection that exposes all accessible bases as schemas.

library(airtable2)
library(DBI)

# Connect to a specific base by name
con <- air_connect(base = "BollardsForArt")

# Connect to a specific base by ID (replace with your own base ID)
con <- air_connect(base = "appXXXXXXXXXXXXXX")  # placeholder — use your base ID

# Using the raw DBI interface directly
con <- DBI::dbConnect(airtable2(), base_id = "appXXXXXXXXXXXXXX")

# Connect to all bases (multi-base mode — all bases appear as schemas)
con_all <- air_connect()

# Open in the RStudio / Positron connection pane
air_pane(base = "appXXXXXXXXXXXXXX")

# Always disconnect when done
DBI::dbDisconnect(con)

If you have set a default base via air_set_base() or the AIRTABLE_BASE_ID environment variable, air_connect() will use it when called with no arguments.

Exploring the connection

Once connected, standard DBI introspection functions work as expected:

# List all tables in the base
DBI::dbListTables(con)
#> [1] "Artists"  "Grants"   "Projects"

# Check whether a table exists
DBI::dbExistsTable(con, "Artists")
#> [1] TRUE

# List the fields (columns) of a table
DBI::dbListFields(con, "Artists")
#> [1] "airtable_id"           "airtable_created_time" "Name"
#> [4] "Age"                   "Active"                "Role"
#> [7] "Disciplines"           "Member Since"          "Email"

Note that every table includes the airtable_id and airtable_created_time columns, which are Airtable’s internal record identifier and creation timestamp.

Reading data

DBI::dbReadTable() reads an entire table and returns a data frame:

# Read the entire Artists table
artists <- DBI::dbReadTable(con, "Artists")
artists
#>   airtable_id airtable_created_time          Name Age Active              Role
#> 1 recABCDEFGH 2026-05-01 10:00:00   Zara Okonkwo  33   TRUE Guerilla Muralist
#> ...

For filtered queries, use DBI::dbSendQuery() with Airtable’s formula syntax (not SQL). The query string is of the form "TableName WHERE <formula>":

# Filtered query using Airtable formula syntax
res <- DBI::dbSendQuery(con, "Artists WHERE {Active} = 1")
active_artists <- DBI::dbFetch(res)
DBI::dbClearResult(res)

# The high-level air_read() helper is equivalent and more idiomatic
active_artists2 <- air_read("Artists", formula = "{Active} = 1")

Both approaches return the same result. air_read() also supports sorting, field selection, and type coercion via the table schema.

Writing data

Use DBI::dbWriteTable() to add or sync records. The append and overwrite arguments control the write mode:

# Append new records (append = TRUE is required to add rows)
new_artists <- data.frame(
  Name = c("Frida K.", "Diego R."),
  Age  = c(47L, 53L)
)
DBI::dbWriteTable(con, "Artists", new_artists, append = TRUE)
#> Inserted 2 records.

# Sync / overwrite: upserts by the first column as the key,
# and deletes any Airtable records not present in updated_artists
updated_artists <- data.frame(
  Name = c("Frida K.", "Diego R.", "Georgia O."),
  Age  = c(47L, 53L, 98L)
)
DBI::dbWriteTable(con, "Artists", updated_artists, overwrite = TRUE)
#> Sync complete: 1 created, 2 updated, 0 deleted, 0 unchanged.

overwrite = TRUE maps to air_sync() under the hood: it upserts records whose key (first column) matches and deletes records that are absent from the supplied data frame. Use air_upsert() directly if you want upsert-without- delete semantics.

Multi-base mode

When air_connect() is called without a base argument, it opens a multi-base connection. In this mode all accessible bases are exposed as DBI schemas, and table names use a "BaseName.TableName" dotted format:

# Open a connection to all accessible bases
con_all <- air_connect()

# Tables are listed in "BaseName.TableName" format
DBI::dbListTables(con_all)
#> [1] "BollardsForArt.Artists"  "BollardsForArt.Grants"
#> [3] "BollardsForArt.Projects" "OtherBase.Contacts"
#> ...

# Check existence and read using the dotted name
DBI::dbExistsTable(con_all, "BollardsForArt.Artists")
#> [1] TRUE

DBI::dbReadTable(con_all, "BollardsForArt.Artists")

# Alternatively, use DBI::Id() for qualified schema + table names
DBI::dbReadTable(con_all, DBI::Id(schema = "BollardsForArt", table = "Artists"))

DBI::dbDisconnect(con_all)

Limitations

The DBI interface provides a convenient entry point, but it does not expose the full power of airtable2. Key limitations:

  • No SQL. Airtable does not have a SQL engine. DBI::dbSendQuery() accepts Airtable formula syntax ("TableName WHERE <formula>"), not SQL.
  • No table creation or deletion. DBI::dbCreateTable() and DBI::dbRemoveTable() are not implemented. Use the Airtable web UI or the low-level at_create_table() / at_delete_table() functions for these operations.
  • No transactions. DBI::dbBegin(), DBI::dbCommit(), and DBI::dbRollback() are not supported.
  • No dbplyr lazy tables. dplyr::tbl(con, "Artists") is not yet supported.

For the full feature set, use the air_* high-level functions:

  • air_read() — read with formula filtering, sorting, field selection, and type coercion
  • air_write() — create new records, optionally creating missing fields
  • air_upsert() — upsert by key without deleting unmatched records
  • air_sync() — diff-based sync with optional deletion of missing records