3 points par GN⁺ 2023-11-30 | 1 commentaires | Partager sur WhatsApp
  • jaq est un clone de l’outil de traitement de données JSON jq, qui vise une implémentation plus précise et prévisible tout en restant compatible avec jq dans la plupart des cas
  • Le programme en ligne de commande jaq peut être utilisé comme remplaçant transparent de jq, et la bibliothèque Rust jaq-core permet de compiler et d’exécuter des programmes jq dans des programmes Rust
  • Il prend en charge YAML, CBOR, TOML et XML, absents de jq, tandis que jaq-core peut être utilisé en toute sécurité dans des environnements multithread et prend en charge des types de données arbitraires au-delà du JSON
  • Dans l’évaluation des performances, jaq-3.0 a été le plus rapide sur 20 des 31 benchmarks, contre 5 pour jq-1.8.1 et 6 pour gojq-0.12.18
  • Côté sécurité, il cherche à garantir l’absence de panic, la sûreté mémoire et des limites d’I/O pour les données d’entrée et les filtres jq, mais ne traite pas les cas d’épuisement des ressources comme le temps, la mémoire ou la pile

Ce que propose jaq

  • jaq est un clone de l’outil de traitement de données JSON jq, prononcé /ʒaːk/ comme Jacques
  • Il prend en charge des formats de données absents de jq
    • YAML

    • CBOR

    • TOML

      • XML
      • Une documentation distincte est disponible, et il peut être testé dans le playground
      • jaq est proposé sous deux formes
      • Programme en ligne de commande jaq : utilisable comme remplaçant transparent de jq
      • Bibliothèque jaq-core : permet de compiler et d’exécuter des programmes jq dans des programmes Rust

Objectifs de conception

  • Précision

    • jaq vise une implémentation de jq plus correcte et plus prévisible, tout en restant compatible avec jq dans la plupart des cas
  • Performances

    • jaq a été créé à l’origine parce que le long temps de démarrage de jq 1.6 était gênant, avec un démarrage d’environ 50 ms dans cet environnement
    • Ce temps de démarrage est particulièrement visible lors du traitement d’un grand nombre de petits fichiers
    • Le temps de démarrage a été fortement amélioré dans jq 1.7, mais jaq reste plus rapide que jq sur plusieurs benchmarks
  • Simplicité

    • jaq recherche une implémentation simple et compacte afin de réduire les risques de bugs et de faciliter les contributions

Installation et compilation

  • Des binaires pour Linux, Mac et Windows sont disponibles sur la page des releases
  • Sur macOS ou Linux, il peut être installé via homebrew
    • brew install jaq
    • brew install --HEAD jaq
  • Pour compiler depuis les sources, un toolchain Rust est nécessaire
  • Si le dépôt a été cloné, il est possible de compiler ou d’installer avec cargo build --release ou cargo install --locked --path jaq
  • jaq est censé fonctionner sur tous les systèmes pris en charge par Rust ; dans le cas contraire, il est demandé d’ouvrir une issue

Évaluation des performances

  • L’évaluation des performances se compose de plusieurs benchmarks comparant jaq, jq et gojq
  • Le benchmark empty mesure le temps de démarrage en exécutant n fois le filtre empty avec une entrée null
  • Le benchmark bf-fib exécute un script Brainfuck qui génère des nombres de Fibonacci à l’aide d’un interpréteur Brainfuck écrit en jq
  • Les données de benchmark ont été générées sur un système Linux équipé d’un AMD Ryzen 5 5500U
    • La commande utilisée est bench.sh target/release/jaq jq-1.8.1 gojq-0.12.17 | tee bench.json
    • Le tableau affiche les résultats de jaq-3.0, jq-1.8.1 et gojq-0.12.18 en millisecondes
    • N/A signifie une erreur ou un dépassement de 10 secondes
  • Résumé des résultats
    • jaq-3.0 est le plus rapide sur 20 benchmarks
    • jq-1.8.1 est le plus rapide sur 5 benchmarks
    • gojq-0.12.18 est le plus rapide sur 6 benchmarks
  • gojq est bien plus rapide sur tree-flatten, car le filtre flatten y est implémenté nativement plutôt que défini

