Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ needless_pass_by_value = "warn"
[workspace.dependencies]
pyo3 = { version = "0.27", features = ["experimental-async"] }
pyo3-async-runtimes = { version = "0.27", features = ["tokio-runtime"] }
# Tracks pyo3 versions in lockstep; converts dynamic SQL result rows
# (serde_json::Value) to native Python objects.
pythonize = { version = "0.27" }
napi = { version = "3", features = ["async", "tokio_rt", "compat-mode"] }
napi-derive = { version = "3", features = ["compat-mode"] }
pyo3-stub-gen = { version = "0.19.0" }
Expand Down
44 changes: 43 additions & 1 deletion crates/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ This is one of four language bindings published from the same Rust core. See the
- [KV Store Client](#kv-store-client)
- [Sets](#sets)
- [Lists](#lists)
- [SQL Client](#sql-client)
- [Error Handling](#error-handling)
- [License](#license)

Expand All @@ -55,7 +56,7 @@ This is one of four language bindings published from the same Rust core. See the

## Quick Start

Construct the SDK once, then reach into the four sub-clients (`admin`, `streams`, `webhooks`, `kvstore`). Subsequent API Reference snippets assume you have a `qn` handle from one of these blocks.
Construct the SDK once, then reach into the five sub-clients (`admin`, `streams`, `webhooks`, `kvstore`, `sql`). Subsequent API Reference snippets assume you have a `qn` handle from one of these blocks.

```rust
// Rust
Expand Down Expand Up @@ -99,6 +100,7 @@ Environment variables (prefix `QN_SDK__`, separator `__`):
| `QN_SDK__STREAMS__BASE_URL` | no | `https://api.quicknode.com/streams/rest/v1/` | Override streams base URL |
| `QN_SDK__WEBHOOKS__BASE_URL` | no | `https://api.quicknode.com/webhooks/rest/v1/` | Override webhooks base URL |
| `QN_SDK__KVSTORE__BASE_URL` | no | `https://api.quicknode.com/kv/rest/v1/` | Override KV store base URL |
| `QN_SDK__SQL__BASE_URL` | no | `https://api.quicknode.com/sql/rest/v1/` | Override SQL Explorer base URL |
| `QN_SDK__HTTP__HEADERS__<NAME>` | no | — | Custom HTTP header sent on every request. Overrides SDK-managed headers (see below). |

### Custom headers and `User-Agent`
Expand Down Expand Up @@ -1689,6 +1691,46 @@ Deletes a list and all of its items.
qn.kvstore.delete_list("my-list").await?;
```

---

### SQL Client

Accessed as `qn.sql`. Runs SQL queries against indexed blockchain data and fetches the database schema. Backed by `https://api.quicknode.com/sql/rest/v1/`.

##### `query`

Executes a SQL query against a cluster and returns the result set. Paginate by writing `LIMIT`/`OFFSET` into the SQL.

**Parameters**: `QueryParams` with `query` (String, required) and `cluster_id` (String, required).

**Returns**: `QueryResponse` — `meta` (`Vec<ColumnMeta>`, each with `name` and `column_type`), `data` (`Vec<serde_json::Value>`, rows as JSON objects keyed by column name), `rows`, `rows_before_limit_at_least`, `statistics` (`QueryStatistics` with `elapsed`, `rows_read`, `bytes_read`), and `credits`.

```rust
// Rust
let resp = qn
.sql
.query(&QueryParams {
query: "SELECT action_type, user FROM hyperliquid_system_actions ORDER BY block_time DESC LIMIT 100".to_string(),
cluster_id: "hyperliquid-core-mainnet".to_string(),
})
.await?;
println!("{} rows, {:?}", resp.rows, resp.data.first());
```

##### `get_schema`

Fetches the database schema for a cluster: table names, columns, types, sort keys, and partition strategies.

**Parameters**: `cluster_id` (`&str`, required).

**Returns**: `ChainSchema` — `chain`, `cluster_id`, and `tables` (`Vec<TableSchema>`, each with `name`, `engine`, `total_rows`, `partition_key`, `sorting_key`, and `columns` of `ColumnSchema { name, column_type }`).

```rust
// Rust
let schema = qn.sql.get_schema("hyperliquid-core-mainnet").await?;
println!("{} tables", schema.tables.len());
```

## Error Handling

Every binding exposes a typed exception hierarchy derived from the core `SdkError`
Expand Down
1 change: 1 addition & 0 deletions crates/core/examples/admin_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ async fn main() {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
};
let headered = QuicknodeSdk::new(&with_headers).expect("build sdk with custom headers");
match headered
Expand Down
70 changes: 70 additions & 0 deletions crates/core/examples/sql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use quicknode_sdk::{errors::SdkError, sql::QueryParams, QuicknodeSdk, SdkFullConfig};

const CLUSTER_ID: &str = "hyperliquid-core-mainnet";

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() {
let config = SdkFullConfig::from_env().expect("Config from env failed");
let qn = QuicknodeSdk::new(&config).expect("sdk failed to initialize");

// ── Query ─────────────────────────────────────────────────────────────────

let params = QueryParams {
query: "SELECT toDateTime(block_time) AS time, action_type, user \
FROM hyperliquid_system_actions \
ORDER BY block_time DESC LIMIT 3"
.to_string(),
cluster_id: CLUSTER_ID.to_string(),
};

match qn.sql.query(&params).await {
Ok(resp) => {
println!(
"query: {} rows ({} before limit), {} credits, {:.4}s",
resp.rows, resp.rows_before_limit_at_least, resp.credits, resp.statistics.elapsed
);
println!(
"columns: {:?}",
resp.meta.iter().map(|c| &c.name).collect::<Vec<_>>()
);
// Read a value out of a dynamic row to confirm the conversion works.
if let Some(row) = resp.data.first() {
println!("first row action_type: {}", row["action_type"]);
}
}
Err(e) => eprintln!("query error: {e}"),
}

// ── Schema ──────────────────────────────────────────────────────────────

match qn.sql.get_schema(CLUSTER_ID).await {
Ok(schema) => {
println!("schema: {} ({} tables)", schema.chain, schema.tables.len());
if let Some(table) = schema.tables.first() {
println!(
"first table: {} ({} columns, {} rows)",
table.name,
table.columns.len(),
table.total_rows
);
}
}
Err(e) => eprintln!("get_schema error: {e}"),
}

// ── Error handling ────────────────────────────────────────────────────────

// An empty query is rejected with a 403 and a JSON body carrying the error
// message.
let bad = QueryParams {
query: String::new(),
cluster_id: CLUSTER_ID.to_string(),
};
match qn.sql.query(&bad).await {
Err(SdkError::Api { status, body }) => {
println!("api error {status}: {}", &body[..body.len().min(120)]);
}
other => eprintln!("expected Api error, got {other:?}"),
}
}
2 changes: 2 additions & 0 deletions crates/core/src/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,6 +1903,7 @@ mod tests {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
})
.unwrap()
}
Expand Down Expand Up @@ -3673,6 +3674,7 @@ mod tests {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
});
assert!(matches!(result, Err(crate::errors::SdkError::Config(_))));
}
Expand Down
26 changes: 25 additions & 1 deletion crates/core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@ impl KvStoreConfig {
}
}

#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
#[cfg_attr(feature = "rust", derive(Builder))]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SqlConfig {
pub base_url: Option<String>,
}

