Skip to content

Commit 86a5e58

Browse files
committed
Updates
1 parent 3bfe13e commit 86a5e58

5 files changed

Lines changed: 359 additions & 5 deletions

File tree

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,69 @@ SpiceClient client = SpiceClient.builder()
213213
.build();
214214
```
215215

216+
### Iterating Through Results
217+
218+
For more control over query results, you can iterate through rows and access individual field values:
219+
220+
```java
221+
import org.apache.arrow.flight.FlightStream;
222+
import org.apache.arrow.vector.FieldVector;
223+
import org.apache.arrow.vector.Float8Vector;
224+
import org.apache.arrow.vector.VarCharVector;
225+
import org.apache.arrow.vector.VectorSchemaRoot;
226+
import org.apache.arrow.vector.types.pojo.Field;
227+
228+
try (SpiceClient client = SpiceClient.builder().build()) {
229+
FlightStream stream = client.query("SELECT * FROM taxi_trips LIMIT 10;");
230+
231+
while (stream.next()) {
232+
try (VectorSchemaRoot root = stream.getRoot()) {
233+
int rowCount = root.getRowCount();
234+
235+
// Print column names and types
236+
for (Field field : root.getSchema().getFields()) {
237+
System.out.printf("Column: %s, Type: %s%n", field.getName(), field.getType());
238+
}
239+
240+
// Iterate through rows generically
241+
for (int row = 0; row < rowCount; row++) {
242+
for (FieldVector vector : root.getFieldVectors()) {
243+
String columnName = vector.getName();
244+
Object value = vector.isNull(row) ? null : vector.getObject(row);
245+
System.out.printf("%s = %s%n", columnName, value);
246+
}
247+
}
248+
249+
// Access specific columns with type safety
250+
FieldVector fareVector = root.getVector("fare_amount");
251+
if (fareVector instanceof Float8Vector) {
252+
Float8Vector fareVec = (Float8Vector) fareVector;
253+
for (int row = 0; row < rowCount; row++) {
254+
if (!fareVec.isNull(row)) {
255+
double fare = fareVec.get(row);
256+
System.out.printf("Fare: $%.2f%n", fare);
257+
}
258+
}
259+
}
260+
261+
// Access string columns
262+
FieldVector vendorVector = root.getVector("vendor_id");
263+
if (vendorVector instanceof VarCharVector) {
264+
VarCharVector strVec = (VarCharVector) vendorVector;
265+
for (int row = 0; row < rowCount; row++) {
266+
if (!strVec.isNull(row)) {
267+
String vendorId = new String(strVec.get(row), java.nio.charset.StandardCharsets.UTF_8);
268+
System.out.printf("Vendor: %s%n", vendorId);
269+
}
270+
}
271+
}
272+
}
273+
}
274+
}
275+
```
276+
277+
See [ExampleIteratingResults.java](/src/main/java/ai/spice/example/ExampleIteratingResults.java) for a comprehensive example.
278+
216279
### Spice.ai Runtime commands
217280

218281
#### Accelerated dataset refresh
@@ -228,6 +291,40 @@ client.refresh("taxi_trips")
228291

229292
```
230293

294+
### Logging
295+
296+
The SDK uses SLF4J for logging, allowing you to plug in your preferred logging implementation (Logback, Log4j2, java.util.logging, etc.).
297+
298+
**Adding a logging implementation (Maven):**
299+
300+
```xml
301+
<!-- Using Logback -->
302+
<dependency>
303+
<groupId>ch.qos.logback</groupId>
304+
<artifactId>logback-classic</artifactId>
305+
<version>1.5.18</version>
306+
</dependency>
307+
308+
<!-- Or using SLF4J Simple (console output) -->
309+
<dependency>
310+
<groupId>org.slf4j</groupId>
311+
<artifactId>slf4j-simple</artifactId>
312+
<version>2.0.17</version>
313+
</dependency>
314+
```
315+
316+
**Log levels used:**
317+
318+
- `DEBUG` - Client initialization, query execution, connection lifecycle
319+
- `WARN` - Recoverable errors during resource cleanup
320+
- `ERROR` - Query failures, connection errors
321+
322+
To enable debug logging with `slf4j-simple`, set the system property:
323+
324+
```bash
325+
-Dorg.slf4j.simpleLogger.defaultLogLevel=debug
326+
```
327+
231328
## 🤝 Connect with us
232329