Modèle de sécurité et limites

  • jaq cherche à garantir les points suivants
    • Aucun panic en dehors des cas d’épuisement des ressources
    • Une sûreté mémoire sans corruption de la mémoire
    • À l’exception de la lecture de fichiers avant l’exécution du filtre jq, les données d’entrée et le filtre jq ne peuvent pas initier d’opérations d’I/O
  • Toute violation de ces garanties est considérée comme un bug et doit être signalée
  • jaq ne traite aucun type d’épuisement des ressources
    • Le temps d’exécution peut devenir illimité
    • La mémoire peut être consommée sans limite
    • L’espace de pile peut être consommé sans limite
  • Par exemple, un dépassement de pile peut se produire lors de la lecture des données d’entrée ou de l’exécution d’un filtre jq
    • jaq -nr 'repeat("[")' | jaq
    • jaq -n 'def f: 1+f; f'

Audits et tests

  • jaq core a fait l’objet d’audits par Radically Open Security dans le cadre de deux subventions NLnet
  • Les premier et deuxième audits de sécurité ont mis au jour des problèmes de gravité moyenne ou faible
  • Tous les problèmes relevés lors des audits ont été traités, et plusieurs cibles de fuzzing pour jaq ont été ajoutées dans jaq-core/fuzz
  • Le parseur JSON de jaq, hifijson, disposait déjà lui aussi de cibles de fuzzing
  • jaq dispose d’une suite de tests de plus de 500 tests

Cas d’usage

  • Un utilisateur a estimé que jaq l’avait beaucoup aidé par rapport à l’implémentation directe de la prise en charge de jq, et que l’extensibilité via le trait ValT lui avait permis d’ajouter facilement la prise en charge de jq à ses propres types
  • Un autre utilisateur a indiqué que son programme Rust utilisant jaq pouvait exécuter trois fois toutes les requêtes sur l’ensemble d’un fichier pendant que le crate PyPI jq de Python et une boucle Python n’exécutaient qu’une seule requête sur ce même fichier
  • Dans le cas de l’interpréteur wsjq, jaq a été jugé nettement plus rapide que les autres implémentations de jq, avec un accent impressionnant sur la précision
    • Sur ce benchmark wsjq, jaq est 5 à 10 fois plus rapide que jq et 15 à 196 fois plus rapide que gojq
  • Un utilisateur qui traitait des données de certificate transparency logs avec certstream-server rencontrait des problèmes avec un pipeline jq, et a indiqué qu’après être passé à jaq, son temps de démarrage plus rapide lui permettait de suivre le rythme même sur une VM peu puissante

Financement

