use duckdb::{Connection, params};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
use super::{ColumnInfo, TablePreview};
use crate::models::{Pokemon, PokemonMove};
#[derive(Clone)]
pub struct DuckDb {
conn: Arc<Mutex<Connection>>,
db_path: String,
}
impl DuckDb {
pub fn new(db_path: &str) -> Result<Self, String> {
let conn = if db_path == ":memory:" {
Connection::open_in_memory()
.map_err(|e| format!("Failed to open in-memory DuckDB: {e}"))?
} else {
let path = Path::new(db_path);
if let Some(parent) = path
.parent()
.filter(|p| !p.as_os_str().is_empty() && !p.exists())
{
let _ = std::fs::create_dir_all(parent);
}
Connection::open(db_path)
.map_err(|e| format!("Failed to open DuckDB file at {db_path}: {e}"))?
};
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
db_path: db_path.to_string(),
})
}
#[must_use]
pub fn db_path(&self) -> &str {
&self.db_path
}
pub async fn init(&self) -> Result<(), String> {
let conn = self.conn.lock().await;
conn.execute(
"CREATE TABLE IF NOT EXISTS pokemon (
id INTEGER PRIMARY KEY,
name VARCHAR NOT NULL,
display_name VARCHAR NOT NULL,
types VARCHAR NOT NULL,
hp INTEGER NOT NULL,
attack INTEGER NOT NULL,
defense INTEGER NOT NULL,
sp_attack INTEGER NOT NULL,
sp_defense INTEGER NOT NULL,
speed INTEGER NOT NULL,
height INTEGER NOT NULL,
weight INTEGER NOT NULL,
sprite_url VARCHAR NOT NULL,
artwork_url VARCHAR NOT NULL,
genus VARCHAR NOT NULL,
description VARCHAR NOT NULL,
moves_json VARCHAR NOT NULL,
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);",
[],
)
.map_err(|e| format!("Failed to create pokemon table in DuckDB: {e}"))?;
// Also create a pokedex_meta table to track sync state
conn.execute(
"CREATE TABLE IF NOT EXISTS pokedex_meta (
key VARCHAR PRIMARY KEY,
value VARCHAR NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);",
[],
)
.map_err(|e| format!("Failed to create pokedex_meta table in DuckDB: {e}"))?;
log::info!("DuckDB schema initialized successfully");
Ok(())
}
pub async fn existing_pokemon_ids(&self) -> Result<std::collections::HashSet<i32>, String> {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare("SELECT id FROM pokemon;")
.map_err(|e| format!("Failed to query pokemon ids: {e}"))?;
let rows = stmt
.query_map([], |row| row.get::<_, i32>(0))
.map_err(|e| format!("Failed to fetch pokemon ids: {e}"))?;
let mut ids = std::collections::HashSet::new();
for id in rows.flatten() {
ids.insert(id);
}
Ok(ids)
}
pub async fn save_pokemons(&self, pokemons: &[Pokemon]) -> Result<usize, String> {
if pokemons.is_empty() {
return Ok(0);
}
let conn = self.conn.lock().await;
conn.execute("BEGIN TRANSACTION;", [])
.map_err(|e| format!("Failed to begin transaction: {e}"))?;
let mut stmt = conn
.prepare(
"INSERT INTO pokemon (
id, name, display_name, types, hp, attack, defense,
sp_attack, sp_defense, speed, height, weight,
sprite_url, artwork_url, genus, description, moves_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
name = excluded.name,
display_name = excluded.display_name,
types = excluded.types,
hp = excluded.hp,
attack = excluded.attack,
defense = excluded.defense,
sp_attack = excluded.sp_attack,
sp_defense = excluded.sp_defense,
speed = excluded.speed,
height = excluded.height,
weight = excluded.weight,
sprite_url = excluded.sprite_url,
artwork_url = excluded.artwork_url,
genus = excluded.genus,
description = excluded.description,
moves_json = excluded.moves_json,
cached_at = now();",
)
.map_err(|e| format!("Failed to prepare insert statement: {e}"))?;
let mut inserted = 0;
for p in pokemons {
let types_str = serde_json::to_string(&p.types)
.map_err(|e| format!("Failed to serialize types: {e}"))?;
let moves_str = serde_json::to_string(&p.moves)
.map_err(|e| format!("Failed to serialize moves: {e}"))?;
stmt.execute(params![
p.id,
p.name,
p.display_name,
types_str,
p.hp,
p.attack,
p.defense,
p.sp_attack,
p.sp_defense,
p.speed,
p.height,
p.weight,
p.sprite_url,
p.artwork_url,
p.genus,
p.description,
moves_str,
])
.map_err(|e| format!("Failed to insert pokemon {}: {e}", p.id))?;
inserted += 1;
}
conn.execute("COMMIT;", [])
.map_err(|e| format!("Failed to commit transaction: {e}"))?;
Ok(inserted)
}
pub async fn seed_if_empty(&self) -> Result<usize, String> {
let count = self.table_row_count("pokemon").await?;
if count >= 151 {
log::info!("DuckDB pokemon table already has {count} entries; skipping seed");
return Ok(usize::try_from(count).unwrap_or(0));
}
let existing_ids = self.existing_pokemon_ids().await?;
let missing_ids: Vec<i32> = (1..=151).filter(|id| !existing_ids.contains(id)).collect();
if missing_ids.is_empty() {
log::info!("All 151 Pokémon already present in DuckDB; skipping seed");
return Ok(usize::try_from(count).unwrap_or(0));
}
log::info!(
"Seeding DuckDB with {} missing Gen 1 Pokémon from PokéAPI...",
missing_ids.len()
);
let fetched = crate::pokeapi::fetch_gen1_pokemon_concurrent(missing_ids, 8).await?;
let inserted = self.save_pokemons(&fetched).await?;
log::info!("Successfully cached {inserted} Pokémon from PokéAPI into DuckDB");
let total = self.table_row_count("pokemon").await?;
Ok(usize::try_from(total).unwrap_or(inserted))
}
#[allow(dead_code, clippy::too_many_lines)]
pub async fn seed_test_data(&self) -> Result<usize, String> {
let sample = vec![
Pokemon {
id: 1,
name: "bulbasaur".to_string(),
display_name: "Bulbasaur".to_string(),
types: vec!["grass".to_string(), "poison".to_string()],
hp: 45,
attack: 49,
defense: 49,
sp_attack: 65,
sp_defense: 65,
speed: 45,
height: 7,
weight: 69,
sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png".to_string(),
artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png".to_string(),
genus: "Seed Pokémon".to_string(),
description: "There is a plant seed on its back right from the day this POKéMON is born. The seed slowly grows larger.".to_string(),
moves: vec![
PokemonMove {
name: "tackle".to_string(),
level: 1,
method: "level-up".to_string(),
},
PokemonMove {
name: "growl".to_string(),
level: 4,
method: "level-up".to_string(),
},
],
},
Pokemon {
id: 2,
name: "ivysaur".to_string(),
display_name: "Ivysaur".to_string(),
types: vec!["grass".to_string(), "poison".to_string()],
hp: 60,
attack: 62,
defense: 63,
sp_attack: 80,
sp_defense: 80,
speed: 60,
height: 10,
weight: 130,
sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/2.png".to_string(),
artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/2.png".to_string(),
genus: "Seed Pokémon".to_string(),
description: "When the bulb on its back grows large, it appears to lose the ability to stand on its hind legs.".to_string(),
moves: vec![
PokemonMove {
name: "tackle".to_string(),
level: 1,
method: "level-up".to_string(),
},
],
},
Pokemon {
id: 3,
name: "venusaur".to_string(),
display_name: "Venusaur".to_string(),
types: vec!["grass".to_string(), "poison".to_string()],
hp: 80,
attack: 82,
defense: 83,
sp_attack: 100,
sp_defense: 100,
speed: 80,
height: 20,
weight: 1000,
sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/3.png".to_string(),
artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/3.png".to_string(),
genus: "Seed Pokémon".to_string(),
description: "The plant blooms when it is absorbing solar energy. It stays on the move to seek sunlight.".to_string(),
moves: vec![
PokemonMove {
name: "tackle".to_string(),
level: 1,
method: "level-up".to_string(),
},
],
},
Pokemon {
id: 25,
name: "pikachu".to_string(),
display_name: "Pikachu".to_string(),
types: vec!["electric".to_string()],
hp: 35,
attack: 55,
defense: 40,
sp_attack: 50,
sp_defense: 50,
speed: 90,
height: 4,
weight: 60,
sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png".to_string(),
artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/25.png".to_string(),
genus: "Mouse Pokémon".to_string(),
description: "It has small electric sacs on both its cheeks. If threatened, it looses electric charges from the sacs.".to_string(),
moves: vec![
PokemonMove {
name: "thunder-shock".to_string(),
level: 1,
method: "level-up".to_string(),
},
],
},
Pokemon {
id: 151,
name: "mew".to_string(),
display_name: "Mew".to_string(),
types: vec!["psychic".to_string()],
hp: 100,
attack: 100,
defense: 100,
sp_attack: 100,
sp_defense: 100,
speed: 100,
height: 4,
weight: 40,
sprite_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/151.png".to_string(),
artwork_url: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/151.png".to_string(),
genus: "New Species Pokémon".to_string(),
description: "A POKéMON of South America that was thought to have been extinct. It is very intelligent and learns any move.".to_string(),
moves: vec![
PokemonMove {
name: "pound".to_string(),
level: 1,
method: "level-up".to_string(),
},
],
},
];
self.save_pokemons(&sample).await
}
#[allow(dead_code)]
pub async fn save_pokemon(&self, p: &Pokemon) -> Result<(), String> {
let conn = self.conn.lock().await;
let types_str = serde_json::to_string(&p.types)
.map_err(|e| format!("Failed to serialize types: {e}"))?;
let moves_str = serde_json::to_string(&p.moves)
.map_err(|e| format!("Failed to serialize moves: {e}"))?;
conn.execute(
"INSERT INTO pokemon (
id, name, display_name, types, hp, attack, defense,
sp_attack, sp_defense, speed, height, weight,
sprite_url, artwork_url, genus, description, moves_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
name = excluded.name,
display_name = excluded.display_name,
types = excluded.types,
hp = excluded.hp,
attack = excluded.attack,
defense = excluded.defense,
sp_attack = excluded.sp_attack,
sp_defense = excluded.sp_defense,
speed = excluded.speed,
height = excluded.height,
weight = excluded.weight,
sprite_url = excluded.sprite_url,
artwork_url = excluded.artwork_url,
genus = excluded.genus,
description = excluded.description,
moves_json = excluded.moves_json,
cached_at = now();",
params![
p.id,
p.name,
p.display_name,
types_str,
p.hp,
p.attack,
p.defense,
p.sp_attack,
p.sp_defense,
p.speed,
p.height,
p.weight,
p.sprite_url,
p.artwork_url,
p.genus,
p.description,
moves_str,
],
)
.map_err(|e| format!("Failed to upsert pokemon {}: {e}", p.id))?;
Ok(())
}
pub async fn get_pokemon_by_id(&self, id: i32) -> Result<Option<Pokemon>, String> {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare(
"SELECT id, name, display_name, types, hp, attack, defense,
sp_attack, sp_defense, speed, height, weight,
sprite_url, artwork_url, genus, description, moves_json
FROM pokemon WHERE id = ?;",
)
.map_err(|e| format!("Failed to prepare get query: {e}"))?;
let mut rows = stmt
.query(params![id])
.map_err(|e| format!("Failed to query pokemon by id: {e}"))?;
if let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read pokemon row: {e}"))?
{
let p = parse_pokemon_row(row)?;
Ok(Some(p))
} else {
Ok(None)
}
}
pub async fn search_pokemon(
&self,
query: &str,
limit: usize,
offset: usize,
) -> Result<Vec<Pokemon>, String> {
let conn = self.conn.lock().await;
let q_clean = query.trim().to_lowercase();
#[allow(clippy::cast_possible_wrap)]
let limit_i64 = limit as i64;
#[allow(clippy::cast_possible_wrap)]
let offset_i64 = offset as i64;
if q_clean.is_empty() {
let mut stmt = conn
.prepare(
"SELECT id, name, display_name, types, hp, attack, defense,
sp_attack, sp_defense, speed, height, weight,
sprite_url, artwork_url, genus, description, moves_json
FROM pokemon ORDER BY id ASC LIMIT ? OFFSET ?;",
)
.map_err(|e| format!("Failed to prepare search: {e}"))?;
let mut rows = stmt
.query(params![limit_i64, offset_i64])
.map_err(|e| format!("Failed to query pokemon: {e}"))?;
let mut results = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read row: {e}"))?
{
results.push(parse_pokemon_row(row)?);
}
Ok(results)
} else {
let num_val = q_clean.trim_start_matches('#').parse::<i32>().unwrap_or(-1);
let pattern = format!("%{q_clean}%");
let mut stmt = conn
.prepare(
"SELECT id, name, display_name, types, hp, attack, defense,
sp_attack, sp_defense, speed, height, weight,
sprite_url, artwork_url, genus, description, moves_json
FROM pokemon
WHERE id = ? OR lower(name) LIKE ? OR lower(display_name) LIKE ? OR lower(types) LIKE ?
ORDER BY id ASC LIMIT ? OFFSET ?;",
)
.map_err(|e| format!("Failed to prepare search: {e}"))?;
let mut rows = stmt
.query(params![
num_val, pattern, pattern, pattern, limit_i64, offset_i64
])
.map_err(|e| format!("Failed to query search results: {e}"))?;
let mut results = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read row: {e}"))?
{
results.push(parse_pokemon_row(row)?);
}
Ok(results)
}
}
pub async fn total_pokemon_count(&self, query: &str) -> Result<usize, String> {
let conn = self.conn.lock().await;
let q_clean = query.trim().to_lowercase();
if q_clean.is_empty() {
let count: i64 = conn
.query_row("SELECT count(*) FROM pokemon;", [], |r| r.get(0))
.map_err(|e| format!("Failed to get count: {e}"))?;
Ok(usize::try_from(count).unwrap_or(0))
} else {
let num_val = q_clean.trim_start_matches('#').parse::<i32>().unwrap_or(-1);
let pattern = format!("%{q_clean}%");
let count: i64 = conn
.query_row(
"SELECT count(*) FROM pokemon
WHERE id = ? OR lower(name) LIKE ? OR lower(display_name) LIKE ? OR lower(types) LIKE ?;",
params![num_val, pattern, pattern, pattern],
|r| r.get(0),
)
.map_err(|e| format!("Failed to get search count: {e}"))?;
Ok(usize::try_from(count).unwrap_or(0))
}
}
// --- Diagnostic and Debug Inspection methods ---
pub async fn list_tables(&self) -> Result<Vec<String>, String> {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = 'main'
ORDER BY table_name;",
)
.map_err(|e| format!("Failed to list tables: {e}"))?;
let mut rows = stmt
.query([])
.map_err(|e| format!("Failed to query tables: {e}"))?;
let mut tables = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read table name: {e}"))?
{
let name: String = row.get(0).unwrap_or_default();
tables.push(name);
}
Ok(tables)
}
pub async fn describe_table(&self, table: &str) -> Result<Vec<ColumnInfo>, String> {
let conn = self.conn.lock().await;
let mut stmt = conn
.prepare(
"SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'main' AND table_name = ?
ORDER BY ordinal_position;",
)
.map_err(|e| format!("Failed to describe table {table}: {e}"))?;
let mut rows = stmt
.query(params![table])
.map_err(|e| format!("Failed to query columns for {table}: {e}"))?;
let mut columns = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read column info: {e}"))?
{
let name: String = row.get(0).unwrap_or_default();
let data_type: String = row.get(1).unwrap_or_default();
let nullable_str: String = row.get(2).unwrap_or_default();
let is_nullable = nullable_str.to_uppercase() == "YES";
let is_pk = name == "id";
columns.push(ColumnInfo {
name,
data_type,
is_nullable,
is_pk,
});
}
Ok(columns)
}
pub async fn table_row_count(&self, table: &str) -> Result<i64, String> {
let conn = self.conn.lock().await;
// Verify table name is alphanumeric/underscore to prevent injection
if !table.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err("Invalid table name".to_string());
}
let query = format!("SELECT count(*) FROM {table};");
let count: i64 = conn
.query_row(&query, [], |r| r.get(0))
.map_err(|e| format!("Failed to get row count for {table}: {e}"))?;
Ok(count)
}
pub async fn preview_table(&self, table: &str, limit: usize) -> Result<TablePreview, String> {
if !table.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err("Invalid table name".to_string());
}
let total_rows = self.table_row_count(table).await?;
let conn = self.conn.lock().await;
let query = format!("SELECT * FROM {table} LIMIT {limit};");
let mut stmt = conn
.prepare(&query)
.map_err(|e| format!("Failed to prepare preview for {table}: {e}"))?;
let mut rows = stmt
.query([])
.map_err(|e| format!("Failed to execute preview for {table}: {e}"))?;
let col_count = rows.as_ref().map_or(0, duckdb::Statement::column_count);
let mut col_names = Vec::new();
if let Some(r) = rows.as_ref() {
for i in 0..col_count {
col_names.push(
r.column_name(i)
.map_or_else(|_| "?".to_string(), String::from),
);
}
}
let mut data_rows = Vec::new();
while let Some(row) = rows
.next()
.map_err(|e| format!("Failed to read preview row: {e}"))?
{
let mut row_vals = Vec::new();
for i in 0..col_count {
let val: duckdb::types::Value = row.get(i).unwrap_or(duckdb::types::Value::Null);
row_vals.push(format_duck_val(&val));
}
data_rows.push(row_vals);
}
Ok(TablePreview {
table_name: table.to_string(),
total_rows,
columns: col_names,
rows: data_rows,
})
}
}
fn format_duck_val(val: &duckdb::types::Value) -> String {
match val {
duckdb::types::Value::Null => "NULL".to_string(),
duckdb::types::Value::Boolean(b) => b.to_string(),
duckdb::types::Value::TinyInt(n) => n.to_string(),
duckdb::types::Value::SmallInt(n) => n.to_string(),
duckdb::types::Value::Int(n) => n.to_string(),
duckdb::types::Value::BigInt(n) => n.to_string(),
duckdb::types::Value::HugeInt(n) => n.to_string(),
duckdb::types::Value::UTinyInt(n) => n.to_string(),
duckdb::types::Value::USmallInt(n) => n.to_string(),
duckdb::types::Value::UInt(n) => n.to_string(),
duckdb::types::Value::UBigInt(n) => n.to_string(),
duckdb::types::Value::Float(f) => format!("{f:.2}"),
duckdb::types::Value::Double(f) => format!("{f:.2}"),
duckdb::types::Value::Text(s) => s.clone(),
other => format!("{other:?}"),
}
}
fn parse_pokemon_row(row: &duckdb::Row) -> Result<Pokemon, String> {
let id: i32 = row.get(0).map_err(|e| format!("col 0 id: {e}"))?;
let name: String = row.get(1).map_err(|e| format!("col 1 name: {e}"))?;
let display_name: String = row.get(2).map_err(|e| format!("col 2 display_name: {e}"))?;
let types_json: String = row.get(3).map_err(|e| format!("col 3 types: {e}"))?;
let hp: i32 = row.get(4).map_err(|e| format!("col 4 hp: {e}"))?;
let attack: i32 = row.get(5).map_err(|e| format!("col 5 attack: {e}"))?;
let defense: i32 = row.get(6).map_err(|e| format!("col 6 defense: {e}"))?;
let sp_attack: i32 = row.get(7).map_err(|e| format!("col 7 sp_attack: {e}"))?;
let sp_defense: i32 = row.get(8).map_err(|e| format!("col 8 sp_defense: {e}"))?;
let speed: i32 = row.get(9).map_err(|e| format!("col 9 speed: {e}"))?;
let height: i32 = row.get(10).map_err(|e| format!("col 10 height: {e}"))?;
let weight: i32 = row.get(11).map_err(|e| format!("col 11 weight: {e}"))?;
let sprite_url: String = row.get(12).map_err(|e| format!("col 12 sprite_url: {e}"))?;
let artwork_url: String = row
.get(13)
.map_err(|e| format!("col 13 artwork_url: {e}"))?;
let genus: String = row.get(14).map_err(|e| format!("col 14 genus: {e}"))?;
let description: String = row
.get(15)
.map_err(|e| format!("col 15 description: {e}"))?;
let moves_json: String = row.get(16).map_err(|e| format!("col 16 moves_json: {e}"))?;
let types: Vec<String> = serde_json::from_str(&types_json).unwrap_or_default();
let moves: Vec<PokemonMove> = serde_json::from_str(&moves_json).unwrap_or_default();
Ok(Pokemon {
id,
name,
display_name,
types,
hp,
attack,
defense,
sp_attack,
sp_defense,
speed,
height,
weight,
sprite_url,
artwork_url,
genus,
description,
moves,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_duckdb_lifecycle() {
let db = DuckDb::new(":memory:").expect("new in-memory duckdb");
db.init().await.expect("init duckdb");
let count = db.seed_test_data().await.expect("seed duckdb");
assert_eq!(count, 5, "Should seed 5 test Pokemon");
let bulbasaur = db
.get_pokemon_by_id(1)
.await
.unwrap()
.expect("bulbasaur exists");
assert_eq!(bulbasaur.display_name, "Bulbasaur");
assert_eq!(bulbasaur.types, vec!["grass", "poison"]);
assert!(bulbasaur.hp > 0);
assert!(!bulbasaur.moves.is_empty());
let mew = db
.get_pokemon_by_id(151)
.await
.unwrap()
.expect("mew exists");
assert_eq!(mew.display_name, "Mew");
// Search test
let search_grass = db.search_pokemon("grass", 50, 0).await.unwrap();
assert!(!search_grass.is_empty());
assert!(search_grass.iter().any(|p| p.name == "bulbasaur"));
let search_id = db.search_pokemon("25", 10, 0).await.unwrap();
assert!(search_id.iter().any(|p| p.name == "pikachu"));
// Table list & describe
let tables = db.list_tables().await.unwrap();
assert!(tables.contains(&"pokemon".to_string()));
let cols = db.describe_table("pokemon").await.unwrap();
assert!(cols.iter().any(|c| c.name == "display_name"));
let preview = db.preview_table("pokemon", 5).await.unwrap();
assert_eq!(preview.rows.len(), 5);
}
}