233330
Use [issues](https://github.com/spiceai/spice-java/issues), [hey@spice.ai](mailto:hey@spice.ai) or [Slack](https://spiceai.org/slack) to send us feedback, suggestions, or if you need help installing or using the library.

docs/release_notes/v0.5.0.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,13 @@ ArrowReader reader = client.queryWithParams(
5151
- Added Apache Arrow ADBC FlightSQL driver (`adbc-driver-flight-sql:0.21.0`)
5252
- Added Apache Arrow ADBC Core (`adbc-core:0.21.0`)
5353
- Added Gson (`gson:2.13.1`)
54+
- Added SLF4J API (`slf4j-api:2.0.17`) for structured logging
5455

5556
### ⬆️ Updated Dependencies
5657

5758
- Apache Arrow Flight SQL: 17.0.0 → 18.3.0
5859
- Netty: 4.1.108.Final → 4.1.130.Final
59-
- SLF4J Simple: 2.0.16 → 2.0.17
60+
- SLF4J: 2.0.16 → 2.0.17 (moved to `slf4j-api`, `slf4j-simple` now test-scoped)
6061

6162
### 🔧 Updated Build Plugins
6263

@@ -66,11 +67,20 @@ ArrowReader reader = client.queryWithParams(
6667
- maven-gpg-plugin: 3.2.1 → 3.2.8
6768
- central-publishing-maven-plugin: 0.5.0 → 0.9.0
6869

70+
### 📝 Logging Support
71+
72+
The SDK now includes structured logging via SLF4J. Users can plug in their preferred logging implementation (Logback, Log4j2, etc.). Log messages are emitted at:
73+
74+
- **DEBUG**: Client initialization, query execution, ADBC connection lifecycle
75+
- **WARN**: Recoverable errors during resource cleanup
76+
- **ERROR**: Query failures, connection errors
77+
6978
## New Files
7079

7180
- `Param.java` - Parameter class with typed factory methods
7281
- `ParameterizedQueryTest.java` - Test suite for parameterized queries
7382
- `ExampleParameterizedQueries.java` - Example usage of parameterized queries
83+
- `ExampleIteratingResults.java` - Comprehensive example showing how to iterate through query results
7484

7585
## Automatic Type Inference
7686

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,16 @@
6363
<artifactId>netty-all</artifactId>
6464
<version>4.1.130.Final</version>
6565
</dependency>
66+
<dependency>
67+
<groupId>org.slf4j</groupId>
68+
<artifactId>slf4j-api</artifactId>
69+
<version>2.0.17</version>
70+
</dependency>
6671
<dependency>
6772
<groupId>org.slf4j</groupId>
6873
<artifactId>slf4j-simple</artifactId>
6974
<version>2.0.17</version>
75+
<scope>test</scope>
7076
</dependency>
7177
<dependency>
7278
<groupId>junit</groupId>

src/main/java/ai/spice/SpiceClient.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,17 @@ of this software and associated documentation files (the "Software"), to deal
112112
import com.google.gson.Gson;
113113

114114
import org.apache.arrow.flight.sql.FlightSqlClient;
115+
import org.slf4j.Logger;
116+
import org.slf4j.LoggerFactory;
115117

116118
/**
117119
* Client to execute SQL queries against Spice.ai Cloud and Spice.ai OSS.
118120
* Supports both regular queries and parameterized queries using ADBC.
119121
*/
120122
public class SpiceClient implements AutoCloseable {
121123

124+
private static final Logger logger = LoggerFactory.getLogger(SpiceClient.class);
125+
122126
private static final long BYTES_PER_MB = 1024L * 1024L;
123127

124128
// Cached Gson instance for JSON serialization (thread-safe)
@@ -210,6 +214,7 @@ public SpiceClient(String appId, String apiKey, URI flightAddress, URI httpAddre
210214
if (Strings.isNullOrEmpty(apiKey)) {
211215
this.flightClient = new FlightSqlClient(builder.build());
212216
initRetryers();
217+
logger.debug("SpiceClient initialized (unauthenticated) - flightAddress={}", this.flightAddress);
213218
return;
214219
}
215220

@@ -238,6 +243,8 @@ public SpiceClient(String appId, String apiKey, URI flightAddress, URI httpAddre
238243

239244
// Initialize cached retryers (immutable, built once)
240245
initRetryers();
246+
247+
logger.debug("SpiceClient initialized (authenticated) - flightAddress={}, appId={}", this.flightAddress, this.appId);
241248
}
242249

243250
/**
@@ -286,10 +293,14 @@ public FlightStream query(String sql) throws ExecutionException {
286293
throw new IllegalArgumentException("No SQL query provided");
287294
}
288295

296+
logger.debug("Executing query: {}", sql);
289297
try {
290-
return this.queryInternalWithRetry(sql);
298+
FlightStream result = this.queryInternalWithRetry(sql);
299+
logger.debug("Query executed successfully");
300+
return result;
291301
} catch (RetryException e) {
292302
Throwable err = e.getLastFailedAttempt().getExceptionCause();
303+
logger.error("Query failed after {} attempts: {}", e.getNumberOfFailedAttempts(), err.getMessage());
293304
throw new ExecutionException("Failed to execute query due to error: " + err.toString(), err);
294305
}
295306
}
@@ -339,13 +350,18 @@ public ArrowReader queryWithParams(String sql, Object... params) throws Executio
339350
throw new IllegalArgumentException("No SQL query provided");
340351
}
341352

353+
logger.debug("Executing parameterized query with {} parameters: {}", params != null ? params.length : 0, sql);
342354
try {
343355
initADBCIfNeeded();
344-
return queryWithParamsInternal(sql, params);
356+
ArrowReader result = queryWithParamsInternal(sql, params);
357+
logger.debug("Parameterized query executed successfully");
358+
return result;
345359
} catch (AdbcException e) {
360+
logger.error("Parameterized query failed: {}", e.getMessage());
346361
throw new ExecutionException("Failed to execute parameterized query: " + e.getMessage(), e);
347362
} catch (RetryException e) {
348363
Throwable err = e.getLastFailedAttempt().getExceptionCause();
364+
logger.error("Parameterized query failed after {} attempts: {}", e.getNumberOfFailedAttempts(), err.getMessage());
349365
throw new ExecutionException("Failed to execute parameterized query due to error: " + err.toString(), err);
350366
}
351367
}
@@ -359,6 +375,8 @@ private synchronized void initADBCIfNeeded() throws AdbcException {
359375
return;
360376
}
361377

378+
logger.debug("Initializing ADBC connection");
379+
362380
// Format the URI for ADBC FlightSQL driver
363381
String uri = this.flightAddress.toString();
364382

@@ -391,6 +409,8 @@ private synchronized void initADBCIfNeeded() throws AdbcException {
391409
FlightSqlDriver driver = new FlightSqlDriver(allocator);
392410
adbcDatabase = driver.open(options);
393411
adbcConnection = adbcDatabase.connect();
412+
413+
logger.debug("ADBC connection established - uri={}", uri);
394414
}
395415

396416
/**
@@ -400,16 +420,18 @@ private void closeADBC() {
400420
if (adbcConnection != null) {
401421
try {
402422
adbcConnection.close();
423+
logger.debug("ADBC connection closed");
403424
} catch (Exception e) {
404-
// Log but don't fail
425+
logger.warn("Error closing ADBC connection: {}", e.getMessage());
405426
}
406427
adbcConnection = null;
407428
}
408429
if (adbcDatabase != null) {
409430
try {
410431
adbcDatabase.close();
432+
logger.debug("ADBC database closed");
411433
} catch (Exception e) {
412-
// Log but don't fail
434+
logger.warn("Error closing ADBC database: {}", e.getMessage());
413435
}
414436
adbcDatabase = null;
415437
}
@@ -787,6 +809,7 @@ public void refreshDataset(String dataset, RefreshOptions refreshOptions) throws
787809
throw new IllegalArgumentException("No dataset name provided");
788810
}
789811

812+
logger.debug("Refreshing dataset: {}", dataset);
790813
try {
791814
HttpRequest.Builder builder = HttpRequest.newBuilder()
792815
.uri(new URI(String.format("%s/v1/datasets/%s/acceleration/refresh", this.httpAddress, dataset)))
@@ -804,19 +827,23 @@ public void refreshDataset(String dataset, RefreshOptions refreshOptions) throws
804827
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
805828

806829
if (response.statusCode() != 201) {
830+
logger.error("Dataset refresh failed - dataset={}, statusCode={}, response={}", dataset, response.statusCode(), response.body());
807831
throw new ExecutionException(
808832
String.format("Failed to trigger dataset refresh. Status Code: %d, Response: %s",
809833
response.statusCode(),
810834
response.body()),
811835
null);
812836
}
837+
logger.debug("Dataset refresh triggered successfully: {}", dataset);
813838
} catch (ExecutionException e) {
814839
// no need to wrap ExecutionException
815840
throw e;
816841
} catch (ConnectException err) {
842+
logger.error("Cannot connect to Spice runtime at {}: {}", this.httpAddress, err.getMessage());
817843
throw new ExecutionException(
818844
String.format("The Spice runtime is unavailable at %s. Is it running?", this.httpAddress), err);
819845
} catch (Exception err) {
846+
logger.error("Dataset refresh failed: {}", err.getMessage());
820847
throw new ExecutionException("Failed to trigger dataset refresh due to error: " + err.toString(), err);
821848
}
822849
}
@@ -845,28 +872,34 @@ private boolean shouldRetry(CallStatus status) {
845872

846873
@Override
847874
public void close() throws Exception {
875+
logger.debug("Closing SpiceClient");
848876
List<Exception> exceptions = new ArrayList<>();
849877

850878
// Close ADBC resources first
851879
try {
852880
closeADBC();
853881
} catch (Exception e) {
882+
logger.warn("Error during ADBC cleanup: {}", e.getMessage());
854883
exceptions.add(e);
855884
}
856885

857886
// Close Flight client
858887
try {
859888
this.flightClient.close();
889+
logger.debug("Flight client closed");
860890
} catch (Exception e) {
891+
logger.warn("Error closing Flight client: {}", e.getMessage());
861892
exceptions.add(e);
862893
}
863894

864895
// Close allocator
865896
try {
866897
if (this.allocator != null) {
867898
this.allocator.close();
899+
logger.debug("Arrow allocator closed");
868900
}
869901
} catch (Exception e) {
902+
logger.warn("Error closing Arrow allocator: {}", e.getMessage());
870903
exceptions.add(e);
871904
}
872905

@@ -875,7 +908,10 @@ public void close() throws Exception {
875908
for (int i = 1; i < exceptions.size(); i++) {
876909
first.addSuppressed(exceptions.get(i));
877910
}
911+
logger.error("SpiceClient closed with {} error(s)", exceptions.size());
878912
throw first;
879913
}
914+
915+
logger.debug("SpiceClient closed successfully");
880916
}
881917
}

0 commit comments

Comments
 (0)