diff --git a/README.md b/README.md index edad893..a9713f7 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ On the module path the artifact needs `--add-modules dev.zudb.natives`. Nothing The Foreign Function and Memory API is the primary path. The downcall handles are written by hand against `zu.h` rather than generated with `jextract`, because the C ABI here is around seventy functions with a stable shape, and a hand-written layer is where the interesting decisions live: which calls are `Linker.Option.critical` because they are short pure accessors, where the out-parameter scratch space comes from so that a query does not allocate, and how a `zu_error` becomes a typed Java exception exactly once. -An SDK that requires a recent JDK in 2026 excludes a large part of the enterprise ecosystem, so there is a JNI provider too. It is a small C shim, one build a platform, and it is the only native code in this repository that is ours. Four decisions in it are worth knowing about. It does not link against `libzu`, it opens one at run time and resolves what it calls, so building it needs a C compiler and the JDK headers and nothing else, and a shim built today opens a `libzu` built next year at the same ABI. Every string crosses as a `byte[]` rather than as a `jstring`, because JNI's own conversions speak modified UTF-8 and the engine validates the real thing, so an emoji handed to `NewStringUTF` is a byte sequence the engine refuses. One symbol is exported under the name JNI derives, and it registers the other hundred and two from a static table, because `JNI_OnLoad` cannot find a class that a module path or an application server's class loader holds. And a failure is built on the Java side, so the two providers cannot come to disagree about which exception a GQLSTATUS class names. +An SDK that requires a recent JDK in 2026 excludes a large part of the enterprise ecosystem, so there is a JNI provider too. It is a small C shim, one build a platform, and it is the only native code in this repository that is ours. Four decisions in it are worth knowing about. It does not link against `libzu`, it opens one at run time and resolves what it calls, so building it needs a C compiler and the JDK headers and nothing else, and a shim built today opens a `libzu` built next year at the same ABI. Every string crosses as a `byte[]` rather than as a `jstring`, because JNI's own conversions speak modified UTF-8 and the engine validates the real thing, so an emoji handed to `NewStringUTF` is a byte sequence the engine refuses. One symbol is exported under the name JNI derives, and it registers the other hundred and four from a static table, because `JNI_OnLoad` cannot find a class that a module path or an application server's class loader holds. And a failure is built on the Java side, so the two providers cannot come to disagree about which exception a GQLSTATUS class names. | Artifact | Baseline | Role | diff --git a/api/surface.txt b/api/surface.txt index 5033c13..edf3071 100644 --- a/api/surface.txt +++ b/api/surface.txt @@ -1,7 +1,8 @@ ctor public dev.zudb.Config(long, long, boolean) -ctor public dev.zudb.Diagnostic(dev.zudb.Status, java.lang.String, java.lang.String, java.lang.String, dev.zudb.Severity, int, int, int, java.lang.String, java.lang.String, boolean) +ctor public dev.zudb.Diagnostic(dev.zudb.Status, java.lang.String, java.lang.String, java.lang.String, dev.zudb.Severity, int, int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean) ctor public dev.zudb.Value$BindingTable() ctor public dev.zudb.Value$Bool(boolean) +ctor public dev.zudb.Value$Bytes(byte[]) ctor public dev.zudb.Value$Field(java.lang.String, dev.zudb.Value) ctor public dev.zudb.Value$Float(double) ctor public dev.zudb.Value$Graph() @@ -36,6 +37,7 @@ field public static final dev.zudb.Status dev.zudb.Status.UNKNOWN field public static final dev.zudb.Status dev.zudb.Status.UNSUPPORTED field public static final dev.zudb.Type dev.zudb.Type.BINDING_TABLE field public static final dev.zudb.Type dev.zudb.Type.BOOL +field public static final dev.zudb.Type dev.zudb.Type.BYTES field public static final dev.zudb.Type dev.zudb.Type.FLOAT field public static final dev.zudb.Type dev.zudb.Type.GRAPH field public static final dev.zudb.Type dev.zudb.Type.INT @@ -62,6 +64,7 @@ method public abstract boolean dev.zudb.spi.ZuBinding.connInTransaction(long) method public abstract boolean dev.zudb.spi.ZuBinding.connUnregister(long, java.lang.String) method public abstract boolean dev.zudb.spi.ZuBinding.databaseIsMemory(long) method public abstract boolean dev.zudb.spi.ZuBinding.valueBoolean(long) +method public abstract byte[] dev.zudb.spi.ZuBinding.valueBytes(long) method public abstract dev.zudb.Diagnostic dev.zudb.spi.ZuBinding.resultNotice(long, int) method public abstract dev.zudb.spi.ZuBinding dev.zudb.spi.ZuProvider.load(java.nio.file.Path) method public abstract double dev.zudb.spi.ZuBinding.valueDouble(long) @@ -73,6 +76,7 @@ method public abstract int dev.zudb.spi.ZuBinding.valueType(long) method public abstract int dev.zudb.spi.ZuProvider.priority() method public abstract java.lang.String dev.zudb.spi.ZuBinding.appenderColumnName(long, int) method public abstract java.lang.String dev.zudb.spi.ZuBinding.connRegisteredName(long, long) +method public abstract java.lang.String dev.zudb.spi.ZuBinding.connTableName(long, int) method public abstract java.lang.String dev.zudb.spi.ZuBinding.databasePath(long) method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultCellString(long, long, int) method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultColName(long, int) @@ -178,7 +182,11 @@ method public boolean dev.zudb.Row.isNull(int) method public boolean dev.zudb.Row.isNull(java.lang.String) method public boolean dev.zudb.Statement.isClosed() method public boolean dev.zudb.Value$Bool.value() +method public boolean dev.zudb.Value$Bytes.equals(java.lang.Object) method public boolean dev.zudb.ZuException.retryable() +method public byte[] dev.zudb.Row.getBytes(int) +method public byte[] dev.zudb.Row.getBytes(java.lang.String) +method public byte[] dev.zudb.Value$Bytes.value() method public dev.zudb.Appender dev.zudb.Appender.append(boolean) method public dev.zudb.Appender dev.zudb.Appender.append(byte[]) method public dev.zudb.Appender dev.zudb.Appender.append(dev.zudb.Value$Temporal$Kind, long) @@ -327,6 +335,7 @@ method public int dev.zudb.Result.columnIndex(java.lang.String) method public int dev.zudb.Result.columns() method public int dev.zudb.Status.value() method public int dev.zudb.Type.value() +method public int dev.zudb.Value$Bytes.hashCode() method public int dev.zudb.Value$Node.table() method public int dev.zudb.Value$Rel.table() method public int dev.zudb.Value$Temporal$Kind.value() @@ -335,18 +344,24 @@ method public int dev.zudb.ZuException$Position.column() method public int dev.zudb.ZuException$Position.line() method public int dev.zudb.ZuException$Position.offset() method public java.lang.String dev.zudb.Appender.columnName(int) +method public java.lang.String dev.zudb.Connection.tableName(int) method public java.lang.String dev.zudb.Database.path() method public java.lang.String dev.zudb.Diagnostic.code() method public java.lang.String dev.zudb.Diagnostic.condition() method public java.lang.String dev.zudb.Diagnostic.docUrl() method public java.lang.String dev.zudb.Diagnostic.excerpt() +method public java.lang.String dev.zudb.Diagnostic.graph() method public java.lang.String dev.zudb.Diagnostic.message() +method public java.lang.String dev.zudb.Diagnostic.schema() +method public java.lang.String dev.zudb.Diagnostic.subject() +method public java.lang.String dev.zudb.Diagnostic.subjectKind() method public java.lang.String dev.zudb.Frame.name() method public java.lang.String dev.zudb.Result.columnName(int) method public java.lang.String dev.zudb.Result.gqlstatus() method public java.lang.String dev.zudb.Row.getString(int) method public java.lang.String dev.zudb.Row.getString(java.lang.String) method public java.lang.String dev.zudb.Row.toString() +method public java.lang.String dev.zudb.Value$Bytes.toString() method public java.lang.String dev.zudb.Value$Field.name() method public java.lang.String dev.zudb.Value$Str.value() method public java.nio.ByteBuffer dev.zudb.Chunk.valid(int) @@ -378,6 +393,10 @@ method public java.util.Optional dev.zudb.ZuException.code() method public java.util.Optional dev.zudb.ZuException.condition() method public java.util.Optional dev.zudb.ZuException.docUrl() method public java.util.Optional dev.zudb.ZuException.excerpt() +method public java.util.Optional dev.zudb.ZuException.graph() +method public java.util.Optional dev.zudb.ZuException.schema() +method public java.util.Optional dev.zudb.ZuException.subject() +method public java.util.Optional dev.zudb.ZuException.subjectKind() method public java.util.Spliterator dev.zudb.Result.spliterator() method public java.util.stream.Stream dev.zudb.Result.chunks() method public java.util.stream.Stream dev.zudb.Result.stream() @@ -419,7 +438,7 @@ method public static dev.zudb.Database dev.zudb.Database.open(java.lang.String, method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path) method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path, dev.zudb.Config) method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.misuse(dev.zudb.Status, java.lang.String) -method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.of(int, java.lang.String, java.lang.String, java.lang.String, int, int, int, int, java.lang.String, java.lang.String, boolean) +method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.of(int, java.lang.String, java.lang.String, java.lang.String, int, int, int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean) method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long) method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long, java.lang.Runnable) method public static dev.zudb.Loader dev.zudb.Loader.create(java.nio.file.Path) @@ -464,7 +483,7 @@ method public void dev.zudb.Result.exportArrow(long) method public void dev.zudb.Result.exportArrow(long, long) method public void dev.zudb.Statement.close() type public abstract interface dev.zudb.Progress -type public abstract interface dev.zudb.Value sealed permits dev.zudb.Value$BindingTable, dev.zudb.Value$Bool, dev.zudb.Value$Float, dev.zudb.Value$Graph, dev.zudb.Value$Int, dev.zudb.Value$List, dev.zudb.Value$Node, dev.zudb.Value$Null, dev.zudb.Value$Path, dev.zudb.Value$Record, dev.zudb.Value$Rel, dev.zudb.Value$Str, dev.zudb.Value$Temporal +type public abstract interface dev.zudb.Value sealed permits dev.zudb.Value$BindingTable, dev.zudb.Value$Bool, dev.zudb.Value$Bytes, dev.zudb.Value$Float, dev.zudb.Value$Graph, dev.zudb.Value$Int, dev.zudb.Value$List, dev.zudb.Value$Node, dev.zudb.Value$Null, dev.zudb.Value$Path, dev.zudb.Value$Record, dev.zudb.Value$Rel, dev.zudb.Value$Str, dev.zudb.Value$Temporal type public abstract interface dev.zudb.spi.ZuBinding type public abstract interface dev.zudb.spi.ZuProvider type public class dev.zudb.ZuClosedException extends dev.zudb.ZuProgrammingException @@ -497,6 +516,7 @@ type public final record dev.zudb.Diagnostic extends java.lang.Record type public static final enum dev.zudb.Value$Temporal$Kind extends java.lang.Enum type public static final record dev.zudb.Value$BindingTable extends java.lang.Record implements dev.zudb.Value type public static final record dev.zudb.Value$Bool extends java.lang.Record implements dev.zudb.Value +type public static final record dev.zudb.Value$Bytes extends java.lang.Record implements dev.zudb.Value type public static final record dev.zudb.Value$Field extends java.lang.Record type public static final record dev.zudb.Value$Float extends java.lang.Record implements dev.zudb.Value type public static final record dev.zudb.Value$Graph extends java.lang.Record implements dev.zudb.Value diff --git a/zudb-ffm/src/main/java/dev/zudb/ffm/Abi.java b/zudb-ffm/src/main/java/dev/zudb/ffm/Abi.java index c7467e0..286ed98 100644 --- a/zudb-ffm/src/main/java/dev/zudb/ffm/Abi.java +++ b/zudb-ffm/src/main/java/dev/zudb/ffm/Abi.java @@ -54,6 +54,10 @@ final class Abi { final MethodHandle errorPosition; final MethodHandle errorOffset; final MethodHandle errorExcerpt; + final MethodHandle errorSubjectKind; + final MethodHandle errorSubject; + final MethodHandle errorGraph; + final MethodHandle errorSchema; final MethodHandle errorFree; final MethodHandle configSet; @@ -73,6 +77,7 @@ final class Abi { final MethodHandle connClose; final MethodHandle connInterrupt; final MethodHandle connRowsRead; + final MethodHandle connTableName; final MethodHandle connSetProgress; final MethodHandle connInTransaction; final MethodHandle begin; @@ -160,6 +165,7 @@ final class Abi { final MethodHandle valueI64; final MethodHandle valueF64; final MethodHandle valueStr; + final MethodHandle valueBytes; final MethodHandle valueTemporal; final MethodHandle valueNode; final MethodHandle valueRel; @@ -193,6 +199,10 @@ final class Abi { errorPosition = h("zu_error_position", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS)); errorOffset = h("zu_error_offset", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS)); errorExcerpt = h("zu_error_excerpt", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS)); + errorSubjectKind = h("zu_error_subject_kind", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS)); + errorSubject = h("zu_error_subject", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS)); + errorGraph = h("zu_error_graph", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS)); + errorSchema = h("zu_error_schema", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS)); errorFree = h("zu_error_free", FunctionDescriptor.ofVoid(ADDRESS)); configSet = @@ -216,6 +226,8 @@ final class Abi { connClose = h("zu_conn_close", FunctionDescriptor.ofVoid(ADDRESS)); connInterrupt = h("zu_conn_interrupt", FunctionDescriptor.of(JAVA_INT, ADDRESS)); connRowsRead = h("zu_conn_rows_read", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS)); + connTableName = h("zu_conn_table_name", FunctionDescriptor.of( + ADDRESS, ADDRESS, JAVA_INT, ADDRESS)); connSetProgress = h("zu_conn_set_progress", FunctionDescriptor.of( JAVA_INT, ADDRESS, ADDRESS, ADDRESS, JAVA_LONG)); connInTransaction = h("zu_conn_in_transaction", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS)); @@ -423,6 +435,7 @@ final class Abi { valueI64 = h("zu_value_i64", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS)); valueF64 = h("zu_value_f64", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS)); valueStr = h("zu_value_str", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS)); + valueBytes = h("zu_value_bytes", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS)); valueTemporal = h("zu_value_temporal", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS, ADDRESS)); valueNode = h("zu_value_node", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS)); diff --git a/zudb-ffm/src/main/java/dev/zudb/ffm/FfmBinding.java b/zudb-ffm/src/main/java/dev/zudb/ffm/FfmBinding.java index 778cf5f..2b0736a 100644 --- a/zudb-ffm/src/main/java/dev/zudb/ffm/FfmBinding.java +++ b/zudb-ffm/src/main/java/dev/zudb/ffm/FfmBinding.java @@ -232,6 +232,19 @@ public long connRowsRead(long conn) { } } + @Override + public String connTableName(long conn, int table) { + Scratch s = Scratch.get(); + MemorySegment sl = s.slots(); + try { + MemorySegment out = + (MemorySegment) abi.connTableName.invokeExact(ptr(conn), table, sl.asSlice(LEN, 8)); + return utf8(out.address(), sl.get(JAVA_LONG, LEN)); + } catch (Throwable t) { + throw fail("zu_conn_table_name", t); + } + } + @Override public void connSetProgress(long conn, dev.zudb.Progress watcher, long intervalMillis) { if (watcher == null) { @@ -697,6 +710,20 @@ public String valueString(long value) { } } + @Override + public byte[] valueBytes(long value) { + Scratch s = Scratch.get(); + MemorySegment sl = s.slots(); + try { + int st = (int) abi.valueBytes.invokeExact(ptr(value), sl.asSlice(OUT, 8), sl.asSlice(LEN, 8)); + check("zu_value_bytes", st, null); + byte[] octets = octets(sl.get(ADDRESS, OUT).address(), sl.get(JAVA_LONG, LEN)); + return octets == null ? new byte[0] : octets; + } catch (Throwable t) { + throw fail("zu_value_bytes", t); + } + } + @Override public long[] valueTemporal(long value) { Scratch s = Scratch.get(); @@ -1600,6 +1627,10 @@ private Diagnostic diagnostic(long err) { String condition = text(abi.errorStandardText, e); String docUrl = text(abi.errorDocUrl, e); String excerpt = text(abi.errorExcerpt, e); + String subjectKind = text(abi.errorSubjectKind, e); + String subject = text(abi.errorSubject, e); + String graph = text(abi.errorGraph, e); + String schema = text(abi.errorSchema, e); int line = -1; int column = -1; int offset = -1; @@ -1612,8 +1643,8 @@ private Diagnostic diagnostic(long err) { offset = sl.get(JAVA_INT, C); } return Diagnostic.of( - status, message, code, condition, severity, line, column, offset, excerpt, docUrl, - retryable == 1); + status, message, code, condition, severity, line, column, offset, excerpt, + subjectKind, subject, graph, schema, docUrl, retryable == 1); } catch (Throwable t) { throw fail("zu_error", t); } finally { @@ -1633,15 +1664,20 @@ private static String text(java.lang.invoke.MethodHandle mh, MemorySegment e) th } private static String utf8(long address, long length) { + byte[] bytes = octets(address, length); + return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8); + } + + /** A run of native bytes, copied out, or null for a null pointer. */ + private static byte[] octets(long address, long length) { if (address == 0) { return null; } - if (length == 0) { - return ""; - } byte[] bytes = new byte[(int) length]; - MemorySegment.copy(reinterpret(address, length), JAVA_BYTE, 0, bytes, 0, bytes.length); - return new String(bytes, StandardCharsets.UTF_8); + if (length > 0) { + MemorySegment.copy(reinterpret(address, length), JAVA_BYTE, 0, bytes, 0, bytes.length); + } + return bytes; } @SuppressWarnings("restricted") diff --git a/zudb-jni/src/main/c/zu.h b/zudb-jni/src/main/c/zu.h index 93e66ad..3d85b0c 100644 --- a/zudb-jni/src/main/c/zu.h +++ b/zudb-jni/src/main/c/zu.h @@ -52,14 +52,14 @@ #include /* The revision of this ABI (dx/02 section 8), which is what a build - * system tests when it has to compile one way against 0.12 and another + * system tests when it has to compile one way against 0.14 and another * against what comes next. The two numbers are counts and not decimals, - * so 0.12 is the revision after 0.11 and a caller comparing them + * so 0.14 is the revision after 0.13 and a caller comparing them * compares each on its own. `cargo xtask package` holds it to the * constant the rest of the workspace reports, `zu version` included, * so a header and a binary that disagree is a failed check rather than * a caller's afternoon. */ -#define ZU_ABI_VERSION "0.12" +#define ZU_ABI_VERSION "0.14" #ifdef __cplusplus extern "C" { @@ -154,6 +154,10 @@ typedef enum zu_status { whole of what a binding can say about the cell. */ #define ZU_TYPE_GRAPH 11 #define ZU_TYPE_BINDING_TABLE 12 +/* GV35, a byte string. It reads through zu_value_bytes and not through + zu_value_str: the octets need not be text, and a host that took them + for a string would decode them. */ +#define ZU_TYPE_BYTES 13 /* Which temporal a temporal cell is, from zu_value_temporal. The unit * follows the kind: days for a date, months for a year-month duration, @@ -227,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 @@ -241,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 @@ -586,6 +609,12 @@ zu_status zu_value_bool(const zu_value *v, int32_t *out); zu_status zu_value_i64(const zu_value *v, int64_t *out); zu_status zu_value_f64(const zu_value *v, double *out); zu_status zu_value_str(const zu_value *v, const char **out, size_t *len); +/* A byte string on the same terms as zu_value_str: pointing into the + * result's bytes, not copied, not NUL-terminated, and len may not be + * NULL. A separate accessor because the two hold different types and + * one answering both would let a host read octets as text without ever + * asking whether they were. */ +zu_status zu_value_bytes(const zu_value *v, const uint8_t **out, size_t *len); /* kind and count are required; offset may be NULL for a host with no * zoned type, and is minutes east of UTC, 0 for the five kinds that * carry none. */ @@ -594,6 +623,11 @@ zu_status zu_value_temporal(const zu_value *v, int32_t *kind, int64_t *count, in * number their rows from zero. Either out-parameter may be NULL. */ zu_status zu_value_node(const zu_value *v, uint32_t *table, uint64_t *offset); zu_status zu_value_rel(const zu_value *v, uint32_t *table, uint64_t *src, uint64_t *dst); +/* What that table id is called, or NULL when no table has that id. Node + * and rel tables share one id space, so one call answers for both + * kinds. NUL-terminated, owned by the connection, and valid until the + * next zu_conn_table_name on it or until it closes; len may be NULL. */ +const char *zu_conn_table_name(zu_conn *conn, uint32_t table, size_t *len); uint64_t zu_value_len(const zu_value *v); zu_status zu_value_at(const zu_value *v, uint64_t i, const zu_value **out); /* A record's fields are in name order and a name appears once, which is diff --git a/zudb-jni/src/main/c/zudb_jni.c b/zudb-jni/src/main/c/zudb_jni.c index 1146fa5..e99348e 100644 --- a/zudb-jni/src/main/c/zudb_jni.c +++ b/zudb-jni/src/main/c/zudb_jni.c @@ -68,6 +68,10 @@ X(error_position, zu_status, (const zu_error *, uint32_t *, uint32_t *)) \ X(error_offset, zu_status, (const zu_error *, uint32_t *)) \ X(error_excerpt, const char *, (const zu_error *, size_t *)) \ + X(error_subject_kind, const char *, (const zu_error *, size_t *)) \ + X(error_subject, const char *, (const zu_error *, size_t *)) \ + X(error_graph, const char *, (const zu_error *, size_t *)) \ + X(error_schema, const char *, (const zu_error *, size_t *)) \ X(error_free, void, (zu_error *)) \ X(config_set, zu_status, \ (zu_config *, const char *, size_t, const char *, size_t, zu_error **)) \ @@ -87,6 +91,7 @@ X(conn_close, void, (zu_conn *)) \ X(conn_interrupt, zu_status, (zu_conn *)) \ X(conn_rows_read, zu_status, (zu_conn *, uint64_t *)) \ + X(conn_table_name, const char *, (zu_conn *, uint32_t, size_t *)) \ X(conn_set_progress, zu_status, (zu_conn *, zu_progress_fn, void *, uint64_t)) \ X(query, zu_status, (zu_conn *, const char *, size_t, zu_result **, zu_error **)) \ X(prepare, zu_status, (zu_conn *, const char *, size_t, zu_stmt **, zu_error **)) \ @@ -130,6 +135,7 @@ X(value_i64, zu_status, (const zu_value *, int64_t *)) \ X(value_f64, zu_status, (const zu_value *, double *)) \ X(value_str, zu_status, (const zu_value *, const char **, size_t *)) \ + X(value_bytes, zu_status, (const zu_value *, const uint8_t **, size_t *)) \ X(value_temporal, zu_status, (const zu_value *, int32_t *, int64_t *, int32_t *)) \ X(value_node, zu_status, (const zu_value *, uint32_t *, uint64_t *)) \ X(value_rel, zu_status, (const zu_value *, uint32_t *, uint64_t *, uint64_t *)) \ @@ -374,6 +380,8 @@ static void raise(JNIEnv *env, zu_status status, zu_error *e, const char *what) env, c_binding, m_diagnostic, (jint)p_error_status(e), field(env, e, p_error_message), field(env, e, p_error_code), field(env, e, p_error_standard_text), (jint)p_error_severity(e), jline, jcolumn, joffset, field(env, e, p_error_excerpt), + field(env, e, p_error_subject_kind), field(env, e, p_error_subject), + field(env, e, p_error_graph), field(env, e, p_error_schema), field(env, e, p_error_doc_url), p_error_retryable(e) == 1 ? JNI_TRUE : JNI_FALSE); p_error_free(e); if (record == NULL) { @@ -406,6 +414,8 @@ static jobject record(JNIEnv *env, zu_error *e) { env, c_binding, m_diagnostic, (jint)p_error_status(e), field(env, e, p_error_message), field(env, e, p_error_code), field(env, e, p_error_standard_text), (jint)p_error_severity(e), jline, jcolumn, joffset, field(env, e, p_error_excerpt), + field(env, e, p_error_subject_kind), field(env, e, p_error_subject), + field(env, e, p_error_graph), field(env, e, p_error_schema), field(env, e, p_error_doc_url), p_error_retryable(e) == 1 ? JNI_TRUE : JNI_FALSE); p_error_free(e); return out; @@ -701,6 +711,13 @@ static jlong n_conn_rows_read(JNIEnv *env, jclass self, jlong conn) { return (jlong)out; } +static jbyteArray n_conn_table_name(JNIEnv *env, jclass self, jlong conn, jint table) { + size_t len = 0; + const char *p = p_conn_table_name(H(zu_conn, conn), (uint32_t)table, &len); + (void)self; + return bytes(env, p, len); +} + /* ---- the progress callback ---- */ /* @@ -1153,6 +1170,15 @@ static jbyteArray n_value_string(JNIEnv *env, jclass self, jlong value) { return bytes(env, p, len); } +static jbyteArray n_value_bytes(JNIEnv *env, jclass self, jlong value) { + const uint8_t *p = NULL; + size_t len = 0; + zu_status st = p_value_bytes(H(zu_value, value), &p, &len); + (void)self; + FAIL_IF_V(env, st, NULL, "zu_value_bytes", NULL); + return bytes(env, (const char *)p, len); +} + static jlongArray n_value_temporal(JNIEnv *env, jclass self, jlong value) { int32_t kind = 0; int64_t count = 0; @@ -1756,6 +1782,7 @@ static const JNINativeMethod methods[] = { {"nConnClose", "(J)V", (void *)n_conn_close}, {"nConnInterrupt", "(J)V", (void *)n_conn_interrupt}, {"nConnRowsRead", "(J)J", (void *)n_conn_rows_read}, + {"nConnTableName", "(JI)[B", (void *)n_conn_table_name}, {"nConnSetProgress", "(JLdev/zudb/Progress;J)J", (void *)n_conn_set_progress}, {"nWatchFree", "(J)V", (void *)n_watch_free}, {"nConnInTransaction", "(J)Z", (void *)n_conn_in_transaction}, @@ -1798,6 +1825,7 @@ static const JNINativeMethod methods[] = { {"nValueLong", "(J)J", (void *)n_value_long}, {"nValueDouble", "(J)D", (void *)n_value_double}, {"nValueString", "(J)[B", (void *)n_value_string}, + {"nValueBytes", "(J)[B", (void *)n_value_bytes}, {"nValueTemporal", "(J)[J", (void *)n_value_temporal}, {"nValueNode", "(J)[J", (void *)n_value_node}, {"nValueRel", "(J)[J", (void *)n_value_rel}, @@ -1879,7 +1907,7 @@ JNIEXPORT jboolean JNICALL Java_dev_zudb_jni_JniBinding_nRegister(JNIEnv *env, j } m_diagnostic = (*env)->GetStaticMethodID(env, self, "diagnostic", - "(I[B[B[BIIII[B[BZ)Ldev/zudb/Diagnostic;"); + "(I[B[B[BIIII[B[B[B[B[B[BZ)Ldev/zudb/Diagnostic;"); m_misuse = (*env)->GetStaticMethodID(env, self, "misuse", "(ILjava/lang/String;)Ldev/zudb/ZuException;"); m_to_exception = diff --git a/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java b/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java index 875b322..a2b8477 100644 --- a/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java +++ b/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java @@ -221,6 +221,10 @@ private static Diagnostic diagnostic( int column, int offset, byte[] excerpt, + byte[] subjectKind, + byte[] subject, + byte[] graph, + byte[] schema, byte[] docUrl, boolean retryable) { return Diagnostic.of( @@ -233,6 +237,10 @@ private static Diagnostic diagnostic( column, offset, str(excerpt), + str(subjectKind), + str(subject), + str(graph), + str(schema), str(docUrl), retryable); } @@ -327,6 +335,11 @@ public long connRowsRead(long conn) { return nConnRowsRead(conn); } + @Override + public String connTableName(long conn, int table) { + return str(nConnTableName(conn, table)); + } + @Override public void connSetProgress(long conn, Progress watcher, long intervalMillis) { long cookie = nConnSetProgress(conn, watcher, intervalMillis); @@ -557,6 +570,12 @@ public String valueString(long value) { return str(nValueString(value)); } + @Override + public byte[] valueBytes(long value) { + byte[] octets = nValueBytes(value); + return octets == null ? new byte[0] : octets; + } + @Override public long[] valueTemporal(long value) { return nValueTemporal(value); @@ -874,6 +893,8 @@ private static native long nDatabaseCreate( private static native long nConnRowsRead(long conn); + private static native byte[] nConnTableName(long conn, int table); + private static native long nConnSetProgress(long conn, Progress watcher, long intervalMillis); private static native void nWatchFree(long cookie); @@ -959,6 +980,8 @@ private static native void nBindTemporal( private static native byte[] nValueString(long value); + private static native byte[] nValueBytes(long value); + private static native long[] nValueTemporal(long value); private static native long[] nValueNode(long value); diff --git a/zudb-tck/src/main/java/dev/zudb/tck/BytesTest.java b/zudb-tck/src/main/java/dev/zudb/tck/BytesTest.java new file mode 100644 index 0000000..e014aaa --- /dev/null +++ b/zudb-tck/src/main/java/dev/zudb/tck/BytesTest.java @@ -0,0 +1,127 @@ +package dev.zudb.tck; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.zudb.Connection; +import dev.zudb.Database; +import dev.zudb.Result; +import dev.zudb.Type; +import dev.zudb.Value; +import dev.zudb.ZuProgrammingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Byte strings, which are octets and not text. + * + *

