Multiple views and tables

This commit is contained in:
Candifloss 2026-04-24 11:00:50 +05:30
parent e12e57aaa7
commit d0e95733ec
4 changed files with 259 additions and 55 deletions

View File

@ -8,4 +8,5 @@ askama = "0.15.6"
axum = "0.8.9" axum = "0.8.9"
clap = { version = "4.6.1", features = ["derive"] } clap = { version = "4.6.1", features = ["derive"] }
rusqlite = { version = "0.39.0", features = ["bundled"] } rusqlite = { version = "0.39.0", features = ["bundled"] }
serde = { version = "1.0.228", features = ["derive"] }
tokio = { version = "1.52.1", features = ["full"] } tokio = { version = "1.52.1", features = ["full"] }

103
src/db.rs
View File

@ -2,42 +2,61 @@ use rusqlite::types::Value;
use rusqlite::{Connection, Result}; use rusqlite::{Connection, Result};
use std::sync::Mutex; use std::sync::Mutex;
/// Thin wrapper around a `SQLite` connection. /// Shared `SQLite` access layer.
/// Ensures safe access via Mutex (one query at a time). ///
/// Internally holds a single `SQLite` connection protected by a Mutex.
/// This ensures only one query runs at a time, which is required because
/// `rusqlite::Connection` is not thread-safe.
///
/// This struct is wrapped in `Arc` at the application level.
pub struct SqliteDb { pub struct SqliteDb {
connection: Mutex<Connection>, /// Single `SQLite` connection guarded by a Mutex.
conn: Mutex<Connection>,
} }
impl SqliteDb { impl SqliteDb {
/// Open a database file and create a shared DB handle. /// Open a `SQLite` database file and create a DB handle.
pub fn open(db_path: &str) -> Result<Self> { pub fn open(db_path: &str) -> Result<Self> {
let connection = Connection::open(db_path)?;
Ok(Self { Ok(Self {
connection: Mutex::new(Connection::open(db_path)?), conn: Mutex::new(connection),
}) })
} }
/// Fetch full table data (columns + rows as strings). /// Acquire a locked connection.
///
/// This is the only place where `.lock()` is called, so all queries
/// consistently follow the same pattern.
fn lock_conn(&self) -> std::sync::MutexGuard<'_, Connection> {
self.conn.lock().expect("failed to lock DB connection")
}
/// Fetch all rows from a table.
///
/// Returns:
/// - column names
/// - rows (each row = Vec<String>)
///
/// NOTE: table name is interpolated directly → must be trusted input.
pub fn fetch_table(&self, table_name: &str) -> Result<(Vec<String>, Vec<Vec<String>>)> { pub fn fetch_table(&self, table_name: &str) -> Result<(Vec<String>, Vec<Vec<String>>)> {
// Lock ensures only one request uses the connection at a time let conn = self.lock_conn();
let conn = self.connection.lock().unwrap();
// SQL query
let query = format!("SELECT * FROM {table_name}"); let query = format!("SELECT * FROM {table_name}");
let mut stmt = conn.prepare(&query)?; let mut stmt = conn.prepare(&query)?;
let column_names = stmt let column_names = stmt
.column_names() .column_names()
.iter() .iter()
.map(|s| s.to_string()) .map(|name| name.to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let column_count = stmt.column_count(); let col_count = stmt.column_count();
let rows_iter = stmt.query_map([], move |row| { let rows_iter = stmt.query_map([], move |row| {
let mut row_values = Vec::with_capacity(column_count); let mut values = Vec::with_capacity(col_count);
for i in 0..column_count { for i in 0..col_count {
let value: Value = row.get(i)?; let value: Value = row.get(i)?;
let cell = match value { let cell = match value {
@ -48,14 +67,66 @@ impl SqliteDb {
Value::Blob(b) => format!("<blob {} bytes>", b.len()), Value::Blob(b) => format!("<blob {} bytes>", b.len()),
}; };
row_values.push(cell); values.push(cell);
} }
Ok(row_values) Ok(values)
})?; })?;
let rows = rows_iter.collect::<Result<Vec<_>>>()?; let rows = rows_iter.collect::<Result<Vec<_>>>()?;
Ok((column_names, rows)) Ok((column_names, rows))
} }
/// List all tables in the database.
pub fn list_tables(&self) -> Result<Vec<String>> {
let conn = self.lock_conn();
let mut stmt =
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")?;
let rows = stmt.query_map([], |row| row.get(0))?;
rows.collect::<Result<Vec<_>>>()
}
/// Get raw CREATE TABLE SQL for a table.
pub fn get_table_schema(&self, table_name: &str) -> Result<String> {
let conn = self.lock_conn();
let mut stmt =
conn.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name=?1")?;
let schema = stmt.query_row([table_name], |row| row.get(0))?;
Ok(schema)
}
/// Get column metadata using PRAGMA `table_info`.
///
/// Returns: `(name, type, is_primary_key)`
pub fn get_table_columns(&self, table_name: &str) -> Result<Vec<(String, String, bool)>> {
let conn = self.lock_conn();
let mut stmt = conn.prepare(&format!("PRAGMA table_info({table_name})"))?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(1)?, // name
row.get::<_, String>(2)?, // type
row.get::<_, i64>(5)? == 1, // pk flag
))
})?;
rows.collect::<Result<Vec<_>>>()
}
/// Count number of rows in a table.
pub fn count_rows(&self, table_name: &str) -> Result<i64> {
let conn = self.lock_conn();
let query = format!("SELECT COUNT(*) FROM {table_name}");
conn.query_row(&query, [], |row| row.get(0))
}
} }

View File

