Skip to content

Commit dcd719d

Browse files
authored
feat: add Nsql and NsqlGenerateSql for the runtime's /v1/nsql endpoint (#55)
* feat: add Nsql and NsqlGenerateSql for the runtime's /v1/nsql endpoint Text-to-SQL was reachable from other SDKs but not from Java. nsql() runs the generated query and returns the rows alongside the SQL; nsqlGenerateSql() stops after generation so the query can be inspected or run separately. * fix: give the authenticated NsqlTest case a real Flight endpoint withApiKey() makes SpiceClient's constructor perform a real Flight handshake, but the API-key test only stood up the HTTP mock server, not a Flight server. The handshake against a dead default Flight address failed unpredictably by platform (reliable on Windows CI, intermittent on Linux/macOS). Start a TestFlightSqlServer with matching credentials for that case. * fix: address review feedback on nsql() - Reject a null/empty-body decoded response instead of letting nsql() return null in violation of its contract. - Box sampleDataEnabled so an unset value is omitted from the request body instead of always sending "sample_data_enabled":false. - Defensively copy NsqlRequest.datasets on the way in and return an unmodifiable view on the way out.
1 parent cbec170 commit dcd719d

7 files changed

Lines changed: 722 additions & 0 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,22 @@ for (SearchMatch match : response.getResults()) {
438438
}
439439
```
440440

441+
### Natural Language to SQL (Nsql)
442+
443+
Use `nsql()` to have the runtime's configured LLM translate a natural-language question
444+
into SQL and run it, or `nsqlGenerateSql()` to only generate the SQL without running it.
445+
Requires an LLM model configured in the Spicepod — see the
446+
[text-to-SQL docs](https://docs.spice.ai/features/text-to-sql).
447+
448+
```java
449+
NsqlResponse response = client.nsql(new NsqlRequest("how many taxi trips were there yesterday?"));
450+
System.out.println(response.getSql()); // the SQL the model generated
451+
System.out.println(response.getData()); // the rows it returned, decoded from JSON
452+
453+
// Inspect or edit the generated SQL without running it:
454+
String sql = client.nsqlGenerateSql(new NsqlRequest("how many taxi trips were there yesterday?"));
455+
```
456+
441457
### Logging
442458

443459
The SDK uses SLF4J for logging, allowing you to plug in your preferred logging implementation (Logback, Log4j2, java.util.logging, etc.).
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
Copyright 2026 The Spice.ai OSS Authors
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package ai.spice;
24+
25+
import com.google.gson.JsonElement;
26+
import com.google.gson.annotations.SerializedName;
27+
28+
/**
29+
* Describes one column of an {@link NsqlResponse}.
30+
*/
31+
public class NsqlField {
32+
@SerializedName("name")
33+
private String name;
34+
35+
@SerializedName("data_type")
36+
private JsonElement dataType;
37+
38+
@SerializedName("nullable")
39+
private boolean nullable;
40+
41+
/**
42+
* The column name.
43+
*
44+
* @return the column name
45+
*/
46+
public String getName() {
47+
return this.name;
48+
}
49+
50+
/**
51+
* The column's Arrow type, in the runtime's raw JSON encoding. Simple
52+
* types encode as a quoted string ({@code "Utf8"}, {@code "Int64"});
53+
* parameterized ones as an object (for example
54+
* {@code {"Timestamp":["Nanosecond",null]}}) — returned as-is rather than
55+
* modeled, since the shape varies by type.
56+
*
57+
* @return the raw data type
58+
*/
59+
public JsonElement getDataType() {
60+
return this.dataType;
61+
}
62+
63+
/**
64+
* Whether the column admits nulls.
65+
*
66+
* @return true if the column is nullable
67+
*/
68+
public boolean isNullable() {
69+
return this.nullable;
70+
}
71+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
Copyright 2026 The Spice.ai OSS Authors
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package ai.spice;
24+
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
29+
import com.google.gson.annotations.SerializedName;
30+
31+
/**
32+
* A natural-language query against the runtime's {@code /v1/nsql} endpoint.
33+
*
34+
* <p>
35+
* Only {@code query} is required. The runtime needs an LLM model configured
36+
* in the Spicepod to translate it; when exactly one is configured, {@code
37+
* model} may be left unset and the runtime selects it.
38+
*/
39+
public class NsqlRequest {
40+
@SerializedName("query")
41+
private final String query;
42+
43+
@SerializedName("model")
44+
private String model;
45+
46+
@SerializedName("datasets")
47+
private List<String> datasets;
48+
49+
// Boxed and left null when unset, rather than a primitive boolean, so Gson
50+
// omits this field entirely instead of always sending "sample_data_enabled":
51+
// false — the runtime already defaults it to false, and gospice's equivalent
52+
// field is "omitempty".
53+
@SerializedName("sample_data_enabled")
54+
private Boolean sampleDataEnabled;
55+
56+
@SerializedName("prompt_cache_key")
57+
private String promptCacheKey;
58+
59+
/**
60+
* Creates a request for the given natural-language query.
61+
*
62+
* @param query the question to answer, in natural language
63+
*/
64+
public NsqlRequest(String query) {
65+
this.query = query;
66+
}
67+
68+
/**
69+
* Names the LLM used to generate SQL. When unset, the runtime uses the
70+
* only compatible model configured in the Spicepod, and reports an error
71+
* if there is not exactly one.
72+
*
73+
* @param model the model name
74+
* @return this request
75+
*/
76+
public NsqlRequest withModel(String model) {
77+
this.model = model;
78+
return this;
79+
}
80+
81+
/**
82+
* Hints which datasets to sample when building model context. This is a
83+
* sampling hint only — it does not restrict which tables the generated
84+
* query may reference. When unset, all datasets are used.
85+
*
86+
* @param datasets the dataset names to sample
87+
* @return this request
88+
*/
89+
public NsqlRequest withDatasets(List<String> datasets) {
90+
this.datasets = datasets == null ? null : new ArrayList<>(datasets);
91+
return this;
92+
}
93+
94+
/**
95+
* Includes sample rows in the context given to the model. It improves
96+
* generation on ambiguous schemas at the cost of sending data values to
97+
* the model.
98+
*
99+
* @param sampleDataEnabled whether to include sample data
100+
* @return this request
101+
*/
102+
public NsqlRequest withSampleDataEnabled(boolean sampleDataEnabled) {
103+
this.sampleDataEnabled = sampleDataEnabled;
104+
return this;
105+
}
106+
107+
/**
108+
* A stable key forwarded to the model provider for prompt caching. Reuse
109+
* it across related requests to benefit from it.
110+
*
111+
* @param promptCacheKey the cache key
112+
* @return this request
113+
*/
114+
public NsqlRequest withPromptCacheKey(String promptCacheKey) {
115+
this.promptCacheKey = promptCacheKey;
116+
return this;
117+
}
118+
119+
public String getQuery() {
120+
return this.query;
121+
}
122+
123+
public String getModel() {
124+
return this.model;
125+
}
126+
127+
public List<String> getDatasets() {
128+
return this.datasets == null ? null : Collections.unmodifiableList(this.datasets);
129+
}
130+
131+
public boolean isSampleDataEnabled() {
132+
return Boolean.TRUE.equals(this.sampleDataEnabled);
133+
}
134+
135+
public String getPromptCacheKey() {
136+
return this.promptCacheKey;
137+
}
138+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright 2026 The Spice.ai OSS Authors
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package ai.spice;
24+
25+
import java.util.Collections;
26+
import java.util.List;
27+
import java.util.Map;
28+
29+
import com.google.gson.annotations.SerializedName;
30+
31+
/**
32+
* The result of a {@link SpiceClient#nsql(NsqlRequest)} call: the SQL the
33+
* runtime's configured LLM generated from a natural-language query, and the
34+
* rows it produced.
35+
*/
36+
public class NsqlResponse {
37+
@SerializedName("sql")
38+
private String sql;
39+
40+
@SerializedName("row_count")
41+
private int rowCount;
42+
43+
@SerializedName("schema")
44+
private NsqlSchema schema;
45+
46+
@SerializedName("data")
47+
private List<Map<String, Object>> data;
48+
49+
/**
50+
* The query the model generated. Worth logging: a surprising result is
51+
* usually a surprising query.
52+
*
53+
* @return the generated SQL
54+
*/
55+
public String getSql() {
56+
return this.sql;
57+
}
58+
59+
/**
60+
* The number of rows returned.
61+
*
62+
* @return the row count
63+
*/
64+
public int getRowCount() {
65+
return this.rowCount;
66+
}
67+
68+
/**
69+
* The schema describing the columns in {@link #getData()}.
70+
*
71+
* @return the schema, or an empty one when the runtime omitted it
72+
*/
73+
public NsqlSchema getSchema() {
74+
return this.schema == null ? new NsqlSchema() : this.schema;
75+
}
76+
77+
/**
78+
* The rows, each keyed by column name. Values are decoded from JSON, so
79+
* they carry JSON's types rather than the Arrow types named in {@link
80+
* #getSchema()} — numbers arrive as {@code Double}. Use {@link
81+
* SpiceClient#nsqlGenerateSql(NsqlRequest)} with {@code query} or {@code
82+
* queryWithParams} when Arrow-typed results matter.
83+
*
84+
* @return the rows, or an empty list when the runtime returned none
85+
*/
86+
public List<Map<String, Object>> getData() {
87+
return this.data == null ? Collections.emptyList() : this.data;
88+
}
89+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2026 The Spice.ai OSS Authors
3+
4+
Permission is hereby granted, free of charge, to any person obtaining a copy
5+
of this software and associated documentation files (the "Software"), to deal
6+
in the Software without restriction, including without limitation the rights
7+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
copies of the Software, and to permit persons to whom the Software is
9+
furnished to do so, subject to the following conditions:
10+
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
SOFTWARE.
21+
*/
22+
23+
package ai.spice;
24+
25+
import java.util.Collections;
26+
import java.util.List;
27+
28+
import com.google.gson.annotations.SerializedName;
29+
30+
/**
31+
* The schema of the rows an {@link SpiceClient#nsql(NsqlRequest)} call
32+
* returned.
33+
*
34+
* <p>
35+
* {@link #getFields()} is empty when the generated query returned no rows —
36+
* the runtime omits the schema body in that case.
37+
*/
38+
public class NsqlSchema {
39+
@SerializedName("fields")
40+
private List<NsqlField> fields;
41+
42+
/**
43+
* The columns of the result, in order.
44+
*
45+
* @return the fields, or an empty list when the runtime omitted them
46+
*/
47+
public List<NsqlField> getFields() {
48+
return this.fields == null ? Collections.emptyList() : this.fields;
49+
}
50+
}

0 commit comments

Comments
 (0)