Changeset 8151340 in Klonkt


Ignore:
Timestamp:
09/04/2026 12:05:08 PM (4 days ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
9946a68
Parents:
aa521c3
Message:

Tijdstempels, leeskant: sorteren op tijd en niet op tekst (shaer-a937)

Deze database draagt twee spellingen door elkaar: '2026-08-06 09:02:01'
van CURRENT_TIMESTAMP en '2026-08-06T02:08:01.000Z' van toISOString().
SQLite vergelijkt tekst, en op positie 10 staat een T tegenover een
spatie -- dus binnen dezelfde dag wint de ISO-vorm altijd, hoe laat hij
ook is. Gemeten op dev: ap_timeline 80 ISO tegen 80 SQL, ap_mentions 26
tegen 38, ap_interactions 2 tegen 6. De menging is er dus echt.

De helper bestond al (STEMPEL in ap-timeline, gebouwd voor de
gesprekslezing) maar stond in een dienst, terwijl elke plek die sorteert
hem nodig heeft. Hij staat nu als isoSql in config/database.js -- daar
hoort hij, want het gaat over hoe deze OPSLAG met tijd omgaat, en elke
aanroeper importeert db toch al. STEMPEL is er nu een verwijzing naar,
zodat er niet twee kopieen van dezelfde regel bestaan.

Genormaliseerd waar de menging LEEFT: de tijdlijn zelf, de reacties
onder een post (de fout die de bead mat), de berichten en de Cirkel.
Plus de archief-export, die op dezelfde kolommen sorteert.

De terugval blijft de RAUWE waarde en niet leeg: isoSql levert ook de
cursor in de gesprekslezing, en een lege stempel zou een client zijn
plek kosten. Waar een onleesbare waarde dan landt is onbepaald, en dat
staat nu ook zo in het commentaar -- eerst schreef ik daar dat zoiets
onderaan hoort te eindigen, en dat deed de code niet.

Vier toetsen met gemengde data, ISO bewust VROEGER op de dag: precies de
stand waarin een ongenormaliseerde sortering omvalt. Tegenbewijs: maak
isoSql een doorgeefluik en alle vier vallen.

Volle suite 1248 groen.

Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    raa521c3 r8151340  
    2121db.pragma('busy_timeout = 5000');   // wait up to 5s for a lock instead of failing immediately
    2222db.pragma('synchronous = NORMAL');  // safe with WAL (no torn writes); fewer fsyncs = faster writes
     23
     24/**
     25 * Tijdstempels in EEN spelling (shaer-a937).
     26 *
     27 * Deze database draagt twee vormen door elkaar: '2026-08-06 09:02:01' van
     28 * CURRENT_TIMESTAMP en '2026-08-06T02:08:01.000Z' van toISOString(). SQLite
     29 * vergelijkt ze als TEKST, en op positie 10 staat een 'T' (0x54) tegenover een
     30 * spatie (0x20) -- dus binnen dezelfde dag wint de ISO-vorm altijd, hoe laat hij
     31 * ook is. Een antwoord van 02:08 kwam zo boven een like van 09:02 te staan.
     32 *
     33 * `NU_ISO` is wat je SCHRIJFT, `isoSql()` is waarmee je VERGELIJKT of SORTEERT.
     34 * De twee horen bij elkaar: het eerste zorgt dat er niets nieuws bijkomt, het
     35 * tweede dat wat er al staat toch goed op volgorde komt.
     36 *
     37 * isoSql valt met COALESCE terug op de RAUWE waarde: strftime geeft NULL op iets
     38 * dat het niet als tijd herkent, en zonder die terugval zou zo'n rij uit de
     39 * sortering vallen -- of erger, als hij ook geSELECTeerd wordt (de cursor in de
     40 * gesprekslezing) zou de client een lege stempel terugkrijgen en zijn plek
     41 * kwijtraken. Waar zo'n onleesbare waarde dan LANDT is onbepaald: hij wordt als
     42 * tekst vergeleken en 'geen datum' staat nu eenmaal boven '2026-...'. Dat is de
     43 * juiste ruil -- data die je niet begrijpt bewaar je, je gooit hem niet weg.
     44 *
     45 * Ze staan HIER en niet in een dienst omdat ze over de opslag gaan: elke plek
     46 * die sorteert importeert `db` toch al uit dit bestand.
     47 */
     48export const NU_ISO = "strftime('%Y-%m-%dT%H:%M:%SZ','now')";
     49export const isoSql = (expr) => `COALESCE(strftime('%Y-%m-%dT%H:%M:%SZ', ${expr}), ${expr})`;
    2350
    2451export function initializeDatabase() {
  • src/services/ArchiveExportService.js

    raa521c3 r8151340  
    1818import path from 'path';
    1919import crypto from 'crypto';
    20 import db from '../config/database.js';
     20import db, { isoSql } from '../config/database.js';
    2121import { MEDIA_ROOT, resolveAudioPath } from '../config/paths.js';
    2222
     
    494494  // is de volgorde van twee posts op dezelfde seconde niet bepaald.
    495495  const posts = db.prepare(`SELECT * FROM posts WHERE site_id = ?
    496                              ORDER BY COALESCE(published_at, created_at) ASC, id ASC`).all(site.id);
     496                             ORDER BY ${isoSql('COALESCE(published_at, created_at)')} ASC, id ASC`).all(site.id);
    497497
    498498  for (const post of posts) {
     
    539539    try {
    540540      replies = db.prepare(`SELECT * FROM ap_interactions WHERE post_id = ? AND kind = 'reply'
    541                              ORDER BY COALESCE(published, created_at) ASC, id ASC`).all(post.id);
     541                             ORDER BY ${isoSql('COALESCE(published, created_at)')} ASC, id ASC`).all(post.id);
    542542    } catch { /* tabel kan ontbreken op een heel oude database */ }
    543543    if (replies.length) {
  • src/services/ap-cirkel.js

    raa521c3 r8151340  
    77 * een werktuig uit de dienstlaag: alleen db.
    88 */
    9 import db from '../config/database.js';
     9import db, { isoSql } from '../config/database.js';
    1010
    1111// ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ──
     
    2929      LEFT JOIN ap_my_reactions rb ON rb.site_slug = t.slug AND rb.target_uri = t.id AND rb.kind = 'boost'
    3030      WHERE t.slug = ? AND (f.auto_boost = 1 OR rb.target_uri IS NOT NULL)
    31       ORDER BY COALESCE(t.published, t.created_at) DESC, t.rowid DESC
     31      ORDER BY ${isoSql('COALESCE(t.published, t.created_at)')} DESC, t.rowid DESC
    3232      LIMIT ? OFFSET ?`);
    3333    return _cirkelPosts.all(slug, limit || 60, offset || 0);
  • src/services/ap-timeline.js

    raa521c3 r8151340  
    1313 * wireTimeline, hetzelfde injectiepatroon als guardianship en ap-c2s.
    1414 */
    15 import db from '../config/database.js';
     15import db, { isoSql } from '../config/database.js';
     16
     17// De helper woont sinds shaer-a937 in config/database.js: elke plek die
     18// sorteert had hem nodig, en twee kopieen van dezelfde regel lopen uit elkaar.
     19// Bovenaan, want de eerste statements hieronder gebruiken hem al.
     20const STEMPEL = isoSql;
    1621
    1722// Het ene werktuig uit de dienstlaag. ActivityPubService vult het onderaan
     
    2631  if (!_insTl) {
    2732    _insTl = db.prepare('INSERT OR IGNORE INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, author_url, content, url, published, media_json, nsfw, cw, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
    28     _listTl = db.prepare('SELECT * FROM ap_timeline WHERE slug = ? ORDER BY COALESCE(published, created_at) DESC LIMIT ? OFFSET ?');
     33    _listTl = db.prepare(`SELECT * FROM ap_timeline WHERE slug = ? ORDER BY ${STEMPEL('COALESCE(published, created_at)')} DESC LIMIT ? OFFSET ?`);
    2934    _delTl = db.prepare('DELETE FROM ap_timeline WHERE id = ?');
    3035  }
     
    119124      JOIN sites s ON s.id = p.site_id
    120125      WHERE s.slug = ? AND i.kind = 'reply'
    121       ORDER BY COALESCE(i.published, i.created_at) DESC LIMIT ?`).all(slug, limit || 60);
     126      ORDER BY ${STEMPEL('COALESCE(i.published, i.created_at)')} DESC LIMIT ?`).all(slug, limit || 60);
    122127  } catch { return []; }
    123128}
     
    242247 * een leeftijd die twee uur verschilde per vorm.
    243248 */
    244 const STEMPEL = (rauw) => `COALESCE(strftime('%Y-%m-%dT%H:%M:%SZ', ${rauw}), ${rauw})`;
    245249
    246250const CONVERSATION_UNION = `
     
    425429      WHERE m.slug = ?
    426430        AND NOT EXISTS (SELECT 1 FROM ap_timeline t WHERE t.slug = m.slug AND t.id = m.object_uri)
    427       ORDER BY COALESCE(m.published, m.created_at) DESC LIMIT ?`).all(slug, limit || 60);
     431      ORDER BY ${STEMPEL('COALESCE(m.published, m.created_at)')} DESC LIMIT ?`).all(slug, limit || 60);
    428432  } catch { return []; }
    429433}
Note: See TracChangeset for help on using the changeset viewer.