Skip to content

Commit 7bc9e45

Browse files
authored
Add query bindings, dataset refresh, and GitHub release publishing flow (#70)
* Add query bindings, dataset refresh, and release publishing flow * test: Add async query tests with parameter handling and dataset refresh * docs: Update README and tests to clarify usage of Arrow parameter types * ci: move ubuntu-22.04 jobs to ubuntu-24.04 (Spice CLI GLIBC 2.38+ floor) The prebuilt Spice CLI from install.spiceai.org now requires GLIBC 2.38/2.39, but ubuntu-22.04 runners ship GLIBC 2.35, so `spice install` fails with "version GLIBC_2.38 not found" and the whole ubuntu-22.04 matrix leg goes red trunk-wide. The failure also cascades: the always() "Stop spice and check logs" step has working-directory: spice_qs, but a failed install/init never creates that directory, so the step itself errors with "No such file or directory" and masks the real cause. Move the matrix entry to ubuntu-24.04 (GLIBC 2.39) and guard the stop-step on hashFiles('spice_qs/**') so a failed install/init no longer cascades into a misleading second failure.
1 parent ec6568c commit 7bc9e45

10 files changed

Lines changed: 2135 additions & 41 deletions

File tree

.github/ISSUE_TEMPLATE/end_game.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,12 @@ assignees: ''
2828
- [ ] Test the [`spice-rs` sample](https://github.com/spiceai/samples/tree/trunk/client-sdk/spice-rs-sdk-sample) using the latest `trunk` SDK version.
2929
- [ ] Update [release notes](https://github.com/spiceai/spice-rs/blob/trunk/docs/release_notes)
3030
- [ ] Ensure all contributors have been acknowledged.
31-
- [ ] Verify the version in `Cargo.toml` is correct and match the milestone version.
31+
- [ ] Verify `Cargo.toml` is set to the milestone version and the release tag will match it.
3232
- [ ] Run [Test CI](https://github.com/spiceai/spice-rs/actions/workflows/build.yml) and ensure it is green on the trunk branch.
3333
- [ ] QA DRI sign-off
3434
- [ ] Docs DRI sign-off
35-
- [ ] Create a new branch `release-v[semver]` for the release from trunk. E.g. `release-v3.0.0`
36-
- [ ] Release the new version by creating and publishing a latest [GitHub Release](https://github.com/spiceai/spice-rs/releases/new) with the tag from the release branch. E.g. `v3.0.0`.
37-
- [ ] Ensure the [publish](https://github.com/spiceai/spice-rs/actions/workflows/publish.yml) workflow has triggered, and successfully published the package.
35+
- [ ] Create or publish the GitHub Release for the target version tag.
36+
- [ ] If this should be the stable SDK release, mark the GitHub Release as Latest.
37+
- [ ] Ensure the [publish](https://github.com/spiceai/spice-rs/actions/workflows/publish.yml) workflow completed successfully and published the package.
3838
- [ ] Run a test pass using the [`spice-rs` sample](https://github.com/spiceai/samples/tree/trunk/client-sdk/spice-rs-sdk-sample) using the latest published version.
39-
- [ ] Update the version in `Cargo.toml` to the next release version.
4039
- [ ] The SDK release is added to the next [Spice release notes](https://github.com/spiceai/spiceai/tree/trunk/docs/release_notes)

.github/workflows/publish.yml

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,154 @@ name: crates-publish
22

33
on:
44
release:
5-
types: [published]
5+
types: [published, released, edited]
66

77
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: Git tag to publish, for example v3.2.0
11+
required: true
12+
type: string
13+
14+
workflow_call:
15+
inputs:
16+
tag:
17+
description: Git tag to publish, for example v3.2.0
18+
required: false
19+
type: string
820

921
jobs:
1022
publish:
23+
concurrency:
24+
group: crates-publish-${{ github.event.release.tag_name || inputs.tag || github.ref }}
25+
cancel-in-progress: false
1126
runs-on: ubuntu-latest
1227
environment: cratesio
1328
permissions:
29+
contents: read
1430
id-token: write # Required for OIDC token exchange with crates.io
1531
steps:
1632
# actions/checkout v4.2.2
1733
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
34+
with:
35+
ref: ${{ github.event.release.tag_name || inputs.tag || github.ref }}
36+
37+
- name: Determine whether publish should run
38+
id: preflight
39+
env:
40+
EVENT_ACTION: ${{ github.event.action }}
41+
EVENT_NAME: ${{ github.event_name }}
42+
GITHUB_TOKEN: ${{ github.token }}
43+
RELEASE_DRAFT: ${{ github.event.release.draft }}
44+
RELEASE_ID: ${{ github.event.release.id }}
45+
RELEASE_TAG: ${{ github.event.release.tag_name || inputs.tag }}
46+
REPOSITORY: ${{ github.repository }}
47+
run: |
48+
set -euo pipefail
49+
50+
should_publish=false
51+
skip_reason=''
52+
53+
if [[ "${EVENT_NAME}" == "release" ]]; then
54+
if [[ "${RELEASE_DRAFT}" == "true" ]]; then
55+
skip_reason='Draft releases do not trigger publishing.'
56+
elif [[ "${EVENT_ACTION}" == "published" || "${EVENT_ACTION}" == "released" ]]; then
57+
should_publish=true
58+
elif [[ "${EVENT_ACTION}" == "edited" ]]; then
59+
latest_release_id="$({
60+
curl -fsSL \
61+
-H "Accept: application/vnd.github+json" \
62+
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
63+
-H "X-GitHub-Api-Version: 2022-11-28" \
64+
"https://api.github.com/repos/${REPOSITORY}/releases/latest" |
65+
jq -r '.id'
66+
} 2>/dev/null || true)"
67+
68+
if [[ -n "${latest_release_id}" && "${latest_release_id}" == "${RELEASE_ID}" ]]; then
69+
should_publish=true
70+
else
71+
skip_reason='Edited release is not the latest published release.'
72+
fi
73+
else
74+
skip_reason="Release action ${EVENT_ACTION} does not trigger publishing."
75+
fi
76+
else
77+
should_publish=true
78+
fi
79+
80+
crate_name="$(awk -F'"' '
81+
$0 == "[package]" { in_package=1; next }
82+
/^\[/ && $0 != "[package]" { in_package=0 }
83+
in_package && $1 ~ /^[[:space:]]*name = / { print $2; exit }
84+
' Cargo.toml)"
85+
86+
crate_version="$(awk -F'"' '
87+
$0 == "[package]" { in_package=1; next }
88+
/^\[/ && $0 != "[package]" { in_package=0 }
89+
in_package && $1 ~ /^[[:space:]]*version = / { print $2; exit }
90+
' Cargo.toml)"
91+
92+
if [[ -z "${crate_name}" || -z "${crate_version}" ]]; then
93+
echo "Failed to determine crate metadata from Cargo.toml" >&2
94+
exit 1
95+
fi
96+
97+
if [[ -n "${RELEASE_TAG}" ]]; then
98+
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
99+
echo "Release tag ${RELEASE_TAG} must match vX.Y.Z or vX.Y.Z-suffix" >&2
100+
exit 1
101+
fi
102+
103+
tag_version="${RELEASE_TAG#v}"
104+
if [[ "${crate_version}" != "${tag_version}" ]]; then
105+
echo "Release tag ${RELEASE_TAG} does not match Cargo.toml version ${crate_version}" >&2
106+
exit 1
107+
fi
108+
elif [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
109+
echo "workflow_dispatch requires a tag input" >&2
110+
exit 1
111+
fi
112+
113+
if [[ "${should_publish}" == "true" ]]; then
114+
if curl -fsSL "https://crates.io/api/v1/crates/${crate_name}/${crate_version}" >/dev/null; then
115+
should_publish=false
116+
skip_reason="${crate_name} ${crate_version} is already published to crates.io."
117+
fi
118+
fi
119+
120+
if [[ "${should_publish}" == "true" ]]; then
121+
decision_message="Publishing ${crate_name} ${crate_version}."
122+
else
123+
decision_message="${skip_reason}"
124+
fi
125+
126+
echo "should_publish=${should_publish}" >> "${GITHUB_OUTPUT}"
127+
echo "crate_name=${crate_name}" >> "${GITHUB_OUTPUT}"
128+
echo "crate_version=${crate_version}" >> "${GITHUB_OUTPUT}"
129+
echo "decision_message=${decision_message}" >> "${GITHUB_OUTPUT}"
130+
131+
- name: Publish decision
132+
run: echo "${{ steps.preflight.outputs.decision_message }}"
18133

19134
# rust-lang/crates-io-auth-action v1.0.1
20135
# https://github.com/rust-lang/crates-io-auth-action/releases/tag/v1.0.1
21136
- uses: rust-lang/crates-io-auth-action@e919bc7605cde86df457cf5b93c5e103838bd879
22137
id: auth
138+
if: ${{ steps.preflight.outputs.should_publish == 'true' }}
23139

24140
- name: Install Rust toolchain
25141
# actions-rs/toolchain v1.0.7
26142
uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af
143+
if: ${{ steps.preflight.outputs.should_publish == 'true' }}
27144
with:
28145
profile: minimal
29146
toolchain: 1.93.1
30147
override: true
31148

32149
- run: cargo build
150+
if: ${{ steps.preflight.outputs.should_publish == 'true' }}
33151

34152
- run: cargo publish
153+
if: ${{ steps.preflight.outputs.should_publish == 'true' }}
35154
env:
36155
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ edition = "2024"
55
rust-version = "1.93.1"
66
description = "SDK for Spice.ai, an open-source runtime and platform for building AI-driven software."
77
license = "Apache-2.0"
8+
repository = "https://github.com/spiceai/spice-rs"
9+
homepage = "https://spice.ai"
10+
documentation = "https://docs.rs/spiceai"
11+
readme = "README.md"
12+
keywords = ["spiceai", "sql", "arrow", "flight", "sdk"]
13+
categories = ["api-bindings", "database"]
814

915
[dependencies]
1016
arrow = { version = "58", features = ["prettyprint"] }
@@ -39,3 +45,4 @@ winver = "1.0.0"
3945

4046
[dev-dependencies]
4147
regex = "1.10.6"
48+
wiremock = "0.6.5"

README.md

Lines changed: 99 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,142 @@
11
# Rust Spice SDK
22

3-
Rust SDK for Spice.ai
3+
Rust SDK for Spice.ai.
44

55
## Installation
66

7-
Add Spice SDK
7+
Add the SDK:
88

99
```bash
1010
cargo add spiceai
1111
```
1212

1313
## Usage
1414

15-
<!-- NOTE: If you're changing the code examples below, make sure you update `tests/readme_test.rs`. -->
15+
### Query a local Spice runtime
1616

17-
### Usage with locally running [spice runtime](https://github.com/spiceai/spiceai)
17+
Follow the [quickstart guide](https://github.com/spiceai/spiceai?tab=readme-ov-file#%EF%B8%8F-quickstart-local-machine) to install and run Spice locally.
1818

19-
Follow the [quickstart guide](https://github.com/spiceai/spiceai?tab=readme-ov-file#%EF%B8%8F-quickstart-local-machine) to install and run spice locally
19+
```rust,no_run
20+
use spiceai::{ClientBuilder, StreamExt};
21+
22+
#[tokio::main]
23+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
24+
let client = ClientBuilder::new().build().await?;
25+
26+
let mut stream = client
27+
.sql(
28+
"SELECT trip_distance, total_amount FROM taxi_trips ORDER BY trip_distance DESC LIMIT 10;",
29+
)
30+
.await?;
31+
32+
while let Some(batch) = stream.next().await {
33+
println!("rows: {}", batch?.num_rows());
34+
}
35+
36+
Ok(())
37+
}
38+
```
39+
40+
### Use Arrow types re-exported by the SDK
41+
42+
The SDK re-exports `arrow` as `spiceai::arrow`, which keeps your Arrow types aligned with the SDK's public API.
2043

2144
```rust,no_run
22-
use spiceai::ClientBuilder;
45+
use spiceai::{arrow::array::Float64Array, ClientBuilder, StreamExt};
2346
2447
#[tokio::main]
25-
async fn main() {
26-
let client = ClientBuilder::new()
27-
.flight_url("http://localhost:50051")
28-
.build()
29-
.await
30-
.unwrap();
48+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
49+
let client = ClientBuilder::new().build().await?;
50+
51+
let mut stream = client
52+
.sql("SELECT trip_distance FROM taxi_trips ORDER BY trip_distance DESC LIMIT 1;")
53+
.await?;
3154
32-
let data = client.query("SELECT trip_distance, total_amount FROM taxi_trips ORDER BY trip_distance DESC LIMIT 10;").await;
55+
if let Some(batch) = stream.next().await {
56+
let batch = batch?;
57+
let values = batch
58+
.column(0)
59+
.as_any()
60+
.downcast_ref::<Float64Array>()
61+
.expect("trip_distance should be Float64");
62+
63+
println!("longest trip: {}", values.value(0));
64+
}
65+
66+
Ok(())
3367
}
3468
```
3569

36-
### New client with <https://spice.ai> cloud
70+
### Parameterized queries
3771

38-
```rust
72+
For common scalar bindings, use `QueryParameters`. For any Arrow data type, wrap a one-element Arrow array with `QueryParameter::array(...)`. For advanced Arrow parameter batches, use `Client::sql_with_params`.
73+
74+
```rust,no_run
75+
use spiceai::{ClientBuilder, QueryParameters, StreamExt};
76+
77+
#[tokio::main]
78+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
79+
let client = ClientBuilder::new().build().await?;
80+
81+
let mut stream = client
82+
.sql_with_bindings(
83+
"SELECT VendorID, fare_amount FROM taxi_trips WHERE VendorID = $1 AND fare_amount > $2 LIMIT 5;",
84+
QueryParameters::new().push(1_i32).push(1.0_f64),
85+
)
86+
.await?;
87+
88+
while let Some(batch) = stream.next().await {
89+
println!("rows: {}", batch?.num_rows());
90+
}
91+
92+
Ok(())
93+
}
94+
```
95+
96+
### Connect to Spice.ai Cloud
97+
98+
```rust,no_run
3999
use spiceai::ClientBuilder;
40100
41101
#[tokio::main]
42-
async fn main() {
102+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
43103
let client = ClientBuilder::new()
44104
.api_key("API_KEY")
45105
.use_spiceai_cloud()
46106
.build()
47-
.await
48-
.unwrap();
107+
.await?;
108+
109+
let _ = client;
110+
Ok(())
49111
}
50112
```
51113

52-
### Arrow Query
114+
### Async query jobs and dataset refresh
53115

54-
SQL Query
116+
Async query management and dataset refresh use the Spice HTTP API, so configure `http_url()` in addition to the Flight endpoint when needed.
55117

56-
```rust
57-
use spiceai::ClientBuilder;
118+
```rust,no_run
119+
use spiceai::{ClientBuilder, DatasetRefreshMode, DatasetRefreshRequest};
58120
59121
#[tokio::main]
60-
async fn main() {
122+
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
61123
let client = ClientBuilder::new()
62-
.api_key("API_KEY")
63-
.use_spiceai_cloud()
124+
.http_url("http://localhost:8090")
64125
.build()
65-
.await
66-
.unwrap();
126+
.await?;
127+
128+
let job = client.query("SELECT * FROM large_table").await?;
129+
println!("query status: {}", job.status().await?);
130+
131+
let response = client
132+
.refresh_dataset_with_options(
133+
"taxi_trips",
134+
DatasetRefreshRequest::new().with_refresh_mode(DatasetRefreshMode::Full),
135+
)
136+
.await?;
67137
68-
let data = client.query("SELECT * FROM taxi_trips LIMIT 10;").await;
138+
println!("{}", response.message);
139+
Ok(())
69140
}
70141
```
71142

0 commit comments

Comments
 (0)