Journal
Sections
Reading settings
Natural voice recorded for this course, the same in every browser.
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Progress
Section titled “Progress”- Data: the pages, links and Git history of this repository at commit
cbcbb42, and the CI runs of the DuckDB course - CI: every lesson’s Cypher compared with its expected output on three OSes, and a SQL cross-check with DuckDB
- Lesson 1: a first graph
- Lesson 2: loading files
- Lesson 3: paths
- Lesson 4: Git history and CI runs as a graph
- Data: the .NET projects of GuitarAlchemist/ga at commit
a26a7893 - Lesson 5: LadybugDB from C#
- Lesson 6: LadybugDB from Java
- Lesson 7: graph algorithms and full-text search
- Lesson 8: persistence, transactions and concurrency
- Lesson 9: a graph lab — translations, prerequisite cycles, bounded reachability
LadybugDB 0.20.4 is somebody else’s database, and writing eight lessons against it turned up nine findings. Six are silent bugs — no error, a wrong result — each with a reproduction that runs in an empty in-memory database; none of them had an upstream issue when the tracker was searched on 2026-09-14, and none has been reported. The last three come from the lessons themselves, and one of them was already open upstream. Lesson 9’s lab added a tenth, the reversed order of a closed witness; it has not been searched upstream.
| Expected | What happens | Where | Measure | Status |
|---|---|---|---|---|
COPY into a rel table from a subquery connects the nodes the MATCH found |
It connects the wrong nodes: the keys are matched positionally rather than by the MATCH |
COPY … FROM (LOAD FROM … MATCH …) |
Reproduction 1 2026-09-14 | Reproduced, no upstream issue, not reported |
count(DISTINCT …) beside sum(…) leaves the sum alone |
The sum comes back NULL when a count(DISTINCT …) precedes it |
Aggregation | Reproduction 2 2026-09-14 | Reproduced, no upstream issue, not reported |
ACYCLIC drops every path that repeats a node |
It keeps some of them | Recursive path patterns | Reproduction 3 2026-09-14 | Reproduced, no upstream issue, not reported |
A COPY that fails leaves the node table as it was |
It corrupts the table | COPY into a node table |
Reproduction 4 2026-09-14 | Reproduced, no upstream issue, not reported |
Grouping by a COUNT subquery keeps the group whose count is 0 |
That group is dropped | GROUP BY over a subquery |
Reproduction 5 2026-09-14 | Reproduced, no upstream issue, not reported |
timestamp() and CAST(… AS TIMESTAMP) honour the offset in the literal |
Both ignore it | Temporal conversion | Reproduction 6 2026-09-14 | Reproduced, no upstream issue, not reported |
INSTALL algo fetches the build matching the CLI |
The 0.20.4 CLI downloads the 0.20.0 build, and LOAD algo then fails on all three runners, with paths from LadybugDB’s own build machine in the message |
algo extension, CLI 0.20.4 |
libnetworkit.so: cannot open shared object file on Linux, Library not loaded: @rpath/libnetworkit.dylib on macOS, The specified module could not be found. on Windows |
Open upstream as issue #857, not opened by this course 2026-09-14 |
| Louvain gives the same communities for the same graph and the same thread count | The sizes differ from one process to the next, with CALL threads = 1 set |
algo extension, Louvain |
[27,20,17,14,14] in one process, [25,20,18,18,11] in the next two; three calls inside one process agree. Nodes with no relationship get louvain_id -1 |
Reproduced, not reported 2026-09-14 |
| A read-only process and a writer on one file behave as the documentation says | The concurrency page says the combination is not allowed, but it is what happens: a read-only process opens a writer’s file on Linux and macOS and sees committed changes through the .wal; a writer opens and checkpoints a file a reader holds, on all three, and the reader keeps answering from the old state |
storage_manager.cpp 87-91 |
On Windows the writer’s LockFileEx makes the read fail with Error 33 instead |
Reproduced; documentation and behaviour disagree 2026-09-14 |
nodes(p) on a closed pattern (a)-[…]->(a) lists the pages in the arrows’ order, as it does on an open pattern |
It lists them against the arrows; the pages and the length are right | nodes() on a closed variable-length pattern; source not located |
Arrows /lab/cycles/ → /lab/paths/ → /lab/intro/ → back: [/lab/cycles/,/lab/intro/,/lab/paths/,/lab/cycles/]. An open path from the same page is in order. Reproduced on the 3-page and 6-page cycles 2026-09-27 |
Reproduced on Windows; upstream tracker not searched, not reported |
Experiments
Section titled “Experiments”One row per measured experiment, with the hypothesis as written before the run in data/lab/preregistration.md.
| Question | Hypothesis (before the run) | Result | Verdict | Entry and code |
|---|---|---|---|---|
After a failed COPY into a relationship table, what is left? |
The table unchanged, and the node key index intact | 2 links, as before; found_by_key 1 |
Confirmed, 0.20.4 on Windows; observed, not a guarantee | 2026-09-27, 09-graph-lab.cypher |
| Missing translations by content identity vs by title | Identity: 1 missing sibling; title: 4 rows, 3 false | 1 (/lab/paths/, es); 4 rows, 3 false |
Confirmed | 2026-09-27 |
| Is a closed-trail bound complete once it reaches the number of pages? | DAG: 0 at bound 3; 6-cycle: 0 at 5, 6 trails of length 6 at 6 | 0; 0; 6 of length 6 | Confirmed | 2026-09-27 |
| Do navigation and prerequisites need separate relationship tables? | Navigation: 2 closed trails of length 2; prerequisites: 0 | 2 of length 2; 0 | Confirmed | 2026-09-27 |
| Does a closed witness list its pages in the arrows’ order? | Yes, as an open path does | No: reversed (QA row above) | Refuted | 2026-09-27 |
2026-09-14 — The data
Section titled “2026-09-14 — The data”extract.py, run with the commitcbcbb42as argument, reads the Markdown files and the Git history at that commit, withgit ls-tree,git showandgit log, not the working copy: the CSV files don’t change when the site does.- 319 pages, 712 internal links, 3,712 external links, 74 commits, 73 parent links and 1,019 changed files.
- The runs are the
runs.jsonof the DuckDB course, 125 runs exported around 14:04 UTC, reused as they are. - The commit dates keep their
-04:00offset incommits.csv; LadybugDB stores them in UTC.
2026-09-14 — Installing LadybugDB
Section titled “2026-09-14 — Installing LadybugDB”- Windows:
lbug_cli-windows-x86_64.zipfrom the v0.20.4 release holds onlylbug.exe, 14,772,736 bytes.lbug --versionprintsLbug 0.20.4. - CI: the workflow downloads the release asset of each OS (
linux-x86_64,windows-x86_64,osx-arm64) and adds the folder toGITHUB_PATH. It installs DuckDB 1.5.5 the same way, for the SQL cross-check of lesson 3. INSTALL jsonput the extension in~/.lbdb/extension/0.20.0/win_amd64/json/: the folder is named after 0.20.0, not 0.20.4. The shell history is in~/.lbdb/history.txt.- In a Git Bash window,
lbug.exedoesn’t find/c/Users/…paths, and backslashes in a Cypher string are escape characters: the scripts use paths relative tocode/ladybugdb, andC:/…works when a full path is needed.
2026-09-14 — The CI
Section titled “2026-09-14 — The CI”- The CLI exits with 0 even when a query fails, and prints the errors on standard output (
embedded_shell.cpp, lines 608-611):check.shcompares the whole output, errors included, so an unexpected error still fails the job. - The progress bar writes escape codes even when the output goes to a pipe:
check.shpasses--no_progress_barand--no_stats. - First push: failed on Linux, passed on Windows and macOS. Deleting a lesson that still had relationships gave a message naming
HAS_LESSONon Windows and macOS andLINKS_TOon Linux: the executor names the first relationship table it checks (delete_executor.cpp, lines 33-42), and their order differs. Lesson 1 now deletes a lesson whose relationships are all in one table. - Same push:
ACYCLICgave 14 paths on Windows and 29 on Linux and macOS (see below). Lesson 3 usesis_acyclic(p), the same on all three. - Two outputs could differ from one run to the next: a
LOAD FROM … LIMIT 3withoutORDER BY, and the first missing key reported by a failingCOPY, which depends on the thread that finds it. The scripts sort, and setCALL threads = 1before thatCOPY. - An unbounded
LINKS_TO*walk on the site graph ran for more than five minutes before I stopped it;* SHORTESTon the same pattern answers at once.
2026-09-14 — Surprises while writing lessons 1-4
Section titled “2026-09-14 — Surprises while writing lessons 1-4”MERGEmatches the whole pattern: without the primary key it fails at bind time, and with the key and another property that differs, it tries to create the node and fails on the duplicate key.:schemaprintsCREATEstatements, withMANY_MANYon every relationship table.- The CSV header is detected without
HEADER = true, and its names don’t have to match the table’s columns. SKIP_DUPLICATE_PK = trueworks onCOPYfrom a file (constants.h, line 111) but isn’t in the documentation, and isn’t supported onCOPYfrom a subquery (bind_copy_from.cpp, line 149).- The CSV sniffer reads the first 256 lines (
constants.h, line 144); a quoted field further down fails theCOPYunlessQUOTEis given. - Empty CSV fields become
NULLby default;NULL_STRINGS = ['NA']keeps them as''. split_part('/es/streeling/', '/', 1)returnses: the empty part before the leading separator is skipped. DuckDB returns''.nodes(e)on a relationship list returns the intermediate nodes only; lists are indexed from 1;ORDER BYon aSTRING[]fails, a cast toSTRINGworks.- Variable-length patterns are walks by default, so two
CHANGEDrelationships in one pattern can be the same one. - The upper bound of a variable-length pattern is limited to 30 (
client_config.h, line 32) untilCALL var_length_extend_max_depth = …; a pattern that needs more returns nothing, without an error. - The 4-link path counts of lesson 3 were checked twice outside LadybugDB: a Python script over
links.csv(walks 1, 5, 12, 29; trails 26; acyclic 1, 5, 9, 10), and a recursive CTE in DuckDB (walks 1, 5, 12, 29), which the CI runs.
2026-09-14 — Lesson 5: the ga data and the C# package
Section titled “2026-09-14 — Lesson 5: the ga data and the C# package”data/ga/extract.pyneeds only the project files: a clone with--filter=blob:noneand a sparse checkout of*.csproj,*.fsprojand*.slnxdownloads the project files of ga without the rest of its content. It skips the copies under.claudefolders.- 111 projects, 266 project references, 478 package references, 136 packages. One reference goes to
reactapp1.client.esproj, a JavaScript project the extraction doesn’t keep: the lesson loads the references withIGNORE_ERRORS. - The
versioncolumn is what each project file says. ga’sDirectory.Build.propsoverrides 14 packages withPackageReference Update, and the extraction doesn’t apply them; the lesson’s queries avoid those packages. - Two projects are named
GaApi.Tests, inTests/Apps/GaApi.TestsandTests/GaApi.Tests; the second isn’t inAllProjects.slnx. 38 projects in all aren’t in it. - NuGet:
LadybugDBhas three versions,0.17.0-alpha.1,0.18.2and0.19.1; 0.19.1 embeds the 0.19.1 engine, storage version 43. The binding isn’t ADO.NET and has no page on docs.ladybugdb.com: I read its source at commit0f58f1a. - First CI push: the C# job failed on the three OSes. The internal ID of
GaApiwas0:11on Windows and0:47on Linux and macOS, from the same CSV;SHORTESTchose a path throughGA.Business.MLon my machine and throughGA.Business.AIon the runners, among two of the same length; and the CLI on the Linux runner printedWarning: failed to create directory: /home/runner/.lbdb/when opening a file read-only. The program now compares IDs instead of printing them and lists theALL SHORTESTpaths sorted;check.shfilters the warning. - The 0.20.4 CLI, opening a 0.19.1 database file read-write, rewrites it to storage version 47 without a message; the 0.19.1 package then fails with
Failed to open Ladybug database at '…', without the reason.--read_onlyleaves the file readable by both. - The binding doesn’t expose
lbug_query_result_has_next_query_result,lbug_connection_interruptorlbug_connection_set_query_timeoutof the C API: aQuerywith several statements returns the first result only, and a long query can’t be stopped from C#.
2026-09-14 — Lesson 6: the Java package
Section titled “2026-09-14 — Lesson 6: the Java package”com.ladybugdb:lbug0.20.4 on Maven Central: one jar of 29,339,834 bytes with the native library forlinux_amd64,linux_arm64,osx_arm64andwindows_amd64, noosx_amd64. It pulls 13 more jars, 7 MB:kotlin-stdlib2.3.20, Apache Arrow 18.2.0, Jackson, FlatBuffers, commons-codec and SLF4J.- The sources jar matches the
ladybug-javarepository atf2fb39f, the submodule commit of LadybugDB v0.20.4. The sources live in acom/lbugdbfolder, but the package iscom.ladybugdb. - The Java page of the documentation still shows
throws ObjectRefDestroyedException; 0.20.4 throwsRuntimeExceptionwith messages such asConnection has been destroyed. - A failed query returns a result with
isSuccess()false;getNext()on it throwsRuntimeExceptionwith the same message. - A
PreparedStatementkeeps the values of its last execution:execute(statement, Map.of("nom", 5))afterMap.of("n", 2.5)runs withn = 2.5and no error. On a new statement, the same map fails withParameter n not found. Value.getValue()throwsType of value is not supported in value_get_valueforLISTandMAP, and doesn’t handleSTRUCT,NODE,RELorRECURSIVE_RELeither; theLbugList,LbugStruct,LbugMapandValue…Utilclasses read them.interval('1 month 2 days')comes back asPT768H: the JNI code converts the interval to seconds with 30-day months.setQueryTimeout(1)stops the walks of lesson 3 withInterrupted., andgetNextQueryResult()reads the second statement of a query: both missing from the .NET package.- The native library is copied to a new temporary file at each JVM start, and
deleteOnExitcan’t delete a loaded DLL on Windows: 13 copies, 190 MB, in%TEMP%after the runs of this lesson on my machine, 3 copies on the Windows runner after three runs, none on the Linux and macOS runners. - The first CI push of the Java job passed on the three OSes: lesson 5 had already made the output independent of internal IDs and of the path
SHORTESTchooses.
2026-09-14 — Lesson 7: extensions, algorithms and full-text search
Section titled “2026-09-14 — Lesson 7: extensions, algorithms and full-text search”- With the 0.20.4 CLI,
INSTALL algodownloads the build for 0.20.0, andLOAD algofails on the three CI runners:libnetworkit.so: cannot open shared object fileon Linux,Library not loaded: @rpath/libnetworkit.dylibon macOS, with paths of LadybugDB’s own build machine in the message, andThe specified module could not be found.on Windows. Issue #857 is open. LOAD ftsworks with 0.20.4 on the three runners, but on my Windows machine the CLI exits with0xC0000409, without printing anything, at the firstCREATE_FTS_INDEX, even on a table of one row. On Linux (WSL), the same script works with 0.20.4.- The 0.19.1 CLI downloads the builds for 0.19.0;
algoandftsload and give the same output on Windows, Linux and macOS. The CI installs it aslbug19;check.shprints the result ofLOAD algoandLOAD ftswith 0.20.4 at each run, without comparing it. - The Windows CLI, 0.19.1 and 0.20.4, imports
libssl-3-x64.dllandlibcrypto-3-x64.dll, not in the zip file; with onlyC:\Windows\System32in thePATH, it exits with0xC0000135. Git for Windows provides both DLLs. Lesson 1 now says so. project_graph_cypherisn’t in the documentation. It creates a graph of typeCYPHER, andpage_rankandweakly_connected_componentson it fail withBinder exception: AA:gds.cpp, line 72, still there in 0.20.4. The engine’s test of the function only creates the graph. Minimal reproduction, with 0.19.1 on Windows:
LOAD algo;CREATE NODE TABLE P(id INT64 PRIMARY KEY, age INT64);CREATE REL TABLE E(FROM P TO P);CREATE (:P {id: 1, age: 5}), (:P {id: 2, age: 20}), (:P {id: 3, age: 7});MATCH (a:P {id: 1}), (b:P {id: 3}) CREATE (a)-[:E]->(b);CALL project_graph_cypher('G1', 'MATCH (n:P) WHERE n.age < 10 RETURN n');CALL page_rank('G1') RETURN node.id, rank;- Louvain on the ga graph: 5 communities of sizes
[27,20,17,14,14]in one CLI process,[25,20,18,18,11]in the next two, withCALL threads = 1; three calls in one process give the same sizes. Nodes without any relationship getlouvain_id-1. - PageRank ranks sum to 0.3534 on the ga graph: the 25 projects without an outgoing reference don’t pass their rank on. Exercise 1 recomputes two ranks with
(1 - 0.85) / N + 0.85 × Σ rank / out_degree, to four decimals. RETURN n + sum(x), withnfromWITH count(*) AS n, fails withCannot evaluate expression with type AGGREGATE_FUNCTION.;RETURN n, n + sum(x)works, and so doesRETURN 2 * sum(x).- FTS: the English stemmer matches queries for query and Persistance for persistant; the French stemmer doesn’t match Persistence. Case is folded, accents aren’t (
donneesfinds nothing). The default stop words are English whatever the stemmer. After aCREATE, the index includes the new node and the other scores change.DROP_FTS_INDEXreportsTable 3_titles_fr_terms has been dropped., an internal table.
2026-09-14 — Lesson 8: transactions, files and processes
Section titled “2026-09-14 — Lesson 8: transactions, files and processes”- An error inside a manual transaction rolls the whole transaction back and ends it, without a message saying so: runtime errors (a duplicate key), binder errors, catalog errors, a
BEGINinside a transaction andCHECKPOINTinside a transaction all do it. The statements that follow run in auto-commit;ROLLBACKandCOMMITthen fail withNo active transaction. The only exception found: a write insideBEGIN TRANSACTION READ ONLYfails, and the transaction stays open. The transactions page promises that nothing of a failed transaction persists, and doesn’t say that the statements after the error run on their own. - A second write transaction is refused at once,
Cannot start a new write transaction in the system. Only one write transaction at a time is allowed in the system., even when it touches other nodes; there’s no waiting and no timeout. A refused auto-commit statement leaves the connection usable. - Java, a refused
BEGIN TRANSACTIONbreaks the connection: the next query throwsRuntimeException: Unknown Erroron Windows, and crashes the JVM withSIGSEGVon Linux (CatalogSet::traverseVersionChainsForTransactionNoLock) and macOS (CatalogSet::containsEntry), exit code 134. ABEGINthat succeeds repairs the connection on Windows; a retriedBEGINdoesn’t crash on any of the three runners (exercise 2: 153, 155 and 24 refusals). Minimal reproduction, an in-memoryDatabaseand two connections:firstrunsBEGIN TRANSACTION,secondrunsBEGIN TRANSACTION(refused),firstrunsCOMMIT,secondruns anyMATCH. The C# package and the other bindings weren’t tested. - The files:
.walappears at the first commit and disappears at the checkpoint when the database closes; no.walfor an uncommitted transaction.kill -9after a commit leaves the.wal, replayed by the next open;kill -9inside a transaction loses the transaction. Closing a JavaDatabasewith an open transaction rolls it back. - A second read-write process fails with
Could not set lock on file, followed by(Error: 33)on Windows and(Error: Resource temporarily unavailable)on Linux. The POSIX code closes the file descriptor beforeF_GETLK, so itsLock is held by PIDmessage can’t appear (local_file_system.cpp, lines 147-172). - A read-only database takes no lock (
storage_manager.cpp, lines 87-91). A read-only process opens a file a writer holds on Linux and macOS, and sees the writer’s committed changes from the.wal; not on Windows, where the writer’sLockFileExmakes the read fail withError 33. A writer opens, writes and checkpoints a file a read-only process holds, on the three OSes, and the reader keeps answering from the old state. The concurrency page says this combination isn’t allowed. - In one Java process, a second read-write
Database, or a read-only one, on a file a read-writeDatabaseholds: refused on Windows, opened on Linux and macOS (fcntllocks belong to the process). new Database(path)throwsjava.lang.Exception, a checked exception it doesn’t declare (lbug_java.cpp, lines 618-636).EXPORT DATABASEin CSV writes the macros intoschema.cypher; the migration page lists amacro.cypherfile that isn’t created. The CSV headers are prefixed witha.. The order of the options incopy.cypherdiffers between Windows and Linux or macOS.IMPORT DATABASEinto a non-empty database fails withProject already exists in catalog.- 0.20.4 opened read-only leaves a 0.19.1 file as it is; opened read-write, it upgrades it from storage version 43 to 47 without a message, and the 0.19.1 CLI then says
Database file version: 47, Current build storage version: 43. - CI: the first run failed on the three
cypherjobs, becausefiles.sh, run underset -e, returned the exit code of the last 0.19.1 CLI;check.shnow ignores it and compares the output. Thejavajobs on Linux and macOS crashed at the refusedBEGIN, now in a mode of its own. The second run failed on macOS only:lssortedProject.csvaftercopy.cypher; the script sorts withLC_ALL=C. - With its output redirected to a file, the CLI writes nothing until it exits:
files.shwaits for the.walfile, or a fixed time, rather than for a line of output.
2026-09-14 — Bugs found in 0.20.4
Section titled “2026-09-14 — Bugs found in 0.20.4”Six silent bugs: no error, a wrong result. None had an issue on LadybugDB’s tracker when I searched on 2026-09-14; I haven’t reported them. Each reproduction below runs in an empty in-memory database (lbug -m csv < repro.cypher) and was run on Windows only, unless noted.
1. COPY from a JSON subquery with MATCH connects the wrong nodes
Section titled “1. COPY from a JSON subquery with MATCH connects the wrong nodes”With r.json holding [{"id":1,"k":"a"},{"id":2,"k":"b"},{"id":3,"k":"c"}]:
LOAD json;CREATE NODE TABLE K(k STRING PRIMARY KEY);CREATE NODE TABLE R(id INT64 PRIMARY KEY);CREATE REL TABLE ON_K(FROM R TO K);CREATE (:K {k: 'a'}), (:K {k: 'b'}), (:K {k: 'c'}), (:R {id: 1}), (:R {id: 2}), (:R {id: 3});COPY ON_K FROM (LOAD FROM 'r.json' WITH id, k MATCH (x:K) WHERE x.k = k RETURN id, k);MATCH (r:R)-[:ON_K]->(x:K) RETURN r.id, x.k ORDER BY x.k;3 tuples have been copied to the ON_K table.r.id,x.k1,a1,b1,cExpected 1,a, 2,b, 3,c, which is what the same COPY gives from a CSV file with the same rows. With (IGNORE_ERRORS = true) added, the three relationships go to a instead: 1,a, 2,a, 3,a. Lesson 4 hit it with 123 runs: the 123 relationships all started from the same run; filtering with WHERE and no MATCH works.
IGNORE_ERRORS = true on a COPY from a subquery, when a key is missing, fails with Error: bad variant access instead of skipping the row.
2. count(DISTINCT …) before sum(…) makes the sum NULL
Section titled “2. count(DISTINCT …) before sum(…) makes the sum NULL”CREATE NODE TABLE P(id INT64 PRIMARY KEY, g STRING, n INT64);CREATE (:P {id: 1, g: 'x', n: 10}), (:P {id: 2, g: 'x', n: 20}), (:P {id: 3, g: 'y', n: 5});MATCH (p:P) RETURN p.g, count(DISTINCT p) AS ps, sum(p.n) AS total ORDER BY p.g;MATCH (p:P) RETURN p.g, sum(p.n) AS total, count(DISTINCT p) AS ps ORDER BY p.g;p.g,ps,totalx,2,""y,1,""p.g,total,psx,30,2y,5,1The same aggregates, in the other order, give the right totals.
Lesson 5 found a more general form, in the 0.19.1 engine of the NuGet package too: with a grouping key, an aggregate after a DISTINCT aggregate in the same projection is wrong. RETURN k.name, collect(DISTINCT u.version) AS versions, count(*) AS projects gives 0 projects for MongoDB.Driver instead of 14.
3. ACYCLIC keeps paths that repeat a node
Section titled “3. ACYCLIC keeps paths that repeat a node”Three nodes, a and b linked both ways, b linked to c:
CREATE NODE TABLE V(id STRING PRIMARY KEY);CREATE REL TABLE E(FROM V TO V);CREATE (:V {id: 'a'}), (:V {id: 'b'}), (:V {id: 'c'});MATCH (x:V {id: 'a'}), (y:V {id: 'b'}) CREATE (x)-[:E]->(y), (y)-[:E]->(x);MATCH (x:V {id: 'b'}), (y:V {id: 'c'}) CREATE (x)-[:E]->(y);MATCH p = (x:V {id: 'a'})-[e:E* ACYCLIC 1..4]->(y:V {id: 'c'}) RETURN properties(nodes(p), 'id') AS path;MATCH p = (x:V {id: 'a'})-[e:E*1..4]->(y:V {id: 'c'}) WHERE is_acyclic(p) RETURN properties(nodes(p), 'id') AS path;path"[a,b,a,b,c]""[a,b,c]"path"[a,b,c]"[a,b,a,b,c] goes through b twice. The ACYCLIC check is in output_writer.cpp, line 280; I haven’t found the cause. On the site graph of lesson 3, LINKS_TO* ACYCLIC 1..4 from the GitHub Actions index to its lesson 7 returned 14 paths on Windows, including one through the same lesson twice, and 29, every walk, on the Linux and macOS runners. A Python enumeration gives 25 paths whose intermediate nodes are distinct, and 10 with is_acyclic’s definition, both ends included.
4. A failed COPY into a node table corrupts it
Section titled “4. A failed COPY into a node table corrupts it”Loaded with COPY, then a COPY that fails on a duplicate key, with n.csv holding the header k and the rows a, b, c:
CREATE NODE TABLE N(k STRING PRIMARY KEY);COPY N FROM 'n.csv' (HEADER = true);COPY N FROM (RETURN 'a' AS k);MATCH (n:N {k: 'b'}) RETURN count(*) AS b_by_key;CREATE (:N {k: 'b'});MATCH (n:N) RETURN n.k ORDER BY n.k;In memory:
Error: Copy exception: Found duplicated primary key value a, which violates the uniqueness constraint of the primary key column.b_by_key0n.kaabcb is there, but its key finds nothing; the CREATE of an existing key succeeds, and the new node holds a, the key the failed COPY rejected, instead of b. In a database file (lbug -m csv d/x.lbdb), this sequence works: b_by_key is 1 and the CREATE fails with Runtime exception: Found duplicated primary key value b.
When the three nodes are created with CREATE instead of COPY, the failed COPY breaks a database file too: after it, CREATE (:N {k: 'z', v: 26}) on a table N(k STRING PRIMARY KEY, v INT64) adds a node that reads back as a,9, the rejected row, in memory and on disk.
5. Grouping by a COUNT subquery drops the group of 0
Section titled “5. Grouping by a COUNT subquery drops the group of 0”CREATE NODE TABLE C(id INT64 PRIMARY KEY);CREATE REL TABLE PARENT(FROM C TO C);CREATE (:C {id: 1}), (:C {id: 2}), (:C {id: 3});MATCH (a:C {id: 2}), (b:C {id: 1}) CREATE (a)-[:PARENT]->(b);MATCH (a:C {id: 3}), (b:C {id: 2}) CREATE (a)-[:PARENT]->(b);MATCH (c:C) RETURN c.id, COUNT { MATCH (c)-[:PARENT]->(:C) } AS parents ORDER BY c.id;MATCH (c:C) RETURN COUNT { MATCH (c)-[:PARENT]->(:C) } AS parents, count(*) AS cs ORDER BY parents;c.id,parents1,02,13,1parents,cs1,2The subquery returns 0 for node 1, but the grouped query has no 0,1 row. OPTIONAL MATCH with count(…) gives both groups (lesson 4).
6. timestamp() and CAST(… AS TIMESTAMP) ignore the offset
Section titled “6. timestamp() and CAST(… AS TIMESTAMP) ignore the offset”RETURN timestamp('2026-09-14T09:30:00-04:00') AS f, CAST('2026-09-14T09:30:00-04:00' AS TIMESTAMP) AS c, CAST('2026-09-14T09:30:00-04:00' AS TIMESTAMP_TZ) AS tz;f,c,tz2026-09-14 09:30:00,2026-09-14 09:30:00,2026-09-14 13:30:00+00Expected 13:30 for the first two as well: COPY and LOAD FROM convert the same text in a CSV file to 13:30 UTC (lesson 4). The offset is dropped without being applied. timestamp(…) in the C# package 0.19.1 returns 09:30 too (lesson 5).
2026-09-27 — Lesson 9: a graph lab
Section titled “2026-09-27 — Lesson 9: a graph lab”A lab on invented pages under /lab/, to extend lessons 2 and 3 rather than repeat them. The predictions went into data/lab/preregistration.md first, hashed at 13:24 EDT (SHA-256 234e0639…040d) before any fixture was loaded. Every expected file was written from it by hand, before the run it checks. Runtime: LadybugDB CLI 0.20.4 on Windows, in-memory databases, CALL threads = 1 before the compared COPY. The whole script runs in 1.5 to 6.5 s.
-
Tracer and missing endpoint: green on the first run. After the failed
COPYintoLINKS_TO, there are still 2 links and the key index works. Lesson 2’s index bug concerned node tables. -
Translations: green on the first run. Title equality gives 3 false reports out of 4.
-
The first run of the cycle cases had two differences from the expected file:
- A script defect, mine:
shortestis a keyword, and the parser refused it as an alias. - An upstream behaviour: a closed trail lists its pages against the arrows (QA row). The pages and the length were as predicted; only the order was not.
A probe on the six-page cycle, run before the addendum was written, separated open paths (in order) from closed ones (reversed). Both are recorded in a dated addendum; the original predictions were left as they were.
- A script defect, mine:
-
Bounds: confirmed. A bound of 5 misses a 6-cycle, and 6 finds it. A bound of 1 hides a prerequisite at depth 2.
Not run: the full check.sh on this machine. Its INSTALL json would download an extension, and this lab was not allowed downloads. Only the two lesson 9 scripts were compared locally.
To verify
Section titled “To verify”- Lesson 9 on Linux and macOS: the lab ran on Windows only.
ladybugdb-examples.ymlcompares it on the three runners. - Whether
nodes()on a closed pattern is reversed on purpose (undocumented) or a bug, and whether it is fixed after 0.20.4. The upstream tracker has not been searched. - The install script (
curl -s https://install.ladybugdb.com | bash) andbrew install ladybug: not run for this course. - The minimal reproductions were run on Windows only. The course scripts that show bugs 1, 2, 4 and 5 on the site data give the same output on the three CI runners.
- Whether these bugs are already fixed on LadybugDB’s main branch, after 0.20.4.
- Whether the major version conflicts of lesson 5’s exercise 2 (
MongoDB.Driver2 and 3,Microsoft.ML.Tokenizers1 and 2) break ga at run time: I haven’t built or run ga. - The C# program on
linux-arm64andosx-x64, and the Java program onlinux_arm64: the CI runners arelinux-x64,win-x64andosx-arm64. - Whether
CREATE_FTS_INDEXalso crashes the 0.20.4 CLI on the Windows runner: the CI runs lesson 7 with 0.19.1 only. - Whether the two isolated projects of
Common,GA.Business.DSL.SourceGenandGA.Business.Core.Generated, are used in ga some other way than aProjectReference. - Whether a read-only process that stays open while a writer checkpoints returns wrong data, and not only old data, when it reads pages it hadn’t cached (lesson 8).
- On Linux and macOS, whether closing the second
Databaseof a process releases thefcntllock of the first one, as closing any descriptor of a file releases the process’s locks on it, and lets another process open the file read-write. - Whether a refused
BEGINalso breaks a connection of the C# package and of the other bindings.