1 commentaires

 
GN⁺ 2023-11-30
Commentaires Hacker News
  • Étant donné que le développement de jq est resté à l’arrêt pendant 5 ans avant de reprendre tout récemment, il n’est pas surprenant que des signalements se soient accumulés pendant ce temps, qu’il s’agisse de bugs connus ou de nouveaux bugs
    On dirait qu’ils vont maintenant retrouver de la vitesse et commencer à résorber progressivement la liste des problèmes non résolus accumulés de longue date

  • J’aime bien l’idée de présenter aussi dans le README des projets similaires ou inspirés, sans forcément les présenter comme des remplaçants
    C’est grâce au README de ce projet que j’ai découvert https://github.com/yamafaktory/jql, et comme c’est un outil que je cherchais depuis longtemps, j’en suis reconnaissant
    Ce n’est pas pour rabaisser JAQ, mais la syntaxe à la JQ est beaucoup trop difficile à comprendre, donc jql me convient mieux

    • Dans cette optique, gron est aussi bien
      Il aplatit le JSON en lignes clé-valeur, ce qui fonctionne bien avec des traitements de flux simples comme grep : https://github.com/tomnomnom/gron
    • Bonne trouvaille, je pense l’essayer
      Cela dit, je m’attendais à une vraie expérience proche de SQL. Je ne comprends pas pourquoi ils ne reprennent pas simplement SQL pour permettre des requêtes du type "SELECT * FROM $json WHERE x>1"
      On dirait que tout le monde veut créer son propre langage de requête symbolique obscur, comme s’il faisait du code golf. J’aimerais qu’on s’éloigne des anciennes syntaxes Unix, extrêmement courtes mais peu évidentes, pour aller vers quelque chose de plus proche de l’approche PowerShell
    • https://github.com/tidwall/jj vaut aussi le détour
    • Je comprends dans une certaine mesure cette gêne, mais au moins jql ne me semble pas être la solution
      |={"b""d"=2, "c"} semble vouloir dire la même chose que select(."b"."d" == 2 or ."c" != null) en jq, mais même si la version jq est plus longue, elle me paraît plus claire
      En pratique, il faudrait sûrement .[] | select(...), mais jql repose peut-être sur une hypothèse similaire, et comme je ne suis pas sûr que l’exemple soit complet, cela ne change pas vraiment la conclusion
    • La homoiconicité de jql a un côté très Lisp
      On dirait qu’on peut aussi l’appliquer à lui-même ou utiliser des sortes de « macros »
  • J’aime l’idée de jq, mais comme je ne l’utilise pas souvent, je dois retourner voir la syntaxe dans le manuel à chaque fois que je veux faire quelque chose
    Malheureusement, 99 % de ce que je fais avec jq, c’est | jq .

    • J’ai eu le même problème
      À part ça, j’ai commencé à créer un langage de configuration, et il s’est avéré qu’il était aussi assez bon pour interroger du JSON : https://docs.ruuda.nl/rcl/rcl_query/
      Voici un exemple que je n’ai pas pu résoudre avec jq mais que j’ai pu traiter avec RCL : https://fosstodon.org/@ruuda/111120049523534027
    • J’avais le même souci, ce qui m’empêchait de vraiment tirer parti de la puissance de jq, et dans ce genre de cas Copilot aide énormément
      Si on lui donne la tâche voulue avec un échantillon réduit du JSON source, il produit le bon script jq
      Pour les besoins complexes, il est plus simple et plus fiable de guider Copilot vers une solution par itérations successives que d’essayer de tout formuler parfaitement d’un seul coup. Pendant ces itérations, on finit aussi parfois par avoir de meilleures idées qu’au départ
      ChatGPT ou d’autres outils devraient fonctionner de façon similaire
    • Récemment, j’ai obtenu rapidement la syntaxe jq dont j’avais besoin avec ChatGPT : https://chat.openai.com/share/40b68d73-d2dd-412d-867f-9f375e...
  • https://github.com/01mf02/jaq/blob/main/Cargo.lock
    Il y a pas mal de dépendances

    • C’est vraiment beaucoup par rapport à gojq : https://github.com/itchyny/gojq/blob/main/go.mod
    • Je me demande comment ce genre de situation évolue généralement dans l’écosystème Rust
      Avec beaucoup de dépendances, le risque qu’elles deviennent fondamentalement incompatibles entre elles au fil du temps semble plus élevé, et la maintenance paraît être un gros travail
      Par exemple, est-ce que ça compilera encore correctement dans 2 ans ?
  • jq est un outil très puissant, mais de nos jours DuckDB est aussi beaucoup utilisé
    Si les données ont plus ou moins une forme tabulaire, SQL est un langage bien plus naturel

    • J’ai essayé Retool il y a quelque temps, et il y avait « Query JSON with SQL », ce qui était assez pratique : https://docs.retool.com/queries/guides/sql/query-json
      C’est un peu similaire à LINQ en C#, mais j’aime davantage SQL parce qu’il est plus standardisé
      Ce serait formidable de pouvoir interroger des collections natives en SQL dans un langage, et mieux encore si ces collections pouvaient être stockées de façon transparente dans Sqlite
      Quand je vois du code qui récupère des données depuis une base ou autre, puis fait un traitement simple avec des boucles ou une API de streams, je trouve toujours ça dommage. Pour ce genre d’usage, SQL est bien plus haut niveau et concis que Java/Kotlin/Python/JavaScript
    • Même impression
      Je stocke toute la sortie JSON brute dans des tables sqlite, puis j’y crée des colonnes virtuelles avant de faire tourner le résultat de select dans une boucle shell
      Ça supprime les boucles imbriquées, et le débogage est bien meilleur parce qu’on peut inspecter dans la base les enregistrements exacts et relancer
      J’ai réalisé que ce que je construisais était un DAG, et je redémarre toujours à partir du dernier enregistrement traité avec succès. Je me demande s’il existe un outil semblable à Make pour exprimer cela
      Make n’a pas de cible SQL, et un orchestrateur DAG complet comme Airflow est trop lourd pour assembler des fragments shell
    • Oui. Pour des données relationnelles avec un schéma strict, SQL est bien meilleur
      Cela dit, il reste difficile d’exprimer des requêtes récursives de manière concise en SQL
    • Pour cet usage, je préfère personnellement textql. Le modèle mental est plus simple
      https://github.com/dinedal/textql
  • Du point de vue de l’exactitude, je me demande si les nombres uint64 peuvent être affichés sans être tronqués
    C’est actuellement l’aspect le plus agaçant de jq pour moi

    • Malheureusement, si l’on considère les nombres JSON comme des flottants 64 bits, alors il faut les traiter ainsi pour respecter le standard, et la précision entière tombe à 53 bits
      Cela dit, la spécification la plus récente, RFC 8259, corrige cela en précisant qu’elle ne définit que le format textuel des nombres, pas leur sémantique
      En pratique, la plupart des implémentations traitent JSON comme un sous-ensemble de JavaScript, ce qui conduit à supposer que les nombres sont des flottants 64 bits
    • Il me semble que cela a été amélioré dans jq 1.7 : https://github.com/jqlang/jq/releases/tag/jq-1.7
      Il est indiqué que les littéraux numériques décimaux préservent la précision, que les comparaisons la respectent aussi, mais que les opérations arithmétiques peuvent entraîner une troncature
    • jq 1.7 préserve les grands entiers, mais dès qu’une opération est effectuée dessus, ils sont tronqués
      Actuellement, c’est tronqué en decimal64, ce qui est un peu déroutant, mais cela devrait être corrigé dans la prochaine version pour revenir à une troncature en binary64(double), conformément à la proposition de la spécification JSON : https://github.com/jqlang/jq/pull/2949
  • Depuis que je suis passé à jless, je ne reviendrais pas en arrière
    L’interface utilisateur est très au-dessus des autres

    • Ce n’est pas la même catégorie
      jq n’est pas un simple visualiseur, c’est un processeur de langage de requête JSON
  • C’est mignon qu’il existe quelque part en Rust une bibliothèque de line art pour terminal, mais quand j’ai essayé d’exécuter jaq, il a déversé des mégaoctets de codes d’échappement dans iTerm, qui a fini par essayer de les envoyer à l’impression
    C’était trop malin pour son propre bien
    Dans un contexte comme echo *json | rush -- jaq -rf ./this-program.jq {} | datamash ..., je ne pense pas qu’il soit approprié d’essayer d’ajouter des effets artistiques sur un TTY
    Quoi qu’il en soit, la cause de l’erreur était que jaq n’a pas strftime

  • Ma première impression est qu’il y a de jolis messages d’erreur, mais pas de halt_error/0
    Après avoir commenté halt_error, c’était plus lent que jq et gojq
    Sur la même entrée, jq prenait environ 0,023 s, gojq environ 0,070 s, et jaq environ 0,103 s
    Le aoc22-13.jq utilisé est https://pastebin.com/raw/YiUjEu2n et input.txt est https://pastebin.com/raw/X0FSyTNf

  • J’ai commencé à utiliser yq à la place de jq, et je me demande s’il y a des différences importantes

    • Cela dépend de quel yq on parle
      Personnellement, je préfère https://github.com/mikefarah/yq à https://github.com/kislyuk/yq
    • jq donne l’impression d’être un outil bien plus robuste que yq
      Je comprends que traiter du YAML est bien plus difficile que du JSON, mais même si yq a rapproché sa syntaxe de celle de jq entre les versions 3 et 4, cela ne semble toujours pas tout à fait identique
      Et puis yq n’a pas de if-then-else, ce qui donne l’impression d’un mauvais choix de conception ou d’un oubli : https://github.com/mikefarah/yq/issues/95
      Quand il faut traiter du YAML, yq fonctionne bien et gère assez bien les commentaires, mais pour du JSON pur, jq reste le meilleur outil