3 Punkte von GN⁺ 2023-11-30 | 1 Kommentare | Auf WhatsApp teilen
  • jaq ist ein Klon des JSON-Datenverarbeitungstools jq und zielt auf eine besser vorhersehbare Implementierung ab, während in den meisten Fällen die Kompatibilität mit jq erhalten bleibt
  • Das Kommandozeilenprogramm jaq kann als Drop-in-Ersatz für jq verwendet werden, und die Rust-Bibliothek jaq-core kann jq-Programme innerhalb von Rust-Programmen kompilieren und ausführen
  • Es bietet Unterstützung für YAML, CBOR, TOML und XML, die es in jq nicht gibt; außerdem lässt sich jaq-core sicher in Multithreading-Umgebungen verwenden und unterstützt beliebige Datentypen über JSON hinaus
  • In der Leistungsbewertung war jaq-3.0 in 20 von 31 Benchmarks am schnellsten, jq-1.8.1 in 5 und gojq-0.12.18 in 6
  • In Bezug auf Sicherheit soll es das Vermeiden von Panics, Speichersicherheit und I/O-Beschränkungen für Eingabedaten und jq-Filter gewährleisten, reagiert jedoch nicht auf Ressourcenerschöpfung wie Zeit-, Speicher- oder Stackverbrauch

Was jaq bietet

  • jaq ist ein Klon des JSON-Datenverarbeitungstools jq; die Aussprache ist /ʒaːk/, wie bei Jacques
  • Es unterstützt Datenformate, die jq nicht unterstützt
    • YAML

    • CBOR

    • TOML

      • XML
      • Es gibt ein separates Manual und einen Playground, in dem man es ausprobieren kann
      • jaq ist in zwei Formen verfügbar
      • Kommandozeilenprogramm jaq: als Drop-in-Ersatz für jq nutzbar
      • Bibliothek jaq-core: jq-Programme innerhalb von Rust-Programmen kompilier- und ausführbar

Designziele

  • Genauigkeit

    • jaq zielt auf eine genauere und besser vorhersehbare jq-Implementierung ab und bleibt dabei in den meisten Fällen mit jq kompatibel
  • Leistung

    • jaq wurde ursprünglich entwickelt, weil die lange Startzeit von jq 1.6 störte; in dieser Umgebung lag die Startzeit bei etwa 50 ms
    • Diese Startzeit fällt besonders ins Gewicht, wenn viele kleine Dateien verarbeitet werden
    • In jq 1.7 wurde die Startzeit deutlich verbessert, aber jaq ist in mehreren Benchmarks weiterhin schneller als jq
  • Einfachheit

    • jaq strebt eine einfache und kleine Implementierung an, um die Wahrscheinlichkeit von Bugs zu verringern und Beiträge zu erleichtern

Installation und Build

  • Binärdateien für Linux, Mac und Windows gibt es auf der Releases-Seite
  • Unter macOS oder Linux kann es mit homebrew installiert werden
    • brew install jaq
    • brew install --HEAD jaq
  • Zum Bauen aus dem Quellcode wird das Rust-Toolchain benötigt
  • Wenn das Repository geklont wurde, kann mit cargo build --release oder cargo install --locked --path jaq gebaut bzw. installiert werden
  • jaq sollte auf allen Systemen laufen, die von Rust unterstützt werden; falls nicht, wird darum gebeten, ein Issue zu melden

Leistungsbewertung

  • Die Leistungsbewertung besteht aus mehreren Benchmarks, die jaq, jq und gojq vergleichen
  • Der empty-Benchmark misst die Startzeit, indem der empty-Filter n-mal mit einer null-Eingabe ausgeführt wird
  • Der bf-fib-Benchmark führt ein Brainfuck-Skript aus, das Fibonacci-Zahlen erzeugt, mithilfe eines in jq geschriebenen Brainfuck-Interpreters
  • Die Benchmark-Daten wurden auf einem Linux-System mit AMD Ryzen 5 5500U erzeugt
    • Verwendeter Befehl: bench.sh target/release/jaq jq-1.8.1 gojq-0.12.17 | tee bench.json
    • In der Tabelle werden die Ergebnisse von jaq-3.0, jq-1.8.1 und gojq-0.12.18 in Millisekunden angezeigt
    • N/A bedeutet einen Fehler oder mehr als 10 Sekunden Laufzeit
  • Zusammenfassung der Ergebnisse
    • jaq-3.0 war in 20 Benchmarks am schnellsten
    • jq-1.8.1 war in 5 Benchmarks am schnellsten
    • gojq-0.12.18 war in 6 Benchmarks am schnellsten
  • gojq ist bei tree-flatten deutlich schneller, weil der flatten-Filter nativ statt als Definition implementiert ist

