From c0d502f50d79b5581dcf3489919317f5ba5ab9c7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 25 Nov 2022 09:31:19 +0100 Subject: [PATCH] First commit --- .gitignore | 6 + .idea/.gitignore | 8 + .idea/libraries/apoc_full.xml | 9 + .idea/misc.xml | 10 + .idea/modules.xml | 8 + .idea/unimore-bda-4.iml | 9 + .idea/vcs.xml | 6 + .vscode/launch.json | 4 + .vscode/tasks.json | 15 + README.md | 1041 +++++++++++++++++ data/neo4j | 1 + media/cratesio-categories.png | Bin 0 -> 3751 bytes media/cratesio-keywords.png | Bin 0 -> 29455 bytes media/query-categorygraph-solution.svg | 1 + media/query-randfull-solution.svg | 1 + media/query-selfdeps-solution.svg | 1 + media/query-serdedeps-plan.svg | 1 + media/query-serdedeps-solution.svg | 1 + media/query-steffocrates-solution.svg | 1 + scripts/alter-default-password.sh | 6 + scripts/create-neo4j-desktop-link.sh | 7 + scripts/fixup-data-files.sh | 15 + scripts/import-cratesio.sh | 18 + scripts/import-cratesio/1-crates.cypher | 66 ++ scripts/import-cratesio/2-keywords.cypher | 13 + .../import-cratesio/3-crates_keywords.cypher | 8 + scripts/import-cratesio/4-categories.cypher | 50 + .../5-crates_categories.cypher | 8 + scripts/import-cratesio/6-users.cypher | 23 + scripts/import-cratesio/7-crate_owners.cypher | 11 + scripts/import-cratesio/8-versions.cypher | 50 + scripts/import-cratesio/9-dependencies.cypher | 38 + scripts/run-db.sh | 4 + scripts/setup-apoc.sh | 9 + 34 files changed, 1449 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/libraries/apoc_full.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/unimore-bda-4.iml create mode 100644 .idea/vcs.xml create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 README.md create mode 120000 data/neo4j create mode 100644 media/cratesio-categories.png create mode 100644 media/cratesio-keywords.png create mode 100644 media/query-categorygraph-solution.svg create mode 100644 media/query-randfull-solution.svg create mode 100644 media/query-selfdeps-solution.svg create mode 100644 media/query-serdedeps-plan.svg create mode 100644 media/query-serdedeps-solution.svg create mode 100644 media/query-steffocrates-solution.svg create mode 100755 scripts/alter-default-password.sh create mode 100755 scripts/create-neo4j-desktop-link.sh create mode 100755 scripts/fixup-data-files.sh create mode 100755 scripts/import-cratesio.sh create mode 100644 scripts/import-cratesio/1-crates.cypher create mode 100644 scripts/import-cratesio/2-keywords.cypher create mode 100644 scripts/import-cratesio/3-crates_keywords.cypher create mode 100644 scripts/import-cratesio/4-categories.cypher create mode 100644 scripts/import-cratesio/5-crates_categories.cypher create mode 100644 scripts/import-cratesio/6-users.cypher create mode 100644 scripts/import-cratesio/7-crate_owners.cypher create mode 100644 scripts/import-cratesio/8-versions.cypher create mode 100644 scripts/import-cratesio/9-dependencies.cypher create mode 100755 scripts/run-db.sh create mode 100755 scripts/setup-apoc.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42b9046 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/data/cratesio +/data/neo4j/data +/data/neo4j/logs +/data/neo4j/run +/data/neo4j/plugins +/data/neo4j/import diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/libraries/apoc_full.xml b/.idea/libraries/apoc_full.xml new file mode 100644 index 0000000..e99245a --- /dev/null +++ b/.idea/libraries/apoc_full.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..2ca2389 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f4c6165 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/unimore-bda-4.iml b/.idea/unimore-bda-4.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/unimore-bda-4.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cd4d5eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,4 @@ +{ + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..c9d8c68 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Neo4J", + "icon": { + "id": "database" + }, + "type": "shell", + "command": "${workspaceFolder}/scripts/run-db.sh", + "problemMatcher": [], + "isBackground": true, + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e3f299 --- /dev/null +++ b/README.md @@ -0,0 +1,1041 @@ +[ Stefano Pigozzi | Use case "creato da zero" | Tema Neo4J | Big Data Analytics | A.A. 2022/2023 | Unimore ] + +# Modellazione di un database Neo4J per le dipendenze delle crates del linguaggio Rust + +> ### Neo4j +> +> Studiare e provare un esempio di applicazione reale di Neo4j, scegliendone uno: +> +> * tra le seguenti sandbox disponibili alla pagina https://neo4j.com/sandbox/: +> * `WWC2019` +> * `OpenStreetMaps` +> * tra gli esempi illustrati nelle guide https://neo4j.com/developer/example-data/#guide-examples: +> * `UKCompanies` +> * `Recipes` +> * tra quelli disponibili alla pagina https://neo4j.com/graphgists/ +> +> Quindi, partendo dallo use case scelto, descrivere lo scenario, i dati ed eventualmente le tecniche non viste a lezione esposte nello use case. +> +> Crearne e descriverne poi una propria versione “estesa” lavorando a uno o più dei seguenti aspetti: +> +> * modificare/migliorare il grafo, ad es. sostituendo i dati o aggiungendone di nuovi (es. importandoli / adattandoli da altre sorgenti esterne), modificando/arricchendo lo schema (es. aggiungendo nuovi archi o tipi di archi, property, ecc. a supporto di particolari interrogazioni); +> * creare nuove interrogazioni e/o operazioni di modifica di varia complessità nell’ambito dello scenario ispirandosi agli argomenti visti a lezione e agli esercizi proposti nello usecase (opzionalmente: lavorando con codice Python); +> * studiare e creare i migliori indici per alcune delle operazioni previste; +> * ... +> +> È anche possibile proporre un proprio use case creato da zero. + +## Sinossi + +Si è realizzato un database a grafo Neo4J che rappresenta l'indice pubblico [Crates.io] delle crates (librerie) del linguaggio di programmazione [Rust], le loro categorie, i loro tag, i loro autori, le loro versioni e le loro dipendenze. + +## Introduzione + +[Rust] è un linguaggio di programmazione compilato che negli ultimi anni sta acquisendo popolarità grazie alle funzionalità innovative di cui è dotato, che facilitano la scrittura di codice corretto e parallelizzabile. + +I progetti in Rust sono organizzati in [crate], unità indivisibili di compilazione, in modo simile ai package di altri linguaggi di programmazione. + +Quando pronte per l'utilizzo, le crates vengono generalmente caricate su [Crates.io], repository ufficiale per librerie e eseguibili del linguaggio, che le indicizza e ne permette il download. + +Nel workflow standard, le crate sono gestite da [Cargo], strumento a linea di comando in grado di recuperare le crates necessarie alla compilazione del progetto corrente e di organizzarne i metadati, contenuti all'interno del file `Cargo.toml` della directory del progetto, di cui si fornisce il seguente esempio: + +```toml +[package] +name = "distributed_arcade" +version = "0.3.0" +authors = ["Stefano Pigozzi "] +edition = "2021" +description = "Fast and simple scoreboard service for games" +repository = "https://github.com/Steffo99/distributed-arcade" +license = "AGPL-3.0-or-later" +keywords = ["game", "scoreboard", "redis", "axum", "web-api"] +categories = ["games"] + +[dependencies] +redis = { version = "0.22.1", features=["r2d2", "ahash", "cluster", "tokio-comp", "connection-manager"] } +axum = { version = "0.5.17" } +tokio = { version = "1.21.2", features=["full"] } +r2d2 = { version = "0.8.10" } +lazy_static = { version = "1.4.0" } +serde = { version = "1.0.147", features=["derive"] } +serde_json = { version = "1.0.87" } +log = { version = "0.4.17" } +pretty_env_logger = { version = "0.4.0" } +rand = { version = "0.8.5" } +regex = { version = "1.7.0" } +async-trait = { version = "0.1.58" } +tower-http = { version = "0.3.4", features=["cors"] } +``` + +Come si può vedere dall'esempio, tra i metadati di una crate gestita da Cargo, possiamo trovare: + +- `name`: il nome della crate +- `version`: la [versione semantica] attuale della crate +- `authors`: nome e email degli autori della crate +- `description`: una breve descrizione in linguaggio naturale dei contenuti della crate +- `repository`: un link al repository Git contenente il sorgente della crate +- `license`: l'[identificatore SPDX] della licenza utilizzata +- `keywords`: fino a 5 termini liberamente scelti dagli autori per rendere più facile trovare la crate nell'indice +- `categories`: fino a 5 categorie dall'[elenco delle categorie previste] +- `dependencies`: nomi, range di versioni e funzionalità delle crate richieste dalla corrente per la compilazione + +Quando una nuova versione di una crate viene caricata su Crates.io, i metadati relativi alla crate come `description` e `repository` vengono sovrascritti con quelli più recenti, mentre i metadati specifici alla versione come `dependencies` vengono salvati assieme nella versione specifica. + +Lo scopo di questo progetto è di indicizzare questi metadati in un database Neo4J per poi effettuarvi alcune analisi, sfruttando gli [open data] resi disponibili da Crates.io come sorgenti di dati. + +## Struttura del progetto + +Il progetto è organizzato nelle seguenti directory: + +- `README.md`: questo stesso file +- `data/cratesio/`: dove vanno inseriti gli open data pubblicati da Crates.io +- `data/neo4j/`: home del DBMS Neo4J da utilizzare +- `scripts/`: contiene script Bash per l'esecuzione rapida di alcune operazioni sul database Neo4J +- `scripts/import-cratesio/`: contiene query Cypher che vengono eseguite per l'importazione dei file di Crates.io + +## Prerequisiti + +### Scelta del database + +È possibile scegliere di utilizzare un DBMS Neo4J generico, oppure di utilizzarne uno gestito da Neo4J Desktop. + +Questo progetto si aspetta che la `NEO4J_HOME` del DBMS scelto si trovi nella cartella `data/neo4j/`. + +#### Neo4J Desktop + +Utilizzare Neo4J Desktop permette fra le varie cose di utilizzare *Neo4J Bloom*, applicazione che permette di esplorare il database grafo interattivamente, ed è quindi l'opzione consigliata. + +Per utilizzare un database Neo4J Desktop, è possibile seguire seguiti i seguenti passi: + +1. Si inizializzi il DBMS su Neo4J Desktop. + +2. Si determini dove la home del DBMS inizializzato è collocata selezionandolo, cliccando l'opzione Altro `···`, e selezionando *Open Folder* → *DBMS*. + +3. Si crei un collegamento simbolico tra la home del DBMS e la posizione `data/neo4j/`: + ```console + $ ./scripts/create-neo4j-desktop-link "/home/$USER/.config/Neo4j Desktop/Application/relate-data/dbmss/dbms-$neo4j_uuid/" + ``` + ```bash + #!/usr/bin/env bash + repo=$(git rev-parse --show-toplevel) + unlink "$repo/data/neo4j" + ln -s "$1" "$repo/data/neo4j" + ``` + +#### Database generico + +Per utilizzare un database generico, è possibile seguire i seguenti passi: + +1. Si inizializzi il DBMS e lo si esegua: + ```console + $ ./scripts/run-db.sh + ``` + ```bash + #!/usr/bin/env bash + repo=$(git rev-parse --show-toplevel) + export NEO4J_HOME="$repo/data/neo4j" + neo4j console + ``` + +2. Mentre il DBMS è in esecuzione, se ne cambi la password predefinita per ottenere accesso a tutte le funzionalità: + ```console + $ ./scripts/alter-default-password.sh + ``` + +### APOC + +Gli open data di Crates.io formattano le date con stringhe in formato `"2023-02-10 07:50:14.995689"` che la funzione [`datetime`] non è in grado di leggere. + +Pertanto, per processarle correttamente, si è scelto di includere la libreria [APOC] per Neo4J, che include la funzione [`apoc.date.parse`] con supporto a formati arbitrari per le date. + +Essa può essere abilitata su Neo4J Desktop dalla scheda *Plugins* del database, oppure attraverso lo script `scripts/setup-apoc.sh`. + +### Heap size incrementata + +Alcune query utilizzate in questo progetto superano il massimo di memoria utilizzabile per l'heap da Neo4J, impostato di default a 2 GB. + +Per aumentarlo a 6 GB, è necessario aprire il file `data/neo4j/conf/neo4j.conf` e modificare nel seguente modo le proprietà: + +```properties +server.memory.heap.initial_size=6g +server.memory.heap.max_size=6g +``` + +## Realizzazione + +### Download dei dati + +Si è scaricato il [dump giornaliero del database] di Crates.io del giorno 19 Febbraio 2023, e lo si è estratto nella directory `data/cratesio/2023-02-19-020031/`. + +Esso contiene tutti i dati disponibili sotto forma di file comma-separated-values all'interno della directory `data/cratesio/2023-02-19-020031/data/`. + +### Ripulitura dei dati + +I file `csv` scaricati non sono immediatamente utilizzabili in Neo4J: + +* non effettuano correttamente escaping dei backslash presenti all'interno delle stringhe +* devono trovarsi nella directory `data/neo4j/import` + +Entrambi i problemi vengono risolti dal seguente script, che corregge l'escaping e piazza la copia corretta in `data/neo4j/import`: + +```console +$ ./scripts/fixup-data-files.sh +``` + +```bash +#!/usr/bin/env bash + +repo=$(git rev-parse --show-toplevel) +cwd=$(pwd) +data_files=$(ls $repo/data/cratesio/*/data/*.csv) + +cd "$repo" + +for file in $data_files; do + echo "Fixing data file $file..." + basefilename=$(basename $file) + sed --expression='s=\\=\\\\=g' $file > "$repo/data/neo4j/import/$basefilename" +done + +cd "$cwd" +``` + +### Importazione dei dati + +I file `csv` ripuliti vengono poi importati nel database Neo4J attraverso una serie di istruzioni Cypher collocate in `./scripts/import-cratesio`, divise in gruppi per tipo di entità importate. + +Possono essere eseguite in gruppo o una alla volta con lo script `scripts/import-cratesio.sh`: + +* una alla volta: + + ```console + $ ./scripts/import-cratesio.sh $NUMERO- + ``` + +* tutte insieme, in ordine: + + ```console + $ ./scripts/import-cratesio.sh + ``` + +Lo script esegue le seguenti istruzioni: + +```bash +#!/usr/bin/env bash +set -e + +export NEO4J_USERNAME="neo4j" +export NEO4J_PASSWORD="unimore-big-data-analytics-4" + +repo=$(git rev-parse --show-toplevel) +cwd=$(pwd) +import_scripts=$(echo $repo/scripts/import-cratesio/$1*.cypher | sort) + +cd "$repo" + +for file in $import_scripts; do + echo "Executing $file..." + cypher-shell --fail-at-end --format verbose < $file +done + +cd "$cwd" +``` + +#### `1` - Creazione dei nodi `:Crate` + +Si creano nel grafo nodi aventi il label `:Crate`, importando i dati delle singole crate contenuti all'interno del file `crates.csv`. + +##### Proprietà + +I nodi `:Crate` avranno le seguenti proprietà: + +- `id`: l'id univoco della crate su Crates.io +- `name`: nome univoco con il quale la crate è stata uploadata su Crates.io +- `created_at`: data del primo upload su Crates.io ("data di creazione") +- `updated_at`: data dell'ultimo upload su Crates.io ("data di aggiornamento") +- `max_upload_size`: dimensione della crate massima uploadabile +- `downloads`: numero di download totale + +Le seguenti proprietà saranno ugualmente definite, ma potrebbero acquisire un valore di `null` qualora non fossero specificate nei metadati delle crate: + +- `description`: breve descrizione della crate +- `readme`: lunga descrizione multilinea della crate +- `documentation`: link alla documentazione della crate +- `homepage`: link alla homepage della crate +- `repository`: link al repository del codice sorgente della crate + +##### Indici + +Per determinare gli indici da creare, si immaginano alcune semplici interrogazioni che potrebbero essere effettuate sull'insieme dei nodi `:Crate`, e si creano indici in grado di risolverle: + +- _ricerca **per id** o **in un intervallo**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_crate_id IF NOT EXISTS + FOR (crate:Crate) + ON (crate.id); + ``` + +- _ricerca di crate **con un numero minimo di download**_: si crea un indice `RANGE` sul campo `downloads`. + + ```cypher + CREATE RANGE INDEX index_crate_downloads IF NOT EXISTS + FOR (crate:Crate) + ON (crate.downloads); + ``` + +- _ricerca di crate **create** o **aggiornate** **prima** o **dopo una certa data**_: si creano due indici `RANGE`, uno sul campo `created_at`, l'altro sul campo `updated_at`. + + ```cypher + CREATE RANGE INDEX index_crate_created_at IF NOT EXISTS + FOR (crate:Crate) + ON (crate.created_at); + + CREATE RANGE INDEX index_crate_updated_at IF NOT EXISTS + FOR (crate:Crate) + ON (crate.updated_at); + ``` +- _ricerca di crate con un **nome**, **prefisso** o **suffisso specifico**_: si crea un indice `TEXT` sul campo `name`. + + ```cypher + CREATE TEXT INDEX index_crate_name IF NOT EXISTS + FOR (crate:Crate) + ON (crate.name); + ``` + +##### Query + +Si scrive una query per l'importazione di dati da file CSV. + +Si utilizza l'istruzione `LOAD CSV WITH HEADERS`, che legge il file CSV dato dalla cartella `$NEO4J_HOME/import` e trasforma ogni riga in un oggetto avente come chiavi i titoli delle colonne definite nella prima riga. + +```cypher +LOAD CSV WITH HEADERS FROM "file:///crates.csv" AS line FIELDTERMINATOR "," +``` + +Essendo il file molto lungo, Neo4J accumula tutti i nodi da creare in una singola transazione, richiedendo al sistema operativo di dedicargli sempre più memoria RAM. + +Per evitare che la memoria si esaurisca e che Neo4J venga terminato, si utilizza l'istruzione `CALL {} IN TRANSACTIONS OF N ROWS`, che applica la transazione ogni N righe del file CSV processate. + +```cypher +CALL { + ... +} IN TRANSACTIONS OF 10000 ROWS +``` + +Infine, si inserisce come argomento di `CALL` la seguente query, che può funzionare sia per inizializzare il database, sia per aggiornarlo con gli ultimi dati: + +1. Prova a vedere se esiste già un nodo `:Crate` con il dato `id`. +2. Se non esiste, lo crea. +3. Al nodo, esistente o non, vengono impostate le restanti proprietà, lette dal CSV e convertite nel tipo di Neo4J che le rappresenta più accuratamente. + +``` +WITH line +MERGE (crate:Crate { id: toInteger(line.id) }) +SET + crate.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + crate.updated_at = apoc.date.parse(line.updated_at, "ms", "yyyy-MM-dd HH:mm:ss"), + crate.max_upload_size = toInteger(line.max_upload_size), + crate.downloads = toInteger(line.downloads), + crate.description = CASE trim(line.description) + WHEN "" + THEN null + ELSE + line.description + END, + crate.documentation = CASE trim(line.documentation) + WHEN "" + THEN null + ELSE + line.documentation + END, + crate.homepage = CASE trim(line.homepage) + WHEN "" + THEN null + ELSE + line.homepage + END, + crate.name = CASE trim(line.name) + WHEN "" + THEN null + ELSE + line.name + END, + crate.readme = CASE trim(line.readme) + WHEN "" + THEN null + ELSE + line.readme + END, + crate.repository = CASE trim(line.repository) + WHEN "" + THEN null + ELSE + line.repository + END +``` + +#### `2` - Creazione dei nodi `:Keyword` + +Come menzionato in precedenza, ciascuna crate può specificare fino a 5 keyword al fine di essere trovata più facilmente nell'indice di Crates.io. + +![Screenshot delle keyword su Crates.io](media/cratesio-keywords.png) + +Si identificano queste keyword come nodi, a cui si assegna il label `:Keyword`. + +Si ripete la stessa procedura di prima per creare un nodo `:Keyword` per ciascuna keyword esistente nel file `keywords.csv`. + +##### Proprietà + +I nodi `:Keyword` avranno come proprietà: + +- `id`: l'id univoco della keyword su Crates.io +- `name`: il nome della keyword +- `creation_date`: la data in cui la keyword è comparsa per la prima volta su Crates.io + +##### Indici + +Si ripete la stessa procedura di prima per creare alcuni indici relativi ai nodi `:Keyword`. + +Query e indici identificati sono: + +- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_keyword_id IF NOT EXISTS + FOR (keyword:Keyword) + ON (keyword.id); + ``` + +- _ricerca **per nome**_: si crea un indice `TEXT` sul campo `name`. + + ```cypher + CREATE TEXT INDEX index_keyword_name IF NOT EXISTS + FOR (keyword:Keyword) + ON (keyword.name); + ``` + +##### Query + +Come prima, si crea una query per inizializzazione o aggiornamento, che, per ogni riga del file `keywords.csv`, applica il seguente algoritmo: + +1. Prova a vedere se esiste già un nodo `:Keyword` con il dato `id`. +2. Se non esiste, lo crea. +3. Al nodo, esistente o non, vengono impostate le restanti proprietà, lette dal CSV e convertite nel tipo di Neo4J che le rappresenta più accuratamente. + +```cypher +LOAD CSV WITH HEADERS FROM "file:///keywords.csv" AS line FIELDTERMINATOR "," +MERGE (keyword:Keyword { id: toInteger(line.id) }) +SET + keyword.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + keyword.name = line.keyword; +``` + +#### `3` - Creazione delle relazioni `:IS_TAGGED_WITH` + +Il file `crates_keywords.csv` associa crate alle loro keyword specificando un'associazione tra id di una crate e id di una keyword in ogni riga. + +Si costruisce a partire da esso la relazione `:IS_TAGGED_WITH`, diretta dalla `:Crate` alla `:Keyword`. + +##### Query + +Quando viene caricata su Crates.io una nuova versione di una crate, le sue keywords possono essere cambiate. + +Per permettere alla query di funzionare sia per l'inizializzazione del grafo, sia per il suo aggiornamento, si effettuano due passi: + +1. Tutte le relazioni `:IS_TAGGED_WITH` sono rimosse. + + ```cypher + MATCH (:Crate)-[relation:IS_TAGGED_WITH]->(:Keyword) + DELETE relation; + ``` + +2. Il file `crates_keywords.csv` viene aperto, e per ciascuna riga viene creata una nuova relazione `:IS_TAGGED_WITH`. + + ``` + LOAD CSV WITH HEADERS FROM "file:///crates_keywords.csv" AS line FIELDTERMINATOR "," + MATCH + (crate:Crate {id: toInteger(line.crate_id)}), + (keyword:Keyword {id: toInteger(line.keyword_id)}) + CREATE (crate)-[:IS_TAGGED_WITH]->(keyword); + ``` + +#### `4` - Creazione dei nodi `:Category` + +Oltre alle keyword, ciascuna crate può scegliere di appartenere a fino a cinque categorie di un thesaurus gerarchico definito dagli amministratori di Crates.io. + +Il file `categories.csv` contiene tutte le categorie esistenti al momento dell'esportazione dei dati da Crates.io. + +Si identificano le categorie come nodi aventi il label `:Category`, la cui gerarchia è rappresentata da relazioni `:CONTAINS`. + +##### Struttura + +I nodi `:Category` avranno le seguenti proprietà, importate dal file CSV: + +- `id`: identificatore univoco della categoria; +- `name`: nome breve in inglese della categoria; +- `slug`: nome breve url-encoded della categoria, mostrato nell'indirizzo web; +- `path`: percorso di identificatori separati da punti che determina dove si trova la categoria nella gerarchia; +- `created_at`: data di creazione della categoria. + +Per facilitare la navigazione del grafo, si aggiunge inoltre la seguente proprietà: + +- `leaf`: l'ultimo identificatore del `path`. + +##### Indici + +Si ipotizzano le seguenti query e indici per i nodi `:Category`: + +- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_category_id IF NOT EXISTS + FOR (category:Category) + ON (category.id); + ``` + +- _ricerca **per nome**_: si crea un indice `TEXT` sul campo `name`. + + ```cypher + CREATE TEXT INDEX index_category_name IF NOT EXISTS + FOR (category:Category) + ON (category.name); + ``` + +- _ricerca **per slug**_: si crea un indice `TEXT` sul campo `slug`. + + ```cypher + CREATE TEXT INDEX index_category_slug IF NOT EXISTS + FOR (category:Category) + ON (category.slug); + ``` + +- _ricerca **per leaf**_: si crea un indice `TEXT` sul campo `leaf`. + + ```cypher + CREATE TEXT INDEX index_category_leaf IF NOT EXISTS + FOR (category:Category) + ON (category.leaf); + ``` + +##### Query + +Essendo le categorie variabili, proprio come le keywords, per permettere l'aggiornamento del database con nuovi dati, inizialmente si eliminano tutti i nodi `:Category`, per poi ricrearli successivamente. + +```cypher +MATCH (category:Category) +DETACH DELETE category; +``` + +Ci si è accorti che all'interno del file CSV il nodo "root" (origine) della gerarchia è assente, quindi lo si crea prima di procedere con l'importazione vera e propria dei dati: + +```cypher +CREATE ( + :Category { + name: "Root", + created_at: datetime(), + description: "Root category. Does not contain any category by itself.", + id: 0, + path: "root", + slug: "root" + } +); +``` + +Si usa in seguito `LOAD CSV WITH HEADERS` per creare nodi `:Category` dal file `categories.csv`, lasciando non specificato il campo `leaf`: + +```cypher +LOAD CSV WITH HEADERS FROM "file:///categories.csv" AS line +CREATE ( + :Category { + name: line.category, + created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + description: line.description, + id: toInteger(line.id), + path: line.path, + slug: line.slug + } +); +``` + +Per ciascun nodo `:Category`, si prende poi il campo `path` e lo si divide in un array di stringhe alle occorrenze del carattere `"."`, utilizzando successivamente la sintassi `[-1]` per accedere all'ultimo elemento, che viene impostato come valore del campo `leaf`: + +```cypher +MATCH (c:Category) +WITH c, split(c.path, ".") AS path +SET c.leaf = path[-1]; +``` + +Ancora, si effettua una seconda scansione su tutti i nodi `:Category`, splittando di nuovo il `path`, ma stavolta prendendo il penultimo (`[-2]`) elemento dell'array per determinare il nodo `:Category` superiore nella gerarchia e poi creando una relazione `:CONTAINS` tra i due nodi: + +```cypher +MATCH (c:Category) +WITH c, split(c.path, ".") AS path +MATCH (d:Category {leaf: path[-2]}) +CREATE (d)-[:CONTAINS]->(c); +``` + +#### `5` - Creazione delle relazioni `:CONTAINS` + +Il file `crates_categories.csv`, esattamente come `crates_keywords.csv`, contiene una coppia di id per riga, uno di una `:Crate` e uno di una `:Category`. + +Si effettua quindi lo stesso esatto procedimento per creare relazioni `:CONTAINS` da `:Category` a `:Crate`. + +```cypher +MATCH (:Category)-[relation:CONTAINS]->(:Crate) +DELETE relation; + +LOAD CSV WITH HEADERS FROM "file:///crates_categories.csv" AS line FIELDTERMINATOR "," +MATCH + (crate:Crate {id: toInteger(line.crate_id)}), + (category:Category {id: toInteger(line.category_id)}) +CREATE (category)-[:CONTAINS]->(crate); +``` + +#### `6` - Creazione dei nodi `:User` + +Crates.io richiede di effettuare il login con account GitHub a tutti coloro che desiderano uploadare una crate. + +Al primo login, viene creata un'entità nel database di Crates.io, che riassume i dettagli dell'account GitHub e gli associa un id interno. + +Si è deciso di inserire nel grafo gli utenti come nodi aventi il label `:User`. + +##### Proprietà + +Si definiscono le seguenti proprietà per i nodi `:User`: + +- `id`: L'id univoco interno assegnato da Crates.io +- `gh_id`: L'id univoco esterno di GitHub +- `avatar`: L'avatar dell'utente su GitHub +- `name`: L'username dell'utente su GitHub +- `full_name`: Nome e cognome dell'utente su GitHub + +##### Indici + +Si ipotizzano le seguenti query per nodi `:User` e i relativi indici: + +- _ricerca per **id di Crates.io**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_user_id IF NOT EXISTS + FOR (user:User) + ON (user.id); + ``` + +- _ricerca per **id di GitHub**_: si crea un indice `RANGE` sul campo `gh_id`. + + ```cypher + CREATE RANGE INDEX index_user_ghid IF NOT EXISTS + FOR (user:User) + ON (user.gh_id); + ``` + +- _ricerca per **username di GitHub**_: si crea un indice `TEXT` sul campo `name`. + + ```cypher + CREATE TEXT INDEX index_user_name IF NOT EXISTS + FOR (user:User) + ON (user.name); + ``` + +- _ricerca per **nome proprio, cognome, o parti di essi**_: si crea un indice `TEXT` sul campo `full_name`. + + ```cypher + CREATE TEXT INDEX index_user_fullname IF NOT EXISTS + FOR (user:User) + ON (user.full_name); + ``` + +##### Query + +Si realizza una query che effettua il merge degli utenti importati in base al loro `id`, e poi ne aggiorna i dati: + +```cypher +LOAD CSV WITH HEADERS FROM "file:///users.csv" AS line FIELDTERMINATOR "," +MERGE (user:User { id: toInteger(line.id) }) +SET + user.avatar = line.gh_avatar, + user.gh_id = toInteger(line.gh_id), + user.name = line.gh_login, + user.full_name = line.name; +``` + +#### `7` - Creazione delle relazioni `:OWNS` + +L'utente che carica per la prima volta una crate con un dato nome ne è detto il *proprietario*. + +Egli, in qualsiasi momento, può aggiungere altri proprietari dall'interfaccia web di Crates.io. + +I proprietari di una crate sono gli unici utenti che possono caricarne nuove versioni. + +Si identifica la proprietà come una relazione tra `:Crate` e `:User` etichettata `:OWNS`. + +Le relazioni di proprietà, assieme ad alcune informazioni ad esse collegate, sono contenute nel file `crate_owners.csv` del dataset. + +##### Proprietà + +Si associano le seguenti proprietà alle relazioni `:OWNS`: + +- `created_at`: data in cui l'utente è stato aggiunto come proprietario alla crate +- `created_by`: id di Crates.io dell'utente che ha aggiunto l'utente come proprietario +- `owner_kind`: sconosciuto, è presente nel dataset ma per ora è sempre impostato a `1` + +##### Query + +Essendo le relazioni di proprietà variabili (un proprietario "secondario" potrebbe essere rimosso dal proprietario "originale"), tutte le relazioni `:OWNS` già esistenti vengono prima eliminate e poi ricreate con i dati aggiornati. + +```cypher +MATCH (:User)-[owns:OWNS]->(:Crate) +DELETE owns; + +LOAD CSV WITH HEADERS FROM "file:///crate_owners.csv" AS line FIELDTERMINATOR "," +MATCH (crate:Crate { id: toInteger(line.crate_id) }) +MATCH (owner:User { id: toInteger(line.owner_id) }) +CREATE (owner)-[ownership:OWNS { + created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + created_by: toInteger(line.created_by), + owner_kind: toInteger(line.owner_kind) +}]->(crate); +``` + +#### `8` - Creazione dei nodi `:Version` e delle relazioni `:HAS_VERSION` e `:PUBLISHED` + +Ogni upload di una crate a Crates.io contiene una stringa nel formato `X.Y.Z` detta *versione*, che, assieme al nome della crate, determina una tupla in grado di identificarlo. + +Il dataset contiene informazioni sulle versioni nel file `versions.csv`. + +Si decide di rappresentare le versioni attraverso nodi etichettati `:Version`, le crate a cui si riferiscono con relazioni `(:Crate)-[:HAS_VERSION]->(:Version)`, e gli utenti che le hanno pubblicate con relazioni `(:User)-[:PUBLISHED]->(:Version)`. + +##### Proprietà + +Si definiscono le seguenti proprietà per i nodi `:Version`: + +- `id`: identificatore numerico incrementale assegnato da Crates.io +- `name`: [nome semantico] della versione, come `v1.2.3` +- `checksum`: hash SHA1 del commit che aggiunge la versione all'indice +- `size`: dimensione in bytes della versione +- `created_at`: data di upload della versione +- `downloads`: numero di download della versione +- `license`: stringa SPDX rappresentante le licenze open source sotto cui la versione è resa disponibile +- `features`: blob JSON contenente metadati sui tag assegnati alle dipendenze opzionali della crate, detti "feature" +- `links`: blob JSON di collegamenti aggiuntivi specificati nei metadati della crate +- `is_yanked`: se il download della versione è stato sconsigliato da un proprietario + +Non si definiscono invece proprietà per le relazioni `:HAS_VERSION` e `:PUBLISHED`. + + + +##### Indici + +Query ipotetiche e indici creati per nodi `:Version` sono: + +- _ricerca **per checksum**_: si crea un indice `LOOKUP` sul campo `checksum`. + + ```cypher + CREATE LOOKUP INDEX index_version_checksum IF NOT EXISTS + FOR (version:Version) + ON (version.checksum); + ``` + +- _ricerca di versioni **più grandi o più piccole di un certo numero di bytes**_: si crea un indice `RANGE` sul campo `size`. + + ```cypher + CREATE RANGE INDEX index_version_size IF NOT EXISTS + FOR (version:Version) + ON (version.size); + ``` + +- _ricerca di versioni rilasciate **prima, durante, o dopo una certa data**_: si crea un indice `RANGE` sul campo `created_at`. + + ```cypher + CREATE RANGE INDEX index_version_created_at IF NOT EXISTS + FOR (version:Version) + ON (version.created_at); + ``` + +- _ricerca di versioni con **almeno un certo numero di download**_: si crea un indice `RANGE` sul campo `downloads`. + + ```cypher + CREATE RANGE INDEX index_version_downloads IF NOT EXISTS + FOR (version:Version) + ON (version.downloads); + ``` + +- _ricerca **per id**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_version_id IF NOT EXISTS + FOR (version:Version) + ON (version.id); + ``` + +- _ricerca per **versione** o **range di versioni**_: si crea un indice `TEXT` sul campo `name`, in quanto esso permette la ricerca di prefissi, suffissi, e sottostringhe, che riassumono efficientemente buona parte delle query effettuabili sul campo + + ```cypher + CREATE TEXT INDEX index_version_name IF NOT EXISTS + FOR (version:Version) + ON (version.name); + ``` + +##### Query + +Essendo le versioni di una crate immutabili, si utilizza lo stesso procedimento utilizzato per i nodi `:Crate` per inizializzazione e aggiornamento del database, ovvero di effettuare un `MERGE` sull'`id` e di impostare sul nodo trovato o creato le varie proprietà aggiornate. + +Nella stessa query si individua la crate a cui appartiene la versione, e si crea una relazione `HAS_VERSION` tra i due nodi; allo stesso modo, si individua l'utente che ha pubblicato la versione, e si crea con esso una relazione `:PUBLISHED`. + +```cypher +LOAD CSV WITH HEADERS FROM "file:///versions.csv" AS line FIELDTERMINATOR "," +CALL { + WITH line + MERGE (version:Version { id: toInteger(line.id) } ) + SET + version.checksum = line.checksum, + version.size = toInteger(line.crate_size), + version.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + version.downloads = toInteger(line.downloads), + version.license = line.license, + version.features = line.features, + version.links = line.links, + version.name = line.num, + version.is_yanked = CASE line.yanked + WHEN "t" + THEN true + ELSE + false + END + WITH line, version + MATCH (crate:Crate { id: toInteger(line.crate_id) }) + MERGE (crate)-[:HAS_VERSION]->(version) + WITH line, version + MATCH (user:User { id: toInteger(line.published_by) }) + MERGE (user)-[:PUBLISHED]->(version) +} IN TRANSACTIONS OF 10000 ROWS; +``` + +#### `9` - Creazione delle relazioni `:DEPENDS_ON` + +Ciascuna versione di una crate deve specificare quali altre versioni di crate richiede per una corretta compilazione. + +Le versioni necessarie sono specificate come [un range di versioni accettabili di una data crate in forma di stringa](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html), e sono dette *dipendenze* della versione specificante, contenute nel file `dependencies.csv` del dataset. + +Alcuni esempi delle precedenti stringhe sono: + +```toml +[dependencies] +rand = "*" # Qualsiasi versione della crate rand è accettabile +serde = "^1.0.0" # Qualsiasi versione della crate serde tra 1.0.0 (inclusa) e 2.0.0 (esclusa) è accettabile +quote = "~1.0.0" # Qualsiasi versione della crate quote tra 1.0.0 (inclusa) e 1.1.0 (esclusa) è accettabile +itoa = "=1.0.0" # Solo la versione 1.0.0 della crate itoa è accettabile +``` + +Si decide di rappresentare le dipendenze come relazioni etichettate come `:DEPENDS_ON`. + +##### Proprietà + +Si stabiliscono le seguenti proprietà per le relazioni di dipendenza: + +- `id`: id univoco che identifica la relazione +- `requirement`: stringa determinante il range di versioni accettabili +- `target`: le piattaforme di compilazione (x86, arm, riscv) in cui è richiesta la dipendenza +- `is_optional`: se la dipendenza è facoltativa, ovvero se si può usare la crate senza di essa (ma con funzionalità ridotte) +- `is_default`: se la dipendenza è facoltativa, ma viene installata di default se non viene esclusa esplicitamente +- `features`: blob JSON riguardo le feature di cui la dipendenza fa parte +- `explicit_name`: se presente, il nome a cui deve essere rinominata la dipendenza durante la compilazione della crate + +##### Indici + +Si ipotizzano le seguenti query e si creano i relativi indici per processarle: + +- _ricerca per **id**_: si crea un indice `RANGE` sul campo `id`. + + ```cypher + CREATE RANGE INDEX index_dependency_id IF NOT EXISTS + FOR ()-[dependency:DEPENDS_ON]->() + ON (dependency.id); + ``` + +- _ricerca per **versione** o **range di versioni**_: si crea un indice `TEXT` sul campo `requirement`, con lo stesso scopo dell'indice per la ricerca per nome semantico dei nodi `:Version`. + + ```cypher + CREATE TEXT INDEX index_dependency_requirement IF NOT EXISTS + FOR ()-[dependency:DEPENDS_ON]->() + ON (dependency.requirement); + ``` + +- _ricerca per **nome esplicito**_: si crea un indice `TEXT` sul campo `explicit_name`. + + ```cypher + CREATE TEXT INDEX index_dependency_explicit_name IF NOT EXISTS + FOR ()-[dependency:DEPENDS_ON]->() + ON (dependency.explicit_name); + ``` + +##### Query + +```cypher +LOAD CSV WITH HEADERS FROM "file:///dependencies.csv" AS line FIELDTERMINATOR "," +CALL { + WITH line + MATCH + (version:Version { id: toInteger(line.version_id) }), + (requirement:Crate { id: toInteger(line.crate_id) }) + MERGE (version)-[dependency:DEPENDS_ON]->(requirement) + SET + dependency.id = line.id, + dependency.is_optional = CASE line.optional + WHEN "t" + THEN true + ELSE + false + END, + dependency.is_default = CASE line.default_features + WHEN "t" + THEN true + ELSE + false + END, + dependency.explicit_name = line.explicit_name, + dependency.features = line.features, + dependency.requirement = line.req, + dependency.target = line.target +} IN TRANSACTIONS OF 10000 ROWS; +``` + +## Alcune interrogazioni interessanti + +Per dimostrare le possibilità offerte dal grafo, si presentano alcune query effettuabili ritenute particolarmente interessanti. + +Le risposte incluse si riferiscono al dataset del 19 Febbraio 2023, non allegato a questa relazione per motivi di dimensioni. + +### Numero totale di crate + +> "Quante crate sono state uploadate a Crates.io, in totale?" + +Si propone la seguente query, che conta il numero di nodi `:Crate`, per rispondere alla domanda: + +```cypher +MATCH (crate:Crate) +RETURN count(crate); +``` + +```text +105287 +``` + +### Numero totale di download di tutte le crate su tutte le versioni + +> "Quante versioni di crate sono state scaricate su Crates.io?" + +Si propone la seguente query, che somma i valori del campo `download` di tutti i nodi `:Version`, per rispondere alla domanda: + +```cypher +MATCH (version:Version) +RETURN sum(version.downloads); +``` + +```text +27475924849 +``` + +### Albero delle categorie + +> "Come è organizzata la gerarchia di categorie di Crates.io?" + +Si propone la seguente query, che recupera tutti i nodi `:Category` e le loro relazioni `:CONTAINS`, per rispondere alla domanda: + +```cypher +MATCH (supercategory:Category)-[conts:CONTAINS*]->(category:Category) +RETURN supercategory, conts, category; +``` + +![Grafo con 88 nodi e 151 relazioni. Un nodo centrale da cui escono tanti altri nodi, da cui a loro volta talvolta escono altri nodi.](media/query-categorygraph-solution.svg) + +### Informazioni relative a una specifica crate + +> "Voglio sapere tutto il possibile sulla [crate `rand`]." + +Si propone la seguente query, che recupera il singolo nodo `:Crate` avente `"rand"` come `name`, per rispondere alla domanda: + +```cypher +MATCH (crate:Crate {name: "rand"}) +RETURN crate; +``` + +In alternativa, si propone la seguente query, che recupera la `:Crate` `rand` e tutti i nodi a cui essa si connette direttamente, e le connessioni: + +```cypher +MATCH (crate:Crate {name: "rand"})-[rel]->(other) +RETURN crate, rel, other; +``` + +![Grafo con 71 nodi e 70 relazioni. Un nodo centrale "rand" da cui escono tanti altri nodi `:Version`, e alcuni altri nodi `:Keyword`, rappresentanti "rng" e "random".](media/query-randfull-solution.svg) + +### Dipendenze di `serde` il cui nome contiene a loro volta con `serde` + +> "Esistono delle crate, che dipendono direttamente dalla [crate `serde`], che hanno a loro volta `serde` nel nome? +> +> (Potrebbero essere ad esempio delle estensioni di qualche tipo.) + +Si propone la seguente query, che trova tutte le versioni che dipendono da `serde` e poi recupera la crate a cui appartengono, per verificare: + +```cypher +MATCH (dependency:Crate)-[vr:HAS_VERSION]->(version:Version)-[dr:DEPENDS_ON]->(serde:Crate {name: "serde"}) +WHERE dependency.name CONTAINS "serde" +RETURN dependency, vr, version, dr, serde; +``` + +![Visualizzazione dell'execution plan della precedente query.](media/query-serdedeps-plan.svg) + +![Grafo parziale con 300 nodi e 671 relazioni. Tanti nodi centrali interconnettono il nodo `serde` a tanti nodi crate all'esterno del grafo.](media/query-serdedeps-solution.svg) + +### Crate dipendenti da loro stesse + +> "Esistono delle crate che dipendono da loro stesse?" + +Si propone la seguente query, con funzionamento molto simile alla precedente, per verificare: + +```cypher +MATCH (a:Crate)-[rhv:HAS_VERSION]->(v:Version)-[rdo:DEPENDS_ON]->(b:Crate) +WHERE a = b +RETURN a, rhv, v, rdo, b +ORDER BY v.downloads DESC; +``` + +> (Approfondendo la ricerca, si è scoperto che [è una tecnica usata per migliorare la compatibilità delle crate](https://github.com/dtolnay/semver-trick)!) + +![Grafo parziale con 300 nodi e 633 relazioni. Tanti nodi sono sparsi per il grafo, e spesso hanno doppi collegamenti.](media/query-selfdeps-solution.svg) + +### Le crate di un utente e tutti i loro dettagli + +> "Quali crate ha rilasciato l'utente Steffo99, e come sono collegate tra loro? + +Si propone la seguente query, che recupera tutte le informazioni interessanti riguardo l'utente specificato, per rispondere alla domanda: + +```cypher +MATCH (user:User {name: "Steffo99"})-[owns:OWNS]->(crate:Crate) +MATCH (crate:Crate)-[is_tagged_with:IS_TAGGED_WITH]->(keyword:Keyword) +MATCH (crate:Crate)-[has_version:HAS_VERSION]->(version:Version) +MATCH (crate:Crate)<-[contains:CONTAINS]-(category:Category) +MATCH (category:Category)<-[supercontains:CONTAINS]-(supercategory:Category) +RETURN user, owns, crate, is_tagged_with, keyword, has_version, version, contains, category, supercontains, supercategory; +``` + +![Grafo con 57 nodi e 870 relazioni. Un nodo rosso centrale "Steffo99" è connesso a tanti altri nodi.](media/query-selfdeps-solution.svg) + + + + +[Crates.io]: https://crates.io +[Rust]: https://www.rust-lang.org +[crate]: https://doc.rust-lang.org/book/ch07-01-packages-and-crates.html +[versione semantica]: https://semver.org +[identificatore SPDX]: https://spdx.org/licenses/ +[Cargo]: https://doc.rust-lang.org/cargo/ +[elenco delle categorie previste]: https://github.com/rust-lang/crates.io/blob/master/src/boot/categories.toml +[open data]: https://crates.io/data-access +[`apoc.date.parse`]: https://neo4j.com/labs/apoc/4.3/overview/apoc.date/apoc.date.parse/ +[`datetime`]: https://neo4j.com/docs/cypher-manual/5/syntax/temporal/ +[dump giornaliero del database]: https://static.crates.io/db-dump.tar.gz +[nome semantico]: https://semver.org/ +[crate `rand`]: https://crates.io/crates/rand +[crate `serde`]: https://crates.io/crates/serde diff --git a/data/neo4j b/data/neo4j new file mode 120000 index 0000000..d32e8af --- /dev/null +++ b/data/neo4j @@ -0,0 +1 @@ +/home/steffo/.config/Neo4j Desktop/Application/relate-data/dbmss/dbms-de822d9c-8f2e-4f04-9646-ac8f2fb719c6/ \ No newline at end of file diff --git a/media/cratesio-categories.png b/media/cratesio-categories.png new file mode 100644 index 0000000000000000000000000000000000000000..6df69acba1a24fd74d211838ff03609416ca981c GIT binary patch literal 3751 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 NvA&reLC5~Ze*lV4Qs)2w literal 0 HcmV?d00001 diff --git a/media/cratesio-keywords.png b/media/cratesio-keywords.png new file mode 100644 index 0000000000000000000000000000000000000000..d13f0b5f3a810ec7fe8547dcc7301d8d48c70c4b GIT binary patch literal 29455 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

%e=$VhfIvbKkBpUj{Ub5Lco<@Wt<#y~F;9Je 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�`T*x6l$Qi*-t$ObIP2YE^{i;S!)&k7OD1|=E!@-GhK`PBL%jmr9Z<%6dTf>3c`$nq%=d<6%yWOj ztg*%ES&T8M#}adfl~)@{(%$8KFP}pLtd03kQaw4bODwR0&pglKaaqdbzi>jOujEFA zon)2+_eT57Mls7mnDqC2Z9on0*3gD*gVBf8GMWy$qA(V{R5s)AvoE8WSEDOA@Qouo+r# z-@%KG5S@raU`Sw;t(SGB7_N7Fs|HOTnzGnr5K0O*p#ZLoCLoE#jAElclPldg(AsOk zG9dN-$veg0@FU(G-|;^vPY=|g-{**@ZsUjm(*&vNS}`2jxJ6b<|IFO?V2Zc0*MBNw z-B-}{1OYAXP^qAwCEk2{CCp?BNVXF~4WF-{aLsjU%>hOOez3}w3n$rM;k)c7%|orZ z+0H>uG8<~@^RU<@U)}@lllvYXV<1BV6F}VdT-ylGd#`4J&=J%7@f;0mrRwgLSY7{S z&*ek8O5XN@4K6UX<;u6R{P%g!M6Mh|R~xnR#~klpnIC->?X!v8y$RG#3Rs z+W>+5{n5>hP1A!*bA4^7Jgcr}yrWU`(h0z#+NJ0W-LyACeEMZ)W!Q5*$)i0PcS-4;&FQYKTDo#S ziGd9M^L_nd8-{}O%UbzOpOA-`*6QsV6Q-{~FCJA_1r>?@>11y!JwNJ&w-ki6nM3X) zFEW=P*lMktTbAIzvhvq;e$?Ood(0E+^OWJ)hI;@1vJ;7P)=dGsPj>$kJ$)p9PLsbb zwSM#5REc|pPu#n2Wn5&h)2yG#lg}CPKHyDNk{HFJsacy;jVez4|7q-#ZXUZVYQYS- zn_yp4%70PV$9>@;EtdZ&(kNeo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONT…CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCON…CONTAINSCONT…CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCON…CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCON…CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCON…CONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINSCONTAINS Root Finance Science Science:… Asynchr… Aerospa… Aerospa… Network progra… Aerospa… No standard libra… No standard libra… Aerospa… Develop… Develop… Science:… Emulators Cryptogr… Cryptogr… Develop… Parser imple… Science:… Develop… External bi… Mathem… Develop… Games Aerospa… Aerospa… Develop… GUI Visualiza… Email Simulation Virtualiz… Authenti… WebAss… Text proce… Multime… Multime… Multime… Data struct… Comma… Hardware support Web progra… Web progra… Graphics Embedd… Develop… Game develo… Database imple… Game engines Configur… Encoding Filesyst… Command line utilit Multime… Localizat… Memory mana… Multime… Operating systems Operating syste… Internati… Compre… Computer vision Develop… Date and time Database interfa… Caching Concurr… Accessi… Compilers Operating syste… Parsing tools Operating syste… Rendering Renderi… Renderi… Rust patterns Renderi… Text editors API bindin… Algorith… Template engine Value format… Web progra… Operating syste… Web progra… Operating syste… \ No newline at end of file diff --git a/media/query-randfull-solution.svg b/media/query-randfull-solution.svg new file mode 100644 index 0000000..9797375 --- /dev/null +++ b/media/query-randfull-solution.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)IS_TAG…IS_TAGGED_WITHHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_…HAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_…HAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_…HAS_VERSIONHAS_VERSIONHAS_VERSIONH…HAS_VERSIONHAS_VERSIONHA…HAS_VERSIONHAS_VERSIONHAS_VERSIONDEPENDS_OND…DEPENDS_ON rand 112 687 0.3.17 0.4.1 0.7.0 0.3.16 0.8.4 0.8.5 0.6.2 0.6.3 0.3.12 0.1.2 0.5.2 0.3.23 0.3.9 0.6.5 0.3.14 0.5.6 0.3.0 0.2.0 0.6.4 0.8.3 0.3.18 0.7.3 0.8.2 0.3.11 0.4.0-pr… 0.3.4 0.6.0-pr… 0.5.5 0.5.3 0.3.3 0.3.2 0.3.1 0.5.0-pr… 0.3.8 0.7.0-pr… 0.8.1 0.7.0-pr… 0.3.10 0.1.1 0.1.3 0.4.4 0.3.20 0.3.15 0.5.0-pr… 0.4.6 0.3.7 0.6.1 0.5.0-pr… 0.6.0-pr… 0.8.0 0.3.13 0.5.0 0.1.4 0.5.1 0.7.1 0.3.19 0.7.2 0.4.2 0.4.3 0.5.4 0.3.5 0.3.22 0.6.0 0.2.1 0.4.5 0.3.6 0.3.21-p… 0.7.0-pr… \ No newline at end of file diff --git a/media/query-selfdeps-solution.svg b/media/query-selfdeps-solution.svg new file mode 100644 index 0000000..454b47d --- /dev/null +++ b/media/query-selfdeps-solution.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSI…DEPENDS_…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VER…DEPENDS…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_V…DEPEN…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERS…DEPENDS_…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERS…DEPENDS_…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_V…DEPEN…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VE…DEPEND…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSI…DEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_…DEPE…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_V…DEPE…HAS_VE…DEPEN…HAS_VERSIONDEPENDS_ONHAS_…DEPE…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDE…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPE…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPE…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ON rand_core 0.3.1 log 0.3.9 num-traits 0.1.43 rand 0.3.23 autocfg 0.1.8 nb 0.1.3 0.3.22 0.2.2 firestorm 0.5.0 0.5.1 rstest 0.12.0 rusttype 0.7.9 0.4.6 raw-win… 0.3.4 0.15.0 trackable 0.2.24 ed25519… 2.2.0 cortex-m 0.6.7 3.0.0 0.16.0 0.11.0 3.1.0 0.6.4 tinystr 0.7.0 0.10.0 nats 0.17.0 fragile 1.2.2 0.23.1 0.13.0 0.18.1 stb_truet… 0.2.8 0.5.11 k9 0.11.1 glyph_br… 0.5.4 0.5.10 ebur128 0.1.6 0.23.0 0.7.0 0.11.5 0.20.1 0.6.0 0.14.0 0.7.1 0.11.6 0.9.0 0.1.7 0.22.0 0.19.1 0.6.5 mint 0.4.9 0.11.0 grenad 0.4.4 0.21.0 0.6.2 2.1.2 0.10.3 critical-s… 0.2.8 0.1.5 0.4.2 cortex-m… 0.5.7 0.6.3 0.4.3 0.19.0 bitter 0.4.0 fastly-sh… 0.5.1 clicolors-… 0.3.2 stable-h… 0.4.1 0.20.0 win32_fil… 0.2.1 embedd… 0.1.3 rustrict 0.5.5 tzdb 0.3.11 0.5.0 0.8.0 curl-sys 0.3.16 oauth1-r… 0.5.1 idem 0.1.1 0.5.4 0.3.2 0.5.1 0.18.0 gpl-gove… 2.1.2 plotters-i… 0.3.3 0.3.2 2.1.1 0.5.2 0.6.5 0.6.2 0.5.0 0.5.10 0.4.1 0.3.21-p… 2.1.0 oauth-cr… 0.1.2 0.5.9 0.10.0 0.3.3 2.0.0 0.24.0 0.6.1 nats-aflo… 0.16.103 0.4.1 0.6.6 digest-w… 0.2.0 0.5.11 0.4.3 0.4.0 0.2.6 0.4.5 nine 0.1.2 0.3.1 vergen 2.0.2 0.5.12 2.0.1 2.0.3 null-term… 0.2.11 0.5.6 0.5.5 0.6.5 unix 0.5.5 0.4.2 bitcoin 0.18.1 0.5.8 0.4.1 0.4.4 0.6.5-alp… 0.4.2 0.4.3 conjecture 0.3.0 semver-t… 0.2.1 hotg-run… 0.9.4 0.4.2 garage_… 0.7.0 0.2.1 gccjit 0.0.1 physical-… 0.0.4 downwa… 0.0.2 0.5.13 json_co… 0.1.3 rust-aws… 0.2.0 0.0.3 0.2.0 0.5.6 print_byt… 0.6.2 0.16.104 aerosol 0.1.3 0.10.4 0.3.9 0.3.14 0.5.9 0.0.5 0.2.4 0.2.5 toql 0.1.8 0.1.6 0.3.0 0.1.7 0.1.4 0.1.3 0.3.13 0.1.5 0.1.4 0.2.3 0.1.2 naja_as… 0.1.2 0.1.1 0.5.5 0.4.1 carol-test 1.1.2 0.6.1 0.4.18 0.4.7 0.4.20 hello_ex… 0.3.4 0.3.5 0.3.6 0.2.2 spl-gove… 2.1.3 seven_s… 0.1.1 0.4.9 fake 2.2.1 pone 0.1.1 0.10.1 0.10.2 0.11.4 aa9384c… 0.0.2 0.3.1 0.4.13 0.7.1 0.1.3 0.4.0 0.6.0 garage_… 0.3.0 0.5.3 0.5.4 docima 0.9.1 af23cfa4… 0.0.1 hotg-run… 0.9.4 0.1.2 0.4.1 0.3.8 0.2.7 0.4.0 0.3.0 2.1.1 0.8.1 0.3.5 tokay 0.5.1 0.4.16 0.5.3 0.4.3 new_pro… 0.1.3 0.3.12 0.2.14 compon… 1.4.0 dilib 0.2.1 0.16.102 evtx 0.6.7 0.2.2 0.3.7 0.4.6 0.3.6 0.2.4 0.3.10 0.2.13 0.3.11 \ No newline at end of file diff --git a/media/query-serdedeps-plan.svg b/media/query-serdedeps-plan.svg new file mode 100644 index 0000000..9aa3417 --- /dev/null +++ b/media/query-serdedeps-plan.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)estimated row54 estimated rows54 estimated rows54 estimated rowsestimated rowsestimated rowsResultProduceResults@neo4jdependency, serde, version, dr, vrdependency, vr, version, dr, serdeFilter@neo4jdependency, serde, version, dr, vrdependency.name CONTAINS $autostring_1 AND dependency:CrateExpand(All)@neo4jdependency, serde, version, dr, vr(version)-[vr:HAS_VERSION]-(dependency)Filter@neo4jserde, dr, versionversion:VersionExpand(All)@neo4jserde, dr, version(serde)<-[dr:DEPENDS_ON]-(version)NodeIndexSeek@neo4jserdeTEXT INDEX serde:Crate(name) WHERE name = $autostring_0 \ No newline at end of file diff --git a/media/query-serdedeps-solution.svg b/media/query-serdedeps-solution.svg new file mode 100644 index 0000000..331e0e1 --- /dev/null +++ b/media/query-serdedeps-solution.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEP…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_…DEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPE…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_…DEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPE…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPE…HAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONHAS_VERSIONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPEN…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_OND…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEP…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_OND…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDE…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEP…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDE…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEP…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPEN…DEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ONDEPENDS_ON serde_js… 0.3.1 serde 0.3.0 serde-bi… 0.5.0 serde_fl… 0.1.2 serde_e… 0.1.1 0.1.0 serde2file 0.6.2 serde_e… 0.2.0 serde_db 0.11.1 0.6.0 0.6.1 0.2.0 serde-lw… 0.0.2 0.0.1 serde_ar… 0.6.0-rc.0 serde_pr… 0.2.1 0.1.0 serde-inl… 0.1.1 0.1.3 0.1.0 0.1.1 0.1.4 serde_vici 0.1.2 serde_m… 0.2.0 0.3.0 0.4.0 0.2.0 serde-sa… 0.3.5 0.2.3 0.2.4 0.2.2 0.1.0 serde_ur… 1.109.0 serde_js… 1.0.93 serde-ge… 0.25.0 serde-ge… 0.1.0 bitflags-… 0.1.0 0.1.2 0.1.1 serde_v8 0.82.0 0.1.0 1.107.0 serde-thi… 0.4.1 0.4.2 serde-qu… 0.1.5 serde-ro… 0.6.0 1.0.92 0.1.2 serde_b… 0.11.9 serde_js… 0.1.0 0.2.0 0.80.0 0.81.0 serde_d… 0.12.1 serde-qu… 0.2.0 serde-qu… 0.2.0 serde_e… 0.3.0 serde-qu… 0.2.0 serde_d… 4.0.12 serde_s… 0.6.1 serde-e… 1.3.0 ethers-i… 0.4.0 serde_st… 0.1.11 0.2.0 serde_s… 0.2.3 0.79.0 1.2.0 cargo-ge… 0.2.4 1.2.1 base64-… 0.7.0 serde_bolt 0.2.3 0.2.4 serde_qs 0.11.0 0.1.9 ex3-serde 0.2.0 0.1.10 0.5.0 serde_w… 2.0.0 0.7.0-rc.1 0.76.0 serde-ne… 0.3.2 serde-be… 0.0.9 serde_ig… 0.1.7 0.1.8 serde_klv 0.2.1 bitflags_… 0.2.4 0.2.0 serde-bi… 0.5.0 0.2.3 serde_ht… 0.2.0 enum_pr… 0.2.3 0.1.7 serde-ha… 0.4.5 serde-env 0.1.1 0.1.6 0.1.5 0.7.0-bet… 0.1.1 0.1.0 serde_a… 0.5.8 0.1.3 0.1.0 0.1.2 0.1.4 0.7.0-bet… serde_d… 1.0.152 1.105.0 0.1.1 serde_test 1.0.152 0.1.0 0.1.0 serde_e… 0.1.0 0.2.2 serde_s… 0.1.2 serde-se… 0.0.9 0.2.1 serde_to… 0.1.6 0.0.8 0.5.7 0.0.7 0.1.1 4.0.10 amqp_s… 0.2.0 0.0.5 0.0.6 0.0.4 0.0.1 1.103.0 0.0.2 0.0.3 1.0.90 serde_st… 0.1.7 serde_re… 0.1.10 1.101.0 serde-en… 0.3.2 serde_js… 0.1.4 serde-m… 0.1.0 miniserde 0.1.28 serde_p… 0.1.9 0.1.5 serde_y… 0.9.16 0.11.8 0.3.1 0.1.2 0.1.3 1.0.91 erased-s… 0.3.24 0.9.15 0.1.2 0.3.0 1.99.0 0.1.1 serde-att… 0.2.1 0.1.1 0.1.6 0.2.0 0.9.17 0.1.0 1.0.151 0.6.0 1.0.151 serde-fie… 0.2.0 serde-da… 0.2.2 serde-au… 0.2.0 serde_c… 0.2.3 serde-fie… 0.2.0 0.3.0 0.75.0 0.74.0 0.2.1 0.3.1 conjure-… 3.4.0 0.3.3 0.1.0 1.97.0 3.3.0 0.1.1 1.0.150 serde-s… 0.1.3 1.0.150 serde-en… 0.4.5 0.2.2 0.2.1 serde_a… 0.2.0 serde-js… 0.5.0 1.95.0 1.0.149 0.2.0 1.0.149 serde_x12 0.6.0 0.1.1 serde-tc 0.4.1 0.1.0 bytesize… 0.2.1 0.73.0 it-json-s… 0.3.5 0.3.0 serde-pa… 0.2.1 0.3.0 0.3.1 0.2.0 serde_a… 0.1.0 0.1.0 1.0.148 1.0.148 serde_tr… 0.2.8 0.5.6 serde_cl… 0.3.3 0.4.4 0.72.0 1.93.0 \ No newline at end of file diff --git a/media/query-steffocrates-solution.svg b/media/query-steffocrates-solution.svg new file mode 100644 index 0000000..f4da5e6 --- /dev/null +++ b/media/query-steffocrates-solution.svg @@ -0,0 +1 @@ +Neo4j Graph VisualizationCreated using Neo4j (http://www.neo4j.com/)OWNSIS_TAGGED_WITHHAS_VERSIONCONTAINSCONTAINSHAS_VERSIONIS_TAGGED_WITHIS_TAGGED_WITHIS_TAGGED_WITHIS_TAGGED_WITHOWNSIS_TAGGED_WITHHAS_VERSIONCONTAINSCONTAINSHAS_VERSIONHAS_VERSIONHAS_VERS…HAS_VERSIONHAS_VERSIONIS_TAGGED_WITHCONTAINSOWNSIS_TAGGED_WITHHAS_VERSIONCONTAINSHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_V…HAS_VE…HAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONIS_TAGGED_WITHIS_TAGGED_WITHIS_TAGGED_WITHIS_TAGGED_WITHCONTAINSCONTAINSOWNSIS_TAGGED_WITHHAS_VERSIONCONTAINSCONTAINSHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_V…HAS_VERSIONHAS_VERSIONHAS_V…HAS_V…HAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONHAS_VERSIONIS_TAGGED_WITHPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLI…PUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHEDPUBLISHED Steffo99 distribut… 1174057 0.3.0 Games Root 0.2.0 123 753150 179 1174054 serde-alt… 1847 0.2.0 Encoding 0.5.1 0.1.0 0.4.0 0.5.0 0.3.0 408045 patched… 283776 0.9.1 0.9.0 0.7.1 0.5.3 0.8.0 0.4.0 0.3.0 0.6.0 0.5.0 0.7.0 0.5.1 2780 43 1350 Parser imple… bobbot 2379 1.0.1 Command line utilit 0.10.0 0.10.1 0.7.0 0.6.0 0.10.3 0.5.0 1.0.0 0.2.0 0.8.0 0.10.2 0.3.0 0.9.0 0.3.2 0.4.0 474959 \ No newline at end of file diff --git a/scripts/alter-default-password.sh b/scripts/alter-default-password.sh new file mode 100755 index 0000000..3e83a95 --- /dev/null +++ b/scripts/alter-default-password.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +export NEO4J_USERNAME="neo4j" +export NEO4J_PASSWORD="neo4j" + +echo "Altering password..." +cypher-shell --database="system" --non-interactive --fail-fast 'ALTER CURRENT USER SET PASSWORD FROM "neo4j" TO "unimore-big-data-analytics-4"' diff --git a/scripts/create-neo4j-desktop-link.sh b/scripts/create-neo4j-desktop-link.sh new file mode 100755 index 0000000..d989f86 --- /dev/null +++ b/scripts/create-neo4j-desktop-link.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +repo=$(git rev-parse --show-toplevel) +unlink "$repo/data/neo4j" +ln -s "$1" "$repo/data/neo4j" + +# Example call: +# ./create-neo4j-desktop-link.sh "/home/steffo/.config/Neo4j Desktop/Application/relate-data/dbmss/dbms-13367bfc-b56d-418c-a9bd-c8c3932e1e0e" \ No newline at end of file diff --git a/scripts/fixup-data-files.sh b/scripts/fixup-data-files.sh new file mode 100755 index 0000000..abfb82c --- /dev/null +++ b/scripts/fixup-data-files.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +repo=$(git rev-parse --show-toplevel) +cwd=$(pwd) +data_files=$(ls $repo/data/cratesio/*/data/*.csv) + +cd "$repo" + +for file in $data_files; do + echo "Fixing data file $file..." + basefilename=$(basename $file) + sed --expression='s=\\=\\\\=g' $file > "$repo/data/neo4j/import/$basefilename" +done + +cd "$cwd" \ No newline at end of file diff --git a/scripts/import-cratesio.sh b/scripts/import-cratesio.sh new file mode 100755 index 0000000..0daa438 --- /dev/null +++ b/scripts/import-cratesio.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -e + +export NEO4J_USERNAME="neo4j" +export NEO4J_PASSWORD="unimore-big-data-analytics-4" + +repo=$(git rev-parse --show-toplevel) +cwd=$(pwd) +import_scripts=$(echo $repo/scripts/import-cratesio/$1*.cypher | sort) + +cd "$repo" + +for file in $import_scripts; do + echo "Executing $file..." + cypher-shell --fail-at-end --format verbose < $file +done + +cd "$cwd" \ No newline at end of file diff --git a/scripts/import-cratesio/1-crates.cypher b/scripts/import-cratesio/1-crates.cypher new file mode 100644 index 0000000..7b5549a --- /dev/null +++ b/scripts/import-cratesio/1-crates.cypher @@ -0,0 +1,66 @@ +CREATE RANGE INDEX index_crate_id IF NOT EXISTS +FOR (crate:Crate) +ON (crate.id); + +CREATE RANGE INDEX index_crate_downloads IF NOT EXISTS +FOR (crate:Crate) +ON (crate.downloads); + +CREATE RANGE INDEX index_crate_created_at IF NOT EXISTS +FOR (crate:Crate) +ON (crate.created_at); + +CREATE RANGE INDEX index_crate_updated_at IF NOT EXISTS +FOR (crate:Crate) +ON (crate.updated_at); + +CREATE TEXT INDEX index_crate_name IF NOT EXISTS +FOR (crate:Crate) +ON (crate.name); + +LOAD CSV WITH HEADERS FROM "file:///crates.csv" AS line FIELDTERMINATOR "," +CALL { + WITH line + MERGE (crate:Crate { id: toInteger(line.id) }) + SET + crate.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + crate.updated_at = apoc.date.parse(line.updated_at, "ms", "yyyy-MM-dd HH:mm:ss"), + crate.max_upload_size = toInteger(line.max_upload_size), + crate.downloads = toInteger(line.downloads), + crate.description = CASE trim(line.description) + WHEN "" + THEN null + ELSE + line.description + END, + crate.documentation = CASE trim(line.documentation) + WHEN "" + THEN null + ELSE + line.documentation + END, + crate.homepage = CASE trim(line.homepage) + WHEN "" + THEN null + ELSE + line.homepage + END, + crate.name = CASE trim(line.name) + WHEN "" + THEN null + ELSE + line.name + END, + crate.readme = CASE trim(line.readme) + WHEN "" + THEN null + ELSE + line.readme + END, + crate.repository = CASE trim(line.repository) + WHEN "" + THEN null + ELSE + line.repository + END +} IN TRANSACTIONS OF 10000 ROWS; \ No newline at end of file diff --git a/scripts/import-cratesio/2-keywords.cypher b/scripts/import-cratesio/2-keywords.cypher new file mode 100644 index 0000000..88ccca6 --- /dev/null +++ b/scripts/import-cratesio/2-keywords.cypher @@ -0,0 +1,13 @@ +CREATE RANGE INDEX index_keyword_id IF NOT EXISTS +FOR (keyword:Keyword) +ON (keyword.id); + +CREATE TEXT INDEX index_keyword_name IF NOT EXISTS +FOR (keyword:Keyword) +ON (keyword.name); + +LOAD CSV WITH HEADERS FROM "file:///keywords.csv" AS line FIELDTERMINATOR "," +MERGE (keyword:Keyword { id: toInteger(line.id) }) +SET + keyword.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + keyword.name = line.keyword; diff --git a/scripts/import-cratesio/3-crates_keywords.cypher b/scripts/import-cratesio/3-crates_keywords.cypher new file mode 100644 index 0000000..2f47f79 --- /dev/null +++ b/scripts/import-cratesio/3-crates_keywords.cypher @@ -0,0 +1,8 @@ +MATCH (:Crate)-[relation:IS_TAGGED_WITH]->(:Keyword) +DELETE relation; + +LOAD CSV WITH HEADERS FROM "file:///crates_keywords.csv" AS line FIELDTERMINATOR "," +MATCH + (crate:Crate {id: toInteger(line.crate_id)}), + (keyword:Keyword {id: toInteger(line.keyword_id)}) +CREATE (crate)-[:IS_TAGGED_WITH]->(keyword); diff --git a/scripts/import-cratesio/4-categories.cypher b/scripts/import-cratesio/4-categories.cypher new file mode 100644 index 0000000..b315af6 --- /dev/null +++ b/scripts/import-cratesio/4-categories.cypher @@ -0,0 +1,50 @@ +CREATE RANGE INDEX index_category_id IF NOT EXISTS +FOR (category:Category) +ON (category.id); + +CREATE TEXT INDEX index_category_name IF NOT EXISTS +FOR (category:Category) +ON (category.name); + +CREATE TEXT INDEX index_category_slug IF NOT EXISTS +FOR (category:Category) +ON (category.slug); + +CREATE TEXT INDEX index_category_leaf IF NOT EXISTS +FOR (category:Category) +ON (category.leaf); + +MATCH (category:Category) +DETACH DELETE category; + +CREATE ( + :Category { + name: "Root", + created_at: datetime(), + description: "Root category. Does not contain any category by itself.", + id: 0, + path: "root", + slug: "root" + } +); + +LOAD CSV WITH HEADERS FROM "file:///categories.csv" AS line +CREATE ( + :Category { + name: line.category, + created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + description: line.description, + id: toInteger(line.id), + path: line.path, + slug: line.slug + } +); + +MATCH (c:Category) +WITH c, split(c.path, ".") AS path +SET c.leaf = path[-1]; + +MATCH (c:Category) +WITH c, split(c.path, ".") AS path +MATCH (d:Category {leaf: path[-2]}) +CREATE (d)-[:CONTAINS]->(c); diff --git a/scripts/import-cratesio/5-crates_categories.cypher b/scripts/import-cratesio/5-crates_categories.cypher new file mode 100644 index 0000000..f847cbc --- /dev/null +++ b/scripts/import-cratesio/5-crates_categories.cypher @@ -0,0 +1,8 @@ +MATCH (:Category)-[relation:CONTAINS]->(:Crate) +DELETE relation; + +LOAD CSV WITH HEADERS FROM "file:///crates_categories.csv" AS line FIELDTERMINATOR "," +MATCH + (crate:Crate {id: toInteger(line.crate_id)}), + (category:Category {id: toInteger(line.category_id)}) +CREATE (category)-[:CONTAINS]->(crate); diff --git a/scripts/import-cratesio/6-users.cypher b/scripts/import-cratesio/6-users.cypher new file mode 100644 index 0000000..91cfeef --- /dev/null +++ b/scripts/import-cratesio/6-users.cypher @@ -0,0 +1,23 @@ +CREATE RANGE INDEX index_user_id IF NOT EXISTS +FOR (user:User) +ON (user.id); + +CREATE RANGE INDEX index_user_ghid IF NOT EXISTS +FOR (user:User) +ON (user.gh_id); + +CREATE TEXT INDEX index_user_name IF NOT EXISTS +FOR (user:User) +ON (user.name); + +CREATE TEXT INDEX index_user_fullname IF NOT EXISTS +FOR (user:User) +ON (user.full_name); + +LOAD CSV WITH HEADERS FROM "file:///users.csv" AS line FIELDTERMINATOR "," +MERGE (user:User { id: toInteger(line.id) }) +SET + user.avatar = line.gh_avatar, + user.gh_id = toInteger(line.gh_id), + user.name = line.gh_login, + user.full_name = line.name; diff --git a/scripts/import-cratesio/7-crate_owners.cypher b/scripts/import-cratesio/7-crate_owners.cypher new file mode 100644 index 0000000..350aff9 --- /dev/null +++ b/scripts/import-cratesio/7-crate_owners.cypher @@ -0,0 +1,11 @@ +MATCH (:User)-[owns:OWNS]->(:Crate) +DELETE owns; + +LOAD CSV WITH HEADERS FROM "file:///crate_owners.csv" AS line FIELDTERMINATOR "," +MATCH (crate:Crate { id: toInteger(line.crate_id) }) +MATCH (owner:User { id: toInteger(line.owner_id) }) +CREATE (owner)-[ownership:OWNS { + created_at: apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + created_by: toInteger(line.created_by), + owner_kind: toInteger(line.owner_kind) +}]->(crate); \ No newline at end of file diff --git a/scripts/import-cratesio/8-versions.cypher b/scripts/import-cratesio/8-versions.cypher new file mode 100644 index 0000000..dae85ba --- /dev/null +++ b/scripts/import-cratesio/8-versions.cypher @@ -0,0 +1,50 @@ +CREATE LOOKUP INDEX index_version_checksum IF NOT EXISTS +FOR (version:Version) +ON (version.checksum); + +CREATE RANGE INDEX index_version_size IF NOT EXISTS +FOR (version:Version) +ON (version.size); + +CREATE RANGE INDEX index_version_created_at IF NOT EXISTS +FOR (version:Version) +ON (version.created_at); + +CREATE RANGE INDEX index_version_downloads IF NOT EXISTS +FOR (version:Version) +ON (version.downloads); + +CREATE RANGE INDEX index_version_id IF NOT EXISTS +FOR (version:Version) +ON (version.id); + +CREATE TEXT INDEX index_version_name IF NOT EXISTS +FOR (version:Version) +ON (version.name); + +LOAD CSV WITH HEADERS FROM "file:///versions.csv" AS line FIELDTERMINATOR "," +CALL { + WITH line + MERGE (version:Version { id: toInteger(line.id) } ) + SET + version.checksum = line.checksum, + version.size = toInteger(line.crate_size), + version.created_at = apoc.date.parse(line.created_at, "ms", "yyyy-MM-dd HH:mm:ss"), + version.downloads = toInteger(line.downloads), + version.license = line.license, + version.features = line.features, + version.links = line.links, + version.name = line.num, + version.is_yanked = CASE line.yanked + WHEN "t" + THEN true + ELSE + false + END + WITH line, version + MATCH (crate:Crate { id: toInteger(line.crate_id) }) + MERGE (crate)-[:HAS_VERSION]->(version) + WITH line, version + MATCH (user:User { id: toInteger(line.published_by) }) + MERGE (user)-[:PUBLISHED]->(version) +} IN TRANSACTIONS OF 10000 ROWS; diff --git a/scripts/import-cratesio/9-dependencies.cypher b/scripts/import-cratesio/9-dependencies.cypher new file mode 100644 index 0000000..5048917 --- /dev/null +++ b/scripts/import-cratesio/9-dependencies.cypher @@ -0,0 +1,38 @@ +CREATE RANGE INDEX index_dependency_id IF NOT EXISTS +FOR ()-[dependency:DEPENDS_ON]->() +ON (dependency.id); + +CREATE TEXT INDEX index_dependency_requirement IF NOT EXISTS +FOR ()-[dependency:DEPENDS_ON]->() +ON (dependency.requirement); + +CREATE TEXT INDEX index_dependency_explicit_name IF NOT EXISTS +FOR ()-[dependency:DEPENDS_ON]->() +ON (dependency.explicit_name); + +LOAD CSV WITH HEADERS FROM "file:///dependencies.csv" AS line FIELDTERMINATOR "," +CALL { + WITH line + MATCH + (version:Version { id: toInteger(line.version_id) }), + (requirement:Crate { id: toInteger(line.crate_id) }) + MERGE (version)-[dependency:DEPENDS_ON]->(requirement) + SET + dependency.id = line.id, + dependency.is_optional = CASE line.optional + WHEN "t" + THEN true + ELSE + false + END, + dependency.is_default = CASE line.default_features + WHEN "t" + THEN true + ELSE + false + END, + dependency.explicit_name = line.explicit_name, + dependency.features = line.features, + dependency.requirement = line.req, + dependency.target = line.target +} IN TRANSACTIONS OF 10000 ROWS; diff --git a/scripts/run-db.sh b/scripts/run-db.sh new file mode 100755 index 0000000..9aff57a --- /dev/null +++ b/scripts/run-db.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +repo=$(git rev-parse --show-toplevel) +export NEO4J_HOME="$repo/data/neo4j" +neo4j console diff --git a/scripts/setup-apoc.sh b/scripts/setup-apoc.sh new file mode 100755 index 0000000..8ab112e --- /dev/null +++ b/scripts/setup-apoc.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +repo=$(git rev-parse --show-toplevel) + +echo "Creating plugins directory..." +mkdir --parents "$repo/data/neo4j/plugins" + +echo "Installing Neo4j Apoc..." +wget 'https://github.com/neo4j/apoc/releases/download/5.5.0/apoc-5.5.0-core.jar' --output-document="$repo/data/neo4j/plugins/apoc-core.jar" +