├── .gitignore ├── example.png ├── Cargo.toml ├── src ├── main.rs └── lib.rs ├── ReadMe.md ├── LICENSE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UhhhWaitWhat/visualize-sqlite/HEAD/example.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "visualize-sqlite" 3 | version = "2.0.0" 4 | edition = "2018" 5 | description = "A simple way to draw a diagram from an sqlite database" 6 | license = "MIT" 7 | readme = "ReadMe.md" 8 | repository = "https://github.com/PaulAvery/visualize-sqlite" 9 | keywords = ["sqlite", "graphviz", "dot", "svg"] 10 | 11 | [dependencies] 12 | diesel = { version = "2.0.0", features = [ "sqlite" ] } 13 | eyre = { version = "0.6.5" } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use eyre::{eyre, Context, Result}; 3 | 4 | use visualize_sqlite::Schema; 5 | 6 | fn open_database() -> Result { 7 | let mut args = std::env::args(); 8 | args.next(); 9 | 10 | match args.next() { 11 | None => Err(eyre!( 12 | "Please pass an sqlite database file as the first argument" 13 | )), 14 | Some(file) => { 15 | std::fs::metadata(&file).wrap_err("failed to open database")?; 16 | SqliteConnection::establish(&file).wrap_err("failed to open database") 17 | } 18 | } 19 | } 20 | 21 | fn main() -> Result<()> { 22 | println!("{}", Schema::load(&mut open_database()?)?); 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Visualize an sqlite database 2 | 3 | Create simple visualizations of sqlite databases in GraphViz `dot` format. 4 | 5 | This version only works with the 2.0 version of diesel. 6 | Use version 1.x of this crate if you need compatibility with an older diesel version. 7 | 8 | **CLI** 9 | 10 | ```bash 11 | visualize-sqlite your_sqlite_database.db | dot -Tpng -Gfontname='Fira Mono' -Gfontcolor='#586e75' -Gbgcolor='#fdf6e3' -Nfontname='Fira Mono' -Nfontcolor='#586e75' -Efontname='Fira Mono' > output.png 12 | ``` 13 | 14 | **API** 15 | 16 | ```rust 17 | use diesel::SqliteConnection; 18 | use visualize_sqlite::Schema; 19 | 20 | fn main() { 21 | let db = SqliteConnection::establish("your_sqlite_database.db").unwrap(); 22 | let dot_input = Schema::load(&mut db).unwrap(); 23 | 24 | println!("{}", dot_input); 25 | } 26 | ``` 27 | 28 | ## Sample Output 29 | 30 | ![Sample Output](./example.png) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Florian Albertz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "diesel" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "01e2adfd0a7a81070ed7beec0c62636458926326c16fedb77796d41e447b282d" 10 | dependencies = [ 11 | "diesel_derives", 12 | "libsqlite3-sys", 13 | ] 14 | 15 | [[package]] 16 | name = "diesel_derives" 17 | version = "2.0.0" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "22a7ab9d7967e6a1a247ea38aedf88ab808b4ac0c159576bc71866ab8f9f9250" 20 | dependencies = [ 21 | "proc-macro-error", 22 | "proc-macro2", 23 | "quote", 24 | "syn", 25 | ] 26 | 27 | [[package]] 28 | name = "eyre" 29 | version = "0.6.5" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "221239d1d5ea86bf5d6f91c9d6bc3646ffe471b08ff9b0f91c44f115ac969d2b" 32 | dependencies = [ 33 | "indenter", 34 | "once_cell", 35 | ] 36 | 37 | [[package]] 38 | name = "indenter" 39 | version = "0.3.3" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 42 | 43 | [[package]] 44 | name = "libsqlite3-sys" 45 | version = "0.22.2" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" 48 | dependencies = [ 49 | "pkg-config", 50 | "vcpkg", 51 | ] 52 | 53 | [[package]] 54 | name = "once_cell" 55 | version = "1.8.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" 58 | 59 | [[package]] 60 | name = "pkg-config" 61 | version = "0.3.19" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" 64 | 65 | [[package]] 66 | name = "proc-macro-error" 67 | version = "1.0.4" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 70 | dependencies = [ 71 | "proc-macro-error-attr", 72 | "proc-macro2", 73 | "quote", 74 | "syn", 75 | "version_check", 76 | ] 77 | 78 | [[package]] 79 | name = "proc-macro-error-attr" 80 | version = "1.0.4" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 83 | dependencies = [ 84 | "proc-macro2", 85 | "quote", 86 | "version_check", 87 | ] 88 | 89 | [[package]] 90 | name = "proc-macro2" 91 | version = "1.0.27" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" 94 | dependencies = [ 95 | "unicode-xid", 96 | ] 97 | 98 | [[package]] 99 | name = "quote" 100 | version = "1.0.9" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" 103 | dependencies = [ 104 | "proc-macro2", 105 | ] 106 | 107 | [[package]] 108 | name = "syn" 109 | version = "1.0.73" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" 112 | dependencies = [ 113 | "proc-macro2", 114 | "quote", 115 | "unicode-xid", 116 | ] 117 | 118 | [[package]] 119 | name = "unicode-xid" 120 | version = "0.2.2" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 123 | 124 | [[package]] 125 | name = "vcpkg" 126 | version = "0.2.15" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 129 | 130 | [[package]] 131 | name = "version_check" 132 | version = "0.9.4" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 135 | 136 | [[package]] 137 | name = "visualize-sqlite" 138 | version = "2.0.0" 139 | dependencies = [ 140 | "diesel", 141 | "eyre", 142 | ] 143 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | use diesel::prelude::*; 5 | use eyre::{Context, Result}; 6 | use std::fmt::Display; 7 | 8 | mod raw { 9 | use diesel::sql_types::*; 10 | 11 | #[derive(QueryableByName)] 12 | pub struct Table { 13 | #[diesel(sql_type = Text)] 14 | pub name: String, 15 | } 16 | 17 | #[derive(QueryableByName)] 18 | pub struct Column { 19 | #[diesel(sql_type = Text)] 20 | pub name: String, 21 | #[diesel(sql_type = Text, column_name = "type")] 22 | pub typ: String, 23 | #[diesel(sql_type = Bool)] 24 | pub notnull: bool, 25 | #[diesel(sql_type = Bool)] 26 | pub pk: bool, 27 | #[diesel(sql_type = Nullable)] 28 | pub dflt_value: Option, 29 | } 30 | 31 | #[derive(QueryableByName)] 32 | pub struct ForeignKey { 33 | #[diesel(sql_type = Text)] 34 | pub table: String, 35 | #[diesel(sql_type = Nullable)] 36 | pub to: Option, 37 | #[diesel(sql_type = Text)] 38 | pub from: String, 39 | } 40 | } 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct Column { 44 | pub name: String, 45 | pub typ: String, 46 | pub nullable: bool, 47 | pub default: Option, 48 | pub primary: bool, 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub struct Table { 53 | pub name: String, 54 | pub columns: Vec, 55 | pub foreign_keys: Vec, 56 | } 57 | 58 | #[derive(Debug, Clone)] 59 | pub struct ForeignKey { 60 | pub target_table: String, 61 | pub target_column: Option, 62 | pub source_table: String, 63 | pub source_column: String, 64 | } 65 | 66 | #[derive(Debug, Clone)] 67 | pub struct Schema(pub Vec); 68 | 69 | impl Schema { 70 | fn get_tables(db: &mut SqliteConnection) -> Result> { 71 | let tables: Vec = diesel::sql_query( 72 | "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT IN ('sqlite_sequence')", 73 | ) 74 | .load(db)?; 75 | 76 | tables 77 | .into_iter() 78 | .map(|table| { 79 | Ok(Table { 80 | foreign_keys: Self::get_keys(db, &table.name) 81 | .wrap_err_with(|| format!("failed to get keys for {}", &table.name))?, 82 | columns: Self::get_columns(db, &table.name) 83 | .wrap_err_with(|| format!("failed to get columns for {}", &table.name))?, 84 | name: table.name, 85 | }) 86 | }) 87 | .collect() 88 | } 89 | 90 | fn get_columns(db: &mut SqliteConnection, table: &str) -> Result> { 91 | let columns: Vec = 92 | diesel::sql_query(format!("SELECT * FROM pragma_table_info('{}')", table)).load(db)?; 93 | 94 | Ok(columns 95 | .into_iter() 96 | .map(|column| Column { 97 | name: column.name, 98 | typ: column.typ, 99 | nullable: !column.notnull, 100 | primary: column.pk, 101 | default: column.dflt_value, 102 | }) 103 | .collect()) 104 | } 105 | 106 | fn get_keys(db: &mut SqliteConnection, table: &str) -> Result> { 107 | let keys: Vec = diesel::sql_query(format!( 108 | "SELECT * FROM pragma_foreign_key_list('{}')", 109 | table 110 | )) 111 | .load(db)?; 112 | 113 | Ok(keys 114 | .into_iter() 115 | .map(|key| ForeignKey { 116 | target_table: key.table, 117 | target_column: key.to, 118 | source_table: table.to_owned(), 119 | source_column: key.from, 120 | }) 121 | .collect()) 122 | } 123 | 124 | pub fn load(db: &mut SqliteConnection) -> eyre::Result { 125 | Ok(Self(Self::get_tables(db)?)) 126 | } 127 | } 128 | 129 | impl Display for Schema { 130 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 131 | writeln!(f, "digraph {{")?; 132 | writeln!(f, "rankdir=LR;")?; 133 | 134 | for table in &self.0 { 135 | table.fmt(f)?; 136 | } 137 | 138 | writeln!(f, "}}") 139 | } 140 | } 141 | 142 | impl Display for Table { 143 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 144 | writeln!( 145 | f, 146 | "{} [shape=plaintext label=<
147 | ", 148 | self.name, self.name 149 | )?; 150 | 151 | for column in &self.columns { 152 | writeln!( 153 | f, 154 | "", 155 | if column.primary { 156 | "bgcolor='#2aa198'" 157 | } else if column.nullable { 158 | "bgcolor='#6c71c4'" 159 | } else { 160 | "" 161 | }, 162 | column.name, 163 | column.name, 164 | column.typ, 165 | )?; 166 | } 167 | 168 | writeln!(f, "
{}
{}{}
>]")?; 169 | 170 | for key in &self.foreign_keys { 171 | key.fmt(f)?; 172 | } 173 | 174 | Ok(()) 175 | } 176 | } 177 | 178 | impl Display for ForeignKey { 179 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 180 | writeln!( 181 | f, 182 | "{}:{} -> {}", 183 | self.source_table, self.source_column, self.target_table 184 | ) 185 | } 186 | } 187 | --------------------------------------------------------------------------------