A lightweight YAML file-based database with CLI tool, Rust API, ODBC driver, and JDBC driver support.
- YAML Storage - Human-readable data format
- CRUD Operations - Create, Read, Update, Delete
- Query Builder - Flexible conditions with AND/OR/NOT
- Fuzzy Search - Case-insensitive keyword search
- Import/Export - JSON and YAML formats
- Backup/Restore - Database snapshots
- CLI Tool - Command-line interface
- ODBC Driver - SQL query support
- JDBC Driver - Java SQL access support
YamlDB uses yq v4 for YAML file parsing and formatting. Release packages may bundle yq; otherwise install the yq command and make sure it is available on PATH.
Examples:
# macOS
brew install yq
# Linux with Homebrew
brew install yq
# Or download a yq v4 binary from https://github.com/mikefarah/yq
yq --versionyq lookup order:
YAMLDB_YQenvironment variable.- Bundled
yqnext to the CLI/ODBC driver, or inbin/next to it. - For JDBC, bundled JAR resources under
bin/<os>-<arch>/yq, for examplebin/linux-amd64/yq. yqoryq.exefromPATH.
cargo install yamldbDownload pre-built binaries from GitHub Releases:
| Platform | CLI | ODBC Driver | JDBC Driver |
|---|---|---|---|
| Linux | yamldb-linux |
libyamldb-linux-odbc.so |
yamldb-jdbc.jar |
| Windows | yamldb-windows.exe |
yamldb-windows-odbc.dll |
yamldb-jdbc.jar |
| macOS | yamldb-macos |
libyamldb-macos-odbc.dylib |
yamldb-jdbc.jar |
YamlDB uses the same source mapping across CLI, Web UI, ODBC, and JDBC.
| Source | Table names | Notes |
|---|---|---|
Single YAML file, for example data.yaml |
data; file stem such as data is also accepted by SQL drivers |
CLI defaults to data |
Directory, for example ./yaml-data |
one table per direct child .yaml/.yml file |
users.yaml becomes table users |
Directory sources are not recursive. Put table files directly inside the selected directory:
yaml-data/
users.yaml
teams.yml
projects.yaml
The equivalent access patterns are:
# CLI
yamldb -f ./yaml-data -t users list
# Web UI
yamldb -f ./yaml-data webui-- ODBC/JDBC
SELECT * FROM usersWhen creating data through CLI or Web UI, a missing table in a directory source is created as <table>.yaml.
| Surface | Single file | Directory tables | Write support | Table metadata |
|---|---|---|---|---|
| CLI | yes | yes, via --table |
yes | tables command |
| Web UI | yes | yes, table selector | yes | table selector |
| ODBC | yes | yes, SELECT * FROM <table> |
read-only | SQLTables / SQLColumns |
| JDBC | yes | yes, SELECT * FROM <table> |
read-only | DatabaseMetaData |
-f, --file <FILE> Specify YAML file path [default: data.yaml]
-t, --table <TABLE> Select a table when --file points to a YAML directory [default: data]
The --table option is global, so it can be placed before or after the subcommand.
# Single-file source
yamldb create user1 --fields name=Alice,age=30,city=Beijing
yamldb get user1
yamldb list
yamldb list --limit 10
yamldb list --format json
yamldb update user1 --fields age=31,city=Guangzhou
yamldb delete user1
# Directory source
yamldb -f ./yaml-data tables
yamldb -f ./yaml-data -t users list
yamldb -f ./yaml-data -t users create user1 --fields name=Alice,age=30
yamldb -f ./yaml-data -t users get user1 --format json
yamldb -f ./yaml-data -t projects create p1 --fields name=Core# Equality
yamldb query --key city --value Beijing
# Comparison operators
yamldb query --key age --value 25 --op gt # > 25
yamldb query --key age --value 30 --op gte # >= 30
yamldb query --key age --value 50 --op lt # < 50
yamldb query --key age --value 25 --op lte # <= 25
yamldb query --key city --value Beijing --op ne # != Beijing
# String operations
yamldb query --key name --value Ali --op contains
# Fuzzy search
yamldb search --keyword alice
yamldb search --keyword alice --key name# Import
yamldb import -i users.json
yamldb import -i users.yaml
yamldb -f ./yaml-data -t users import -i users.yaml
# Export
yamldb export -o backup.json
yamldb export -o backup.yaml --format yaml
yamldb -f ./yaml-data -t users export -o users.json
# Backup
yamldb backup -o backup.yaml
yamldb -f ./yaml-data -t users backup -o users-backup.yaml# Statistics
yamldb stats
yamldb -f ./yaml-data -t users stats
# Count records
yamldb count
yamldb -f ./yaml-data -t users count
# Check existence
yamldb exists user1
yamldb -f ./yaml-data -t users exists user1
# Clear database
yamldb clear --force
yamldb -f ./yaml-data -t users clear --forceStart a local browser UI for the selected YAML file:
yamldb -f data.yaml webui
yamldb -f /path/to/yaml-directory webui
yamldb -f data.yaml webui --host 127.0.0.1 --port 8080The Web UI uses the same source mapping as the ODBC/JDBC drivers. A single YAML file is shown as table data; a directory shows each .yaml/.yml file as a table named after the file stem. It exposes list, search, create/update, and delete actions for the selected table.
It binds to 127.0.0.1:8080 by default and does not provide authentication, so keep it on localhost unless you put it behind your own access control.
use yamldb::{Record, YamlDb};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Open database
let mut db = YamlDb::new("data.yaml");
db.load()?;
// Create record
let mut record = Record::new("user1");
record.set("name", "Alice")
.set("age", 30)
.set("city", "Beijing");
db.create(record)?;
// Read record
let record = db.read("user1")?;
println!("Name: {:?}", record.get_str("name"));
println!("Age: {:?}", record.get_i64("age"));
// Update single field
db.update_field("user1", "age", serde_yaml::Value::Number(31.into()))?;
// Delete record
db.delete("user1")?;
Ok(())
}// In-memory database (no file I/O)
let mut db = YamlDb::memory();
let mut record = Record::new("test");
record.set("key", "value");
db.create(record)?;let mut record = Record::new("user1");
// Set values (chainable)
record.set("name", "Alice")
.set("age", 30)
.set("active", true);
// Get typed values
record.get_str("name"); // Some("Alice")
record.get_i64("age"); // Some(30)
record.get_f64("score"); // None
record.get_bool("active"); // Some(true)
// Check keys
record.has_key("name"); // true
record.keys(); // ["name", "age", "active"]
// Merge records
let mut other = Record::new("user1");
other.set("email", "alice@example.com");
record.merge(&other);
// Convert to JSON
let json = record.to_json()?;use yamldb::{QueryOp, YamlDb};
let db = YamlDb::new("data.yaml");
db.load()?;
// Comparison operators
let result = db.query(&QueryOp::eq("city", "Beijing"));
let result = db.query(&QueryOp::ne("city", "Shanghai"));
let result = db.query(&QueryOp::gt("age", serde_yaml::Value::Number(25.into())));
let result = db.query(&QueryOp::gte("age", serde_yaml::Value::Number(30.into())));
let result = db.query(&QueryOp::lt("age", serde_yaml::Value::Number(50.into())));
let result = db.query(&QueryOp::lte("age", serde_yaml::Value::Number(25.into())));
// String operations
let result = db.query(&QueryOp::contains("name", "Ali"));
let result = db.query(&QueryOp::starts_with("name", "Ali"));
let result = db.query(&QueryOp::ends_with("name", "Smith"));
// Logical operators
let result = db.query(&QueryOp::and(vec![
QueryOp::eq("city", "Beijing"),
QueryOp::gte("age", serde_yaml::Value::Number(28.into())),
]));
let result = db.query(&QueryOp::or(vec![
QueryOp::eq("city", "Beijing"),
QueryOp::eq("city", "Shanghai"),
]));
let result = db.query(&QueryOp::negate(QueryOp::eq("city", "Beijing")));// Search in specific field (case-insensitive)
let result = db.search("name", "alice");
// Search in all fields
let result = db.search_all("alice");let result = db.query(&QueryOp::eq("city", "Beijing"));
// Count
println!("Found: {}", result.count());
println!("Is empty: {}", result.is_empty());
// Access records
if let Some(first) = result.first() {
println!("First: {}", first.id);
}
if let Some(last) = result.last() {
println!("Last: {}", last.id);
}
// Pagination
let page1 = result.page(1, 10); // Page 1, 10 items per page
let page2 = result.page(2, 10); // Page 2
// Skip and limit
let skipped = result.skip(5);
let limited = result.limit(10);
// Get all IDs
let ids = result.ids();
// Iterate
for record in result.iter() {
println!("{}: {:?}", record.id, record.data);
}
// Convert to Vec
let all = result.to_vec();// Read multiple records
let records = db.read_many(&["user1", "user2", "user3"]);
// Update multiple records
let updates = vec![
("user1".to_string(), HashMap::from([("age".to_string(), serde_yaml::Value::Number(31.into()))])),
("user2".to_string(), HashMap::from([("age".to_string(), serde_yaml::Value::Number(26.into()))])),
];
let updated = db.update_many(updates)?;
// Delete multiple records
let deleted = db.delete_many(&["user1", "user2"])?;
// Upsert (insert or update)
let mut record = Record::new("user1");
record.set("name", "Alice");
db.upsert(record)?;use std::path::Path;
// Get statistics
let stats = db.stats();
println!("Total records: {}", stats.total_records);
println!("Unique keys: {:?}", stats.unique_keys);
println!("File size: {:?} bytes", stats.file_size);
// Backup
db.backup(Path::new("backup.yaml"))?;
// Clear all records
db.clear()?;use std::path::Path;
// Import from JSON
let count = db.import_json(Path::new("users.json"))?;
println!("Imported {} records", count);
// Import from YAML
let count = db.import_yaml(Path::new("users.yaml"))?;
// Export to JSON
db.export_json(Path::new("backup.json"))?;
// Export to YAML
db.export_yaml(Path::new("backup.yaml"))?;YamlDB includes an ODBC driver for read-only SQL access from ODBC clients.
DRIVER={YamlDB};DBQ=data.yaml;
DRIVER={YamlDB};FILE=data.yaml;
DRIVER={YamlDB};DBQ=/path/to/yaml-directory;
When DBQ/FILE points to a single YAML file, query it as table data.
When it points to a directory, each .yaml or .yml file is exposed as a table named after the file stem, for example users.yaml becomes table users.
| Source path | Tables | Example SQL |
|---|---|---|
data.yaml |
data |
SELECT * FROM data |
users.yaml |
data, users |
SELECT * FROM users |
/path/to/yaml-directory |
one table per .yaml/.yml file |
SELECT * FROM users |
For directory sources, only files directly inside the selected directory are exposed. Nested directories are ignored.
-- Select all
SELECT * FROM data
-- Where clause
SELECT * FROM data WHERE city = 'Beijing'
-- Comparison operators
SELECT * FROM data WHERE age > 25
SELECT * FROM data WHERE age >= 28
SELECT * FROM data WHERE age < 30
SELECT * FROM data WHERE age <= 25
SELECT * FROM data WHERE city != 'Shanghai'
-- AND/OR conditions
SELECT * FROM data WHERE city = 'Beijing' AND age >= 28
SELECT * FROM data WHERE city = 'Beijing' OR city = 'Shanghai'
-- Directory source
SELECT * FROM users WHERE age >= 28Supported SQL is intentionally small: SELECT * FROM <table> with optional WHERE comparisons joined by AND or OR. Only SELECT * is supported; projected column lists, joins, grouping, ordering, inserts, updates, and deletes are not SQL features of the drivers. Use CLI or Web UI for writes.
cargo build --releaseOutput:
- Windows:
target/release/yamldb.dll - Linux:
target/release/libyamldb.so - macOS:
target/release/libyamldb.dylib
Windows:
- Open ODBC Data Source Administrator
- Go to "Drivers" tab
- Click "Add"
- Browse to
yamldb.dll
Linux:
Add to /etc/odbcinst.ini:
[YamlDB]
Description=YamlDB ODBC Driver
Driver=/path/to/libyamldb.soUse the ODBC driver when your client supports native ODBC connections:
- Register the YamlDB ODBC shared library in the operating system ODBC manager.
- Create a DSN or connection with
DRIVER={YamlDB}. - Set
DBQorFILEto either a YAML file or a directory containing YAML files. - Query
datafor a single file, or query a table named after a YAML file in a directory.
Example directory DSN:
DRIVER={YamlDB};DBQ=/home/alice/yaml-data;
import pyodbc
conn = pyodbc.connect('DRIVER={YamlDB};DBQ=data.yaml;')
cursor = conn.cursor()
cursor.execute("SELECT * FROM data WHERE city = 'Beijing'")
for row in cursor:
print(row)
conn.close()using System.Data.Odbc;
var conn = new OdbcConnection("DRIVER={YamlDB};DBQ=data.yaml;");
conn.Open();
var cmd = new OdbcCommand("SELECT * FROM data WHERE age > 25", conn);
var reader = cmd.ExecuteReader();
while (reader.Read())
{
Console.WriteLine($"{reader["id"]}: {reader["name"]}");
}
conn.Close();YamlDB includes a lightweight JDBC driver for read-only SQL access from Java and tools such as DBeaver. The driver can use bundled yq, -Dyamldb.yq=/path/to/yq, YAMLDB_YQ, or yq from PATH.
jdbc:yamldb:data.yaml
jdbc:yamldb:file:data.yaml
jdbc:yamldb:/path/to/yaml-directory
For DBeaver and similar JDBC tools, choose the YamlDB JDBC jar and use one of the URLs above. A single YAML file is exposed as table data; a directory exposes every .yaml/.yml file as a table named after the file stem. The JDBC driver provides table and column metadata for browsing directory sources.
- Build or download
yamldb-jdbc.jar. - In DBeaver, create a new Driver.
- Add
yamldb-jdbc.jarto the driver libraries. - Set the driver class to
io.github.markchenim.yamldb.jdbc.YamlDbJdbcDriver. - Set the URL template to
jdbc:yamldb:{file}or enter a full JDBC URL manually. - Use a JDBC URL such as
jdbc:yamldb:/home/alice/data.yamlorjdbc:yamldb:/home/alice/yaml-data. - If DBeaver cannot connect, make sure the DBeaver process can find
yq, or set a JVM property such as-Dyamldb.yq=/opt/yamldb/yq. - Test the connection, then browse tables or run SQL.
For a directory source like:
yaml-data/
users.yaml
teams.yml
the visible tables are users and teams.
The JDBC driver supports the same small read-only SQL subset as the ODBC driver:
SELECT * FROM data
SELECT * FROM data WHERE city = 'Beijing'
SELECT * FROM data WHERE age >= 28
SELECT * FROM data WHERE city = 'Beijing' AND age >= 28
SELECT * FROM data WHERE city = 'Beijing' OR city = 'Shanghai'
SELECT * FROM users WHERE age >= 28Nested YAML values such as arrays or objects are returned as JSON text through string getters.
The JDBC driver is read-only and supports the same SQL limits as ODBC: SELECT * FROM <table> plus optional WHERE comparisons joined by AND or OR.
Windows:
powershell -ExecutionPolicy Bypass -File jdbc\build.ps1Linux/macOS:
bash jdbc/build.shOutput:
jdbc/target/yamldb-jdbc.jar
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
Class.forName("io.github.markchenim.yamldb.jdbc.YamlDbJdbcDriver");
try (Connection conn = DriverManager.getConnection("jdbc:yamldb:data.yaml");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM data WHERE city = 'Beijing'")) {
while (rs.next()) {
System.out.println(rs.getString("id") + ": " + rs.getString("name"));
}
}- id: user1
name: Alice
age: 30
city: Beijing
- id: user2
name: Bob
age: 25
city: Shanghai[
{"id": "user1", "name": "Alice", "age": 30, "city": "Beijing"},
{"id": "user2", "name": "Bob", "age": 25, "city": "Shanghai"}
]use yamldb::{YamlDb, YamlDbError};
match db.create(record) {
Ok(_) => println!("Success"),
Err(YamlDbError::DuplicateKey(id)) => eprintln!("Duplicate: {}", id),
Err(YamlDbError::NotFound(id)) => eprintln!("Not found: {}", id),
Err(YamlDbError::Io(e)) => eprintln!("IO error: {}", e),
Err(YamlDbError::Yaml(e)) => eprintln!("YAML error: {}", e),
Err(YamlDbError::Json(e)) => eprintln!("JSON error: {}", e),
Err(e) => eprintln!("Error: {}", e),
}yamldb/
├── src/
│ ├── lib.rs # Core library
│ ├── main.rs # CLI tool
│ └── odbc.rs # ODBC driver
├── tests/
│ ├── integration.rs # Unit tests
│ └── odbc.rs # ODBC tests
├── jdbc/
│ ├── src/main/java # JDBC driver
│ └── src/test/java # JDBC smoke test
├── Cargo.toml
├── README.md
├── README.zh-CN.md
├── CHANGELOG.md
└── LICENSE
MIT