#[cfg(feature = "python")]
#[gen_stub_pymethods]
#[pymethods]
impl SqlConfig {
#[new]
#[pyo3(signature = (base_url=None))]
pub fn new(base_url: Option<String>) -> Self {
SqlConfig { base_url }
}
}

#[cfg_attr(feature = "python", gen_stub_pyclass)]
#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
#[cfg_attr(feature = "node", napi(object))]
Expand All @@ -152,6 +172,7 @@ pub struct SdkFullConfig {
pub streams: Option<StreamsConfig>,
pub webhooks: Option<WebhooksConfig>,
pub kvstore: Option<KvStoreConfig>,
pub sql: Option<SqlConfig>,
}

impl SdkFullConfig {
Expand All @@ -163,6 +184,7 @@ impl SdkFullConfig {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
}
}

Expand All @@ -189,14 +211,15 @@ impl SdkFullConfig {
#[pymethods]
impl SdkFullConfig {
#[new]
#[pyo3(signature = (api_key, http=None, admin=None, streams=None, webhooks=None, kvstore=None))]
#[pyo3(signature = (api_key, http=None, admin=None, streams=None, webhooks=None, kvstore=None, sql=None))]
pub fn new(
api_key: String,
http: Option<HttpConfig>,
admin: Option<AdminConfig>,
streams: Option<StreamsConfig>,
webhooks: Option<WebhooksConfig>,
kvstore: Option<KvStoreConfig>,
sql: Option<SqlConfig>,
) -> Self {
SdkFullConfig {
api_key,
Expand All @@ -205,6 +228,7 @@ impl SdkFullConfig {
streams,
webhooks,
kvstore,
sql,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/kvstore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@ mod tests {
kvstore: Some(KvStoreConfig {
base_url: Some(base_url),
}),
sql: None,
})
.unwrap()
}
Expand Down
22 changes: 20 additions & 2 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ pub mod admin;
pub mod config;
pub mod errors;
pub mod kvstore;
pub mod sql;
pub mod streams;
pub mod webhooks;

pub use config::{
AdminConfig, ClientInfo, HttpConfig, KvStoreConfig, SdkFullConfig, StreamsConfig,
AdminConfig, ClientInfo, HttpConfig, KvStoreConfig, SdkFullConfig, SqlConfig, StreamsConfig,
WebhooksConfig,
};
pub use kvstore::{
Expand All @@ -15,6 +16,10 @@ pub use kvstore::{
GetSetsParams, GetSetsResponse, KvSetEntry, KvStoreApiClient, ListContainsItemResponse,
UpdateListParams,
};
pub use sql::{
ChainSchema, ColumnMeta, ColumnSchema, QueryParams, QueryResponse, QueryStatistics,
SqlApiClient, TableSchema,
};

use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use reqwest::Client as ReqwestClient;
Expand Down Expand Up @@ -64,6 +69,7 @@ impl std::fmt::Debug for SdkConfig {
.field("streams_base_url", &self.0.streams.base_url)
.field("webhooks_base_url", &self.0.webhooks.base_url)
.field("kvstore_base_url", &self.0.kvstore.base_url)
.field("sql_base_url", &self.0.sql.base_url)
.finish()
}
}
Expand All @@ -74,6 +80,7 @@ struct SdkConfigInner {
streams: streams::ResolvedStreamsConfig,
webhooks: webhooks::ResolvedWebhooksConfig,
kvstore: kvstore::ResolvedKvStoreConfig,
sql: sql::ResolvedSqlConfig,
}

impl SdkConfig {
Expand Down Expand Up @@ -159,6 +166,7 @@ impl SdkConfig {
streams: streams::ResolvedStreamsConfig::from_config(config.streams.as_ref())?,
webhooks: webhooks::ResolvedWebhooksConfig::from_config(config.webhooks.as_ref())?,
kvstore: kvstore::ResolvedKvStoreConfig::from_config(config.kvstore.as_ref())?,
sql: sql::ResolvedSqlConfig::from_config(config.sql.as_ref())?,
})))
}