Sicherheitsmodell und Einschränkungen

  • jaq versucht Folgendes zu gewährleisten
    • Keine Panics, außer in Fällen von Ressourcenerschöpfung
    • Speichersicherheit, also keine Beschädigung des Speichers
    • Abgesehen vom Lesen von Dateien vor der Ausführung von jq-Filtern können Eingabedaten und jq-Filter keine I/O-Operationen starten
  • Wenn diese Garantien verletzt werden, gilt das als Bug und sollte gemeldet werden
  • jaq adressiert keinerlei Form von Ressourcenerschöpfung
    • Die Ausführungszeit kann unbegrenzt lang werden
    • Es kann unbegrenzt Speicher verbrauchen
    • Es kann unbegrenzt Stack-Speicher verbrauchen
  • Beispielsweise kann es beim Einlesen von Eingabedaten oder beim Ausführen von jq-Filtern zu Stack-Overflow kommen
    • jaq -nr 'repeat("[")' | jaq
    • jaq -n 'def f: 1+f; f'

Audits und Tests

  • jaq core wurde im Rahmen von zwei NLnet-Grants von Radically Open Security auditiert
  • Beim ersten und zweiten Sicherheitsaudit wurden Probleme mit mittlerem oder niedrigem Schweregrad gefunden
  • Alle Punkte aus den Sicherheitsaudits wurden bearbeitet, und unter jaq-core/fuzz wurden mehrere Fuzzing-Targets für jaq hinzugefügt
  • Der JSON-Parser hifijson von jaq hatte bereits Fuzzing-Targets
  • jaq verfügt über eine Testsuite mit mehr als 500 Tests

Anwendungsfälle von Nutzern

  • Ein Nutzer bewertete jaq als deutlich hilfreicher, als jq-Support selbst direkt zu implementieren, und hob hervor, dass sich dank der Erweiterbarkeit über das ValT-Trait jq-Unterstützung leicht zu eigenen Typen hinzufügen ließ
  • Ein anderer Nutzer berichtete, dass ein Rust-Programm mit jaq dreimal alle Abfragen auf einer gesamten Datei ausführen konnte, während das jq-PyPI-Paket für Python zusammen mit einer Python-Schleife nur eine einzige Abfrage über die gesamte Datei schaffte
  • Im Fall des wsjq-Interpreters wurde jaq als deutlich schneller als andere jq-Implementierungen beschrieben, und der Fokus auf Genauigkeit wurde positiv hervorgehoben
    • In diesem wsjq-Benchmark war jaq 5- bis 10-mal schneller als jq und 15- bis 196-mal schneller als gojq
  • Ein Nutzer, der Certificate-Transparency-Log-Daten mit certstream-server verarbeitete, hatte Probleme mit einer jq-Pipeline; nach dem Wechsel zu jaq konnte er dank der schnelleren Startzeit selbst auf einer schwach ausgestatteten VM mithalten

Finanzierung

