diff --git a/Cargo.lock b/Cargo.lock index 75de74c..df0e730 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "zu" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "zu-common", "zu-encoding", @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "zu-common" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "thiserror", ] @@ -1301,7 +1301,7 @@ dependencies = [ [[package]] name = "zu-encoding" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "ruzstd", "zu-common", @@ -1310,7 +1310,7 @@ dependencies = [ [[package]] name = "zu-exec" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "zu-common", "zu-query", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "zu-query" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "crossbeam-deque", "zu-common", @@ -1331,7 +1331,7 @@ dependencies = [ [[package]] name = "zu-s3" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "crc32c", "object_store", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "zu-sqlite" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "rusqlite", "zu-common", @@ -1352,7 +1352,7 @@ dependencies = [ [[package]] name = "zu-storage" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "zu-common", "zu-encoding", @@ -1361,7 +1361,7 @@ dependencies = [ [[package]] name = "zu-vector" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "zu-common", ] @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "zu-zu1" version = "0.0.1" -source = "git+https://github.com/tamnd/zu?rev=130f67db924bcd0f766ee814b0da2edae32150d4#130f67db924bcd0f766ee814b0da2edae32150d4" +source = "git+https://github.com/tamnd/zu?rev=6753ded13a215bf5e8fe70ce41e69b35321e3e73#6753ded13a215bf5e8fe70ce41e69b35321e3e73" dependencies = [ "crc32c", "loom", diff --git a/Cargo.toml b/Cargo.toml index f8081f2..7699ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ crate-type = ["cdylib"] # with (ADR 0002), so a revision is the honest way to say which one. # A local checkout is used instead with a `paths` override in # `.cargo/config.toml`, which is untracked on purpose. -zudb = { package = "zu", git = "https://github.com/tamnd/zu", rev = "130f67db924bcd0f766ee814b0da2edae32150d4" } -zu-common = { git = "https://github.com/tamnd/zu", rev = "130f67db924bcd0f766ee814b0da2edae32150d4" } +zudb = { package = "zu", git = "https://github.com/tamnd/zu", rev = "6753ded13a215bf5e8fe70ce41e69b35321e3e73" } +zu-common = { git = "https://github.com/tamnd/zu", rev = "6753ded13a215bf5e8fe70ce41e69b35321e3e73" } # N-API by way of napi-rs (ADR 0002). `napi9` is the version of N-API # this addon declares it needs, which is what makes one binary work # across Node 24, Node 26, Electron and Bun without a rebuild: the diff --git a/README.md b/README.md index 7197cec..0f54de8 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,8 @@ The same memory is on both sides of that: `table.getChild("age").data[0].values` Two things are not buffers, and both are named by the type rather than found out by looking. A column of nodes, rels, paths, lists or records has no fixed width cell, so it arrives as `items`, holding the same JavaScript values `query` would have made. A column of nothing but nulls has a length and nothing else, because there is nothing to put in a buffer. A column that mixes two types is refused, naming the column and the row that did it, since a columnar result holds one type per column and a column that quietly became strings is worse than one that would not build. +A statement that matched nothing still comes back with its columns, each one the type the plan declared and each one empty: a string column of no rows has its one starting offset, an integer column of no rows has a buffer of no elements. So a table built from an empty answer has the schema the same statement would have had with rows in it, and a loop that concatenates a page at a time does not have to hold the first page that had anything in it as a special case. + `bigIntMode` says nothing here. A columnar read has one physical layout per type and an INT64 column is 64 bit cells however a caller would rather read one, which is the difference between a buffer and a value. The mode still decides what is inside `items`, where this client is making objects anyway. ## Preparing a statement diff --git a/binding.d.cts b/binding.d.cts index ed35b83..fd91a7d 100644 --- a/binding.d.cts +++ b/binding.d.cts @@ -593,6 +593,7 @@ export interface ZuProgressOptions { * already been left behind. */ export interface ZuProgress extends Disposable { + /** Stops the watch. The callback is not called again. */ stop(): void } diff --git a/etc/zudb.api.md b/etc/zudb.api.md index 89f320c..1f1ae18 100644 --- a/etc/zudb.api.md +++ b/etc/zudb.api.md @@ -373,7 +373,6 @@ export interface ZuProfile { // @public export interface ZuProgress extends Disposable { - // (undocumented) stop(): void } diff --git a/test/columnar.test.mjs b/test/columnar.test.mjs index bc4679e..2181961 100644 --- a/test/columnar.test.mjs +++ b/test/columnar.test.mjs @@ -240,14 +240,26 @@ test('what no buffer covers arrives as the values themselves', async (t) => { test('a statement that matched nothing is columns of no rows', async (t) => { const { conn } = await twoPeople(t) - const read = await conn.columnar("MATCH (p:person) WHERE p.name = 'nobody' RETURN p.id AS id") + const read = await conn.columnar( + "MATCH (p:person) WHERE p.name = 'nobody' RETURN p.id AS id, p.name AS name", + ) + const { id, name } = named(read) assert.equal(read.rows, 0) - assert.equal(read.columns.length, 1) - assert.equal(read.columns[0].length, 0) - // Nothing settled the type, so it is the type of nothing, which is - // the one every columnar format has for exactly this. - assert.equal(read.columns[0].type, 'null') + assert.equal(read.columns.length, 2) + // No row settled the type, so it is the type the plan declared and + // not the type of nothing: an empty answer has the schema the same + // statement would have had with rows in it, which is what makes a + // table built from one appendable to a table built from the other. + assert.equal(id.type, 'int') + assert.equal(id.length, 0) + assert.equal(id.values.length, 0) + // A string column of no rows still carries the offset every string + // column starts from, so the subarray arithmetic holds at zero rows + // rather than being a case a reader has to special case. + assert.equal(name.type, 'string') + assert.equal(name.data.length, 0) + assert.deepEqual([...name.offsets], [0]) }) test('a statement that projects nothing has no columns and says so', async (t) => { @@ -351,15 +363,23 @@ test('a million rows come back down one buffer and the loop stays free', async ( let ticks = 0 const timer = setInterval(() => (ticks += 1), 1) + const at = performance.now() const read = await conn.columnar('MATCH (n:number) RETURN n.at AS at') + const took = performance.now() - at clearInterval(timer) assert.equal(read.rows, rows) assert.equal(read.columns[0].values.length, rows) assert.equal(read.columns[0].values[rows - 1], BigInt(rows)) // The whole read is on the threadpool, so the timer kept firing - // throughout it rather than queueing behind it. - assert.ok(ticks > 20, `the event loop ticked ${ticks} times`) + // throughout it rather than queueing behind it. The bar is a tick + // every ten milliseconds of the read and not a fixed count, because + // a blocked loop fires none however long the read takes and a fixed + // count turns every speedup into a failure. + assert.ok( + ticks > took / 10, + `the event loop ticked ${ticks} times in ${took.toFixed(0)} ms`, + ) }) // The types every fixed-width column maps to, which is the whole of diff --git a/test/register.test.mjs b/test/register.test.mjs index 80528d6..2c54efb 100644 --- a/test/register.test.mjs +++ b/test/register.test.mjs @@ -520,5 +520,14 @@ test('registering costs the same whatever the frame holds', async (t) => { best[rows] = Math.min(best[rows], Number(process.hrtime.bigint() - started) / 1e6) } } - assert.ok(best[5_000_000] < 2, `registering 5m rows took ${best[5_000_000].toFixed(2)} ms`) + // Against the ten row call rather than against a millisecond count, + // because the claim is that the cost does not follow the rows and a + // fixed budget measures the machine's load as much as the code: a way + // in that walked the rows would be five hundred thousand times the + // small frame and not twenty. + assert.ok( + best[5_000_000] < best[10] * 20 + 2, + `registering 5m rows took ${best[5_000_000].toFixed(2)} ms ` + + `against ${best[10].toFixed(2)} ms for ten`, + ) })