The point of a separate type is that nothing here decodes: a client that + * read these as a string would turn octets that are not UTF-8 into + * replacement characters and never say it had. + */ +public class BytesTest { + + private static Database db; + private static Connection conn; + + @BeforeAll + static void engine() { + Libzu.require(); + db = Database.memory(); + conn = db.connect(); + } + + @AfterAll + static void done() { + if (conn != null) { + conn.close(); + } + if (db != null) { + db.close(); + } + } + + @Test + void aByteStringLiteralComesBackAsBytes() { + try (Result r = conn.query("RETURN X'00AB00' AS b")) { + assertEquals(Type.BYTES, r.row(0).type("b")); + Value.Bytes bytes = assertInstanceOf(Value.Bytes.class, r.row(0).get("b")); + assertArrayEquals(new byte[] {0x00, (byte) 0xAB, 0x00}, bytes.value()); + } + } + + @Test + void theTypedAccessorHandsOverTheOctets() { + try (Result r = conn.query("RETURN X'DEADBEEF' AS b")) { + assertArrayEquals( + new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}, r.row(0).getBytes("b")); + assertArrayEquals(r.row(0).getBytes("b"), r.row(0).getBytes(0)); + } + } + + @Test + void noOctetsAtAllIsAnEmptyArrayAndNotANull() { + try (Result r = conn.query("RETURN X'' AS b")) { + assertEquals(Type.BYTES, r.row(0).type("b")); + assertArrayEquals(new byte[0], r.row(0).getBytes("b")); + } + } + + @Test + void aNullCellIsNullRatherThanAThrow() { + try (Result r = conn.query("RETURN null AS b")) { + assertNull(r.row(0).getBytes("b")); + } + } + + @Test + void aStringIsRefusedRatherThanEncoded() { + try (Result r = conn.query("RETURN 'ada' AS s")) { + // The two are different types. A client that quietly turned one into + // the other would be answering a question nobody asked. + assertThrows(ZuProgrammingException.class, () -> r.row(0).getBytes("s")); + } + } + + @Test + void byteStringsWithTheSameOctetsAreOneValue() { + // A record over an array compares by identity unless it is written out, + // and two byte strings holding the same octets are one value. + assertEquals(new Value.Bytes(new byte[] {1, 2}), new Value.Bytes(new byte[] {1, 2})); + assertEquals( + new Value.Bytes(new byte[] {1, 2}).hashCode(), + new Value.Bytes(new byte[] {1, 2}).hashCode()); + assertNotEquals(new Value.Bytes(new byte[] {1, 2}), new Value.Bytes(new byte[] {1, 3})); + } + + @Test + void aByteStringSpellsItselfInHex() { + assertEquals("X'00AB00'", new Value.Bytes(new byte[] {0, (byte) 0xAB, 0}).toString()); + assertEquals("X''", new Value.Bytes(new byte[0]).toString()); + } + + @Test + void theArrayIsTheCallersOwnCopy() { + try (Result r = conn.query("RETURN X'0102' AS b")) { + byte[] first = r.row(0).getBytes("b"); + first[0] = 0x7F; + assertArrayEquals(new byte[] {0x01, 0x02}, r.row(0).getBytes("b")); + } + } + + @Test + void aByteStringInATreeOutlivesTheResult() { + Value value; + try (Result r = conn.query("RETURN [X'01', X'02'] AS v")) { + value = r.row(0).get("v"); + } + Value.List list = assertInstanceOf(Value.List.class, value); + assertEquals(new Value.Bytes(new byte[] {1}), list.items().get(0)); + assertEquals(new Value.Bytes(new byte[] {2}), list.items().get(1)); + } +} diff --git a/zudb-tck/src/main/java/dev/zudb/tck/TableNameTest.java b/zudb-tck/src/main/java/dev/zudb/tck/TableNameTest.java new file mode 100644 index 0000000..5d06dc8 --- /dev/null +++ b/zudb-tck/src/main/java/dev/zudb/tck/TableNameTest.java @@ -0,0 +1,80 @@ +package dev.zudb.tck; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import dev.zudb.Connection; +import dev.zudb.Database; +import dev.zudb.Loader; +import dev.zudb.Result; +import dev.zudb.Value; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Turning the number in a node back into the name the statement wrote. + * + *

A node is a table and a row of it, and the table comes over as the id the + * engine keeps it under, because two tables number their rows from zero and + * the number is what identifies the node. Every host that prints a node wants + * the name, so the ABI has one call for it. + */ +public class TableNameTest { + + @TempDir Path dir; + + @BeforeAll + static void engine() { + Libzu.require(); + } + + private Path people() { + Path path = dir.resolve("named.zu"); + try (Loader loader = Loader.create(path)) { + loader.table("person", "knows", 2); + loader.column("uid", 1L, 2L); + loader.edges(new int[] {0}, new int[] {1}); + loader.finish(); + } + return path; + } + + @Test + void aNodesTableIdHasAName() { + try (Database db = Database.open(people()); + Connection conn = db.connect(); + Result r = conn.query("MATCH (p:person) RETURN p ORDER BY p.uid")) { + Value.Node node = assertInstanceOf(Value.Node.class, r.row(0).get(0)); + assertEquals("person", conn.tableName(node.table())); + } + } + + @Test + void aRelsTableIdHasAName() { + // Node and rel tables share one id space, so one call answers for both + // kinds, and the two ids are not the same id. + try (Database db = Database.open(people()); + Connection conn = db.connect(); + Result r = conn.query("MATCH (a:person)-[e:knows]->(b:person) RETURN a, e")) { + Value.Node node = assertInstanceOf(Value.Node.class, r.row(0).get(0)); + Value.Rel rel = assertInstanceOf(Value.Rel.class, r.row(0).get(1)); + assertNotEquals(node.table(), rel.table()); + assertEquals("person", conn.tableName(node.table())); + assertEquals("knows", conn.tableName(rel.table())); + } + } + + @Test + void anIdNoTableHasIsNullRatherThanAThrow() { + try (Database db = Database.open(people()); + Connection conn = db.connect()) { + // Not an error: asking is how a host finds out, and a host walking ids + // to see what a database holds would otherwise have to catch. + assertNull(conn.tableName(9999)); + } + } +} diff --git a/zudb/src/main/java/dev/zudb/Connection.java b/zudb/src/main/java/dev/zudb/Connection.java index 3e38753..96d73a0 100644 --- a/zudb/src/main/java/dev/zudb/Connection.java +++ b/zudb/src/main/java/dev/zudb/Connection.java @@ -278,6 +278,29 @@ public long rowsRead() { return zu.connRowsRead(open()); } + /** + * What a table id is called. + * + *

A {@link Value.Node} carries the number the engine keeps its table + * under rather than the name, because two tables number their rows from + * zero and the number is what identifies the node. This is how to get back + * to the name the statement wrote: + * + *

{@code
+   * Value.Node n = row.getNode("p");
+   * String label = conn.tableName(n.table());   // "person"
+   * }
+ * + *

Node and rel tables share one id space, so this answers for a + * {@link Value.Rel} as well. + * + * @param table the table id + * @return the name, or null when no table has that id + */ + public String tableName(int table) { + return zu.connTableName(open(), table); + } + /** * Asks to be called back every so often while a statement runs, with how * far it has got and whether it should go on. diff --git a/zudb/src/main/java/dev/zudb/Diagnostic.java b/zudb/src/main/java/dev/zudb/Diagnostic.java index 33057ba..c842d31 100644 --- a/zudb/src/main/java/dev/zudb/Diagnostic.java +++ b/zudb/src/main/java/dev/zudb/Diagnostic.java @@ -26,6 +26,14 @@ * @param column the 1-based column in characters, or -1 * @param offset the 0-based byte index into the statement, or -1 * @param excerpt the line the position is on without its newline, or null + * @param subjectKind what 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, or null + * @param subject the name itself, written the way the statement wrote it and + * with nothing around it, or null. Null exactly when subjectKind is + * @param graph the graph the statement was running in, or null when the + * failure happened before there was one + * @param schema the schema the statement was running in, or null * @param docUrl where this condition is written up, or null * @param retryable whether running the same statement again could succeed */ @@ -39,6 +47,10 @@ public record Diagnostic( int column, int offset, String excerpt, + String subjectKind, + String subject, + String graph, + String schema, String docUrl, boolean retryable) { @@ -109,6 +121,10 @@ public ZuException toException() { * @param column the 1-based column, or -1 * @param offset the 0-based byte index, or -1 * @param excerpt the line the position is on, or null + * @param subjectKind what {@code zu_error_subject_kind} answered, or null + * @param subject what {@code zu_error_subject} answered, or null + * @param graph what {@code zu_error_graph} answered, or null + * @param schema what {@code zu_error_schema} answered, or null * @param docUrl where this condition is written up, or null * @param retryable whether running the same statement again could succeed * @return the record @@ -123,6 +139,10 @@ public static Diagnostic of( int column, int offset, String excerpt, + String subjectKind, + String subject, + String graph, + String schema, String docUrl, boolean retryable) { return new Diagnostic( @@ -135,6 +155,10 @@ public static Diagnostic of( column, offset, excerpt, + subjectKind, + subject, + graph, + schema, docUrl, retryable); } @@ -151,6 +175,7 @@ public static Diagnostic of( */ public static Diagnostic misuse(Status status, String message) { return new Diagnostic( - status, message, null, null, Severity.EXCEPTION, -1, -1, -1, null, null, false); + status, message, null, null, Severity.EXCEPTION, -1, -1, -1, null, null, null, null, null, + null, false); } } diff --git a/zudb/src/main/java/dev/zudb/Result.java b/zudb/src/main/java/dev/zudb/Result.java index ad221d1..3bae89b 100644 --- a/zudb/src/main/java/dev/zudb/Result.java +++ b/zudb/src/main/java/dev/zudb/Result.java @@ -548,6 +548,8 @@ Value read(long value) { return new Value.Float(zu.valueDouble(value)); case STR: return new Value.Str(zu.valueString(value)); + case BYTES: + return new Value.Bytes(zu.valueBytes(value)); case NODE: { long[] n = zu.valueNode(value); return new Value.Node((int) n[0], n[1]); diff --git a/zudb/src/main/java/dev/zudb/Row.java b/zudb/src/main/java/dev/zudb/Row.java index cc4f49b..d937c2d 100644 --- a/zudb/src/main/java/dev/zudb/Row.java +++ b/zudb/src/main/java/dev/zudb/Row.java @@ -223,6 +223,42 @@ public String getString(String column) { return getString(result.columnIndex(column)); } + /** + * A cell as a byte string. + * + *

Null rather than a throw for a cell with nothing in it, on the same + * terms as {@link #getString(int)}: an array reference can say that. + * + *

A string cell is refused rather than encoded. The two are different + * types and a client that quietly turned one into the other would be + * answering a question nobody asked. + * + * @param column the column, counting from zero + * @return the octets, this caller's own copy, or null if the cell is null + * @throws ZuProgrammingException if the cell holds something that is not a + * byte string + */ + public byte[] getBytes(int column) { + Value v = get(column); + if (v instanceof Value.Bytes b) { + return b.value(); + } + if (v instanceof Value.Null) { + return null; + } + throw wrong(column, v, "a byte string"); + } + + /** + * A cell as a byte string. + * + * @param column what the statement called it + * @return the octets, or null if the cell is null + */ + public byte[] getBytes(String column) { + return getBytes(result.columnIndex(column)); + } + /** * A cell as a date, a time, a datetime or a duration. * diff --git a/zudb/src/main/java/dev/zudb/Type.java b/zudb/src/main/java/dev/zudb/Type.java index eded06a..316de30 100644 --- a/zudb/src/main/java/dev/zudb/Type.java +++ b/zudb/src/main/java/dev/zudb/Type.java @@ -34,7 +34,9 @@ public enum Type { /** A graph, one of the two reference values, which has no contents to read. */ GRAPH(11), /** A binding table, the other reference value. */ - BINDING_TABLE(12); + BINDING_TABLE(12), + /** A byte string, which is octets and not text. */ + BYTES(13); private final int value; diff --git a/zudb/src/main/java/dev/zudb/Value.java b/zudb/src/main/java/dev/zudb/Value.java index 6737c8f..4e0911f 100644 --- a/zudb/src/main/java/dev/zudb/Value.java +++ b/zudb/src/main/java/dev/zudb/Value.java @@ -85,13 +85,57 @@ record Float(double value) implements Value {} */ record Str(String value) implements Value {} + /** + * A byte string, which is octets and not text. + * + *

A type of its own rather than a {@link Value.Str} that happens to hold + * bytes, because a host that took them for a string would decode them, and + * octets that are not UTF-8 do not survive that. + * + *

The array is this value's own copy and nothing else holds it, so + * writing to it changes nothing but the copy. The three methods below are + * written out because a record over an array compares by identity + * otherwise, and two byte strings holding the same octets are one value. + * + * @param value what it is + */ + record Bytes(byte[] value) implements Value { + + @Override + public boolean equals(Object other) { + return other instanceof Bytes b && java.util.Arrays.equals(value, b.value); + } + + @Override + public int hashCode() { + return java.util.Arrays.hashCode(value); + } + + /** + * The octets in hex, which is how the corpus and the engine both spell + * one, rather than the array's identity. + * + * @return {@code X'00AB'}, and {@code X''} for no octets at all + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(value.length * 2 + 3).append("X'"); + for (byte b : value) { + sb.append(HEX[(b >> 4) & 0xF]).append(HEX[b & 0xF]); + } + return sb.append('\'').toString(); + } + + private static final char[] HEX = "0123456789ABCDEF".toCharArray(); + } + /** * A node, which is a table and a row of it, because neither identifies a * node on its own: two tables number their rows from zero. * - * @param table which table, as the number the engine keeps it under. The C - * ABI has no call that turns that number into a name, so this client - * hands over the number rather than a guess + * @param table which table, as the number the engine keeps it under. + * {@link Connection#tableName(int)} turns that number into the name the + * statement wrote * @param offset which row of it */ record Node(int table, long offset) implements Value {} diff --git a/zudb/src/main/java/dev/zudb/Zu.java b/zudb/src/main/java/dev/zudb/Zu.java index 13111e3..a217284 100644 --- a/zudb/src/main/java/dev/zudb/Zu.java +++ b/zudb/src/main/java/dev/zudb/Zu.java @@ -37,7 +37,7 @@ public final class Zu { * has every symbol this client calls, which is the mismatch that actually * bites, and it names the missing one. */ - public static final String ABI_VERSION = "0.12"; + public static final String ABI_VERSION = "0.14"; private static final Logger LOG = System.getLogger("dev.zudb"); diff --git a/zudb/src/main/java/dev/zudb/ZuException.java b/zudb/src/main/java/dev/zudb/ZuException.java index e7ddec0..762a78c 100644 --- a/zudb/src/main/java/dev/zudb/ZuException.java +++ b/zudb/src/main/java/dev/zudb/ZuException.java @@ -42,7 +42,7 @@ public class ZuException extends RuntimeException { /** * The whole record this failure was built from, for a caller that would - * rather pass one value around than nine. + * rather pass one value around than fifteen. * * @return the record, never null */ @@ -117,6 +117,51 @@ public Optional excerpt() { return Optional.ofNullable(diagnostic.excerpt()); } + /** + * 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. + * + *

Kept apart from {@link #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. This and {@link #subject()} are both present + * or both empty. + * + * @return the kind, if the condition is about something named + */ + public Optional subjectKind() { + return Optional.ofNullable(diagnostic.subjectKind()); + } + + /** + * The name the condition is about, written the way the statement wrote it + * and with nothing around it, which is what an editor underlines. + * + * @return the name, if the condition is about something named + */ + public Optional subject() { + return Optional.ofNullable(diagnostic.subject()); + } + + /** + * The graph the statement was running in, which ISO 39075 subclause 23.2 + * asks a diagnostic record to name. + * + * @return the graph, empty when the failure happened before there was one + */ + public Optional graph() { + return Optional.ofNullable(diagnostic.graph()); + } + + /** + * The schema the statement was running in. + * + * @return the schema, empty when the failure happened before there was one + */ + public Optional schema() { + return Optional.ofNullable(diagnostic.schema()); + } + /** * The page that documents this condition, so a program hands a reader a * page rather than five characters to search for. diff --git a/zudb/src/main/java/dev/zudb/spi/ZuBinding.java b/zudb/src/main/java/dev/zudb/spi/ZuBinding.java index d08f3df..2737c44 100644 --- a/zudb/src/main/java/dev/zudb/spi/ZuBinding.java +++ b/zudb/src/main/java/dev/zudb/spi/ZuBinding.java @@ -210,6 +210,19 @@ public interface ZuBinding { */ long connRowsRead(long conn); + /** + * What a table id is called. + * + *

Node and rel tables share one id space, so one call answers for both + * kinds. This is what turns the number in a {@code Value.Node} into the name + * the statement wrote. + * + * @param conn the connection + * @param table the table id + * @return the name, or null when no table has that id + */ + String connTableName(long conn, int table); + /** * Asks to be called back every so often while a statement runs. * @@ -622,6 +635,18 @@ public interface ZuBinding { */ String valueString(long value); + /** + * A value as a byte string. + * + *

A separate call from {@link #valueString(long)} because the two hold + * different types, and one answering both would let a host read octets as + * text without ever asking whether they were. + * + * @param value the value, which must be a byte string + * @return the octets, copied out, never null + */ + byte[] valueBytes(long value); + /** * A value as a temporal. * diff --git a/zudb/src/test/java/dev/zudb/DiagnosticTest.java b/zudb/src/test/java/dev/zudb/DiagnosticTest.java index c117390..050f62a 100644 --- a/zudb/src/test/java/dev/zudb/DiagnosticTest.java +++ b/zudb/src/test/java/dev/zudb/DiagnosticTest.java @@ -78,6 +78,10 @@ void anExceptionCarriesTheWholeRecord() { 9, 17, "MATCH (p:persn)", + "label", + "persn", + "main", + "public", "https://zudb.dev/errors/42N51", false); ZuException e = d.toException(); @@ -87,15 +91,31 @@ void anExceptionCarriesTheWholeRecord() { assertEquals(Severity.EXCEPTION, e.severity()); assertEquals(new ZuException.Position(2, 9, 17), e.position().orElseThrow()); assertEquals("MATCH (p:persn)", e.excerpt().orElseThrow()); + // The kind is its own word rather than glued to the front of the name, + // so asking whether this is about a label is one string compared + // against one word. + assertEquals("label", e.subjectKind().orElseThrow()); + assertEquals("persn", e.subject().orElseThrow()); + assertEquals("main", e.graph().orElseThrow()); + assertEquals("public", e.schema().orElseThrow()); assertFalse(e.retryable()); } + @Test + void aRecordAboutNothingNamedCarriesNoSubject() { + ZuException e = diagnostic(Status.ERROR, "22012").toException(); + assertTrue(e.subjectKind().isEmpty()); + assertTrue(e.subject().isEmpty()); + assertTrue(e.graph().isEmpty()); + assertTrue(e.schema().isEmpty()); + } + @Test void aCaretUnderlinesTheColumnTheExcerptCounts() { Diagnostic d = new Diagnostic( Status.ERROR, "bad", "42001", null, Severity.EXCEPTION, 1, 8, 7, "RETURN ?", null, - false); + null, null, null, null, false); String caret = d.toException().caret().orElseThrow(); assertEquals("RETURN ?", caret.lines().findFirst().orElseThrow()); assertTrue(caret.endsWith("^")); @@ -111,7 +131,9 @@ void aRecordWithNoPositionHasNoCaret() { @Test void theRawFactoryMapsBothNumbers() { Diagnostic d = - Diagnostic.of(3, "boom", "22012", "data exception", 4, 1, 1, 0, null, null, true); + Diagnostic.of( + 3, "boom", "22012", "data exception", 4, 1, 1, 0, null, null, null, null, null, null, + true); assertEquals(Status.ERROR, d.status()); assertEquals(Severity.EXCEPTION, d.severity()); assertTrue(d.retryable()); @@ -126,6 +148,7 @@ void aStatusThisClientDoesNotKnowIsUnknownRatherThanAThrow() { private static Diagnostic diagnostic(Status status, String code) { return new Diagnostic( - status, "boom", code, null, Severity.EXCEPTION, -1, -1, -1, null, null, false); + status, "boom", code, null, Severity.EXCEPTION, -1, -1, -1, null, null, null, null, null, + null, false); } }