1 Kommentare

 
GN⁺ 2023-11-30
Hacker-News-Kommentare
  • Wenn man bedenkt, dass die Entwicklung von jq fünf Jahre lang stillstand und erst kürzlich wiederbelebt wurde, ist es nicht verwunderlich, dass sich in der Zwischenzeit Meldungen zu bekannten wie neuen Bugs angesammelt haben.
    Jetzt dürfte das Projekt wieder Fahrt aufnehmen und die lange aufgelaufene Liste offener Punkte nach und nach abarbeiten.

  • Mir gefällt die Art, im README ähnliche oder inspirierte Projekte mit vorzustellen, statt sie als Ersatz darzustellen.
    Durch das README dieses Projekts bin ich auf https://github.com/yamafaktory/jql gestoßen; das war ein Tool, nach dem ich schon lange gesucht hatte, danke dafür.
    Ohne JAQ schlechtmachen zu wollen: Die jq-artige Syntax ist einfach zu schwer zu verstehen, daher passt jql besser zu mir.

    • Aus dieser Perspektive ist auch gron gut.
      Es flacht JSON in Zeilen im Key-Value-Format ab, sodass es gut zu einfachen Stream-Operationen wie grep passt: https://github.com/tomnomnom/gron
    • Ein guter Fund, ich werde es mal ausprobieren.
      Allerdings hatte ich eine wirklich SQL-ähnliche Erfahrung erwartet. Ich verstehe nicht, warum man nicht einfach SQL kopiert und Abfragen wie "SELECT * FROM $json WHERE x>1" ermöglicht.
      Es wirkt, als wollten alle ihre eigene obskure, symbolische Abfragesprache bauen, als wäre es Code Golf. Ich wünschte, man käme weg von extrem knapper, aber nicht selbsterklärender alter Unix-Syntax und näher an den PowerShell-Ansatz heran.
    • https://github.com/tidwall/jj ist ebenfalls einen Blick wert.
    • Ich kann diesen Schmerzpunkt bis zu einem gewissen Grad nachvollziehen, aber zumindest jql wirkt nicht wie die Lösung.
      |={"b""d"=2, "c"} scheint etwa dasselbe zu bedeuten wie jqs select(."b"."d" == 2 or ."c" != null), und die jq-Variante fühlt sich trotz größerer Länge klarer an.
      In der Praxis bräuchte man wohl .[] | select(...), aber bei jql kann es ähnliche Annahmen geben; da ich nicht sicher bin, ob das Beispiel vollständig ist, ändert das am Fazit nicht viel.
    • Die Homoikonizität von jql wirkt ziemlich Lisp-artig.
      Es scheint auch möglich zu sein, es auf sich selbst anzuwenden oder „Makros“ zu verwenden.
  • Ich mag die Idee von jq, aber weil ich es nicht häufig benutze, muss ich jedes Mal die Syntax im Handbuch nachschlagen, wenn ich etwas Bestimmtes erreichen will.
    Leider bestehen 99 % dessen, was ich mit jq mache, aus | jq ..

    • Ich hatte dasselbe Problem.
      Unabhängig davon begann ich, eine Konfigurationssprache zu bauen, und stellte fest, dass sie sich auch für JSON-Abfragen ziemlich gut eignete: https://docs.ruuda.nl/rcl/rcl_query/
      Hier ist ein Beispiel, das ich mit jq nicht lösen konnte, mit RCL aber schon: https://fosstodon.org/@ruuda/111120049523534027
    • Wegen desselben Problems konnte ich die Mächtigkeit von jq nie richtig nutzen; in solchen Fällen ist Copilot wirklich hilfreich.
      Wenn man die gewünschte Aufgabe zusammen mit einem gekürzten Beispiel des ursprünglichen JSON liefert, erzeugt es ein korrektes jq-Skript.
      Bei komplexen Anforderungen ist es einfacher und zuverlässiger, sich mit Copilot schrittweise zu einer Lösung vorzuarbeiten, statt alles auf einmal exakt zu beschreiben. Während der Iteration kommen einem manchmal sogar bessere Ideen als am Anfang.
      ChatGPT oder andere Tools dürften ähnlich funktionieren.
    • Kürzlich habe ich mit ChatGPT schnell die nötige jq-Syntax bekommen: https://chat.openai.com/share/40b68d73-d2dd-412d-867f-9f375e...
  • https://github.com/01mf02/jaq/blob/main/Cargo.lock
    Es gibt ziemlich viele Abhängigkeiten.

    • Im Vergleich zu gojq sind es wirklich viele: https://github.com/itchyny/gojq/blob/main/go.mod
    • Ich frage mich, wie so etwas im Rust-Ökosystem normalerweise verläuft.
      Bei vielen Abhängigkeiten scheint mit der Zeit das Risiko zu wachsen, dass sie grundsätzlich inkompatibel werden, und die Wartung dürfte zu einer großen Aufgabe werden.
      Ich frage mich zum Beispiel, ob es auch in zwei Jahren noch problemlos kompiliert.
  • jq ist ein sehr mächtiges Tool, aber heutzutage wird auch DuckDB viel genutzt.
    Wenn die Daten einigermaßen tabellarisch sind, ist SQL die deutlich natürlichere Sprache.

    • Ich habe früher Retool ausprobiert; dort gab es „Query JSON with SQL“, und das war ziemlich praktisch: https://docs.retool.com/queries/guides/sql/query-json
      Es ist ein wenig ähnlich wie LINQ in C#, aber SQL ist stärker standardisiert, deshalb gefällt es mir besser.
      Es wäre großartig, wenn man native Collections innerhalb einer Sprache mit SQL abfragen könnte; noch besser wäre es, wenn sich Collections transparent in Sqlite speichern ließen.
      Wenn ich Code sehe, der nach dem Abrufen von Daten aus einer Datenbank o. Ä. nur einfache Verarbeitung per Schleife oder Stream-API macht, finde ich das immer schade. Für solche Zwecke ist SQL viel höher abstrahiert und knapper als Java/Kotlin/Python/JavaScript.
    • Fühlt sich ähnlich an.
      Ich speichere die komplette JSON-Rohausgabe in sqlite-Tabellen, erstelle dort virtuelle Spalten und lasse dann das Ergebnis von select durch eine Shell-Schleife laufen.
      Dadurch lösen sich verschachtelte Schleifen auf, und die Debugbarkeit wird viel besser, weil man die exakten Datensätze in der DB prüfen und erneut ausführen kann.
      Mir wurde klar, dass das, was ich baue, ein DAG ist, und ich starte immer ab dem zuletzt erfolgreich verarbeiteten Datensatz neu. Ich frage mich, ob es ein Make-ähnliches Tool gibt, um das auszudrücken.
      Make hat keine SQL-Targets, und vollwertige DAG-Prozessoren wie Airflow sind zu schwergewichtig, um Shell-Schnipsel zusammenzukleben.
    • Genau. Für relationale Daten mit strengem Schema ist SQL viel besser.
      Trotzdem ist es weiterhin schwer, rekursive Abfragen in SQL knapp auszudrücken.
    • Für diesen Zweck gefällt mir persönlich textql besser. Das mentale Modell ist einfacher.
      https://github.com/dinedal/textql
  • Aus Sicht der Korrektheit frage ich mich, ob uint64-Zahlen ohne Abschneiden angezeigt werden können.
    Das ist derzeit der nervigste Punkt an jq.

    • Leider gilt: Wenn man JSON-Zahlen als 64-Bit-Gleitkommazahlen betrachtet, müsste man sie standardkonform so behandeln, und die Ganzzahlpräzision liegt dann bei 53 Bit.
      Allerdings stellt die neuere Spezifikation RFC 8259 klar, dass sie nur die Textform von Zahlen festlegt und keine Semantik definiert.
      In der Praxis behandeln die meisten Implementierungen JSON aber als Teilmenge von JavaScript, was zu der Annahme führt, dass Zahlen 64-Bit-Gleitkommazahlen sind.
    • Soweit ich weiß, wurde das in jq 1.7 verbessert: https://github.com/jqlang/jq/releases/tag/jq-1.7
      Dort heißt es, dass Dezimalzahl-Literale zur Erhaltung der Präzision verwendet werden und Vergleichsoperatoren die Präzision respektieren, während arithmetische Operationen abschneiden können.
    • jq 1.7 bewahrt große Ganzzahlen, aber sobald irgendeine Operation darauf ausgeführt wird, werden sie abgeschnitten.
      Derzeit wird auf decimal64 abgeschnitten, was etwas verwirrend ist; im nächsten Release soll das entsprechend dem Vorschlag der JSON-Spezifikation auf binary64(double) geändert werden: https://github.com/jqlang/jq/pull/2949
  • Seit ich zu jless gewechselt bin, habe ich nicht mehr zurückgeschaut.
    Die Benutzeroberfläche ist den anderen weit voraus.

    • Das ist nicht dieselbe Kategorie.
      jq ist kein einfacher Viewer, sondern ein Prozessor für eine JSON-Abfragesprache.
  • Es ist niedlich, dass irgendwo in Rust eine Terminal-Line-Art-Bibliothek steckt, aber als ich jaq ausprobierte, kippte es Megabytes an Escape-Codes in iTerm, bis iTerm schließlich versuchte, es an den Drucker auszugeben.
    Das war zu clever.
    In einem Kontext wie echo *json | rush -- jaq -rf ./this-program.jq {} | datamash ... halte ich es nicht für passend, dem TTY künstlerische Effekte zu verpassen.
    Die Ursache des Fehlers war jedenfalls, dass jaq kein strftime hat.

  • Mein erster Eindruck ist, dass es zwar schöne Fehlermeldungen gibt, aber kein halt_error/0.
    Nachdem ich halt_error auskommentiert hatte, war es langsamer als jq und gojq.
    Mit derselben Eingabe brauchte jq etwa 0,023 Sekunden, gojq etwa 0,070 Sekunden und jaq etwa 0,103 Sekunden.
    Das verwendete aoc22-13.jq ist https://pastebin.com/raw/YiUjEu2n, und input.txt ist https://pastebin.com/raw/X0FSyTNf.

  • Ich habe angefangen, statt jq yq zu verwenden, und frage mich, ob es wichtige Unterschiede gibt.

    • Das hängt davon ab, welches yq gemeint ist.
      Persönlich bevorzuge ich https://github.com/mikefarah/yq gegenüber https://github.com/kislyuk/yq.
    • jq fühlt sich wie ein deutlich robusteres Tool an als yq.
      Ich verstehe, dass YAML-Verarbeitung viel schwieriger ist als JSON, aber obwohl yq beim Wechsel von Version 3 auf 4 die Syntax näher an jq gebracht hat, ist sie irgendwie immer noch nicht ganz gleich.
      Außerdem hat yq kein if-then-else, was wie schlechtes Design oder ein Versäumnis wirkt: https://github.com/mikefarah/yq/issues/95
      Wenn man YAML verarbeiten muss, funktioniert yq gut und kommt auch mit Kommentaren ziemlich gut zurecht, aber für reine JSON-Verarbeitung ist jq das bessere Tool.