> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reverse-ETL source query refusals: the single-SELECT rule and catalog kinds

> What CDP_SOURCE_QUERY_MULTI_STATEMENT_FORBIDDEN and CDP_SOURCE_KIND_NOT_YET_AVAILABLE mean when you author a warehouse source, why the guards exist, and how to rework your query or pick a live kind.

# Reverse-ETL source query refusals

When you create a warehouse source on `POST /api/v1/cdp/sources`, Orbit validates the query text and the connector kind before anything is persisted and before anything runs against your warehouse. Two refusals are specific to authoring sources; this page explains each one, what to do instead, and how to read the source catalog so you know up front which kinds are live. The destination half of reverse-ETL is covered by [the reverse-ETL destination model](https://docs.orbit.devotel.io/concepts/reverse-etl-destination-model.md) and the [warehouse exports guide](https://docs.orbit.devotel.io/guides/reverse-etl-warehouse-exports.md).

## 1. The single-SELECT rule

Every warehouse source query must pass three checks, in order:

1. It starts with `SELECT` or `WITH`. Source queries are read-only; anything else is refused with `VALIDATION_ERROR` and `details.reason: "CDP_SOURCE_QUERY_MUST_BE_SELECT"`.
2. It is exactly one statement. One trailing semicolon is accepted and stripped; any other semicolon anywhere in the query text refuses the request with `CDP_SOURCE_QUERY_MULTI_STATEMENT_FORBIDDEN` (HTTP 400).
3. It contains the `\{cursor\}` placeholder exactly once. `\{cursor\}` is the incremental watermark: each run binds your last-seen value to it and advances it to the maximum of your cursor column, so rows are synced once, not re-emitted every tick. Without it the request is refused with `VALIDATION_ERROR` and `details.reason: "CDP_SOURCE_QUERY_MISSING_CURSOR_PLACEHOLDER"`.

Why one statement, enforced this strictly: the scheduled sync runs the exact query text you saved against your warehouse connection, and it validates that text before it ever executes. Rejecting anything that is not a single read-only statement at create time is a fail-closed guarantee that a malformed or batched query can never partially execute on your warehouse mid-sync. The same checks run again when you update a source's query, so a source cannot drift into a multi-statement query after creation.

The multi-statement check is deliberately textual: it does not parse or tokenize your SQL, so a semicolon inside a string literal is rejected too. If a literal in your query needs one, rewrite the literal so it does not.

## 2. What to do instead of multiple statements

* **One source per query.** If you were chaining statements to read two tables, create two sources; each emits its own event name on its own schedule.
* **Move the staging into the warehouse.** If earlier statements built intermediate results (temporary tables, stepwise transforms), materialize the final result as a view in your warehouse and point a single `SELECT` at it. A `WITH ... SELECT` (common table expressions) covers most multi-step shaping inside one statement and is explicitly accepted.
* **Compose downstream.** A source emits one event per returned row. Joins between two synced tables happen either in the warehouse, before the query reads them, or in the downstream tooling that consumes the exported events, not in the source query.

A minimal accepted query looks like:

```sql theme={null}
SELECT id, updated_at, total
FROM orders
WHERE updated_at > COALESCE(\{cursor\}::timestamptz, '1970-01-01')
ORDER BY updated_at
LIMIT 1000
```

## 3. CDP\_SOURCE\_KIND\_NOT\_YET\_AVAILABLE: catalogued vs live

The source catalogs list every connector Orbit publishes, including connectors whose ingestion is not live yet. A not-yet-live connector is catalogued for discovery: the picker shows it with a **Request access** action instead of **Add source**, and creating a source with that kind over the API returns `CDP_SOURCE_KIND_NOT_YET_AVAILABLE` (HTTP 400). The refusal is not a defect in your request; it is the catalog's honest maturity signal holding at the API boundary — the connector row is published, ingestion for that kind is not accepting sources yet.

Kinds you can create and sync today: **postgres, bigquery, snowflake, and databricks**. The warehouse catalog currently lists **redshift** as coming soon; sending `kind: "redshift"` is the canonical way to see this refusal.

Where the live kinds are shown:

* `GET /api/v1/cdp/warehouse-source-catalog` returns the warehouse connectors with a `status` per row (`ga` = live, `coming_soon` = published but not yet persistable), and `GET /api/v1/cdp/warehouse-source-catalog/:connector_id/activation` reports whether a specific kind is creatable today.
* The source picker in the dashboard renders the same catalog: `ga` rows get **Add source**, `coming_soon` rows get **Request access**.
* The other source families follow the same pattern: `GET /api/v1/cdp/source-catalog` (cloud apps), `GET /api/v1/cdp/streaming-source-catalog` (streaming/SDK), and `GET /api/v1/cdp/event-bus-source-catalog` (event buses) each publish their connectors with the same statuses and activation checks.

## 4. Decision table

| Code | HTTP | Cause | Fix |
| - | - | - | - |
| `CDP_SOURCE_QUERY_MULTI_STATEMENT_FORBIDDEN` | 400 | `sql_query` contains more than one statement: a `;` anywhere other than a single trailing terminator, including inside string literals. | Ship one `SELECT` or `WITH ... SELECT` per source. Stage intermediates as warehouse views or CTEs; split multi-table reads into one source per query. |
| `CDP_SOURCE_KIND_NOT_YET_AVAILABLE` | 400 | The `kind` you sent is published in the source catalog but its ingestion is not live yet (today: `redshift`). | Create the source with a live kind (`postgres`, `bigquery`, `snowflake`, `databricks`), or use the catalog's request-access path for the coming-soon kind. |
| `VALIDATION_ERROR` (`details.reason: CDP_SOURCE_QUERY_MUST_BE_SELECT`) | 400 | `sql_query` does not start with `SELECT` or `WITH`. | Rewrite the query as a read-only `SELECT` / `WITH ... SELECT`. |
| `VALIDATION_ERROR` (`details.reason: CDP_SOURCE_QUERY_MISSING_CURSOR_PLACEHOLDER`) | 400 | `sql_query` has no `\{cursor\}` placeholder. | Add `\{cursor\}` exactly once, typically in the `WHERE` clause against your cursor column. |

## 5. FAQ

**Can I end my query with a semicolon?**
Yes. Exactly one trailing semicolon is stripped before the check runs. A second semicolon anywhere else refuses the request.

**Are CTEs allowed?**
Yes. `WITH ... SELECT` is a single statement and is explicitly accepted, so multi-step shaping that fits in common table expressions needs no workaround.

**Is the refusal at create time or at sync time?**
At create and update time. The guards run before the source is persisted, so a refused query never becomes a failing scheduled run; nothing has executed against your warehouse.

**Will a coming-soon kind become available?**
The catalog row flips to `ga` when ingestion goes live, and the picker's **Request access** action becomes **Add source** at the same moment. Poll the catalog endpoint or watch the picker rather than retrying `POST /api/v1/cdp/sources` against a coming-soon kind.

## See also

* [The reverse-ETL destination model](https://docs.orbit.devotel.io/concepts/reverse-etl-destination-model.md) — the destination-side capability model this page mirrors on the source side.
* [Reverse-ETL warehouse exports guide](https://docs.orbit.devotel.io/guides/reverse-etl-warehouse-exports.md) — configuring the warehouse side end to end.
* [CDP connectors and catalogs](https://docs.orbit.devotel.io/guides/cdp-connectors-catalogs.md) — browsing the source and destination catalogs.
* [Error codes reference](https://docs.orbit.devotel.io/reference/error-codes.md) — the generated registry rows for `CDP_SOURCE_QUERY_MULTI_STATEMENT_FORBIDDEN` and `CDP_SOURCE_KIND_NOT_YET_AVAILABLE`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.