// Kein `server-only` hier, anders als in ./index.ts und ./pool.ts: diese Datei // baut nur Abfragen zusammen und hält weder Verbindung noch Zugangsdaten. Mit // der Sperre wären die reinen Tests von lib/org.ts nicht mehr ladbar, obwohl // dort nur ein Baum aus Zeilen gebaut wird. import { sql, type Expression, type RawBuilder } from "kysely"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; // Mehrere unabhängige Lesevorgänge in **einer** Rundreise. // // ═══ Warum das nötig ist ═══ // // Eine Transaktion hängt an genau einer Verbindung, und über eine Verbindung // laufen Abfragen nacheinander — auch die, die in einem Promise.all stehen. // Der Treiber stellt sie in eine Schlange. `Promise.all` sieht nach // Gleichzeitigkeit aus und ist hier keine. // // Gemessen an der echten Datenbank: die Umlaufzeit beträgt rund 36 ms, zehn // belanglose `select 1` über eine Verbindung brauchen 343 ms, über zehn // Verbindungen 39 ms. Der Aufbau der Übersichtsseite — zehn Abfragen, die // zusammen keine 200 kB liefern — kostete so knapp eine Sekunde, fast // ausschliesslich Warten. // // Mehr Verbindungen sind trotzdem nicht die Antwort: der Sitzungskontext für // RLS gilt je Transaktion (siehe ./index.ts), und mehrere Transaktionen je // Anfrage vervielfachen die Verbindungen, die die Datenbank zulässt. Also // weniger Rundreisen statt mehr Leitungen: Postgres kann jede Teilabfrage als // JSON-Spalte in *einem* Ergebnis liefern. // // const { einheiten, standorte } = await tx // .selectNoFrom((eb) => [ // jsonArrayFrom(eb.selectFrom("org_units").select([...])).as("einheiten"), // jsonArrayFrom(eb.selectFrom("locations").selectAll()).as("standorte"), // ]) // .executeTakeFirstOrThrow(); // // Typisiert wie jede andere Kysely-Abfrage, mit Parametern, ohne Handarbeit // an der Zeichenkette. // // ═══ Die eine Falle ═══ // // Innerhalb von json_agg formatiert Postgres die Werte selbst, und der // Treiber kommt nicht mehr daran (lib/db/pool.ts stellt ihn dort auf die // Formen um, die die Typen beschreiben). Für die meisten Typen macht das // nichts — im Gegenteil: // // date → "2022-03-30" wie ausserhalb // numeric → 38.5 wie ausserhalb // uuid → Zeichenkette wie ausserhalb // timestamptz → "2026-08-03T12:08:06.272938+00:00" // ← **anders**: ausserhalb "…272Z" // // Der Unterschied ist nicht kosmetisch. Zeitstempel werden im Projekt als // Zeichenketten verglichen (lib/history.ts entscheidet daran, was später // passiert ist), und "+00:00" sortiert gegen "Z" falsch herum. Deshalb geht // **jede** timestamptz-Spalte in einer gebündelten Abfrage durch zeitstempel(). export { jsonArrayFrom, jsonObjectFrom }; /** * Eine timestamptz-Spalte in der Form, die der Treiber ausserhalb von JSON * liefert — ISO-8601 in UTC, auf Millisekunden gekürzt. * * Ohne das käme aus einer gebündelten Abfrage eine andere Zeichenkette als * aus derselben Abfrage einzeln gestellt. */ export function zeitstempel(spalte: Expression | string): RawBuilder { const ref = typeof spalte === "string" ? sql.ref(spalte) : spalte; return sql`to_char(${ref} at time zone 'utc', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`; }