@ -1,38 +1,97 @@
use askama::Template; use askama::Template;
use axum::{extract::State, response::Html}; use axum::{
extract::{Query, State},
response::Html,
};
use serde::Deserialize;
use crate::AppState; use crate::AppState;
/// Askama template for rendering a table page. /// Template context for the main table page.
/// Field names must match variables used in `templates/table.html`. ///
/// This struct is passed to Askama and directly maps to variables
/// used in `templates/table.html`.
#[derive(Template)] #[derive(Template)]
#[template(path = "table.html")] #[template(path = "table.html")]
pub struct TableTemplate { pub struct TableTemplate {
/// All table names (for dropdown)
pub tables: Vec<String>,
/// Currently selected table
pub table_name: String, pub table_name: String,
/// Active view: "data" or "structure"
pub view: String,
/// Data view
pub columns: Vec<String>, pub columns: Vec<String>,
pub rows: Vec<Vec<String>>, pub rows: Vec<Vec<String>>,
/// Structure view
pub schema: Option<String>,
pub row_count: Option<i64>,
pub columns_info: Option<Vec<(String, String, bool)>>, // (name, type, pk)
} }
/// HTTP handler for GET / /// Query parameters for `/`
/// `State<AppState>` is extracted by Axum from the router. #[derive(Deserialize)]
/// This gives the handler access to shared application state. pub struct TableQuery {
pub async fn index(State(app_state): State<AppState>) -> Html<String> { pub table: Option<String>,
// `table_name` hardcoded for now; will become dynamic later pub view: Option<String>,
let table_name = "users"; }
// Query DB: returns (column names, row data as strings) /// GET /
let (columns, rows) = app_state // Access shared SQLite handle from AppState ///
.db // Shared SQLite wrapper handle /// Flow:
.fetch_table(table_name) /// 1. Read query params
.unwrap(); // Fetch data from the table (column names, rows) as strings /// 2. Resolve selected table + view
/// 3. Fetch required data depending on view
/// 4. Render template
pub async fn index(
State(app_state): State<AppState>,
Query(query): Query<TableQuery>,
) -> Html<String> {
// Fetch available tables (for dropdown)
let tables = app_state.db.list_tables().unwrap();
// Prepare data for HTML template // Determine selected table
let template = TableTemplate { let table_name = query
table_name: table_name.into(), .table
columns, .unwrap_or_else(|| tables.first().cloned().unwrap_or_default());
rows,
// Determine active view (default = data)
let view = query.view.unwrap_or_else(|| "data".to_string());
// Data view
let (columns, rows) = if view == "data" {
app_state.db.fetch_table(&table_name).unwrap()
} else {
(Vec::new(), Vec::new())
}; };
// Render template → HTML string → wrap as HTTP response // Structure view
let (schema, row_count, columns_info) = if view == "structure" {
(
Some(app_state.db.get_table_schema(&table_name).unwrap()),
Some(app_state.db.count_rows(&table_name).unwrap()),
Some(app_state.db.get_table_columns(&table_name).unwrap()),
)
} else {
(None, None, None)
};
// Build template context
let template = TableTemplate {
tables,
table_name,
view,
columns,
rows,
schema,
row_count,
columns_info,
};
// Render HTML response
Html(template.render().unwrap()) Html(template.render().unwrap())
} }

View File

@ -6,27 +6,100 @@
</head> </head>
<body> <body>
<h1>{{ table_name }}</h1> <!-- Top bar: table selector -->
<form method="get">
<label>
Table:
<select name="table" onchange="this.form.submit()">
{% for t in tables %}
<option value="{{ t }}" {% if t == &table_name %}selected{% endif %}>
{{ t }}
</option>
{% endfor %}
</select>
</label>
<table border="1" cellspacing="0" cellpadding="4"> <!-- Preserve current view when switching tables -->
<thead> <input type="hidden" name="view" value="{{ view }}">
<tr> </form>
{% for col in columns %}
<th>{{ col }}</th>
{% endfor %}
</tr>
</thead>
<tbody> <br>
{% for row in rows %}
<tr> <!-- Tabs -->
{% for cell in row %} <p>
<td>{{ cell }}</td> <a href="/?table={{ table_name }}&view=data">
{% endfor %} {% if view == "data" %}<b>Data</b>{% else %}Data{% endif %}
</tr> </a>
{% endfor %} |
</tbody> <a href="/?table={{ table_name }}&view=structure">
</table> {% if view == "structure" %}<b>Structure</b>{% else %}Structure{% endif %}
</a>
</p>
<!-- DATA VIEW -->
{% if view == "data" %}
<table border="1" cellspacing="0" cellpadding="4">
<thead>
<tr>
{% for col in columns %}
<th>{{ col }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
<!-- STRUCTURE VIEW -->
{% elif view == "structure" %}
<h2>Table: {{ table_name }}</h2>
{% if let Some(count) = row_count %}
<p><b>Rows:</b> {{ count }}</p>
{% endif %}
<!-- Columns -->
{% if let Some(cols) = columns_info %}
<details>
<summary><b>Columns</b></summary>
<table border="1" cellspacing="0" cellpadding="4">
<tr>
<th>Name</th>
<th>Type</th>
<th>PK</th>
</tr>
{% for c in cols %}
<tr>
<td>{{ c.0 }}</td>
<td>{{ c.1 }}</td>
<td>{% if c.2 %}✓{% endif %}</td>
</tr>
{% endfor %}
</table>
</details>
{% endif %}
<br>
<!-- Schema -->
{% if let Some(s) = schema %}
<details>
<summary><b>Schema</b></summary>
<pre>{{ s }}</pre>
</details>
{% endif %}
{% endif %}
</body> </body>
</html> </html>