Expand All @@ -181,6 +189,10 @@ impl SdkConfig {
pub(crate) fn kvstore(&self) -> &kvstore::ResolvedKvStoreConfig {
&self.0.kvstore
}

pub(crate) fn sql(&self) -> &sql::ResolvedSqlConfig {
&self.0.sql
}
}

/// Top-level entry point for the Quicknode SDK. Holds sub-clients for each
Expand All @@ -196,6 +208,9 @@ pub struct QuicknodeSdk {
/// Key-Value Store client: manages sets (single values) and lists
/// (ordered collections) under string keys.
pub kvstore: kvstore::KvStoreApiClient,
/// SQL Explorer client: executes SQL queries against indexed blockchain
/// data and fetches the database schema.
pub sql: sql::SqlApiClient,
}

impl QuicknodeSdk {
Expand All @@ -216,7 +231,8 @@ impl QuicknodeSdk {
admin: admin::AdminApiClient::new(sdk_config.clone()),
streams: streams::StreamsApiClient::new(sdk_config.clone()),
webhooks: webhooks::WebhooksApiClient::new(sdk_config.clone()),
kvstore: kvstore::KvStoreApiClient::new(sdk_config),
kvstore: kvstore::KvStoreApiClient::new(sdk_config.clone()),
sql: sql::SqlApiClient::new(sdk_config),
})
}

Expand Down Expand Up @@ -247,6 +263,7 @@ mod headers_tests {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
}
}

Expand Down Expand Up @@ -335,6 +352,7 @@ mod headers_tests {
streams: None,
webhooks: None,
kvstore: None,
sql: None,
};

let sdk = QuicknodeSdk::new(&cfg).unwrap();
Expand Down
Loading
Loading