Skip to content

Commit 3445b09

Browse files
committed
feat(router): add SQL query support to both SDKs
- Add SqlResult, SqlMeta, SqlColumn types in TypeScript and Python - Add sql(query: string) method to Router class - Export types from both SDKs - Full type safety Fixes #1032
1 parent 60200cd commit 3445b09

3 files changed

Lines changed: 187 additions & 0 deletions

File tree

sdks/python/pmxt/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .router import Router
2525
from .feed_client import FeedClient
2626
from .server_manager import ServerManager
27+
from .router import SqlResult, SqlMeta, SqlColumn
2728
from .errors import (
2829
PmxtError,
2930
BadRequest,
@@ -269,4 +270,8 @@ def restart_server() -> None:
269270
"OrderSide",
270271
"OrderType",
271272
"CandleInterval",
273+
# SQL
274+
'SqlResult',
275+
'SqlMeta',
276+
'SqlColumn',
272277
]

sdks/python/pmxt/router.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,29 @@
2323
UnifiedEvent,
2424
)
2525
from pmxt_internal.exceptions import ApiException
26+
from dataclasses import dataclass
27+
from typing import Any, Dict, List, Optional, Union
28+
29+
30+
@dataclass
31+
class SqlColumn:
32+
"""Column metadata for SQL query results."""
33+
name: str
34+
type: str
35+
36+
37+
@dataclass
38+
class SqlMeta:
39+
"""Metadata about SQL query results."""
40+
columns: List[SqlColumn]
41+
rows: int
42+
43+
44+
@dataclass
45+
class SqlResult:
46+
"""Result of a SQL query."""
47+
data: List[Dict[str, Any]]
48+
meta: SqlMeta
2649

2750

2851
def _parse_market(raw: Any) -> Any:
@@ -434,6 +457,75 @@ def fetch_matched_event_clusters(
434457

435458
raw = self._get_catalog_path("/v0/matched-event-clusters", params)
436459
return [_parse_event_cluster(c) for c in _extract_response_list(raw)]
460+
461+
# ------------------------------------------------------------------
462+
# SQL Query
463+
# ------------------------------------------------------------------
464+
465+
def sql(self, query: str) -> SqlResult:
466+
"""
467+
Execute a SQL query against the ClickHouse database.
468+
469+
This endpoint provides direct SQL access to the prediction market
470+
catalog, enabling complex analytics, custom queries, and data export.
471+
472+
Args:
473+
query: SQL query string
474+
475+
Returns:
476+
SqlResult: Query results with data and metadata
477+
478+
Example:
479+
```python
480+
router = Router(pmxt_api_key="...")
481+
result = router.sql("SELECT * FROM markets LIMIT 10")
482+
for row in result.data:
483+
print(row["question"])
484+
```
485+
"""
486+
url = f"{self._resolve_sidecar_host()}/v0/sql"
487+
headers = {
488+
"Content-Type": "application/json",
489+
"Accept": "application/json",
490+
}
491+
headers.update(self._get_auth_headers())
492+
493+
body = json.dumps({"query": query})
494+
495+
try:
496+
response = self._fetch_with_retry(
497+
lambda: self._api_client.call_api(
498+
method="POST",
499+
url=url,
500+
header_params=headers,
501+
body=body,
502+
)
503+
)
504+
response.read()
505+
data = json.loads(response.data)
506+
507+
result_data = data.get("data", [])
508+
meta_data = data.get("meta", {})
509+
510+
columns = []
511+
for col in meta_data.get("columns", []):
512+
columns.append(SqlColumn(
513+
name=col.get("name", ""),
514+
type=col.get("type", "string")
515+
))
516+
517+
meta = SqlMeta(
518+
columns=columns,
519+
rows=meta_data.get("rows", len(result_data))
520+
)
521+
522+
return SqlResult(data=result_data, meta=meta)
523+
524+
except ApiException as e:
525+
raise self._parse_api_exception(e) from None
526+
except Exception as e:
527+
raise self._parse_api_exception(e) from None
528+
437529

438530
# ------------------------------------------------------------------
439531
# Price comparison

sdks/typescript/pmxt/router.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,26 @@ function withQuestionAlias<T extends UnifiedMarket>(market: T): T {
3131
return market;
3232
}
3333

34+
35+
/**
36+
* SQL query result from ClickHouse
37+
*/
38+
export interface SqlColumn {
39+
name: string;
40+
type: string;
41+
}
42+
43+
export interface SqlMeta {
44+
columns: SqlColumn[];
45+
rows: number;
46+
}
47+
48+
export interface SqlResult {
49+
data: Record<string, unknown>[];
50+
meta: SqlMeta;
51+
}
52+
53+
3454
function convertMarket(raw: any): UnifiedMarket {
3555
const outcomes = (raw.outcomes || []).map((o: any) => ({
3656
outcomeId: o.outcomeId,
@@ -376,6 +396,76 @@ export class Router extends Exchange {
376396
}
377397
}
378398

399+
// ------------------------------------------------------------------
400+
// SQL Query
401+
// ------------------------------------------------------------------
402+
403+
/**
404+
* Execute a SQL query against the ClickHouse database.
405+
*
406+
* This endpoint provides direct SQL access to the prediction market
407+
* catalog, enabling complex analytics, custom queries, and data export.
408+
*
409+
* @param query - SQL query string
410+
* @returns Query results with data and metadata
411+
*
412+
* @example
413+
* ```typescript
414+
* const router = new Router({ pmxtApiKey: "..." });
415+
*
416+
* // Simple query
417+
* const result = await router.sql('SELECT * FROM markets LIMIT 10');
418+
* console.log(result.data); // Array of market rows
419+
* console.log(result.meta.columns); // Column metadata
420+
*
421+
* // Complex analytics
422+
* const result = await router.sql(`
423+
* SELECT
424+
* sourceExchange,
425+
* COUNT(*) as marketCount,
426+
* AVG(volume24h) as avgVolume
427+
* FROM markets
428+
* WHERE resolutionDate > NOW() - INTERVAL 7 DAY
429+
* GROUP BY sourceExchange
430+
* ORDER BY avgVolume DESC
431+
* `);
432+
* ```
433+
*
434+
* @throws {PmxtError} If the query fails or is not allowed
435+
*/
436+
async sql(query: string): Promise<SqlResult> {
437+
await this.initPromise;
438+
439+
try {
440+
const json = await this.sidecarReadRequest(
441+
'sql',
442+
{ query },
443+
[{ query }]
444+
);
445+
446+
const data = this.handleResponse(json);
447+
448+
if (!data) {
449+
return { data: [], meta: { columns: [], rows: 0 } };
450+
}
451+
452+
// Handle different response shapes
453+
const resultData = data.data || data;
454+
const resultMeta = data.meta || { columns: [], rows: 0 };
455+
456+
return {
457+
data: Array.isArray(resultData) ? resultData : [],
458+
meta: {
459+
columns: resultMeta.columns || [],
460+
rows: resultMeta.rows || (Array.isArray(resultData) ? resultData.length : 0)
461+
}
462+
};
463+
} catch (error) {
464+
if (error instanceof Error) throw error;
465+
throw new Error(`SQL query failed: ${error}`);
466+
}
467+
}
468+
379469
/**
380470
* Fetch connected clusters of semantically matched markets across venues.
381471
*

0 commit comments

Comments
 (0)