# Database connectors: delete support and native SQL queries

> Delete rows through TaxiQL with @DeleteOperation, and run hand-written SQL - joins, aggregations, dialect-specific statements - with @SqlQuery.

Source: https://orbitalhq.com/changelog/2026-07-16-jdbc-deletes-and-native-sql
Date: 2026-07-16
Version: 0.38.0

_Available since 0.38.0-M2_

Two additions to our database connectors in this release: deleting rows through TaxiQL, and running SQL
you've written yourself.

## Deleting rows

Databases connected to Orbital have long supported reads, inserts, updates and upserts. Deletes now join
the family - annotate a `write operation` with `@DeleteOperation`:

```taxi
@DatabaseService(connection = "customers-database")
service CustomerService {
   @DeleteOperation
   write operation deleteCustomer(Customer):DeleteResult

   @DeleteOperation
   write operation deleteCustomers(Customer[]):DeleteResult
}
```

Rows are matched on the model's `@Id` fields (composite keys work too - rows match on the combination),
and you can pass a single instance or a whole array. Invoking it looks like any other mutation:

```taxi
given {
   customers : Customer[] = [
      { id : 123 },
      { id : 456 }
   ]
}
call CustomerService::deleteCustomers
```

You get back a `deletedCount` telling you how many rows went away. Deletes run in a single transaction,
so a failure partway through rolls the whole thing back rather than leaving you half-deleted.

## Native SQL queries

Orbital's `table` operations cover most day-to-day querying, and keep your queries portable across
databases. But sometimes you just want to write the SQL yourself - a join, an aggregation, or something
dialect-specific. The new `@SqlQuery` annotation is that escape hatch:

```taxi
@DatabaseService(connection = "films-database")
service FilmStats {
   @SqlQuery(sql = """
      SELECT d.name AS director, COUNT(f.id) AS filmCount
      FROM film f
      JOIN director d ON f.director_id = d.id
      GROUP BY d.name
   """)
   operation filmsPerDirector() : DirectorFilmSummary[]
}
```

The SQL is passed to your database exactly as written, and result columns map back to your Taxi model's
fields by name or alias. One annotation covers both directions: a plain `operation` runs the SQL as a
query, while a `write operation` runs it as a statement (`UPDATE`, `DELETE`, `INSERT`) and returns the
number of rows affected.

Operation parameters bind into the SQL with the `:parameterName` syntax:

```taxi
@SqlQuery(sql = "SELECT title, release_year AS releaseYear FROM film WHERE release_year > :year")
operation recentFilms(year : ReleaseYear) : Film[]
```

Values are always bound as prepared-statement parameters - never spliced into the SQL text - so
user-supplied values can't alter your query.

Both features work across all of Orbital's SQL database connectors. Full details are in the
[database docs](/docs/describing-data-sources/databases#deleting-data-from-a-database), including
[native query behaviour notes](/docs/describing-data-sources/databases#running-native-sql-queries).
