Skip to content

Commit 7eba663

Browse files
feat: Add SQL queries support in /v1/sql endpoint (#9301)
* refactor(cubesql): Use &str instead of &String * refactor(backend-native): Extract create_session function * refactor(backend-native): Extract with_session function * refactor(cubesql): Extract QueryPlan::try_as_logical_plan * feat: Add SQL queries support in /v1/sql endpoint * Add docs * Remove mention of data_source and query_plan response fields from /v1/sql docs --------- Co-authored-by: Igor Lukanin <igor@cube.dev>
1 parent e19beb5 commit 7eba663

File tree

16 files changed

+1045
-209
lines changed

16 files changed

+1045
-209
lines changed

docs/pages/product/apis-integrations/queries.mdx

+12-3
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ The same query using the REST API syntax looks as follows:
142142
### Query with post-processing
143143

144144
**Queries with post-processing are specific to the [SQL API][ref-sql-api].**
145-
They are structured in such a way that a [regular query](#regular-query) is
145+
Generally, they are structured in such a way that a [regular query](#regular-query) is
146146
part of a `FROM` clause or a common table expression (CTE):
147147

148148
```sql
@@ -178,8 +178,17 @@ limited set of SQL functions and operators.
178178

179179
#### Example
180180

181-
See an example of a query with post-processing. In this query, we derive new
182-
dimensions, post-aggregate measures, and perform additional filtering:
181+
The simplest example of a query with post-processing:
182+
183+
```sql
184+
SELECT VERSION();
185+
```
186+
187+
This query invokes a function that is implemented by the SQL API and executed without
188+
querying the upstream data source.
189+
190+
Now, see a more complex example of a query with post-processing. In this query, we derive
191+
new dimensions, post-aggregate measures, and perform additional filtering:
183192

184193
```sql
185194
SELECT

docs/pages/product/apis-integrations/rest-api.mdx

+5-11
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,7 @@ accessible for everyone.
130130
| `data` | [`/v1/load`][ref-ref-load], [`/v1/sql`][ref-ref-sql] | ✅ Yes |
131131
| `graphql` | `/graphql` | ✅ Yes |
132132
| `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No |
133-
134-
<InfoBox>
135-
136-
Exception: `/livez` and `/readyz` endpoints don't belong to any scope. Access to
137-
these endpoints can't be controlled using API scopes.
138-
139-
</InfoBox>
133+
| No scope | `/livez`, `/readyz` | ✅ Yes, always |
140134

141135
You can set accessible API scopes _for all requests_ using the
142136
`CUBEJS_DEFAULT_API_SCOPES` environment variable. For example, to disallow
@@ -282,10 +276,10 @@ example, the following query will retrieve rows 101-200 from the `Orders` cube:
282276
[ref-conf-basepath]: /reference/configuration/config#basepath
283277
[ref-conf-contexttoapiscopes]:
284278
/reference/configuration/config#contexttoapiscopes
285-
[ref-ref-load]: /product/apis-integrations/rest-api/reference#v1load
286-
[ref-ref-meta]: /product/apis-integrations/rest-api/reference#v1meta
287-
[ref-ref-sql]: /product/apis-integrations/rest-api/reference#v1sql
288-
[ref-ref-paj]: /product/apis-integrations/rest-api/reference#v1pre-aggregationsjobs
279+
[ref-ref-load]: /product/apis-integrations/rest-api/reference#base_pathv1load
280+
[ref-ref-meta]: /product/apis-integrations/rest-api/reference#base_pathv1meta
281+
[ref-ref-sql]: /product/apis-integrations/rest-api/reference#base_pathv1sql
282+
[ref-ref-paj]: /product/apis-integrations/rest-api/reference#base_pathv1pre-aggregationsjobs
289283
[ref-security-context]: /product/auth/context
290284
[ref-graphql-api]: /product/apis-integrations/graphql-api
291285
[ref-orchestration-api]: /product/apis-integrations/orchestration-api

docs/pages/product/apis-integrations/rest-api/reference.mdx

+55-13
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,57 @@ values.
9999

100100
## `{base_path}/v1/sql`
101101

102-
Get the SQL Code generated by Cube to be executed in the database.
102+
Takes an API query and returns the SQL query that can be executed against the data source
103+
that is generated by Cube. This endpoint is useful for debugging, understanding how
104+
Cube translates API queries into SQL queries, and providing transparency to SQL-savvy
105+
end users.
103106

104-
| Parameter | Description |
105-
| --------- | ------------------------------------------------------------------------- |
106-
| query | URLencoded Cube [Query](/product/apis-integrations/rest-api/query-format) |
107+
Using this endpoint to take the SQL query and execute it against the data source directly
108+
is not recommended as it bypasses Cube's caching layer and other optimizations.
107109

108-
Response
110+
Request parameters:
109111

110-
- `sql` - JSON Object with the following properties
111-
- `sql` - Formatted SQL query with parameters
112-
- `order` - Order fields and direction used in SQL query
113-
- `cacheKeyQueries` - Key names and TTL of Cube data cache
114-
- `preAggregations` - SQL queries used to build pre-aggregation tables
112+
| Parameter, type | Description | Required |
113+
| --- | --- | --- |
114+
| `format`, `string` | Query format:<br/>`sql` for [SQL API][ref-sql-api] queries,<br/>`rest` for [REST API][ref-rest-api] queries (default) | ❌ No |
115+
| `query`, `string` | Query as an URL-encoded JSON object or SQL query | ✅ Yes |
116+
| `disable_post_processing`, `boolean` | Flag that affects query planning, `true` or `false` | ❌ No |
115117

116-
Example request:
118+
If `disable_post_processing` is set to `true`, Cube will try to generate the SQL
119+
as if the query is run without [post-processing][ref-query-wpp], i.e., if it's run as a
120+
query with [pushdown][ref-query-wpd].
121+
122+
<WarningBox>
123+
124+
Currently, the `disable_post_processing` parameter is not yet supported.
125+
126+
</WarningBox>
127+
128+
The response will contain a JSON object with the following properties under the `sql` key:
129+
130+
| Property, type | Description |
131+
| --- | --- |
132+
| `status`, `string` | Query planning status, `ok` or `error` |
133+
| `sql`, `array` | Two-element array (see below) |
134+
| `sql[0]`, `string` | Generated query with parameter placeholders |
135+
| `sql[1]`, <nobr>`array` or `object`</nobr> | Generated query parameters |
136+
137+
For queries with the `sql` format, the response will also include the following additional
138+
properties under the `sql` key:
139+
140+
| Property, type | Description |
141+
| --- | --- |
142+
| `query_type`, `string` | `regular` for [regular][ref-regular-queries] queries,<br/>`post_processing` for queries with [post-processing][ref-query-wpp],<br/>`pushdown` for queries with [pushdown][ref-query-wpd] |
143+
144+
For queries with the `sql` format, in case of an error, the response will only contain
145+
`status`, `query_type`, and `error` properties.
146+
147+
For example, an error will be returned if `disable_post_processing` was set to `true` but
148+
the query can't be run without post-processing.
149+
150+
### Example
151+
152+
Request:
117153

118154
```bash{outputLines: 2-6}
119155
curl \
@@ -124,7 +160,7 @@ curl \
124160
http://localhost:4000/cubejs-api/v1/sql
125161
```
126162

127-
Example response:
163+
Response:
128164

129165
```json
130166
{
@@ -464,4 +500,10 @@ Keep-Alive: timeout=5
464500
[ref-recipes-data-blending]: /product/data-modeling/concepts/data-blending#data-blending
465501
[ref-rest-api]: /product/apis-integrations/rest-api
466502
[ref-basepath]: /product/apis-integrations/rest-api#base-path
467-
[ref-datasources]: /product/configuration/advanced/multiple-data-sources
503+
[ref-datasources]: /product/configuration/advanced/multiple-data-sources
504+
[ref-sql-api]: /product/apis-integrations/sql-api
505+
[ref-rest-api]: /product/apis-integrations/rest-api
506+
[ref-data-sources]: /product/configuration/advanced/multiple-data-sources
507+
[ref-regular-queries]: /product/apis-integrations/queries#regular-query
508+
[ref-query-wpp]: /product/apis-integrations/queries#query-with-post-processing
509+
[ref-query-wpd]: /product/apis-integrations/queries#query-with-pushdown

packages/cubejs-api-gateway/src/gateway.ts

+43
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
QueryType as QueryTypeEnum, ResultType
3434
} from './types/enums';
3535
import {
36+
BaseRequest,
3637
RequestContext,
3738
ExtendedRequestContext,
3839
Request,
@@ -324,6 +325,17 @@ class ApiGateway {
324325
}));
325326

326327
app.get(`${this.basePath}/v1/sql`, userMiddlewares, userAsyncHandler(async (req: any, res) => {
328+
// TODO parse req.query with zod/joi/...
329+
330+
if (req.query.format === 'sql') {
331+
await this.sql4sql({
332+
query: req.query.query,
333+
context: req.context,
334+
res: this.resToResultFn(res)
335+
});
336+
return;
337+
}
338+
327339
await this.sql({
328340
query: req.query.query,
329341
context: req.context,
@@ -332,6 +344,17 @@ class ApiGateway {
332344
}));
333345

334346
app.post(`${this.basePath}/v1/sql`, jsonParser, userMiddlewares, userAsyncHandler(async (req, res) => {
347+
// TODO parse req.body with zod/joi/...
348+
349+
if (req.body.format === 'sql') {
350+
await this.sql4sql({
351+
query: req.body.query,
352+
context: req.context,
353+
res: this.resToResultFn(res)
354+
});
355+
return;
356+
}
357+
335358
await this.sql({
336359
query: req.body.query,
337360
context: req.context,
@@ -1281,6 +1304,26 @@ class ApiGateway {
12811304
return [queryType, normalizedQueries, queryNormalizationResult.map((it) => remapToQueryAdapterFormat(it.normalizedQuery))];
12821305
}
12831306

1307+
protected async sql4sql({
1308+
query,
1309+
context,
1310+
res,
1311+
}: {query: string} & BaseRequest) {
1312+
try {
1313+
await this.assertApiScope('data', context.securityContext);
1314+
1315+
const result = await this.sqlServer.sql4sql(query, context.securityContext);
1316+
res({ sql: result });
1317+
} catch (e: any) {
1318+
this.handleError({
1319+
e,
1320+
context,
1321+
query,
1322+
res,
1323+
});
1324+
}
1325+
}
1326+
12841327
public async sql({
12851328
query,
12861329
context,

packages/cubejs-api-gateway/src/sql-server.ts

+6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {
33
registerInterface,
44
shutdownInterface,
55
execSql,
6+
sql4sql,
67
SqlInterfaceInstance,
78
Request as NativeRequest,
89
LoadRequestMeta,
10+
Sql4SqlResponse,
911
} from '@cubejs-backend/native';
1012
import type { ShutdownMode } from '@cubejs-backend/native';
1113
import { displayCLIWarning, getEnv } from '@cubejs-backend/shared';
@@ -62,6 +64,10 @@ export class SQLServer {
6264
await execSql(this.sqlInterfaceInstance!, sqlQuery, stream, securityContext);
6365
}
6466

67+
public async sql4sql(sqlQuery: string, securityContext?: any): Promise<Sql4SqlResponse> {
68+
return sql4sql(this.sqlInterfaceInstance!, sqlQuery, securityContext);
69+
}
70+
6571
protected buildCheckSqlAuth(options: SQLServerOptions): CheckSQLAuthFn {
6672
return (options.checkSqlAuth && this.wrapCheckSqlAuthFn(options.checkSqlAuth))
6773
|| this.createDefaultCheckSqlAuthFn(options);

packages/cubejs-backend-native/js/index.ts

+22
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,21 @@ export type DBResponsePrimitive =
124124
number |
125125
string;
126126

127+
// TODO type this better, to make it proper disjoint union
128+
export type Sql4SqlOk = {
129+
sql: string,
130+
values: Array<string | null>,
131+
};
132+
export type Sql4SqlError = { error: string };
133+
export type Sql4SqlCommon = {
134+
query_type: {
135+
regular: boolean;
136+
post_processing: boolean;
137+
pushdown: boolean;
138+
}
139+
};
140+
export type Sql4SqlResponse = Sql4SqlCommon & (Sql4SqlOk | Sql4SqlError);
141+
127142
let loadedNative: any = null;
128143

129144
export function loadNative() {
@@ -389,6 +404,13 @@ export const execSql = async (instance: SqlInterfaceInstance, sqlQuery: string,
389404
await native.execSql(instance, sqlQuery, stream, securityContext ? JSON.stringify(securityContext) : null);
390405
};
391406

407+
// TODO parse result from native code
408+
export const sql4sql = async (instance: SqlInterfaceInstance, sqlQuery: string, securityContext?: any): Promise<Sql4SqlResponse> => {
409+
const native = loadNative();
410+
411+
return native.sql4sql(instance, sqlQuery, securityContext ? JSON.stringify(securityContext) : null);
412+
};
413+
392414
export const buildSqlAndParams = (cubeEvaluator: any): String => {
393415
const native = loadNative();
394416

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::future::Future;
2+
use std::net::SocketAddr;
3+
use std::str::FromStr;
4+
use std::sync::Arc;
5+
6+
use cubesql::compile::DatabaseProtocol;
7+
use cubesql::config::ConfigObj;
8+
use cubesql::sql::{Session, SessionManager};
9+
use cubesql::CubeError;
10+
11+
use crate::auth::NativeAuthContext;
12+
use crate::config::NodeCubeServices;
13+
14+
pub async fn create_session(
15+
services: &NodeCubeServices,
16+
native_auth_ctx: Arc<NativeAuthContext>,
17+
) -> Result<Arc<Session>, CubeError> {
18+
let config = services
19+
.injector()
20+
.get_service_typed::<dyn ConfigObj>()
21+
.await;
22+
23+
let session_manager = services
24+
.injector()
25+
.get_service_typed::<SessionManager>()
26+
.await;
27+
28+
let (host, port) = match SocketAddr::from_str(
29+
config
30+
.postgres_bind_address()
31+
.as_deref()
32+
.unwrap_or("127.0.0.1:15432"),
33+
) {
34+
Ok(addr) => (addr.ip().to_string(), addr.port()),
35+
Err(e) => {
36+
return Err(CubeError::internal(format!(
37+
"Failed to parse postgres_bind_address: {}",
38+
e
39+
)))
40+
}
41+
};
42+
43+
let session = session_manager
44+
.create_session(DatabaseProtocol::PostgreSQL, host, port, None)
45+
.await?;
46+
47+
session
48+
.state
49+
.set_auth_context(Some(native_auth_ctx.clone()));
50+
51+
Ok(session)
52+
}
53+
54+
pub async fn with_session<T, F, Fut>(
55+
services: &NodeCubeServices,
56+
native_auth_ctx: Arc<NativeAuthContext>,
57+
f: F,
58+
) -> Result<T, CubeError>
59+
where
60+
F: FnOnce(Arc<Session>) -> Fut,
61+
Fut: Future<Output = Result<T, CubeError>>,
62+
{
63+
let session_manager = services
64+
.injector()
65+
.get_service_typed::<SessionManager>()
66+
.await;
67+
let session = create_session(services, native_auth_ctx).await?;
68+
let connection_id = session.state.connection_id;
69+
70+
// From now there's a session we should close before returning, as in `finally`
71+
let result = { f(session).await };
72+
73+
session_manager.drop_session(connection_id).await;
74+
75+
result
76+
}

packages/cubejs-backend-native/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod auth;
77
pub mod channel;
88
pub mod config;
99
pub mod cross;
10+
pub mod cubesql_utils;
1011
pub mod gateway;
1112
pub mod logger;
1213
pub mod node_export;
@@ -15,6 +16,7 @@ pub mod node_obj_serializer;
1516
pub mod orchestrator;
1617
#[cfg(feature = "python")]
1718
pub mod python;
19+
pub mod sql4sql;
1820
pub mod stream;
1921
pub mod template;
2022
pub mod transport;

0 commit comments

Comments
 (0)