use rustyline::DefaultEditor;
use sekejap::CoreDB;
use std::io::{self, IsTerminal, Read};
struct Args {
path: Option<String>,
sql: Option<String>,
}
fn parse_args() -> Args {
let mut args = std::env::args().skip(1).peekable();
let mut path = None;
let mut sql = None;
while let Some(arg) = args.next() {
match arg.as_str() {
"--path" | "-p" => {
path = args.next();
}
"--help" | "-h" => {
print_usage();
std::process::exit(0);
}
"--version" | "-V" => {
println!("sekejap {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
other => {
if path.is_none() {
path = Some(other.to_string());
} else {
sql = Some(other.to_string());
}
}
}
}
Args { path, sql }
}
fn print_usage() {
println!(
"sekejap {}
USAGE:
sekejap open in-memory REPL
sekejap <path> open persistent DB in REPL
sekejap --path <path> same (explicit flag)
sekejap <path> \"<SQL>\" run SQL and exit
sekejap --path <path> \"<SQL>\" run SQL and exit
echo \"SELECT...;\" | sekejap pipe SQL script
OPTIONS:
-p, --path <path> database directory path
-h, --help show this help
-V, --version show version",
env!("CARGO_PKG_VERSION")
);
}
fn open_db(path: &Option<String>) -> (CoreDB, String) {
match path {
Some(p) => match CoreDB::open(p) {
Ok(db) => (db, p.clone()),
Err(e) => {
eprintln!("error: cannot open '{}': {}", p, e);
std::process::exit(1);
}
},
None => (CoreDB::new(), String::from(":memory:")),
}
}
fn run_sql(db: &mut CoreDB, sql: &str) -> bool {
let first = sql.split_whitespace().next().unwrap_or("").to_uppercase();
match first.as_str() {
"SELECT" => match db.query(sql) {
Err(e) => eprintln!("error: {e}"),
Ok(set) => print_hits(set.collect()),
},
"MATCH" => {
let is_pipeline = sql.split_whitespace().any(|w| w.to_uppercase() == "WITH");
if is_pipeline {
match db.pipeline_query(sql) {
Err(e) => eprintln!("error: {e}"),
Ok(hits) => print_hits(hits),
}
} else {
match db.query(sql) {
Err(e) => eprintln!("error: {e}"),
Ok(set) => print_hits(set.collect()),
}
}
}
"INSERT" | "UPDATE" | "DELETE" | "CREATE" | "DROP" => match db.execute(sql) {
Err(e) => eprintln!("error: {e}"),
Ok(n) => {
if n == 0 {
println!("ok");
} else if n == 1 {
println!("ok — 1 row affected");
} else {
println!("ok — {} rows affected", n);
}
}
},
"SHOW" => match db.show(sql) {
Err(e) => eprintln!("error: {e}"),
Ok(hits) => print_hits(hits),
},
_ => eprintln!("unknown statement — supported: SELECT MATCH SHOW INSERT UPDATE DELETE CREATE"),
}
true
}
fn print_hits(hits: Vec<sekejap::Hit>) {
let count = hits.len();
for hit in &hits {
match &hit.payload {
Some(v) => println!(
"{}",
serde_json::to_string_pretty(v).unwrap_or_else(|_| v.to_string())
),
None => println!("{}", hit.slug),
}
}
if count == 1 {
println!("── 1 row ──");
} else {
println!("── {} rows ──", count);
}
}
fn run_dot(db: &mut CoreDB, label: &mut String, line: &str) -> bool {
let parts: Vec<&str> = line.splitn(2, ' ').collect();
match parts[0] {
".quit" | ".q" | ".exit" => return false,
".help" => print_repl_help(),
".open" => {
let p = parts.get(1).map(|s| s.trim()).unwrap_or("");
if p.is_empty() {
eprintln!("usage: .open <path>");
} else {
match CoreDB::open(p) {
Ok(new_db) => {
*db = new_db;
*label = p.to_string();
println!("opened: {p}");
}
Err(e) => eprintln!("error: {e}"),
}
}
}
".tables" => {
let names = db.collection_names();
if names.is_empty() {
println!("(no collections)");
} else {
for name in names {
println!("{name}");
}
}
}
".schema" => {
let target = parts.get(1).map(|s| s.trim());
let names = db.collection_names();
let cols: Vec<&str> = match target {
Some(t) if !t.is_empty() => vec![t],
_ => names.iter().map(String::as_str).collect(),
};
let mut found_any = false;
for col in cols {
if let Some(ddl) = db.schema_ddl(col) {
println!("{ddl};");
found_any = true;
} else if target.is_some() {
println!("-- no CREATE TABLE for '{col}'");
found_any = true;
}
}
if !found_any {
println!("(no schemas declared — use CREATE TABLE to add one)");
}
}
".compact" => match db.compact() {
Ok(_) => println!("compacted"),
Err(e) => eprintln!("error: {e}"),
},
".stats" => {
let nodes = db.node_count();
let edges = db.edge_count();
let colls = db.collection_names().len();
println!("nodes : {nodes}");
println!("edges : {edges}");
println!("collections : {colls}");
}
".edges" => {
let arg = parts.get(1).map(|s| s.trim()).unwrap_or("");
if arg.is_empty() {
let schema = db.edge_schema();
if schema.is_empty() {
println!("(no edges)");
} else {
println!("{:<25} {:<20} {}", "from", "type", "to");
println!("{}", "-".repeat(65));
for (from, kind, to) in &schema {
println!("{:<25} {:<20} {}", from, kind, to);
}
}
} else {
let types = db.edge_types_from_collection(arg);
if types.is_empty() {
println!("(no outgoing edges from '{arg}')");
} else {
for t in &types {
println!("{t}");
}
}
}
}
other => eprintln!("unknown command: {other} (try .help)"),
}
true
}
fn print_repl_help() {
println!(
r#"
sekejap dot commands
────────────────────
.open <path> open (or create) a persistent DB — replaces current DB
.tables list all collections
.schema [name] show CREATE TABLE DDL (all collections if name omitted)
.compact flush snapshot, truncate WAL
.stats show node / edge / collection counts
.edges show full graph schema (from_col → type → to_col), distinct
.edges <col> show distinct edge types leaving a collection
.help show this help
.quit / .q / .exit exit (also Ctrl+D)
SQL (end each statement with ;)
────────────────────────────────
SELECT * FROM collection [WHERE ...] [ORDER BY ...] [LIMIT n] [OFFSET n];
SELECT * FROM ALL [WHERE ...];
INSERT INTO collection (_key, field, ...) VALUES ('key', val, ...);
MATCH ('slug')-[:edge]->(a)-[:edge]->(b)
RETURN b.field AS alias, SUM(a.score * b.weight) AS total
GROUP BY b.field ORDER BY total DESC LIMIT 10;
UPDATE collection SET field = val [WHERE ...];
DELETE FROM collection [WHERE ...];
CREATE TABLE collection (_key TEXT PRIMARY KEY, field TYPE, ...);
Graph edges
───────────
INSERT ('from')-[:KIND {{strength: n}}]->('to');
DELETE ('from')-[:KIND]->('to');
MATCH (a:col)-[:rel*1..3]->(b:col) WHERE a._key = 'x' RETURN b;
Filters
───────
= != > < >= <= BETWEEN n AND n
IN (v1, v2) NOT IN (v1, v2)
LIKE 'pat' ILIKE 'pat'
IS NULL IS NOT NULL
AND OR NOT
Spatial
───────
ST_DWithin(geometry, POINT(lon lat), km)
ST_Contains / ST_Within / ST_Intersects
Vector
──────
WHERE VECTOR_NEAR(field, [f32, ...], k)
"#
);
}
fn run_script(db: &mut CoreDB, script: &str) {
let mut label = String::new();
let mut buf = String::new();
let mut in_str = false;
let mut str_char = '\0';
for line in script.lines() {
let trimmed = line.trim();
if !in_str && buf.trim().is_empty() && trimmed.starts_with('.') {
if !run_dot(db, &mut label, trimmed) {
return;
}
continue;
}
if !in_str && (trimmed.is_empty() || trimmed.starts_with("--")) {
continue;
}
for ch in trimmed.chars() {
match ch {
'\'' | '"' if !in_str => { in_str = true; str_char = ch; buf.push(ch); }
c if in_str && c == str_char => { in_str = false; buf.push(ch); }
';' if !in_str => {
let stmt = buf.trim().to_string();
buf.clear();
if !stmt.is_empty() {
run_sql(db, &stmt);
}
}
_ => buf.push(ch),
}
}
if !buf.trim().is_empty() {
buf.push(' ');
}
}
let stmt = buf.trim().to_string();
if !stmt.is_empty() {
run_sql(db, &stmt);
}
}
fn repl(mut db: CoreDB, mut label: String) {
let history_path = std::env::var("HOME").ok()
.map(|h| std::path::PathBuf::from(h).join(".sekejap_history"));
let mut rl = DefaultEditor::new().expect("failed to init readline");
if let Some(ref p) = history_path {
let _ = rl.load_history(p);
}
println!("sekejap {} — {label}", env!("CARGO_PKG_VERSION"));
println!("type .help for commands, .quit to exit\n");
let mut buf = String::new();
loop {
let prompt = if buf.trim().is_empty() {
"sekejap> ".to_string()
} else {
" ...> ".to_string()
};
let line = match rl.readline(&prompt) {
Ok(l) => l,
Err(rustyline::error::ReadlineError::Eof)
| Err(rustyline::error::ReadlineError::Interrupted) => break,
Err(e) => {
eprintln!("readline error: {e}");
break;
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let _ = rl.add_history_entry(trimmed);
if trimmed.starts_with('.') {
buf.clear();
if !run_dot(&mut db, &mut label, trimmed) {
break;
}
continue;
}
if !buf.is_empty() {
buf.push(' ');
}
buf.push_str(trimmed);
if buf.trim_end().ends_with(';') {
let sql = buf.trim_end_matches(';').trim().to_string();
buf.clear();
if !sql.is_empty() {
run_sql(&mut db, &sql);
}
}
}
if let Some(ref p) = history_path {
let _ = rl.save_history(p);
}
}
fn main() {
let args = parse_args();
let (mut db, label) = open_db(&args.path);
if let Some(sql) = args.sql {
run_script(&mut db, &sql);
return;
}
if !io::stdin().is_terminal() {
let mut script = String::new();
io::stdin()
.read_to_string(&mut script)
.expect("failed to read stdin");
run_script(&mut db, &script);
return;
}
repl(db, label);
}