Skip to content
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Cross-compiling **to** darwin from anything that is not darwin is the one direct
## What you get

- `context.Context` first on every call that can block. Cancelling it calls into the engine's interrupt, and the failure that comes back answers `errors.Is` against both `context.Canceled` and `zu.Interrupted`, because a caller who wrote the deadline and a caller handling the failure ask different questions of the same error. A context that can never be cancelled starts no goroutine.
- `errors.Is` and `errors.As` against `*zu.Error`, which carries `Code`, `Message`, `StandardText`, `DocURL`, `Severity`, `Retryable`, `Position` and the source line the position points into. The statuses are sentinels of their own: `errors.Is(err, zu.Conflict)` is the retry question and needs no unwrapping.
- `errors.Is` and `errors.As` against `*zu.Error`, which carries `Code`, `Message`, `StandardText`, `DocURL`, `Severity`, `Retryable`, `Position` and the source line the position points into. It also carries the rest of the diagnostic record ISO 39075 subclause 23.2 asks for: `SubjectKind` and `Subject` are what the condition is about when it is about something the statement named, and `Graph` and `Schema` are where the statement was running. A program that highlights the offending label in an editor reads the name off the record rather than out of an English sentence. The statuses are sentinels of their own: `errors.Is(err, zu.Conflict)` is the retry question and needs no unwrapping.
- Range-over-func iteration: `rows.All()` is an `iter.Seq[Row]`, and `zu.Iter[T]` is an `iter.Seq2[T, error]` that streams into your own type.
- Three levels of reading, in the order you reach for them: `Scan` into concrete destinations, `Collect[T]` and `Iter[T]` into a struct matched by `zu` tags or by name, and `Int64s`, `Float64s`, `NodeOffsets` and `Valid` for a whole column borrowed from the result without a copy.
- The seven temporal types spelled out rather than flattened into `time.Time`. A date is a `zu.Date`, a time of day is a `zu.LocalTime`, and a year-month duration is a `zu.YearMonth`, because a `time.Time` made out of a time of day is a date somebody invented. The three that name an instant scan into a `time.Time` when you ask for one.
Expand Down
4 changes: 4 additions & 0 deletions api/surface.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,16 @@ pkg github.com/tamnd/zu-go, field Date.Days int32
pkg github.com/tamnd/zu-go, field Error.Code string
pkg github.com/tamnd/zu-go, field Error.DocURL string
pkg github.com/tamnd/zu-go, field Error.Excerpt string
pkg github.com/tamnd/zu-go, field Error.Graph string
pkg github.com/tamnd/zu-go, field Error.Message string
pkg github.com/tamnd/zu-go, field Error.Position Position
pkg github.com/tamnd/zu-go, field Error.Retryable bool
pkg github.com/tamnd/zu-go, field Error.Schema string
pkg github.com/tamnd/zu-go, field Error.Severity Severity
pkg github.com/tamnd/zu-go, field Error.StandardText string
pkg github.com/tamnd/zu-go, field Error.Status Status
pkg github.com/tamnd/zu-go, field Error.Subject string
pkg github.com/tamnd/zu-go, field Error.SubjectKind string
pkg github.com/tamnd/zu-go, field LocalDateTime.Nanos int64
pkg github.com/tamnd/zu-go, field LocalTime.Nanos int64
pkg github.com/tamnd/zu-go, field Node.Offset uint64
Expand Down
10 changes: 5 additions & 5 deletions bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,13 @@ func TestACharacterStringScansIntoOctetsBecauseThatIsWhatItIs(t *testing.T) {
// is whether the slice is nil.
func TestAByteStringOfNoOctetsIsNotTheSameAsNoByteString(t *testing.T) {
conn := memory(t)
rows := query(t, conn, `RETURN X'' AS empty, null AS nothing`)
rows := query(t, conn, `RETURN X'' AS empty, null AS missing`)
if !rows.Next() {
t.Fatal("no row")
}

var empty, nothing []byte
if err := rows.Scan(&empty, &nothing); err != nil {
var empty, missing []byte
if err := rows.Scan(&empty, &missing); err != nil {
t.Fatalf("scanning the two: %v", err)
}
if empty == nil {
Expand All @@ -116,8 +116,8 @@ func TestAByteStringOfNoOctetsIsNotTheSameAsNoByteString(t *testing.T) {
if len(empty) != 0 {
t.Errorf("X'' came back as %#v", empty)
}
if nothing != nil {
t.Errorf("a null came back as %#v rather than as nil", nothing)
if missing != nil {
t.Errorf("a null came back as %#v rather than as nil", missing)
}

// And through the untyped read, where the two are a []byte and an
Expand Down
8 changes: 7 additions & 1 deletion corpus/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,14 @@ func TestEveryWayACaseCanFailIsReportedInFull(t *testing.T) {
// A case the engine has not caught up to is unsupported and not a
// failure, which is what lets the corpus be the contract and the engine
// catch up to it. The two classes that say so are 42 and 0A.
//
// The statement has to be one the engine really has not reached, and a
// test like this is a canary by construction: the day CREATE lands, this
// stops testing what it says it tests and has to pick another spelling.
// It was SELECT before, which the engine now parses.
func TestACaseAheadOfTheEngineIsUnsupportedAndNotAFailure(t *testing.T) {
got := only(t, " - name: one\n doc: d\n query: SELECT 1\n columns:\n - n\n"+
got := only(t, " - name: one\n doc: d\n query: CREATE NODE TABLE person(uid INT64)\n"+
" columns:\n - n\n"+
" rows:\n")
if got.Outcome != Unsupported {
t.Errorf("came to %s: %s", got.Outcome.mark(), got.Detail)
Expand Down
24 changes: 24 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,26 @@ type Error struct {
// under a caret, since a line cut to fit would put the column
// somewhere it is not.
Excerpt string
// SubjectKind is what kind of thing the condition is about, when
// it is about something the statement named: one lower case word
// out of graph, schema, label, property, variable, type and
// function. It is kept apart from Subject rather than glued to the
// front of it, so that asking whether a failure is about a label
// is one string compared against one word.
SubjectKind string
// Subject is the name the condition is about, written the way the
// statement wrote it and with nothing around it, which is what an
// editor underlines. It and SubjectKind are both empty or both
// set.
Subject string
// Graph is the graph the statement was running in, which ISO 39075
// subclause 23.2 asks a diagnostic record to name. It is empty
// when the failure happened before there was one, which is the
// case for a connection that never opened.
Graph string
// Schema is the schema the statement was running in, empty on the
// same terms as Graph.
Schema string

// cause is the context error behind an interruption, so that a
// query stopped by a cancelled context answers to
Expand Down Expand Up @@ -284,6 +304,10 @@ func take(status C.zu_status, handle *C.zu_error) *Error {
e.StandardText = text(C.zu_error_standard_text(handle, &n), n)
e.DocURL = text(C.zu_error_doc_url(handle, &n), n)
e.Excerpt = text(C.zu_error_excerpt(handle, &n), n)
e.SubjectKind = text(C.zu_error_subject_kind(handle, &n), n)
e.Subject = text(C.zu_error_subject(handle, &n), n)
e.Graph = text(C.zu_error_graph(handle, &n), n)
e.Schema = text(C.zu_error_schema(handle, &n), n)
e.Severity = Severity(C.zu_error_severity(handle))
e.Retryable = C.zu_error_retryable(handle) == 1

Expand Down
8 changes: 4 additions & 4 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,20 +113,20 @@ func ExampleNamed() {

// Parameters are named rather than positional, so there is no order
// to get wrong. The dollar may be written or left off.
rows, err := conn.Query(ctx, `RETURN $a + $b AS sum`,
rows, err := conn.Query(ctx, `RETURN $a + $b AS total`,
zu.Named("a", 40), zu.Named("b", 2))
if err != nil {
log.Fatal(err)
}
defer rows.Close()

var sum int64
var total int64
if rows.Next() {
if err := rows.Scan(&sum); err != nil {
if err := rows.Scan(&total); err != nil {
log.Fatal(err)
}
}
fmt.Println(sum)
fmt.Println(total)
// Output: 42
}

Expand Down
19 changes: 19 additions & 0 deletions include/zu.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,21 @@ const char *zu_version(void);
* is longer than anyone would read under a caret, since a line cut to
* fit would put the column somewhere it is not.
*
* zu_error_subject_kind and zu_error_subject are what the condition is
* about, when it is about something the statement named: the kind is
* one lower-case word out of graph, schema, label, property, variable,
* type and function, and the subject is the name itself, written the
* way the statement wrote it and with nothing around it. They are both
* NULL or both set. A binding that highlights the offending label in
* an editor wants the name on its own, and the kind is kept in its own
* accessor rather than glued to the front of it so that asking "is
* this about a label" is one string compared against one word.
*
* zu_error_graph and zu_error_schema are where the statement was
* running, which ISO 39075 subclause 23.2 asks a diagnostic record to
* name. They are NULL when the failure happened before there was one,
* which is the case for a connection that never opened.
*
* The message says all of this in words and keeps saying it, so
* printing it alone is still a complete report. The fields are for the
* caller that would rather underline the token than read the numbers
Expand All @@ -245,6 +260,10 @@ int32_t zu_error_retryable(const zu_error *e); /* -1 for a NULL error */
zu_status zu_error_position(const zu_error *e, uint32_t *line, uint32_t *column);
zu_status zu_error_offset(const zu_error *e, uint32_t *offset);
const char *zu_error_excerpt(const zu_error *e, size_t *len);
const char *zu_error_subject_kind(const zu_error *e, size_t *len);
const char *zu_error_subject(const zu_error *e, size_t *len);
const char *zu_error_graph(const zu_error *e, size_t *len);
const char *zu_error_schema(const zu_error *e, size_t *len);
void zu_error_free(zu_error *e);

/* How a database is opened. The only struct that crosses this boundary
Expand Down
2 changes: 1 addition & 1 deletion lib/darwin-amd64/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0ba1219a26cd735f1da63e3fddaba9cf6370248
a92d9c1b9c511cbff8b49b426ffe04f73f0d96d4
Binary file modified lib/darwin-amd64/libzu.a
Binary file not shown.
2 changes: 1 addition & 1 deletion lib/darwin-arm64/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0ba1219a26cd735f1da63e3fddaba9cf6370248
a92d9c1b9c511cbff8b49b426ffe04f73f0d96d4
Binary file modified lib/darwin-arm64/libzu.a
Binary file not shown.
2 changes: 1 addition & 1 deletion lib/linux-amd64/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0ba1219a26cd735f1da63e3fddaba9cf6370248
a92d9c1b9c511cbff8b49b426ffe04f73f0d96d4
Binary file modified lib/linux-amd64/libzu.a
Binary file not shown.
2 changes: 1 addition & 1 deletion lib/linux-arm64/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0ba1219a26cd735f1da63e3fddaba9cf6370248
a92d9c1b9c511cbff8b49b426ffe04f73f0d96d4
Binary file modified lib/linux-arm64/libzu.a
Binary file not shown.
2 changes: 1 addition & 1 deletion lib/windows-amd64/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
f0ba1219a26cd735f1da63e3fddaba9cf6370248
a92d9c1b9c511cbff8b49b426ffe04f73f0d96d4
Binary file modified lib/windows-amd64/libzu.a
Binary file not shown.
6 changes: 3 additions & 3 deletions loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ func TestAColumnOfEveryKindReadsBackAsWhatItWas(t *testing.T) {
took := []time.Duration{86_402 * time.Second, 0}
aged := []YearMonth{{Months: 14}, {Months: -1}}

if err := l.Int64s("count", []int64{1, -2}); err != nil {
if err := l.Int64s("tally", []int64{1, -2}); err != nil {
t.Fatal(err)
}
if err := l.Float64s("ratio", []float64{1.5, -0.25}); err != nil {
Expand Down Expand Up @@ -251,12 +251,12 @@ func TestAColumnOfEveryKindReadsBackAsWhatItWas(t *testing.T) {
}

conn := opened(t, path)
rows := query(t, conn, "MATCH (p:person) RETURN p.count AS count, p.ratio AS ratio, "+
rows := query(t, conn, "MATCH (p:person) RETURN p.tally AS tally, p.ratio AS ratio, "+
"p.flag AS flag, p.name AS name, p.born AS born, p.woke AS woke, "+
"p.seen AS seen, p.took AS took, p.aged AS aged")

type row struct {
Count int64
Tally int64
Ratio float64
Flag bool
Name string
Expand Down
42 changes: 42 additions & 0 deletions misuse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,48 @@ func TestEveryRefusalCarriesTheConditionAndThePlace(t *testing.T) {
}
}

// The rest of the diagnostic record ISO 39075 subclause 23.2 asks for:
// what the condition is about and where the statement was running. A
// caller that has to find the offending name inside an English sentence
// is parsing prose, which is the whole reason these are fields.
func TestARefusalAboutSomethingNamedSaysWhatItIsAbout(t *testing.T) {
conn, _ := seeded(t)
err := conn.Exec(t.Context(), "MATCH (a:person) RETURN b.uid AS uid")

var e *Error
if !errors.As(err, &e) {
t.Fatalf("a refused statement is not a *zu.Error: %v", err)
}
// The kind is its own word rather than glued to the front of the
// name, so asking whether this is about a variable is one string
// compared against one word.
if e.SubjectKind != "variable" {
t.Errorf("the condition is about a %q", e.SubjectKind)
}
if e.Subject != "b" {
t.Errorf("the name the condition is about is %q", e.Subject)
}
if e.Graph == "" || e.Schema == "" {
t.Errorf("the statement ran in graph %q schema %q", e.Graph, e.Schema)
}
}

// A condition about no name carries none, rather than carrying a guess
// or the empty string as though it were one. Both halves of the subject
// go together, which is what lets a caller test one of them.
func TestARefusalAboutNothingNamedCarriesNoSubject(t *testing.T) {
conn, _ := seeded(t)
err := conn.Exec(t.Context(), "RETURN 1 / 0 AS n")

var e *Error
if !errors.As(err, &e) {
t.Fatalf("a refused statement is not a *zu.Error: %v", err)
}
if e.SubjectKind != "" || e.Subject != "" {
t.Errorf("a division by zero is about %q %q", e.SubjectKind, e.Subject)
}
}

// openFiles counts the descriptors this process holds, on the systems
// that will say. A count that stands still across a thousand opens is
// the assertion; the absolute number is nobody's business.
Expand Down
2 changes: 1 addition & 1 deletion rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func TestACellReadAsSomethingItIsNotNamesTheColumn(t *testing.T) {

func TestANullNeedsSomewhereThatHoldsOne(t *testing.T) {
conn := memory(t)
rows := query(t, conn, "RETURN null AS nothing")
rows := query(t, conn, "RETURN null AS missing")
if !rows.Next() {
t.Fatal("no row")
}
Expand Down
2 changes: 1 addition & 1 deletion stmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestAValueThisEngineDoesNotTakeIsRefusedBeforeItRuns(t *testing.T) {

func TestAPreparedStatementRunsAgainWithOnlyWhatChangedRebound(t *testing.T) {
conn := memory(t)
stmt, err := conn.Prepare(t.Context(), "RETURN $a + $b AS sum")
stmt, err := conn.Prepare(t.Context(), "RETURN $a + $b AS total")
if err != nil {
t.Fatalf("preparing: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion zusql/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func BenchmarkQueryRowDirect(b *testing.B) {

func BenchmarkPrepared(b *testing.B) {
db := benchDB(b)
stmt, err := db.Prepare("RETURN $n + 1 AS next")
stmt, err := db.Prepare("RETURN $n + 1 AS following")
if err != nil {
b.Fatal(err)
}
Expand Down
6 changes: 3 additions & 3 deletions zusql/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ func Example_named() {
}
defer db.Close()

var sum int64
err = db.QueryRow("RETURN $a + $b AS sum", sql.Named("a", 20), sql.Named("b", 22)).Scan(&sum)
var total int64
err = db.QueryRow("RETURN $a + $b AS total", sql.Named("a", 20), sql.Named("b", 22)).Scan(&total)
if err != nil {
panic(err)
}
fmt.Println(sum)
fmt.Println(total)

_, err = db.Query("RETURN $a AS a", 1)
fmt.Println(err != nil)
Expand Down
2 changes: 1 addition & 1 deletion zusql/zusql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestAResultWithNoRowsEndsTheLoopAndSaysNoRows(t *testing.T) {

func TestAnArgumentIsBoundByName(t *testing.T) {
db := open(t)
got := one(t, db, "RETURN $a + $b AS sum", sql.Named("a", 20), sql.Named("b", 22))
got := one(t, db, "RETURN $a + $b AS total", sql.Named("a", 20), sql.Named("b", 22))
if got != 42 {
t.Errorf("20 + 22 came back as %d", got)
}
Expand Down
Loading