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
7 changes: 5 additions & 2 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ lakespeak export last # to stdout
| `--force` | Overwrite the output file if it exists |

The result is **re-fetched from Databricks** rather than cached locally: a local copy of governed
data would have none of the governance, and Databricks is already the system of record. That means
an expired cached result fails here rather than silently returning stale rows.
data would have none of the governance, and Databricks is already the system of record.

If the cached result has expired, this command re-runs the query rather than failing. That costs
warehouse time, which is the right trade here because you explicitly asked to export the rows —
`ask` deliberately does not re-run on your behalf.

If the result is incomplete — truncated by Databricks, or continuing beyond the rows this version
reads — the command says so on stderr. It never writes a partial export silently.
Expand Down
6 changes: 6 additions & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ This was wrong until a post-ship review caught it: the client relied on `manifes
reports statement-level truncation by Databricks and is `false` for a merely-chunked result. A large
result was returned as its first chunk labelled complete. Fetching remaining chunks is v0.2 work.

## Bound parameter values are shown, but only in JSON

When Genie binds values into the generated SQL, `--format json` reports them under
`query.parameters`. The terminal and Markdown renderers show the statement without them, so a
report can contain SQL whose placeholders are unexplained. Surfacing them everywhere is v0.2.

## Not implemented in v0.1

Full-result downloads beyond the first chunk, visualization rendering, conversation list and resume
Expand Down
15 changes: 14 additions & 1 deletion src/LakeSpeak.Cli/Commands/ExportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,23 @@ private static async Task<int> RunAsync(CliHost host, ParseResult parseResult, C
recent.AgentId, recent.ConversationId, recent.MessageId, recent.AttachmentId, cancellationToken)
.ConfigureAwait(false);

if (result is null)
{
// The cached result aged out. Re-running the attachment's query is the documented
// recovery, and it is the right call here specifically because the user asked to
// export: they want the rows, and re-executing costs warehouse time they have
// implicitly agreed to. `ask` deliberately does not do this on their behalf.
host.Output.Status("The cached result expired; re-running the query…");

result = await host.Client.ReExecuteQueryAsync(
recent.AgentId, recent.ConversationId, recent.MessageId, recent.AttachmentId, cancellationToken)
.ConfigureAwait(false);
}

if (result is null)
{
throw new CliUsageException(
"Databricks no longer has that query result. Cached results expire; ask the question again.");
"Databricks returned no result for that query, even after re-running it. Ask the question again.");
}

var csv = CsvWriter.Write(result);
Expand Down
28 changes: 27 additions & 1 deletion src/LakeSpeak.Rendering/ResultWriters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,16 @@ private sealed record Envelope
// display name of a state changes.
Status = r.State.ToString().ToLowerInvariant(),
Answer = r.Text,
Query = r.Query is null ? null : new QueryRef { Sql = r.Query.Sql, Title = r.Query.Title },
Query = r.Query is null
? null
: new QueryRef
{
Sql = r.Query.Sql,
Title = r.Query.Title,
Parameters = r.Query.Parameters is { Count: > 0 } p
? p.Select(x => new ParameterRef { Name = x.Keyword, Type = x.SqlType, Value = x.Value }).ToList()
: null,
},
Result = r.Result is null ? null : ResultRef.From(r.Result),
SuggestedQuestions = r.SuggestedQuestions.Count == 0 ? null : r.SuggestedQuestions,
DurationMs = (long)r.Metadata.Duration.TotalMilliseconds,
Expand All @@ -133,6 +142,23 @@ private sealed record QueryRef

[JsonPropertyName("title")]
public string? Title { get; init; }

// The values Genie bound into the SQL. Without these a reader sees the statement but not
// what it actually ran with, which is half the point of showing the SQL at all.
[JsonPropertyName("parameters")]
public IReadOnlyList<ParameterRef>? Parameters { get; init; }
}

private sealed record ParameterRef
{
[JsonPropertyName("name")]
public string? Name { get; init; }

[JsonPropertyName("type")]
public string? Type { get; init; }

[JsonPropertyName("value")]
public string? Value { get; init; }
}

private sealed record ResultRef
Expand Down
40 changes: 40 additions & 0 deletions tests/LakeSpeak.Genie.Tests/OutputFidelityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,46 @@ public void Markdown_escapes_pipes_so_a_cell_cannot_break_the_table()
markdown.ShouldContain("a\\|b");
}

[Fact]
public void Json_carries_the_values_bound_into_the_sql()
{
// Arrange — the bind values were captured from the wire and then dropped before any
// renderer saw them, so a reader got the statement but not what it actually ran with.
var response = new GenieResponse(
"a", "c", "m", GenieMessageState.Completed, "answer",
new GenieQuery("SELECT * FROM t WHERE region = :region", Parameters:
[
new GenieQueryParameter("region", "STRING", "Germany"),
]),
null, [], new GenieResponseMetadata(TimeSpan.Zero, 1));

// Act
using var parsed = System.Text.Json.JsonDocument.Parse(MachineOutput.ToJson(response));

// Assert
var parameter = parsed.RootElement.GetProperty("query").GetProperty("parameters")[0];
parameter.GetProperty("name").GetString().ShouldBe("region");
parameter.GetProperty("type").GetString().ShouldBe("STRING");
parameter.GetProperty("value").GetString().ShouldBe("Germany");
}

[Fact]
public void Json_omits_parameters_entirely_when_the_query_had_none()
{
// Arrange — an empty array would imply Genie reported zero bind values; absent is the
// honest encoding of "not applicable".
var response = new GenieResponse(
"a", "c", "m", GenieMessageState.Completed, "answer",
new GenieQuery("SELECT 1"), null, [], new GenieResponseMetadata(TimeSpan.Zero, 1));

// Act
using var parsed = System.Text.Json.JsonDocument.Parse(MachineOutput.ToJson(response));

// Assert
parsed.RootElement.GetProperty("query")
.TryGetProperty("parameters", out _).ShouldBeFalse();
}

[Fact]
public void Json_uses_a_stable_lowercase_status_vocabulary()
{
Expand Down
Loading