find_sqlite/
lib.rs

1use std::{
2    fs::{self, File, Metadata},
3    io::{ErrorKind, Read},
4    os::unix::fs::{MetadataExt, PermissionsExt},
5    path::Path,
6};
7
8use anyhow::anyhow;
9use rayon::iter::{ParallelBridge, ParallelIterator};
10use walkdir::WalkDir;
11
12const INDENT: &str = "    ";
13
14pub struct Options {
15    pub show_metadata: bool,
16    pub show_schema: bool,
17    pub batch_separator: String,
18    pub format_sql: bool,
19    pub format_sql_pretty: bool,
20}
21
22pub fn run(path: &Path, opt: Options) {
23    WalkDir::new(path)
24        .into_iter()
25        .par_bridge()
26        .filter_map(|entry_result| entry_result.ok())
27        .map(|entry| entry.into_path())
28        .filter_map(|path| {
29            fs::metadata(&path)
30                .inspect_err(|error| {
31                    tracing::warn!(
32                        ?error,
33                        ?path,
34                        "Failed to fetch file metadata."
35                    );
36                })
37                .ok()
38                .map(|meta| (path, meta))
39        })
40        .filter(|(_path, meta)| meta.is_file())
41        .filter_map(|(path, meta)| {
42            file_has_sqlite_header(&path)
43                .inspect_err(|error| {
44                    tracing::warn!(
45                        ?error,
46                        ?path,
47                        "Failed to check file for SQLite header."
48                    );
49                })
50                .ok()
51                .and_then(|has_header| has_header.then_some((path, meta)))
52        })
53        .filter_map(|(path, meta)| {
54            file_fetch_schema(&path, opt.format_sql, opt.format_sql_pretty)
55                .inspect_err(|error| {
56                    tracing::warn!(?error, ?path, "Failed to fetch schema.");
57                })
58                .ok()
59                .map(|schema| (path, meta, schema))
60        })
61        .filter_map(|(path, meta, schema)| {
62            metadata_fmt(&meta)
63                .inspect_err(|error| {
64                    tracing::warn!(
65                        ?error,
66                        ?path,
67                        "Failed to format metadata."
68                    );
69                })
70                .ok()
71                .map(|meta| (path, meta, schema))
72        })
73        .for_each(|(path, meta, schema)| {
74            let meta = if opt.show_metadata {
75                format!("\n{INDENT}meta\n{meta}")
76            } else {
77                String::new()
78            };
79            let schema = if opt.show_schema {
80                format!("\n{INDENT}schema\n{schema}")
81            } else {
82                String::new()
83            };
84            let batch_sep = if opt.show_metadata || opt.show_schema {
85                opt.batch_separator.as_str()
86            } else {
87                ""
88            };
89            // XXX Single print statement to avoid interleaved output.
90            println!("{path:?}{meta}{schema}{batch_sep}");
91        });
92}
93
94pub fn tracing_init(level: Option<tracing::Level>) -> anyhow::Result<()> {
95    use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter, Layer};
96
97    if let Some(level) = level {
98        let layer_stderr = fmt::Layer::new()
99            .with_writer(std::io::stderr)
100            .with_ansi(true)
101            .with_file(false)
102            .with_line_number(true)
103            .with_thread_ids(true)
104            .with_filter(
105                EnvFilter::from_default_env().add_directive(level.into()),
106            );
107        tracing::subscriber::set_global_default(
108            tracing_subscriber::registry().with(layer_stderr),
109        )?;
110    }
111    Ok(())
112}
113
114fn file_has_sqlite_header(path: &Path) -> anyhow::Result<bool> {
115    const SQLITE_HEADER: &[u8; 16] = b"SQLite format 3\0";
116
117    let mut file = File::open(path)?;
118    let mut buf = [0u8; SQLITE_HEADER.len()];
119    let read_result = file.read_exact(&mut buf);
120    match read_result.map_err(|e| e.kind()) {
121        Err(ErrorKind::UnexpectedEof) => {
122            return Ok(false);
123        }
124        Err(e) => {
125            return Err(anyhow!("{e:?} path={path:?}"));
126        }
127        Ok(()) => {}
128    }
129    Ok(buf[..].eq(SQLITE_HEADER))
130}
131
132fn metadata_fmt(meta: &Metadata) -> anyhow::Result<String> {
133    let user = meta.uid();
134    let group = meta.gid();
135    let size = human_units::Size(meta.len());
136    let perm = umask::Mode::from(meta.permissions().mode());
137    let mtime = humantime::format_rfc3339(meta.modified()?);
138    let atime = humantime::format_rfc3339(meta.accessed()?);
139    let btime = humantime::format_rfc3339(meta.created()?);
140    let lines = [
141        format!("btime {btime}"),
142        format!("mtime {mtime}"),
143        format!("atime {atime}"),
144        format!("size {size}"),
145        format!("perm {perm}"),
146        format!("owner {user}:{group}"),
147    ];
148    let meta = lines
149        .iter()
150        .map(|line| format!("{INDENT}{INDENT}{line}"))
151        .collect::<Vec<String>>()
152        .join("\n");
153    Ok(meta)
154}
155
156fn file_fetch_schema(
157    path: &Path,
158    format_sql: bool,
159    format_sql_pretty: bool,
160) -> anyhow::Result<String> {
161    let conn = rusqlite::Connection::open(path)?;
162    let sql = "SELECT sql FROM sqlite_master WHERE type IN ('table', 'view', 'index')";
163    let mut statement = conn.prepare(sql)?;
164    let mut schema = Vec::new();
165    let mut rows = statement.query([])?;
166    while let Some(row) = rows.next()? {
167        match row.get::<_, String>(0) {
168            Err(error) => {
169                tracing::warn!(?error, ?row, "Failed to access a row.");
170            }
171            Ok(sql) => {
172                let sql = if format_sql {
173                    if format_sql_pretty {
174                        sql_fmt_pretty(&sql)
175                    } else {
176                        sql_fmt(&sql)
177                    }
178                } else {
179                    sql
180                };
181                let sql = sql
182                    .lines()
183                    .map(|line| format!("{INDENT}{INDENT}{line}"))
184                    .collect::<Vec<String>>()
185                    .join("\n");
186                schema.push(sql);
187            }
188        }
189    }
190    schema.sort();
191    let schema = schema.join("\n");
192    Ok(schema)
193}
194
195/// Normalize format - remove inconsistent spaces and newlines.
196fn sql_fmt(sql: &str) -> String {
197    sql.split_whitespace().collect::<Vec<&str>>().join(" ")
198}
199
200fn sql_fmt_pretty(sql: &str) -> String {
201    use sqlformat::{FormatOptions, Indent, QueryParams};
202
203    let mut opt = FormatOptions::default();
204    opt.indent = Indent::Spaces(4);
205    opt.uppercase = Some(true);
206    sqlformat::format(&sql, &QueryParams::None, &opt)
207}